diff --git a/crates/pdfs-core/src/control.rs b/crates/pdfs-core/src/control.rs index b33a2e4..f64ad4d 100644 --- a/crates/pdfs-core/src/control.rs +++ b/crates/pdfs-core/src/control.rs @@ -91,6 +91,12 @@ pub enum Request { /// Sets of photos that are byte-identical copies of each other, going by /// the server's content hash. Replies with [`Response::PhotoDuplicates`]. PhotoDuplicates, + /// Sets of shots that look alike going by their thumbnails: resized or + /// re-encoded copies, and bursts of one moment. Starts hashing the + /// thumbnails not hashed yet in the background and replies at once with + /// the sets found so far; a front-end asks again until the reply's + /// `hashing` is `None`. Replies with [`Response::PhotoSimilar`]. + PhotoSimilar, /// The towns the photos were taken in, the one with the most photos first. /// Only photos whose uploader recorded a location count. Replies with /// [`Response::Places`]. @@ -1481,7 +1487,8 @@ pub struct PhotoLocation { } /// How many of the timeline's photos a running refresh has read the metadata -/// (and so the location) of, out of how many. Part of [`Response::Places`]. +/// (and so the location) of, out of how many. Part of [`Response::Places`], +/// and of [`Response::PhotoSimilar`] for how many shots have been hashed. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] pub struct MappingProgress { pub done: usize, @@ -1849,6 +1856,17 @@ pub enum Response { /// The duplicate sets (reply to [`Request::PhotoDuplicates`]), newest /// first. Each set lists the copy worth keeping first. PhotoDuplicates { sets: Vec> }, + /// The sets of alike shots (reply to [`Request::PhotoSimilar`]), newest + /// first, each in capture order; and how far hashing has got, `None` + /// once no pass is running. `uncompared` counts the shots left out + /// because no thumbnail could be read for them. + PhotoSimilar { + sets: Vec>, + #[serde(default)] + hashing: Option, + #[serde(default)] + uncompared: usize, + }, /// The towns the photos were taken in (reply to [`Request::PhotoPlaces`]), /// and how far a running timeline refresh has got reading photo locations /// — `None` when none is reading any. diff --git a/crates/pdfs-fuse/src/control.rs b/crates/pdfs-fuse/src/control.rs index ef1f4aa..3feea09 100644 --- a/crates/pdfs-fuse/src/control.rs +++ b/crates/pdfs-fuse/src/control.rs @@ -465,6 +465,14 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U Ok(sets) => CtlResponse::PhotoDuplicates { sets }, Err(e) => CtlResponse::error(e), }, + Ok(CtlRequest::PhotoSimilar) => match core.photo_similar() { + Ok((sets, hashing, uncompared)) => CtlResponse::PhotoSimilar { + sets, + hashing, + uncompared, + }, + Err(e) => CtlResponse::error(e), + }, Ok(CtlRequest::PhotoPlaces) => match core.photo_places() { Ok(items) => CtlResponse::Places { items, diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index 484ee4b..32b3d62 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -557,6 +557,9 @@ struct Core { /// The same, for the album listing. albums_refreshing: Arc, trash_refreshing: Arc, + /// True while the similar-photo pass hashes thumbnails (see + /// [`Core::photo_similar`]), so asking again doesn't start a second one. + similar_hashing: Arc, /// Fires whenever a trash refresh persists a batch or finishes, so a /// `ListTrash` request waiting on a first-ever refresh wakes on progress /// instead of polling or blocking for the whole run. See diff --git a/crates/pdfs-fuse/src/mount.rs b/crates/pdfs-fuse/src/mount.rs index f19aca2..bfe11f9 100644 --- a/crates/pdfs-fuse/src/mount.rs +++ b/crates/pdfs-fuse/src/mount.rs @@ -392,6 +392,7 @@ pub fn mount( upload_cancel: Arc::new(Mutex::new(HashMap::new())), timeline_refreshing: Arc::new(AtomicBool::new(false)), timeline_progress: Arc::new(Mutex::new(None)), + similar_hashing: Arc::new(AtomicBool::new(false)), albums_refreshing: Arc::new(AtomicBool::new(false)), trash_refreshing: Arc::new(AtomicBool::new(false)), trash_progress: Arc::new(tokio::sync::Notify::new()), diff --git a/crates/pdfs-fuse/src/photos.rs b/crates/pdfs-fuse/src/photos.rs index d7e5a54..f4cb472 100644 --- a/crates/pdfs-fuse/src/photos.rs +++ b/crates/pdfs-fuse/src/photos.rs @@ -43,6 +43,8 @@ const THUMB_QUALITY: u8 = 82; /// thumbnails. Bounded: a screenful of 20 MB digicam JPEGs would otherwise /// saturate the link and starve the rest of the daemon. pub(crate) const THUMB_GEN_CONCURRENCY: usize = 4; +/// Shots the similar-photo pass fetches thumbnails for at a time. +const SIMILAR_CHUNK: usize = 64; /// Images processed between recursive-build progress updates. const THUMB_BUILD_CHUNK: usize = 16; /// A recursive build may wait briefly for an opportunistic tile job that @@ -81,6 +83,46 @@ fn ratio_of(bytes: &[u8]) -> Option { (height > 0).then(|| f64::from(width) / f64::from(height)) } +/// The difference hash of each cached thumbnail in `paths`, keyed like the +/// input, or [`pdfs_core::similar::NO_HASH`] for one that can't be decoded or +/// shows no picture. Decoding is spread over the machine's cores. +fn similar_hashes(paths: Vec<(String, PathBuf)>) -> Vec<(String, u64)> { + fn hash(path: &Path) -> u64 { + image::ImageReader::open(path) + .ok() + .and_then(|reader| reader.with_guessed_format().ok()) + .and_then(|reader| reader.decode().ok()) + .and_then(|image| { + let luma = image.to_luma8(); + pdfs_core::similar::difference_hash( + luma.width() as usize, + luma.height() as usize, + luma.as_raw(), + ) + }) + .unwrap_or(pdfs_core::similar::NO_HASH) + } + let threads = std::thread::available_parallelism().map_or(1, |n| n.get()); + let per_thread = paths.len().div_ceil(threads).max(1); + std::thread::scope(|scope| { + let workers: Vec<_> = paths + .chunks(per_thread) + .map(|chunk| { + scope.spawn(move || { + chunk + .iter() + .map(|(uid, path)| (uid.clone(), hash(path))) + .collect::>() + }) + }) + .collect(); + workers + .into_iter() + .flat_map(|worker| worker.join().unwrap_or_default()) + .collect() + }) +} + /// A thumbnail the daemon made itself, and the aspect ratio of the photo it was /// made from (free at that point — the full image had to be decoded anyway). struct GeneratedThumb { @@ -751,6 +793,83 @@ impl Core { .collect()) } + /// The sets of shots that look alike, as wire items (see + /// [`db::Db::photo_similar`]), how far hashing has got while it runs, and + /// how many shots couldn't be compared once it's done. Starts a hashing + /// pass for the shots not hashed yet unless one is running. + pub(crate) fn photo_similar( + &self, + ) -> CoreResult<(Vec>, Option, usize)> { + self.spawn_similar_hashing(); + let sets = self.db.photo_similar().map_err(CoreError::from)?; + let (done, total) = self.db.similar_progress().map_err(CoreError::from)?; + let hashing = self + .similar_hashing + .load(Ordering::SeqCst) + .then_some(MappingProgress { done, total }); + let uncompared = if hashing.is_some() { 0 } else { total - done }; + let sets = sets + .into_iter() + .map(|set| set.into_iter().map(|p| self.photo_item(p)).collect()) + .collect(); + Ok((sets, hashing, uncompared)) + } + + fn spawn_similar_hashing(&self) { + if self.similar_hashing.swap(true, Ordering::SeqCst) { + return; + } + let core = self.clone(); + let spawned = std::thread::Builder::new() + .name("pdfs-similar".into()) + .spawn(move || { + if let Err(e) = core.hash_similar() { + warn!(error = %e, "similar-photo hashing failed"); + } + core.similar_hashing.store(false, Ordering::SeqCst); + }); + if let Err(e) = spawned { + warn!(error = %e, "could not start similar-photo hashing"); + self.similar_hashing.store(false, Ordering::SeqCst); + } + } + + /// Hash the look of every shot not hashed yet, a chunk at a time. Only + /// thumbnails the server has are used: a photo without one is skipped + /// rather than downloaded whole, and tried again on the next pass. + fn hash_similar(&self) -> CoreResult<()> { + let mut tried: HashSet = HashSet::new(); + loop { + if self.shutdown.is_stopping() { + return Ok(()); + } + // Shots tried this pass without a thumbnail are still unhashed and + // come back first, so ask past them. + let chunk: Vec = self + .db + .photos_unhashed(tried.len() + SIMILAR_CHUNK) + .map_err(CoreError::from)? + .into_iter() + .filter(|photo| tried.insert(photo.uid.clone())) + .take(SIMILAR_CHUNK) + .filter_map(|photo| parse_uid(&photo.uid)) + .collect(); + if chunk.is_empty() { + return Ok(()); + } + let paths: Vec<(String, PathBuf)> = self + .photo_thumbs_with(&chunk, false) + .into_iter() + .filter_map(|thumb| Some((thumb.uid, PathBuf::from(thumb.path?)))) + .collect(); + for (uid, hash) in similar_hashes(paths) { + self.db + .photo_set_similar_hash(&uid, hash) + .map_err(CoreError::from)?; + } + } + } + /// The towns the photos were taken in, as wire items. A town the bundled /// table no longer knows (the stored id came from an older table) is left /// out rather than shown without a name. @@ -961,6 +1080,13 @@ impl Core { /// milliseconds where the raw costs a full download and a raw decode. The /// tile still answers under the uid that was asked for. pub(crate) fn photo_thumbs(&self, requested: &[NodeUid]) -> Vec { + self.photo_thumbs_with(requested, true) + } + + /// [`Core::photo_thumbs`], with local generation only when `generate` is + /// set. Without it, a photo the server has no thumbnail for is left + /// without one rather than downloaded whole. + fn photo_thumbs_with(&self, requested: &[NodeUid], generate: bool) -> Vec { let ttype = ThumbnailType::Thumbnail.as_i32(); // Only a raw can be stood in for, and that is one batched read; the // per-shot group lookup is then paid for raws alone, not for a page of @@ -1061,7 +1187,7 @@ impl Core { // that never generated one. Off the request path: a full-size photo // takes far longer to fetch than the whole rest of the batch, and the // thumbnails that *are* ready must not wait behind it. - if !missing.is_empty() { + if generate && !missing.is_empty() { self.spawn_generate_thumbs(missing, &tags, &names, ThumbJob::Photos); } } @@ -1964,10 +2090,11 @@ mod thumb_tests { use super::{ RawTempFile, ScaleAttempt, THUMB_EDGE, UPLOAD_THUMB_MAX_BYTES, exiftool_binary, fit_upload_thumb, is_video_name, ratio_of, scale_thumbnail, scale_thumbnail_with_exiftool, - thumbnail_build_may_start, upload_thumbnails_with, video_thumbnail, + similar_hashes, thumbnail_build_may_start, upload_thumbnails_with, video_thumbnail, }; use pdfs_core::control::{ThumbnailBuildStatus, is_thumbnail_image_name}; use proton_drive_rs::ThumbnailType; + use std::collections::HashMap; use std::ffi::OsStr; use std::os::unix::fs::PermissionsExt as _; use std::time::Duration; @@ -2241,6 +2368,46 @@ mod thumb_tests { assert!(thumbnail_build_may_start(&idle, "pictures/second").unwrap()); } + #[test] + fn thumbnails_hash_alike_across_sizes_and_undecodable_ones_get_no_hash() { + let tmp = scratch("similar-hashes"); + std::fs::create_dir_all(&tmp).unwrap(); + // One picture at two sizes: a diagonal gradient with a bright corner. + let picture = |width: u32, height: u32| { + let image = image::GrayImage::from_fn(width, height, |x, y| { + let (fx, fy) = (x * 4 / width, y * 4 / height); + image::Luma([if fx + fy == 0 { + 250 + } else { + (fx * 40 + fy * 20) as u8 + }]) + }); + let mut bytes = Vec::new(); + image::codecs::jpeg::JpegEncoder::new(&mut bytes) + .encode_image(&image) + .unwrap(); + bytes + }; + std::fs::write(tmp.join("big"), picture(512, 384)).unwrap(); + std::fs::write(tmp.join("small"), picture(256, 192)).unwrap(); + std::fs::write(tmp.join("torn"), b"not a picture").unwrap(); + + let hashes: HashMap = similar_hashes( + ["big", "small", "torn", "gone"] + .iter() + .map(|name| (name.to_string(), tmp.join(name))) + .collect(), + ) + .into_iter() + .collect(); + let _ = std::fs::remove_dir_all(&tmp); + assert_eq!(hashes.len(), 4); + assert_ne!(hashes["big"], pdfs_core::similar::NO_HASH); + assert!(pdfs_core::similar::alike(hashes["big"], hashes["small"])); + assert_eq!(hashes["torn"], pdfs_core::similar::NO_HASH); + assert_eq!(hashes["gone"], pdfs_core::similar::NO_HASH); + } + #[test] fn an_uploaded_photo_carries_its_thumbnail() { let tmp = scratch("upload-photo"); diff --git a/crates/pdfs-gui/src/app/pages/duplicates.rs b/crates/pdfs-gui/src/app/pages/duplicates.rs index 75eca68..cb60e3e 100644 --- a/crates/pdfs-gui/src/app/pages/duplicates.rs +++ b/crates/pdfs-gui/src/app/pages/duplicates.rs @@ -6,14 +6,58 @@ const COPY_SIZE: i32 = 150; /// Each set's copies with their Keep toggles. type SetToggles = Vec>; +/// How often the dialog asks again while the daemon hashes thumbnails. +const HASHING_POLL: std::time::Duration = std::time::Duration::from_secs(1); + +/// What the finder looks for. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Finder { + /// Byte-identical copies, going by the server's content hash. + Exact, + /// Shots that look alike, going by their thumbnails. + Similar, +} + +/// The dialog's parts that a search fills in. +struct FinderView { + ui: Rc, + status: adw::StatusPage, + progress: gtk4::ProgressBar, + note: gtk4::Label, + stack: gtk4::Stack, + trash: gtk4::Button, + toolbar: adw::ToolbarView, + /// Bumped by every new search and when the dialog closes, so a reply or a + /// poll for an older one is dropped. + generation: Cell, + /// The Trash button's handler for the review on show. + clicked: RefCell>, +} + /// Open the duplicate finder: every set of photos stored more than once, side /// by side, with the copy worth keeping ticked. The copies left unticked go to -/// Trash — one file each, never the rest of their shot. +/// Trash — one file each, never the rest of their shot. The Similar switch +/// looks for shots that look alike instead. pub(crate) fn show_duplicates(ui: &Rc) { + let progress = gtk4::ProgressBar::builder() + .show_text(true) + .width_request(280) + .halign(gtk4::Align::Center) + .visible(false) + .build(); + let note = gtk4::Label::builder() + .wrap(true) + .justify(gtk4::Justification::Center) + .visible(false) + .build(); + note.add_css_class("dim-label"); + let status_extra = gtk4::Box::new(gtk4::Orientation::Vertical, 12); + status_extra.append(&progress); + status_extra.append(¬e); let status = adw::StatusPage::builder() .icon_name("edit-copy-symbolic") - .title(gettext("Looking for duplicates…")) .vexpand(true) + .child(&status_extra) .build(); status.add_css_class("compact"); @@ -21,7 +65,6 @@ pub(crate) fn show_duplicates(ui: &Rc) { stack.add_named(&status, Some("status")); let trash = gtk4::Button::builder() - .label(gettext("Move to Trash")) .halign(gtk4::Align::End) .sensitive(false) .build(); @@ -32,8 +75,25 @@ pub(crate) fn show_duplicates(ui: &Rc) { bottom.set_halign(gtk4::Align::End); bottom.append(&trash); + let exact = gtk4::ToggleButton::builder() + .label(pgettext("duplicates", "Exact")) + .tooltip_text(gettext("Photos stored more than once")) + .active(true) + .build(); + let similar = gtk4::ToggleButton::builder() + .label(pgettext("duplicates", "Similar")) + .tooltip_text(gettext("Photos that look alike")) + .group(&exact) + .build(); + let switch = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); + switch.add_css_class("linked"); + switch.append(&exact); + switch.append(&similar); + let header = adw::HeaderBar::new(); + header.set_title_widget(Some(&switch)); + let toolbar = adw::ToolbarView::new(); - toolbar.add_top_bar(&adw::HeaderBar::new()); + toolbar.add_top_bar(&header); toolbar.set_content(Some(&stack)); toolbar.add_bottom_bar(&bottom); toolbar.set_reveal_bottom_bars(false); @@ -46,47 +106,189 @@ pub(crate) fn show_duplicates(ui: &Rc) { .build(); dialog.present(ui_window(ui).as_ref()); - let rx = spawn_request(ui.dirs.control_socket(), Request::PhotoDuplicates); - let ui = ui.clone(); + let view = Rc::new(FinderView { + ui: ui.clone(), + status, + progress, + note, + stack, + trash, + toolbar, + generation: Cell::new(0), + clicked: RefCell::new(None), + }); + let view_closed = view.clone(); + dialog.connect_closed(move |_| { + view_closed + .generation + .set(view_closed.generation.get().wrapping_add(1)); + }); + let view_switch = view.clone(); + similar.connect_toggled(move |similar| { + let finder = if similar.is_active() { + Finder::Similar + } else { + Finder::Exact + }; + search(&view_switch, finder); + }); + search(&view, Finder::Exact); +} + +/// Drop what the dialog shows and look again, for `finder`. +fn search(view: &Rc, finder: Finder) { + let generation = view.generation.get().wrapping_add(1); + view.generation.set(generation); + if let Some(review) = view.stack.child_by_name("review") { + view.stack.remove(&review); + } + if let Some(handler) = view.clicked.take() { + view.trash.disconnect(handler); + } + view.toolbar.set_reveal_bottom_bars(false); + view.status.set_icon_name(Some("edit-copy-symbolic")); + view.status.set_title(&match finder { + Finder::Exact => gettext("Looking for duplicates…"), + Finder::Similar => gettext("Looking for similar photos…"), + }); + view.status.set_description(None); + view.progress.set_visible(false); + view.note.set_visible(false); + view.stack.set_visible_child_name("status"); + ask(view, finder, generation); +} + +/// Ask the daemon for `finder`'s sets and show them, or, while it is still +/// hashing thumbnails, how far it has got — and ask again in a moment. +fn ask(view: &Rc, finder: Finder, generation: u32) { + let request = match finder { + Finder::Exact => Request::PhotoDuplicates, + Finder::Similar => Request::PhotoSimilar, + }; + let rx = spawn_request(view.ui.dirs.control_socket(), request); + let view = view.clone(); glib::spawn_future_local(async move { - let sets = match rx.recv().await { - Ok(Ok(Response::PhotoDuplicates { sets })) => sets, + let reply = rx.recv().await; + if view.generation.get() != generation { + return; + } + let failed = match finder { + Finder::Exact => gettext("Couldn't look for duplicates"), + Finder::Similar => gettext("Couldn't look for similar photos"), + }; + let (sets, uncompared) = match reply { + Ok(Ok(Response::PhotoDuplicates { sets })) => (sets, 0), + Ok(Ok(Response::PhotoSimilar { + hashing: Some(MappingProgress { done, total }), + .. + })) => { + view.status.set_title(&gettext("Comparing photos…")); + view.status.set_description(Some(&gettext( + "Photos are compared by their thumbnails. The first time takes a while.", + ))); + view.progress.set_fraction(if total == 0 { + 0.0 + } else { + done as f64 / total as f64 + }); + view.progress.set_text(Some(&gettext_f( + // Translators: {done} and {total} are numbers of photos. + "{done} of {total}", + &[("done", &thousands(done)), ("total", &thousands(total))], + ))); + view.progress.set_visible(true); + glib::timeout_add_local_once(HASHING_POLL, move || { + if view.generation.get() == generation { + ask(&view, finder, generation); + } + }); + return; + } + Ok(Ok(Response::PhotoSimilar { + sets, + hashing: None, + uncompared, + })) => (sets, uncompared), Ok(Ok(Response::Error { message, .. })) => { - status.set_icon_name(Some("dialog-warning-symbolic")); - status.set_title(&gettext("Couldn't look for duplicates")); - status.set_description(Some(&message)); + view.status.set_icon_name(Some("dialog-warning-symbolic")); + view.status.set_title(&failed); + view.status.set_description(Some(&message)); + view.progress.set_visible(false); return; } _ => { - status.set_icon_name(Some("dialog-warning-symbolic")); - status.set_title(&gettext("Couldn't look for duplicates")); - status.set_description(Some(&gettext("The mount service didn't respond."))); + view.status.set_icon_name(Some("dialog-warning-symbolic")); + view.status.set_title(&failed); + view.status + .set_description(Some(&gettext("The mount service didn't respond."))); + view.progress.set_visible(false); return; } }; + view.progress.set_visible(false); if sets.is_empty() { - status.set_icon_name(Some("emblem-ok-symbolic")); - status.set_title(&gettext("No duplicates")); - status.set_description(Some(&gettext( - "Every photo in your library is stored once.", - ))); + view.status.set_icon_name(Some("emblem-ok-symbolic")); + match finder { + Finder::Exact => { + view.status.set_title(&gettext("No duplicates")); + view.status.set_description(Some(&gettext( + "Every photo in your library is stored once.", + ))); + } + Finder::Similar => { + view.status.set_title(&gettext("No similar photos")); + view.status.set_description(Some(&gettext( + "No two photos in your library look alike.", + ))); + } + } + if uncompared > 0 { + view.note.set_label(&uncompared_note(uncompared)); + view.note.set_visible(true); + } return; } - let review = duplicate_review(&ui, sets, &trash); - stack.add_named(&review, Some("review")); - stack.set_visible_child_name("review"); - toolbar.set_reveal_bottom_bars(true); - schedule_thumbs(&ui); + let (review, handler) = duplicate_review(&view.ui, sets, &view.trash, finder, uncompared); + view.clicked.replace(Some(handler)); + view.stack.add_named(&review, Some("review")); + view.stack.set_visible_child_name("review"); + view.toolbar.set_reveal_bottom_bars(true); + schedule_thumbs(&view.ui); }); } -/// The review list: one section per set, each copy a thumbnail with a Keep -/// toggle. Wires `trash` to move every copy not kept to Trash. -fn duplicate_review(ui: &Rc, sets: Vec>, trash: >k4::Button) -> gtk4::Widget { +/// Says how many photos the similar finder left out for want of a thumbnail. +fn uncompared_note(n: usize) -> String { + ngettext_f( + "{n} photo couldn't be compared because it has no thumbnail yet.", + "{n} photos couldn't be compared because they have no thumbnail yet.", + n as u64, + &[], + ) +} + +/// The review list: one section per set, each photo a thumbnail with a Keep +/// toggle. Wires `trash` to move every photo not kept to Trash, and returns +/// that handler so a new search can take it off again. +/// +/// Exact copies start with only the first of each set kept. Similar photos +/// all start kept: photos that look alike can still be different shots. +fn duplicate_review( + ui: &Rc, + sets: Vec>, + trash: >k4::Button, + finder: Finder, + uncompared: usize, +) -> (gtk4::Widget, glib::SignalHandlerId) { let intro = gtk4::Label::builder() - .label(gettext( - "These photos are stored more than once. The copies you don't keep move to Trash, where you can restore them.", - )) + .label(match finder { + Finder::Exact => gettext( + "These photos are stored more than once. The copies you don't keep move to Trash, where you can restore them.", + ), + Finder::Similar => gettext( + "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them.", + ), + }) .wrap(true) .xalign(0.0) .build(); @@ -98,6 +300,15 @@ fn duplicate_review(ui: &Rc, sets: Vec>, trash: >k4::Button content.set_margin_start(18); content.set_margin_end(18); content.append(&intro); + if uncompared > 0 { + let note = gtk4::Label::builder() + .label(uncompared_note(uncompared)) + .wrap(true) + .xalign(0.0) + .build(); + note.add_css_class("dim-label"); + content.append(¬e); + } // Every copy's toggle, by set, so the button can count what goes. let toggles: Rc> = Rc::new(RefCell::new(Vec::new())); @@ -106,19 +317,28 @@ fn duplicate_review(ui: &Rc, sets: Vec>, trash: >k4::Button continue; }; let date = section_heading(first.capture_time, Grouping::Day); - // Translators: heading of one set in the duplicates dialog; {date} is the day the photo was taken. - let title = ngettext_f( - "{date}, {n} copy", - "{date}, {n} copies", - set.len() as u64, - &[("date", &date)], - ); + let title = match finder { + // Translators: heading of one set in the duplicates dialog; {date} is the day the photo was taken. + Finder::Exact => ngettext_f( + "{date}, {n} copy", + "{date}, {n} copies", + set.len() as u64, + &[("date", &date)], + ), + // Translators: heading of one set of photos that look alike; {date} is the day the first was taken. + Finder::Similar => ngettext_f( + "{date}, {n} photo", + "{date}, {n} photos", + set.len() as u64, + &[("date", &date)], + ), + }; let heading = gtk4::Label::builder().label(title).xalign(0.0).build(); heading.add_css_class("heading"); let row = gtk4::Box::new(gtk4::Orientation::Horizontal, 12); let mut set_toggles = Vec::new(); for (index, photo) in set.into_iter().enumerate() { - let (card, keep) = copy_card(ui, &photo, index == 0); + let (card, keep) = copy_card(ui, &photo, index == 0 || finder == Finder::Similar); row.append(&card); set_toggles.push((photo, keep)); } @@ -139,12 +359,20 @@ fn duplicate_review(ui: &Rc, sets: Vec>, trash: >k4::Button let trash = trash.clone(); move || { let n = doomed(&toggles.borrow()).len(); - trash.set_label(&ngettext_f( - "Move {n} copy to Trash", - "Move {n} copies to Trash", - n as u64, - &[], - )); + trash.set_label(&match finder { + Finder::Exact => ngettext_f( + "Move {n} copy to Trash", + "Move {n} copies to Trash", + n as u64, + &[], + ), + Finder::Similar => ngettext_f( + "Move {n} photo to Trash", + "Move {n} photos to Trash", + n as u64, + &[], + ), + }); trash.set_sensitive(n > 0); } }; @@ -171,12 +399,14 @@ fn duplicate_review(ui: &Rc, sets: Vec>, trash: >k4::Button } let ui_trash = ui.clone(); - trash.connect_clicked(move |button| { + let handler = trash.connect_clicked(move |button| { let uids = doomed(&toggles.borrow()); if uids.is_empty() { return; } - trash_photos(&ui_trash, uids, true); + // An exact copy is one file of its shot; a similar photo stands for + // its whole shot, raw and all. + trash_photos(&ui_trash, uids, finder == Finder::Exact); if let Some(dialog) = button .ancestor(adw::Dialog::static_type()) .and_downcast::() @@ -190,7 +420,7 @@ fn duplicate_review(ui: &Rc, sets: Vec>, trash: >k4::Button .vexpand(true) .child(&content) .build(); - scroll.upcast() + (scroll.upcast(), handler) } /// The uids of every copy not kept. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d994029..e23cb5a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,8 +11,16 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). ## [Unreleased] -Schema: **34**. Photos gain a `similar_hash` column for the coming similar-photo finder. It -stays empty until the daemon hashes thumbnails, so updating reads and downloads nothing. +Schema: **34**. Photos gain a `similar_hash` column for the similar-photo finder. It stays +empty until the finder is first opened, so updating reads and downloads nothing. + +### Added +- **Similar photos.** The duplicate finder gains a Similar switch that also finds resized or + re-encoded copies and shots of one moment, by comparing the thumbnails. The first search + hashes every thumbnail the server has, with a progress bar; photos without one are not + downloaded and are counted as not compared. Every photo in a set starts ticked Keep, since + photos that look alike can be different shots, and a photo not kept goes to Trash with the + rest of its shot. ## [2.3.0] — 2026-09-28 diff --git a/po/de.po b/po/de.po index 8353078..b8f5b79 100644 --- a/po/de.po +++ b/po/de.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -315,7 +315,6 @@ msgstr "Umbenennen" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -767,7 +766,7 @@ msgstr "Das Album konnte nicht erstellt werden" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1908,28 +1907,82 @@ msgstr "Identität übernommen" msgid "Couldn't adopt that computer" msgstr "Der Computer konnte nicht übernommen werden" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Duplikate werden gesucht …" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Exakt" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Mehrfach gespeicherte Fotos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Ähnlich" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Fotos, die sich ähneln" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplikate" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Duplikate werden gesucht …" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Ähnliche Fotos werden gesucht …" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Duplikate konnten nicht gesucht werden" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Ähnliche Fotos konnten nicht gesucht werden" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Fotos werden verglichen …" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Fotos werden anhand ihrer Vorschaubilder verglichen. Beim ersten Mal dauert das eine Weile." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} von {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "Keine Duplikate" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Jedes Foto in deiner Mediathek ist nur einmal gespeichert." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "Keine ähnlichen Fotos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "Keine zwei Fotos in deiner Mediathek sehen sich ähnlich." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} Foto konnte nicht verglichen werden, weil es noch kein Vorschaubild hat." +msgstr[1] "{n} Fotos konnten nicht verglichen werden, weil sie noch kein Vorschaubild haben." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1937,21 +1990,40 @@ msgstr "" "Diese Fotos sind mehrfach gespeichert. Die Kopien, die du nicht behältst, " "werden in den Papierkorb verschoben, wo du sie wiederherstellen kannst." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Diese Fotos sehen sich ähnlich. Es können Kopien eines Fotos oder verschiedene Aufnahmen eines Moments sein, daher werden alle behalten, bis du sie abwählst. Die Fotos, die du nicht behältst, werden in den Papierkorb verschoben, wo du sie wiederherstellen kannst." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} Kopie" msgstr[1] "{date}, {n} Kopien" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} Foto" +msgstr[1] "{date}, {n} Fotos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "{n} Kopie in den Papierkorb verschieben" msgstr[1] "{n} Kopien in den Papierkorb verschieben" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "{n} Foto in den Papierkorb verschieben" +msgstr[1] "{n} Fotos in den Papierkorb verschieben" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Behalten" @@ -3008,13 +3080,6 @@ msgid_plural "{n} years ago" msgstr[0] "vor {n} Jahr" msgstr[1] "vor {n} Jahren" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} Foto" -msgstr[1] "{date}, {n} Fotos" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/en_GB.po b/po/en_GB.po index 1b78b4c..ef0b9e2 100644 --- a/po/en_GB.po +++ b/po/en_GB.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: English (United Kingdom)\n" @@ -311,7 +311,6 @@ msgstr "Rename" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -759,7 +758,7 @@ msgstr "Couldn't create the album" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1883,28 +1882,82 @@ msgstr "Identity adopted" msgid "Couldn't adopt that computer" msgstr "Couldn't adopt that computer" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Looking for duplicates…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Exact" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Photos stored more than once" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Similar" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Photos that look alike" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplicates" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Looking for duplicates…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Looking for similar photos…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Couldn't look for duplicates" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Couldn't look for similar photos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Comparing photos…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Photos are compared by their thumbnails. The first time takes a while." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} of {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "No duplicates" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Every photo in your library is stored once." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "No similar photos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "No two photos in your library look alike." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} photo couldn't be compared because it has no thumbnail yet." +msgstr[1] "{n} photos couldn't be compared because they have no thumbnail yet." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1912,21 +1965,40 @@ msgstr "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} copy" msgstr[1] "{date}, {n} copies" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} photo" +msgstr[1] "{date}, {n} photos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "Move {n} copy to Trash" msgstr[1] "Move {n} copies to Trash" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "Move {n} photo to Trash" +msgstr[1] "Move {n} photos to Trash" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Keep" @@ -2962,13 +3034,6 @@ msgid_plural "{n} years ago" msgstr[0] "{n} year ago" msgstr[1] "{n} years ago" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} photo" -msgstr[1] "{date}, {n} photos" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/es.po b/po/es.po index 2f9b753..a1ef0a7 100644 --- a/po/es.po +++ b/po/es.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Spanish\n" @@ -312,7 +312,6 @@ msgstr "Renombrar" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -763,7 +762,7 @@ msgstr "No se pudo crear el álbum" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1899,28 +1898,82 @@ msgstr "Identidad adoptada" msgid "Couldn't adopt that computer" msgstr "No se pudo adoptar ese equipo" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Buscando duplicados…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Exactas" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Fotos guardadas más de una vez" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Similares" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Fotos que se parecen" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplicados" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Buscando duplicados…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Buscando fotos similares…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "No se pudieron buscar duplicados" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "No se pudieron buscar fotos similares" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Comparando fotos…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Las fotos se comparan por sus miniaturas. La primera vez tarda un rato." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} de {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "No hay duplicados" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Cada foto de su biblioteca está guardada una sola vez." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "No hay fotos similares" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "No hay dos fotos de su biblioteca que se parezcan." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} foto no se pudo comparar porque aún no tiene miniatura." +msgstr[1] "{n} fotos no se pudieron comparar porque aún no tienen miniatura." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1928,21 +1981,40 @@ msgstr "" "Estas fotos están guardadas más de una vez. Las copias que no conserve se " "moverán a la papelera, donde podrá restaurarlas." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Estas fotos se parecen. Pueden ser copias de una misma foto o tomas distintas de un mismo momento, así que se conservan todas hasta que las desmarque. Las fotos que no conserve se moverán a la papelera, donde podrá restaurarlas." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} copia" msgstr[1] "{date}, {n} copias" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} foto" +msgstr[1] "{date}, {n} fotos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "Mover {n} copia a la papelera" msgstr[1] "Mover {n} copias a la papelera" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "Mover {n} foto a la papelera" +msgstr[1] "Mover {n} fotos a la papelera" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Conservar" @@ -2994,13 +3066,6 @@ msgid_plural "{n} years ago" msgstr[0] "hace {n} año" msgstr[1] "hace {n} años" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} foto" -msgstr[1] "{date}, {n} fotos" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/fr.po b/po/fr.po index 333e2da..50d6932 100644 --- a/po/fr.po +++ b/po/fr.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: French\n" @@ -315,7 +315,6 @@ msgstr "Renommer" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -767,7 +766,7 @@ msgstr "Impossible de créer l’album" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1906,28 +1905,82 @@ msgstr "Identité adoptée" msgid "Couldn't adopt that computer" msgstr "Impossible d’adopter cet ordinateur" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Recherche des doublons…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Exacts" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Photos stockées plusieurs fois" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Similaires" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Photos qui se ressemblent" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Doublons" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Recherche des doublons…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Recherche des photos similaires…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Impossible de rechercher les doublons" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Impossible de rechercher les photos similaires" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Comparaison des photos…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Les photos sont comparées d’après leurs miniatures. La première fois prend un moment." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} sur {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "Aucun doublon" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Chaque photo de votre bibliothèque n’est stockée qu’une fois." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "Aucune photo similaire" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "Aucune photo de votre bibliothèque ne ressemble à une autre." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} photo n’a pas pu être comparée, car elle n’a pas encore de miniature." +msgstr[1] "{n} photos n’ont pas pu être comparées, car elles n’ont pas encore de miniature." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1935,21 +1988,40 @@ msgstr "" "Ces photos sont stockées plusieurs fois. Les copies que vous ne gardez pas " "sont mises à la corbeille, d’où vous pouvez les restaurer." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Ces photos se ressemblent. Il peut s’agir de copies d’une même photo ou de prises différentes d’un même moment : elles sont donc toutes gardées tant que vous ne les décochez pas. Les photos que vous ne gardez pas sont mises à la corbeille, d’où vous pouvez les restaurer." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} copie" msgstr[1] "{date}, {n} copies" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} photo" +msgstr[1] "{date}, {n} photos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "Mettre {n} copie à la corbeille" msgstr[1] "Mettre {n} copies à la corbeille" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "Mettre {n} photo à la corbeille" +msgstr[1] "Mettre {n} photos à la corbeille" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Garder" @@ -3009,13 +3081,6 @@ msgid_plural "{n} years ago" msgstr[0] "il y a {n} an" msgstr[1] "il y a {n} ans" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} photo" -msgstr[1] "{date}, {n} photos" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/it.po b/po/it.po index 5a3928e..eed9d4b 100644 --- a/po/it.po +++ b/po/it.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Italian\n" @@ -311,7 +311,6 @@ msgstr "Rinomina" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -763,7 +762,7 @@ msgstr "Impossibile creare l'album" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1904,28 +1903,82 @@ msgstr "Identità adottata" msgid "Couldn't adopt that computer" msgstr "Impossibile adottare quel computer" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Ricerca dei duplicati…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Esatti" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Foto salvate più di una volta" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Simili" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Foto che si somigliano" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplicati" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Ricerca dei duplicati…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Ricerca delle foto simili…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Impossibile cercare i duplicati" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Impossibile cercare le foto simili" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Confronto delle foto…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Le foto vengono confrontate in base alle miniature. La prima volta richiede un po’ di tempo." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} di {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "Nessun duplicato" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Ogni foto della libreria è salvata una sola volta." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "Nessuna foto simile" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "Nella libreria non ci sono due foto che si somigliano." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} foto non è stata confrontata perché non ha ancora una miniatura." +msgstr[1] "{n} foto non sono state confrontate perché non hanno ancora una miniatura." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1933,21 +1986,40 @@ msgstr "" "Queste foto sono salvate più di una volta. Le copie che non tieni vengono " "spostate nel cestino, da dove puoi ripristinarle." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Queste foto si somigliano. Possono essere copie di una stessa foto o scatti diversi di uno stesso momento, quindi vengono tenute tutte finché non le deselezioni. Le foto che non tieni vengono spostate nel cestino, da dove puoi ripristinarle." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} copia" msgstr[1] "{date}, {n} copie" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} foto" +msgstr[1] "{date}, {n} foto" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "Sposta {n} copia nel cestino" msgstr[1] "Sposta {n} copie nel cestino" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "Sposta {n} foto nel cestino" +msgstr[1] "Sposta {n} foto nel cestino" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Tieni" @@ -2998,13 +3070,6 @@ msgid_plural "{n} years ago" msgstr[0] "{n} anno fa" msgstr[1] "{n} anni fa" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} foto" -msgstr[1] "{date}, {n} foto" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/nl.po b/po/nl.po index 49ecd41..82a9dab 100644 --- a/po/nl.po +++ b/po/nl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Dutch\n" @@ -312,7 +312,6 @@ msgstr "Hernoemen" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -764,7 +763,7 @@ msgstr "Kon het album niet aanmaken" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1902,28 +1901,82 @@ msgstr "Identiteit overgenomen" msgid "Couldn't adopt that computer" msgstr "Kon die computer niet overnemen" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Duplicaten zoeken…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Exact" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Foto's die meer dan eens zijn opgeslagen" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Vergelijkbaar" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Foto's die op elkaar lijken" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplicaten" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Duplicaten zoeken…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Vergelijkbare foto's zoeken…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Kon niet naar duplicaten zoeken" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Kon niet naar vergelijkbare foto's zoeken" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Foto's vergelijken…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Foto's worden vergeleken aan de hand van hun miniaturen. De eerste keer duurt dat even." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} van {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "Geen duplicaten" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Elke foto in je bibliotheek is maar één keer opgeslagen." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "Geen vergelijkbare foto's" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "Er zijn geen twee foto's in je bibliotheek die op elkaar lijken." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} foto kon niet worden vergeleken omdat die nog geen miniatuur heeft." +msgstr[1] "{n} foto's konden niet worden vergeleken omdat ze nog geen miniatuur hebben." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1931,21 +1984,40 @@ msgstr "" "Deze foto's zijn meer dan eens opgeslagen. De kopieën die je niet houdt, " "gaan naar de prullenbak, waar je ze kunt terugzetten." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Deze foto's lijken op elkaar. Het kunnen kopieën van één foto zijn of verschillende opnamen van één moment, dus ze worden allemaal gehouden tot je ze uitvinkt. De foto's die je niet houdt, gaan naar de prullenbak, waar je ze kunt terugzetten." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} kopie" msgstr[1] "{date}, {n} kopieën" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} foto" +msgstr[1] "{date}, {n} foto's" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "{n} kopie naar prullenbak verplaatsen" msgstr[1] "{n} kopieën naar prullenbak verplaatsen" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "{n} foto naar prullenbak verplaatsen" +msgstr[1] "{n} foto's naar prullenbak verplaatsen" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Houden" @@ -2998,13 +3070,6 @@ msgid_plural "{n} years ago" msgstr[0] "{n} jaar geleden" msgstr[1] "{n} jaar geleden" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} foto" -msgstr[1] "{date}, {n} foto's" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/pdfs.pot b/po/pdfs.pot index 6388d47..3243c17 100644 --- a/po/pdfs.pot +++ b/po/pdfs.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.3.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -288,7 +288,6 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -734,7 +733,7 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1832,48 +1831,125 @@ msgstr "" msgid "Couldn't adopt that computer" msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "" + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "" +"{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "" +msgstr[1] "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." msgstr "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "" +"These photos look alike. They can be copies of one photo or different shots " +"of one moment, so all of them are kept until you untick them. The photos you " +"don't keep move to Trash, where you can restore them." +msgstr "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "" +msgstr[1] "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "" +msgstr[1] "" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "" @@ -2879,13 +2955,6 @@ msgid_plural "{n} years ago" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "" -msgstr[1] "" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/pl.po b/po/pl.po index 5a1df9d..b7d8f5c 100644 --- a/po/pl.po +++ b/po/pl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Polish\n" @@ -314,7 +314,6 @@ msgstr "Zmień nazwę" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -771,7 +770,7 @@ msgstr "Nie można utworzyć albumu" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1930,28 +1929,83 @@ msgstr "Przejęto tożsamość" msgid "Couldn't adopt that computer" msgstr "Nie można przejąć tożsamości tego komputera" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Wyszukiwanie duplikatów…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Dokładne" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Zdjęcia zapisane więcej niż raz" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Podobne" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Zdjęcia, które wyglądają podobnie" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplikaty" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Wyszukiwanie duplikatów…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Wyszukiwanie podobnych zdjęć…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Nie można wyszukać duplikatów" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Nie można wyszukać podobnych zdjęć" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Porównywanie zdjęć…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "Zdjęcia są porównywane na podstawie miniatur. Za pierwszym razem trwa to chwilę." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} z {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "Brak duplikatów" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Każde zdjęcie w bibliotece jest zapisane tylko raz." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "Brak podobnych zdjęć" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "W bibliotece nie ma dwóch podobnych zdjęć." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "Nie udało się porównać {n} zdjęcia, bo nie ma jeszcze miniatury." +msgstr[1] "Nie udało się porównać {n} zdjęć, bo nie mają jeszcze miniatur." +msgstr[2] "Nie udało się porównać {n} zdjęć, bo nie mają jeszcze miniatur." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1959,7 +2013,11 @@ msgstr "" "Te zdjęcia są zapisane więcej niż raz. Kopie, których nie zachowasz, trafią " "do kosza, skąd można je przywrócić." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Te zdjęcia wyglądają podobnie. Mogą to być kopie jednego zdjęcia albo różne ujęcia jednej chwili, dlatego wszystkie są zachowywane, dopóki ich nie odznaczysz. Zdjęcia, których nie zachowasz, trafią do kosza, skąd można je przywrócić." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" @@ -1967,7 +2025,16 @@ msgstr[0] "{date}, {n} kopia" msgstr[1] "{date}, {n} kopie" msgstr[2] "{date}, {n} kopii" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} zdjęcie" +msgstr[1] "{date}, {n} zdjęcia" +msgstr[2] "{date}, {n} zdjęć" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" @@ -1975,7 +2042,15 @@ msgstr[0] "Przenieś {n} kopię do kosza" msgstr[1] "Przenieś {n} kopie do kosza" msgstr[2] "Przenieś {n} kopii do kosza" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "Przenieś {n} zdjęcie do kosza" +msgstr[1] "Przenieś {n} zdjęcia do kosza" +msgstr[2] "Przenieś {n} zdjęć do kosza" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Zachowaj" @@ -3034,14 +3109,6 @@ msgstr[0] "{n} rok temu" msgstr[1] "{n} lata temu" msgstr[2] "{n} lat temu" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} zdjęcie" -msgstr[1] "{date}, {n} zdjęcia" -msgstr[2] "{date}, {n} zdjęć" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state" diff --git a/po/pt_BR.po b/po/pt_BR.po index dc4f24a..02541d4 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-09-28 03:11+0200\n" +"POT-Creation-Date: 2026-09-28 03:33+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Portuguese (Brazil)\n" @@ -311,7 +311,6 @@ msgstr "Renomear" #: crates/pdfs-gui/src/app/pages/browser.rs:414 #: crates/pdfs-gui/src/app/pages/browser.rs:1306 #: crates/pdfs-gui/src/app/pages/browser.rs:1338 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:24 #: crates/pdfs-gui/src/app/pages/photos.rs:607 #: crates/pdfs-gui/src/app/pages/photos.rs:2063 #: crates/pdfs-gui/src/app/pages/photos.rs:2067 @@ -763,7 +762,7 @@ msgstr "Não foi possível criar o álbum" #: crates/pdfs-gui/src/app/pages/devices.rs:532 #: crates/pdfs-gui/src/app/pages/devices.rs:682 #: crates/pdfs-gui/src/app/pages/devices.rs:912 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:63 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:223 #: crates/pdfs-gui/src/app/pages/locations.rs:342 #: crates/pdfs-gui/src/app/pages/locations.rs:516 #: crates/pdfs-gui/src/app/pages/locations.rs:739 @@ -1894,28 +1893,82 @@ msgstr "Identidade adotada" msgid "Couldn't adopt that computer" msgstr "Não foi possível adotar aquele computador" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:15 -msgid "Looking for duplicates…" -msgstr "Procurando duplicatas…" +#: crates/pdfs-gui/src/app/pages/duplicates.rs:79 +msgctxt "duplicates" +msgid "Exact" +msgstr "Exatas" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:42 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:80 +msgid "Photos stored more than once" +msgstr "Fotos armazenadas mais de uma vez" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:84 +msgctxt "duplicates" +msgid "Similar" +msgstr "Semelhantes" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:85 +msgid "Photos that look alike" +msgstr "Fotos parecidas" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:102 msgid "Duplicates" msgstr "Duplicatas" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:56 -#: crates/pdfs-gui/src/app/pages/duplicates.rs:62 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:151 +msgid "Looking for duplicates…" +msgstr "Procurando duplicatas…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:152 +msgid "Looking for similar photos…" +msgstr "Procurando fotos semelhantes…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:176 msgid "Couldn't look for duplicates" msgstr "Não foi possível procurar duplicatas" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:69 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:177 +msgid "Couldn't look for similar photos" +msgstr "Não foi possível procurar fotos semelhantes" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:185 +msgid "Comparing photos…" +msgstr "Comparando fotos…" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:187 +msgid "Photos are compared by their thumbnails. The first time takes a while." +msgstr "As fotos são comparadas pelas miniaturas. Na primeira vez, isso demora um pouco." + +#. Translators: {done} and {total} are numbers of photos. +#: crates/pdfs-gui/src/app/pages/duplicates.rs:196 +#, rust-format +msgid "{done} of {total}" +msgstr "{done} de {total}" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:233 msgid "No duplicates" msgstr "Nenhuma duplicata" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:71 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 msgid "Every photo in your library is stored once." msgstr "Cada foto da sua biblioteca está armazenada uma única vez." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:88 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:239 +msgid "No similar photos" +msgstr "Nenhuma foto semelhante" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:241 +msgid "No two photos in your library look alike." +msgstr "Não há duas fotos parecidas na sua biblioteca." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:263 +#, rust-format +msgid "{n} photo couldn't be compared because it has no thumbnail yet." +msgid_plural "{n} photos couldn't be compared because they have no thumbnail yet." +msgstr[0] "{n} foto não pôde ser comparada porque ainda não tem miniatura." +msgstr[1] "{n} fotos não puderam ser comparadas porque ainda não têm miniatura." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:286 msgid "" "These photos are stored more than once. The copies you don't keep move to " "Trash, where you can restore them." @@ -1923,21 +1976,40 @@ msgstr "" "Estas fotos estão armazenadas mais de uma vez. As cópias que você não " "mantiver vão para a lixeira, de onde podem ser restauradas." -#: crates/pdfs-gui/src/app/pages/duplicates.rs:111 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:289 +msgid "These photos look alike. They can be copies of one photo or different shots of one moment, so all of them are kept until you untick them. The photos you don't keep move to Trash, where you can restore them." +msgstr "Estas fotos são parecidas. Podem ser cópias de uma mesma foto ou cliques diferentes de um mesmo momento, por isso todas são mantidas até você desmarcá-las. As fotos que você não mantiver vão para a lixeira, de onde podem ser restauradas." + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:323 #, rust-format msgid "{date}, {n} copy" msgid_plural "{date}, {n} copies" msgstr[0] "{date}, {n} cópia" msgstr[1] "{date}, {n} cópias" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:143 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:330 +#: crates/pdfs-gui/src/app/pages/photos.rs:2994 +#, rust-format +msgid "{date}, {n} photo" +msgid_plural "{date}, {n} photos" +msgstr[0] "{date}, {n} foto" +msgstr[1] "{date}, {n} fotos" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:364 #, rust-format msgid "Move {n} copy to Trash" msgid_plural "Move {n} copies to Trash" msgstr[0] "Mover {n} cópia para a lixeira" msgstr[1] "Mover {n} cópias para a lixeira" -#: crates/pdfs-gui/src/app/pages/duplicates.rs:235 +#: crates/pdfs-gui/src/app/pages/duplicates.rs:370 +#, rust-format +msgid "Move {n} photo to Trash" +msgid_plural "Move {n} photos to Trash" +msgstr[0] "Mover {n} foto para a lixeira" +msgstr[1] "Mover {n} fotos para a lixeira" + +#: crates/pdfs-gui/src/app/pages/duplicates.rs:465 msgid "Keep" msgstr "Manter" @@ -2986,13 +3058,6 @@ msgid_plural "{n} years ago" msgstr[0] "há {n} ano" msgstr[1] "há {n} anos" -#: crates/pdfs-gui/src/app/pages/photos.rs:2994 -#, rust-format -msgid "{date}, {n} photo" -msgid_plural "{date}, {n} photos" -msgstr[0] "{date}, {n} foto" -msgstr[1] "{date}, {n} fotos" - #. Translators: a check item in a photo's menu, on while the photo is a favorite. #: crates/pdfs-gui/src/app/pages/photos.rs:3231 msgctxt "state"