Merge PR #690
# Conflicts: # crates/ui-egui/locales/de.json # crates/ui-egui/locales/es.json # crates/ui-egui/locales/fr.json # crates/ui-egui/locales/ja.json # crates/ui-egui/locales/pt-br.json # crates/ui-egui/locales/ru.json # crates/ui-egui/locales/uk.json # crates/ui-egui/locales/zh-hans.json # crates/ui-egui/locales/zh-hant.json # crates/ui-egui/src/tests_panels.rs # docs/parity-checklist.md
This commit is contained in:
commit
40492d238e
|
|
@ -4,7 +4,7 @@
|
|||
use lightcraft_catalog::{Album, AlbumId, ColorLabel, CopyrightStatus, Filter, Flag, GroupBy, Op, PhotoId, Sort, SortKey};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, bool_or, cmd, has_active, has_selection, ok, str_param};
|
||||
use super::{CommandSpec, always, bad, bool_or, cmd, has_active, has_library_selection, has_selection, ok, str_param};
|
||||
use crate::{LibrarySource, Result, Selection, Session};
|
||||
|
||||
fn select_after_deletion(s: &mut Session, before: &[PhotoId]) {
|
||||
|
|
@ -149,6 +149,24 @@ fn ids_param(p: &Value) -> Option<Vec<PhotoId>> {
|
|||
p.get("ids").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(PhotoId).collect())
|
||||
}
|
||||
|
||||
/// The targets an album can hold and how many were browsed (Local) photos, which album views
|
||||
/// never show (issue #684): those are skipped, and reported, instead of becoming invisible members.
|
||||
fn album_targets(s: &Session, targets: Vec<PhotoId>) -> (Vec<PhotoId>, usize) {
|
||||
let before = targets.len();
|
||||
let kept: Vec<PhotoId> = targets.into_iter().filter(|id| s.catalog.photo(*id).is_none_or(|p| !p.local)).collect();
|
||||
let skipped = before - kept.len();
|
||||
(kept, skipped)
|
||||
}
|
||||
|
||||
/// Album targets, or the error for a selection that was nothing but Local photos.
|
||||
fn album_targets_or_err(s: &Session, command: &str, targets: Vec<PhotoId>) -> Result<(Vec<PhotoId>, usize)> {
|
||||
let (kept, skipped) = album_targets(s, targets);
|
||||
if kept.is_empty() && skipped > 0 {
|
||||
return Err(bad(command, super::LOCAL_ONLY));
|
||||
}
|
||||
Ok((kept, skipped))
|
||||
}
|
||||
|
||||
/// Apply one op per target as a single undo step.
|
||||
fn for_targets(s: &mut Session, p: &Value, label: &str, f: impl Fn(PhotoId) -> Option<Op>) -> Result<Value> {
|
||||
let targets = s.targets(p);
|
||||
|
|
@ -866,22 +884,31 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
}
|
||||
),
|
||||
// ---- albums
|
||||
cmd!("album.create", "New Album", ["File"], None, "{name, parent?: folderId, folder?: bool, addSelected?: bool}", always, |s, p| {
|
||||
let name = str_param(p, "name").unwrap_or("Untitled Album").trim().to_string();
|
||||
if name.is_empty() {
|
||||
return Err(bad("album.create", "empty name"));
|
||||
cmd!(
|
||||
"album.create",
|
||||
"New Album",
|
||||
["File"],
|
||||
None,
|
||||
"{name, parent?: folderId, folder?: bool, addSelected?: bool} → {id, skipped} (skipped: Local photos of the selection, which albums can't hold)",
|
||||
always,
|
||||
|s, p| {
|
||||
let name = str_param(p, "name").unwrap_or("Untitled Album").trim().to_string();
|
||||
if name.is_empty() {
|
||||
return Err(bad("album.create", "empty name"));
|
||||
}
|
||||
let folder = bool_or(p, "folder", false);
|
||||
let parent = p.get("parent").and_then(Value::as_u64).map(AlbumId);
|
||||
let (photos, skipped) =
|
||||
if !folder && bool_or(p, "addSelected", false) { album_targets(s, s.targets(&Value::Null)) } else { (vec![], 0) };
|
||||
let id = s.catalog.alloc_album_id();
|
||||
let cover = photos.first().copied();
|
||||
s.commit(
|
||||
if folder { "New Folder" } else { "New Album" },
|
||||
Op::AddAlbum { album: Album { id, name, parent, folder, photos, cover, smart: None, quick: false, order: None } },
|
||||
)?;
|
||||
Ok(json!({"id": id.0, "skipped": skipped}))
|
||||
}
|
||||
let folder = bool_or(p, "folder", false);
|
||||
let parent = p.get("parent").and_then(Value::as_u64).map(AlbumId);
|
||||
let photos = if !folder && bool_or(p, "addSelected", false) { s.targets(&Value::Null) } else { vec![] };
|
||||
let id = s.catalog.alloc_album_id();
|
||||
let cover = photos.first().copied();
|
||||
s.commit(
|
||||
if folder { "New Folder" } else { "New Album" },
|
||||
Op::AddAlbum { album: Album { id, name, parent, folder, photos, cover, smart: None, quick: false, order: None } },
|
||||
)?;
|
||||
Ok(json!({"id": id.0}))
|
||||
}),
|
||||
),
|
||||
cmd!(
|
||||
"album.createSmart",
|
||||
"New Smart Album…",
|
||||
|
|
@ -1103,34 +1130,44 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
Ok(json!({"changed": changed}))
|
||||
}
|
||||
),
|
||||
cmd!("album.addPhotos", "Add to Album", ["Photo"], None, "{id: albumId, ids?: [photoIds]} (default: selection)", has_selection, |s, p| {
|
||||
let id = album_param(p, "id", "album.addPhotos")?;
|
||||
let targets = ids_param(p).unwrap_or_else(|| s.targets(&Value::Null));
|
||||
let al = s.catalog.album(id).ok_or_else(|| bad("album.addPhotos", "no such album"))?;
|
||||
if al.is_smart() || al.folder {
|
||||
return Err(bad("album.addPhotos", "smart albums and folders can't hold photos"));
|
||||
}
|
||||
let mut photos = al.photos.clone();
|
||||
let before = photos.len();
|
||||
for t in targets {
|
||||
if !photos.contains(&t) {
|
||||
photos.push(t);
|
||||
cmd!(
|
||||
"album.addPhotos",
|
||||
"Add to Album",
|
||||
["Photo"],
|
||||
None,
|
||||
"{id: albumId, ids?: [photoIds]} (default: selection) → {added, skipped}; Local photos are skipped (albums hold library photos), only Local ones is an error",
|
||||
has_library_selection,
|
||||
|s, p| {
|
||||
let id = album_param(p, "id", "album.addPhotos")?;
|
||||
let targets = ids_param(p).unwrap_or_else(|| s.targets(&Value::Null));
|
||||
let al = s.catalog.album(id).ok_or_else(|| bad("album.addPhotos", "no such album"))?;
|
||||
if al.is_smart() || al.folder {
|
||||
return Err(bad("album.addPhotos", "smart albums and folders can't hold photos"));
|
||||
}
|
||||
let (targets, skipped) = album_targets_or_err(s, "album.addPhotos", targets)?;
|
||||
let mut photos = al.photos.clone();
|
||||
let before = photos.len();
|
||||
for t in targets {
|
||||
if !photos.contains(&t) {
|
||||
photos.push(t);
|
||||
}
|
||||
}
|
||||
let added = photos.len() - before;
|
||||
let cover = al.cover.or(photos.first().copied());
|
||||
s.commit("Add to Album", Op::Batch { ops: vec![Op::SetAlbumPhotos { id, photos }, Op::SetAlbumCover { id, cover }] })?;
|
||||
Ok(json!({"added": added, "skipped": skipped}))
|
||||
}
|
||||
let added = photos.len() - before;
|
||||
let cover = al.cover.or(photos.first().copied());
|
||||
s.commit("Add to Album", Op::Batch { ops: vec![Op::SetAlbumPhotos { id, photos }, Op::SetAlbumCover { id, cover }] })?;
|
||||
Ok(json!({"added": added}))
|
||||
}),
|
||||
),
|
||||
cmd!(
|
||||
"album.toggleTarget",
|
||||
"Add to Target Album",
|
||||
[],
|
||||
None,
|
||||
"{ids?} — B: adds the photos to the target album (the Quick Collection unless one is set), or removes them when they're all in it → {album, added}",
|
||||
has_selection,
|
||||
"{ids?} — B: adds the photos to the target album (the Quick Collection unless one is set), or removes them when they're all in it → {album, added, skipped}; Local photos are skipped",
|
||||
has_library_selection,
|
||||
|s, p| {
|
||||
let targets = ids_param(p).unwrap_or_else(|| s.targets(&Value::Null));
|
||||
let targets = album_targets_or_err(s, "album.toggleTarget", ids_param(p).unwrap_or_else(|| s.targets(&Value::Null)))?;
|
||||
let (targets, skipped) = targets;
|
||||
let target = s.target_album.filter(|a| s.catalog.album(*a).is_some_and(|al| !al.is_smart() && !al.folder));
|
||||
let id = match target.or_else(|| s.catalog.quick_collection()) {
|
||||
Some(id) => id,
|
||||
|
|
@ -1154,7 +1191,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
if all_in { "Remove from Target Album" } else { "Add to Target Album" },
|
||||
Op::Batch { ops: vec![Op::SetAlbumPhotos { id, photos }, Op::SetAlbumCover { id, cover }] },
|
||||
)?;
|
||||
Ok(json!({"album": id.0, "name": name, "added": !all_in, "count": s.catalog.album_count(id)}))
|
||||
Ok(json!({"album": id.0, "name": name, "added": !all_in, "count": s.catalog.album_count(id), "skipped": skipped}))
|
||||
}
|
||||
),
|
||||
cmd!("album.setTarget", "Set as Target Album", [], None, "{id: albumId | null} (null = the Quick Collection)", always, |s, p| {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,14 @@ pub fn has_active(s: &Session) -> std::result::Result<(), String> {
|
|||
pub fn has_selection(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.selection.ids.is_empty() && s.selection.active.is_none() { Err("no photos selected".into()) } else { Ok(()) }
|
||||
}
|
||||
/// Albums hold library photos: a selection of only browsed (Local) photos has nothing an album
|
||||
/// can take (issue #684; Add to My Photos first).
|
||||
pub fn has_library_selection(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
if s.targets(&serde_json::Value::Null).iter().any(|id| s.catalog.photo(*id).is_none_or(|p| !p.local)) { Ok(()) } else { Err(LOCAL_ONLY.into()) }
|
||||
}
|
||||
/// Why an album command refused a Local-only selection.
|
||||
pub const LOCAL_ONLY: &str = "only Local photos: albums hold library photos (Add to My Photos first)";
|
||||
pub fn can_undo(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.undo.is_empty() { Err("nothing to undo".into()) } else { Ok(()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -873,6 +873,8 @@ mod tests;
|
|||
#[cfg(test)]
|
||||
mod tests_album_order;
|
||||
#[cfg(test)]
|
||||
mod tests_albums_local;
|
||||
#[cfg(test)]
|
||||
mod tests_color;
|
||||
#[cfg(test)]
|
||||
mod tests_crop_angle;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
//! Albums hold library photos (issue #684): browsed (Local) photos are skipped by
|
||||
//! `album.create {addSelected}`, `album.addPhotos` and `album.toggleTarget` (B), which report how
|
||||
//! many; a Local-only selection is refused and leaves Photo ▸ Add to Album off.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lightcraft_catalog::{AlbumId, PhotoId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Selection, Session};
|
||||
|
||||
fn temp_dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("lc-albums-local-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn png(p: &Path, seed: u8) {
|
||||
let img = lightcraft_raster::Rgba8::from_fn(16, 12, |x, y| [(x * 9) as u8, (y * 11) as u8, seed, 255]);
|
||||
let b =
|
||||
crate::export::encode_image(&img, &crate::export::ExportOptions { format: crate::export::ExportFormat::Png, ..Default::default() }).unwrap();
|
||||
std::fs::write(p, b).unwrap();
|
||||
}
|
||||
|
||||
/// A session browsing a folder of two files, one of them added to My Photos: (session, local, library).
|
||||
fn browsed(tag: &str) -> (Session, PhotoId, PhotoId) {
|
||||
let dir = temp_dir(tag);
|
||||
png(&dir.join("a.png"), 1);
|
||||
png(&dir.join("b.png"), 2);
|
||||
let mut s = Session::new().with_fs();
|
||||
s.execute("library.browse", &json!({"path": dir.to_string_lossy()})).unwrap();
|
||||
let mut ids: Vec<PhotoId> = s.catalog.photos().filter(|p| p.local).map(|p| p.id).collect();
|
||||
ids.sort();
|
||||
assert_eq!(ids.len(), 2, "both files are browsed");
|
||||
let (local, library) = (ids[0], ids[1]);
|
||||
s.execute("photo.addToLibrary", &json!({"ids": [library.0]})).unwrap();
|
||||
assert!(!s.catalog.photo(library).unwrap().local && s.catalog.photo(local).unwrap().local);
|
||||
(s, local, library)
|
||||
}
|
||||
|
||||
fn enabled(s: &Session, id: &str) -> bool {
|
||||
s.commands().into_iter().find(|c| c.id == id).unwrap_or_else(|| panic!("no command {id}")).enabled
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_local_only_selection_is_refused_and_leaves_add_to_album_off() {
|
||||
let (mut s, local, library) = browsed("refused");
|
||||
let album = AlbumId(s.execute("album.create", &json!({"name": "Trip", "addSelected": false})).unwrap()["id"].as_u64().unwrap());
|
||||
s.selection = Selection::single(local);
|
||||
assert!(!enabled(&s, "album.addPhotos"), "Photo ▸ Add to Album is off for a Local photo");
|
||||
for (command, params) in [
|
||||
("album.addPhotos", json!({"id": album.0})),
|
||||
("album.addPhotos", json!({"id": album.0, "ids": [local.0]})),
|
||||
("album.toggleTarget", json!({})),
|
||||
] {
|
||||
let e = s.execute(command, ¶ms).unwrap_err().to_string();
|
||||
assert!(e.contains("Local") && e.contains("Add to My Photos"), "{command}: {e}");
|
||||
}
|
||||
assert!(s.catalog.album(album).unwrap().photos.is_empty(), "nothing invisible was added");
|
||||
assert!(s.catalog.quick_collection().is_none(), "no Quick Collection was made for nothing");
|
||||
// the album is still created from a Local-only selection, just without it
|
||||
let r = s.execute("album.create", &json!({"name": "Empty", "addSelected": true})).unwrap();
|
||||
assert_eq!(r["skipped"], 1, "{r}");
|
||||
assert!(s.catalog.album(AlbumId(r["id"].as_u64().unwrap())).unwrap().photos.is_empty());
|
||||
// once in My Photos the same photo goes in
|
||||
s.execute("photo.addToLibrary", &json!({"ids": [local.0]})).unwrap();
|
||||
assert!(enabled(&s, "album.addPhotos"));
|
||||
let r = s.execute("album.addPhotos", &json!({"id": album.0})).unwrap();
|
||||
assert_eq!((r["added"].as_u64(), r["skipped"].as_u64()), (Some(1), Some(0)), "{r}");
|
||||
assert_eq!(s.catalog.album(album).unwrap().photos, vec![local]);
|
||||
let _ = library;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mixed_selection_skips_the_local_photos_and_says_how_many() {
|
||||
let (mut s, local, library) = browsed("mixed");
|
||||
s.selection = Selection { ids: vec![local, library], active: Some(library) };
|
||||
assert!(enabled(&s, "album.addPhotos"), "one library photo is enough");
|
||||
let r = s.execute("album.create", &json!({"name": "Trip", "addSelected": true})).unwrap();
|
||||
let album = AlbumId(r["id"].as_u64().unwrap());
|
||||
assert_eq!(r["skipped"], 1, "{r}");
|
||||
assert_eq!(s.catalog.album(album).unwrap().photos, vec![library]);
|
||||
assert_eq!(s.catalog.album(album).unwrap().cover, Some(library), "the cover is a photo the album shows");
|
||||
|
||||
let other = AlbumId(s.execute("album.create", &json!({"name": "Other", "addSelected": false})).unwrap()["id"].as_u64().unwrap());
|
||||
let r = s.execute("album.addPhotos", &json!({"id": other.0})).unwrap();
|
||||
assert_eq!((r["added"].as_u64(), r["skipped"].as_u64()), (Some(1), Some(1)), "{r}");
|
||||
assert_eq!(s.catalog.album(other).unwrap().photos, vec![library]);
|
||||
|
||||
let r = s.execute("album.toggleTarget", &json!({})).unwrap();
|
||||
assert_eq!((r["added"].as_bool(), r["skipped"].as_u64()), (Some(true), Some(1)), "{r}");
|
||||
let quick = s.catalog.quick_collection().expect("B made the Quick Collection");
|
||||
assert_eq!(s.catalog.album(quick).unwrap().photos, vec![library]);
|
||||
// B again: the library photo is the whole addable selection and it is in, so it comes out
|
||||
let r = s.execute("album.toggleTarget", &json!({})).unwrap();
|
||||
assert_eq!(r["added"], false, "{r}");
|
||||
assert!(s.catalog.album(quick).unwrap().photos.is_empty());
|
||||
}
|
||||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "+{n} weitere",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 Aufgabe läuft noch: {names}. Beenden bricht sie ab.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} Aufgaben laufen noch: {names}. Beenden bricht sie ab.",
|
||||
"{what} merge cancelled": "{what}-Zusammenfügung abgebrochen"
|
||||
"{what} merge cancelled": "{what}-Zusammenfügung abgebrochen",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Übersprungen: {n} lokale Fotos{0:.0} – Alben enthalten Bibliotheksfotos (zuerst zu „Meine Fotos“ hinzufügen)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2046,5 +2046,7 @@
|
|||
"Synchronizing folder": "Ordner wird synchronisiert",
|
||||
"Looking for changes": "Änderungen werden gesucht",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Dies ist ein Lightroom-Katalog, keine LightCraft-Bibliothek. Stattdessen in eine LightCraft-Bibliothek importieren?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Der Import liest den Katalog, ohne ihn zu ändern, und fügt seine Fotos, Bearbeitungen, Stichwörter und Sammlungen der jetzt geöffneten Bibliothek hinzu."
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Der Import liest den Katalog, ohne ihn zu ändern, und fügt seine Fotos, Bearbeitungen, Stichwörter und Sammlungen der jetzt geöffneten Bibliothek hinzu.",
|
||||
"Albums hold library photos: Add to My Photos first": "Alben enthalten Bibliotheksfotos: zuerst zu „Meine Fotos“ hinzufügen",
|
||||
"Add to My Photos First": "Zuerst zu „Meine Fotos“ hinzufügen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "+{n} más",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 tarea sigue en curso: {names}. Salir la detiene.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tareas siguen en curso: {names}. Salir las detiene.",
|
||||
"{what} merge cancelled": "Combinación {what} cancelada"
|
||||
"{what} merge cancelled": "Combinación {what} cancelada",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Fotos locales omitidas: {n}{0:.0} — los álbumes contienen fotos de la biblioteca (añádelas primero a Mis fotos)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1891,5 +1891,7 @@
|
|||
"Synchronizing folder": "Sincronizando carpeta",
|
||||
"Looking for changes": "Buscando cambios",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Esto es un catálogo de Lightroom, no una biblioteca de LightCraft. ¿Importarlo a una biblioteca de LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "La importación lee el catálogo sin modificarlo y añade sus fotos, ediciones, palabras clave y colecciones a la biblioteca abierta ahora."
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "La importación lee el catálogo sin modificarlo y añade sus fotos, ediciones, palabras clave y colecciones a la biblioteca abierta ahora.",
|
||||
"Albums hold library photos: Add to My Photos first": "Los álbumes contienen fotos de la biblioteca: añádelas primero a Mis fotos",
|
||||
"Add to My Photos First": "Añadir primero a Mis fotos"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "+{n} autres",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 tâche est encore en cours : {names}. Quitter l'arrête.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tâches sont encore en cours : {names}. Quitter les arrête.",
|
||||
"{what} merge cancelled": "Fusion {what} annulée"
|
||||
"{what} merge cancelled": "Fusion {what} annulée",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Photos locales ignorées : {n}{0:.0} — les albums contiennent des photos de la bibliothèque (ajoutez-les d’abord à Mes photos)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1891,5 +1891,7 @@
|
|||
"Resize panel window": "Resize panel window",
|
||||
"Resize panels": "Resize panels",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Ceci est un catalogue Lightroom, pas une bibliothèque LightCraft. L’importer plutôt dans une bibliothèque LightCraft ?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "L’importation lit le catalogue sans le modifier et ajoute ses photos, retouches, mots-clés et collections à la bibliothèque ouverte."
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "L’importation lit le catalogue sans le modifier et ajoute ses photos, retouches, mots-clés et collections à la bibliothèque ouverte.",
|
||||
"Albums hold library photos: Add to My Photos first": "Les albums contiennent des photos de la bibliothèque : ajoutez-les d’abord à Mes photos",
|
||||
"Add to My Photos First": "Ajouter d’abord à Mes photos"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "ほか{n}件",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1件のタスクが実行中です:{names}。終了すると停止します。",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n}件のタスクが実行中です:{names}。終了すると停止します。",
|
||||
"{what} merge cancelled": "{what}合成をキャンセルしました"
|
||||
"{what} merge cancelled": "{what}合成をキャンセルしました",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "ローカル写真{n}枚をスキップしました{0:.0}:アルバムに入れられるのはライブラリの写真です(先にマイフォトに追加してください)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1893,5 +1893,7 @@
|
|||
"Synchronizing folder": "フォルダを同期中",
|
||||
"Looking for changes": "変更を確認中",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "これは Lightroom カタログで、LightCraft ライブラリではありません。代わりに LightCraft ライブラリに読み込みますか?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "読み込みはカタログを変更せずに読み取り、写真・編集・キーワード・コレクションを現在開いているライブラリに追加します。"
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "読み込みはカタログを変更せずに読み取り、写真・編集・キーワード・コレクションを現在開いているライブラリに追加します。",
|
||||
"Albums hold library photos: Add to My Photos first": "アルバムに入れられるのはライブラリの写真です:先にマイフォトに追加してください",
|
||||
"Add to My Photos First": "先にマイフォトに追加"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "+{n} mais",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 tarefa ainda está em andamento: {names}. Sair a interrompe.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tarefas ainda estão em andamento: {names}. Sair as interrompe.",
|
||||
"{what} merge cancelled": "{what} mesclagem cancelada"
|
||||
"{what} merge cancelled": "{what} mesclagem cancelada",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Fotos locais ignoradas: {n}{0:.0} — álbuns contêm fotos da biblioteca (adicione-as às Minhas fotos primeiro)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1891,5 +1891,7 @@
|
|||
"Synchronizing folder": "Sincronizando pasta",
|
||||
"Looking for changes": "Procurando alterações",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Este é um catálogo do Lightroom, não uma biblioteca do LightCraft. Importá-lo para uma biblioteca do LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "A importação lê o catálogo sem alterá-lo e adiciona as fotos, edições, palavras-chave e coleções dele à biblioteca aberta agora."
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "A importação lê o catálogo sem alterá-lo e adiciona as fotos, edições, palavras-chave e coleções dele à biblioteca aberta agora.",
|
||||
"Albums hold library photos: Add to My Photos first": "Álbuns contêm fotos da biblioteca: adicione-as às Minhas fotos primeiro",
|
||||
"Add to My Photos First": "Adicionar às Minhas fotos primeiro"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "ещё {n}",
|
||||
"1 task is still running: {names}. Quitting stops it.": "Ещё выполняется задача: {names}. Выход прервёт её.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "Ещё выполняется задач: {n} — {names}. Выход прервёт их.",
|
||||
"{what} merge cancelled": "{what}: слияние отменено"
|
||||
"{what} merge cancelled": "{what}: слияние отменено",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Пропущено локальных фото: {n}{0:.0} — в альбомах только фото из библиотеки (сначала добавьте их в «Мои фото»)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1605,5 +1605,7 @@
|
|||
"Synchronizing folder": "Синхронизация папки",
|
||||
"Looking for changes": "Поиск изменений",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Это каталог Lightroom, а не библиотека LightCraft. Импортировать его в библиотеку LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Импорт читает каталог, не изменяя его, и добавляет его фото, правки, ключевые слова и коллекции в открытую сейчас библиотеку."
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Импорт читает каталог, не изменяя его, и добавляет его фото, правки, ключевые слова и коллекции в открытую сейчас библиотеку.",
|
||||
"Albums hold library photos: Add to My Photos first": "В альбомах только фото из библиотеки: сначала добавьте их в «Мои фото»",
|
||||
"Add to My Photos First": "Сначала добавить в «Мои фото»"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "+{n} ще",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 завдання ще виконується: {names}. Вихід зупинить його.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "Ще виконуються завдання ({n}): {names}. Вихід зупинить їх.",
|
||||
"{what} merge cancelled": "Об’єднання {what} скасовано"
|
||||
"{what} merge cancelled": "Об’єднання {what} скасовано",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Пропущено локальних фото: {n}{0:.0} — в альбомах лише фото з бібліотеки (спершу додайте їх до моїх фото)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2085,5 +2085,7 @@
|
|||
"Resize panel window": "Resize panel window",
|
||||
"Resize panels": "Resize panels",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "Це каталог Lightroom, а не бібліотека LightCraft. Імпортувати його до бібліотеки LightCraft?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Імпорт читає каталог, не змінюючи його, і додає його фото, редагування, ключові слова та колекції до відкритої зараз бібліотеки."
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "Імпорт читає каталог, не змінюючи його, і додає його фото, редагування, ключові слова та колекції до відкритої зараз бібліотеки.",
|
||||
"Albums hold library photos: Add to My Photos first": "В альбомах лише фото з бібліотеки: спершу додайте їх до моїх фото",
|
||||
"Add to My Photos First": "Спершу додати до моїх фото"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "还有 {n} 项",
|
||||
"1 task is still running: {names}. Quitting stops it.": "仍有 1 个任务在运行:{names}。退出将停止它。",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 个任务在运行:{names}。退出将停止它们。",
|
||||
"{what} merge cancelled": "已取消 {what} 合并"
|
||||
"{what} merge cancelled": "已取消 {what} 合并",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "已跳过 {n} 张本地照片{0:.0}:相册只能收录图库中的照片(请先添加到我的照片)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1891,5 +1891,7 @@
|
|||
"Synchronizing folder": "正在同步文件夹",
|
||||
"Looking for changes": "正在查找更改",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "这是 Lightroom 目录,不是 LightCraft 图库。要改为将其导入 LightCraft 图库吗?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "导入只读取目录、不做任何更改,并将其中的照片、编辑、关键字和收藏夹添加到当前打开的图库。"
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "导入只读取目录、不做任何更改,并将其中的照片、编辑、关键字和收藏夹添加到当前打开的图库。",
|
||||
"Albums hold library photos: Add to My Photos first": "相册只能收录图库中的照片:请先添加到我的照片",
|
||||
"Add to My Photos First": "先添加到我的照片"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,5 +230,6 @@
|
|||
"+{n} more": "還有 {n} 項",
|
||||
"1 task is still running: {names}. Quitting stops it.": "仍有 1 個工作在執行:{names}。結束將停止它。",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 個工作在執行:{names}。結束將停止它們。",
|
||||
"{what} merge cancelled": "已取消{what}合併"
|
||||
"{what} merge cancelled": "已取消{what}合併",
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "已略過 {n} 張本機照片{0:.0}:相簿只能收錄圖庫中的照片(請先加入我的照片)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1891,5 +1891,7 @@
|
|||
"Synchronizing folder": "正在同步資料夾",
|
||||
"Looking for changes": "正在尋找變更",
|
||||
"This is a Lightroom catalog, not a LightCraft library. Import it into a LightCraft library instead?": "這是 Lightroom 目錄,不是 LightCraft 照片庫。要改為將其匯入 LightCraft 照片庫嗎?",
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "匯入只讀取目錄、不做任何變更,並將其中的照片、編輯、關鍵字和收藏夾加入目前開啟的照片庫。"
|
||||
"The import reads the catalog without changing it and adds its photos, edits, keywords and collections to the library open now.": "匯入只讀取目錄、不做任何變更,並將其中的照片、編輯、關鍵字和收藏夾加入目前開啟的照片庫。",
|
||||
"Albums hold library photos: Add to My Photos first": "相簿只能收錄圖庫中的照片:請先加入我的照片",
|
||||
"Add to My Photos First": "先加入我的照片"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -575,15 +575,23 @@ fn expanded(app: &LightcraftApp, name: &str) -> Option<Vec<MenuNode>> {
|
|||
v
|
||||
}
|
||||
"Add to Album" => {
|
||||
let sel = !app.session.selection.ids.is_empty() || has;
|
||||
let mut albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.name.clone(), a.id.0)).collect();
|
||||
albums.sort_by_key(|(n, _)| n.to_lowercase());
|
||||
let mut v: Vec<MenuNode> =
|
||||
albums.into_iter().map(|(name, id)| item("album.addPhotos", json!({"id": id}), name, None, sel, None)).collect();
|
||||
if !v.is_empty() {
|
||||
let targets = app.session.targets(&Value::Null);
|
||||
let sel = !targets.is_empty();
|
||||
// albums hold library photos (issue #684): a Local-only selection is told what to do first
|
||||
let local_only = crate::panels::grid::local_only(app, &targets);
|
||||
let mut v: Vec<MenuNode> = vec![];
|
||||
if local_only {
|
||||
v.push(item("photo.addToLibrary", Value::Null, crate::i18n::tr("Add to My Photos First"), None, false, None));
|
||||
v.push(MenuNode::Separator);
|
||||
}
|
||||
v.push(item("dialog.newAlbum", Value::Null, "New Album…", None, true, None));
|
||||
let mut albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.name.clone(), a.id.0)).collect();
|
||||
albums.sort_by_key(|(n, _)| n.to_lowercase());
|
||||
let any = !albums.is_empty();
|
||||
v.extend(albums.into_iter().map(|(name, id)| item("album.addPhotos", json!({"id": id}), name, None, sel && !local_only, None)));
|
||||
if any {
|
||||
v.push(MenuNode::Separator);
|
||||
}
|
||||
v.push(item("dialog.newAlbum", Value::Null, "New Album…", None, !local_only, None));
|
||||
v
|
||||
}
|
||||
_ => return None,
|
||||
|
|
|
|||
|
|
@ -1245,6 +1245,11 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
*error = Some(e);
|
||||
}
|
||||
}
|
||||
Ok(r) if matches!(dlg, Dialog::NewAlbum { .. }) && r.get("skipped").and_then(serde_json::Value::as_u64).is_some_and(|n| n > 0) => {
|
||||
// albums hold library photos (issue #684): say which part of the selection stayed out
|
||||
crate::panels::grid::toast_skipped(app, ctx, &r);
|
||||
close = true;
|
||||
}
|
||||
Ok(_) if keeps_open(app, &dlg) => {
|
||||
if let Dialog::SamModel { error, .. } = &mut dlg {
|
||||
*error = None;
|
||||
|
|
|
|||
|
|
@ -801,6 +801,32 @@ pub fn drag_feedback(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
|
||||
pub use super::filterbar::label_color;
|
||||
|
||||
/// Albums hold library photos (issue #684): true when `ids` are photos and all of them are
|
||||
/// browsed (Local) ones, which album views never show.
|
||||
pub fn local_only(app: &LightcraftApp, ids: &[PhotoId]) -> bool {
|
||||
!ids.is_empty() && ids.iter().all(|id| app.session.catalog.photo(*id).is_some_and(|p| p.local))
|
||||
}
|
||||
|
||||
/// Why an album action is off for Local photos.
|
||||
pub fn albums_hint() -> String {
|
||||
crate::i18n::tr("Albums hold library photos: Add to My Photos first").to_string()
|
||||
}
|
||||
|
||||
/// Tell the user when an album command skipped Local photos of the selection (`skipped` in its
|
||||
/// result).
|
||||
pub fn toast_skipped(app: &mut LightcraftApp, ctx: &egui::Context, r: &serde_json::Value) {
|
||||
if let Some(n) = r.get("skipped").and_then(serde_json::Value::as_u64).filter(|n| *n > 0) {
|
||||
app.toast(
|
||||
ctx,
|
||||
crate::i18n::tr_format!(
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)",
|
||||
if n == 1 { "" } else { "s" },
|
||||
n = n
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// "Set Color Label" items for the active photo, shared by context menus.
|
||||
pub fn label_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let current = app.session.active().and_then(|id| app.session.catalog.photo(id)).and_then(|p| p.label);
|
||||
|
|
@ -913,14 +939,26 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
});
|
||||
ui.menu_button(crate::i18n::tr("Set Color Label"), |ui| label_menu(app, ui));
|
||||
ui.menu_button(crate::i18n::tr("Add to Album"), |ui| {
|
||||
let albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.id.0, a.name.clone())).collect();
|
||||
for (aid, name) in albums {
|
||||
if ui.button(name).clicked() {
|
||||
let _ = app.run("album.addPhotos", json!({"id": aid}));
|
||||
if local_only(app, &app.session.targets(&serde_json::Value::Null)) {
|
||||
// albums hold library photos (issue #684): nothing here to add until they join My Photos
|
||||
let add = ui.add_enabled(false, egui::Button::new(crate::i18n::tr("Add to Album"))).on_disabled_hover_text(albums_hint());
|
||||
register(ui.ctx(), "button:contextAddToAlbum", add.rect);
|
||||
} else {
|
||||
let add = ui.menu_button(crate::i18n::tr("Add to Album"), |ui| {
|
||||
let albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.id.0, a.name.clone())).collect();
|
||||
for (aid, name) in albums {
|
||||
let b = ui.button(name);
|
||||
register(ui.ctx(), format!("button:contextAlbum:{aid}"), b.rect);
|
||||
if b.clicked() {
|
||||
match app.run("album.addPhotos", json!({"id": aid})) {
|
||||
Ok(r) => toast_skipped(app, ui.ctx(), &r),
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
register(ui.ctx(), "button:contextAddToAlbum", add.response.rect);
|
||||
}
|
||||
// in a (non-smart) album: take the selection out of it, or make this photo its cover
|
||||
if let lightcraft_engine::LibrarySource::Album(aid) = app.session.source
|
||||
&& app.session.catalog.album(aid).is_some_and(|a| !a.folder && !a.is_smart())
|
||||
|
|
|
|||
|
|
@ -1010,12 +1010,36 @@ fn drop_target(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response
|
|||
if !over {
|
||||
return;
|
||||
}
|
||||
let released = ui.input(|i| i.pointer.any_released());
|
||||
// albums hold library photos (issue #684): Local ones can't be dropped in; the row doesn't
|
||||
// light up, and a release says why
|
||||
let photos: Vec<lightcraft_catalog::PhotoId> = ids.iter().map(|id| lightcraft_catalog::PhotoId(*id)).collect();
|
||||
if super::grid::local_only(app, &photos) {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::NotAllowed);
|
||||
if released {
|
||||
app.toast(ui.ctx(), super::grid::albums_hint());
|
||||
app.ui.dragging_photos = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.painter().rect_stroke(resp.rect.shrink2(vec2(8.0, 1.0)), 4.0, egui::Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
let n = ids.len();
|
||||
if released {
|
||||
match app.run("album.addPhotos", json!({"id": a.id.0, "ids": ids})) {
|
||||
Ok(_) => app.toast(ui.ctx(), crate::i18n::tr_format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, a.name, n = n)),
|
||||
Ok(r) => {
|
||||
let n = r.get("added").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
let skipped = r.get("skipped").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
let mut msg = crate::i18n::tr_format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, a.name, n = n);
|
||||
if skipped > 0 {
|
||||
msg.push_str(" · ");
|
||||
msg.push_str(&crate::i18n::tr_format!(
|
||||
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)",
|
||||
if skipped == 1 { "" } else { "s" },
|
||||
n = skipped
|
||||
));
|
||||
}
|
||||
app.toast(ui.ctx(), msg);
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
app.ui.dragging_photos = None;
|
||||
|
|
@ -1047,8 +1071,17 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
|||
}
|
||||
ui.separator();
|
||||
}
|
||||
if !a.folder && !a.is_smart() && ui.button(crate::i18n::tr("Add Selected Photos")).clicked() {
|
||||
let _ = app.run("album.addPhotos", json!({"id": a.id.0}));
|
||||
if !a.folder && !a.is_smart() {
|
||||
let local_only = super::grid::local_only(app, &app.session.targets(&serde_json::Value::Null));
|
||||
let b = ui
|
||||
.add_enabled(!local_only, egui::Button::new(crate::i18n::tr("Add Selected Photos")))
|
||||
.on_disabled_hover_text(super::grid::albums_hint());
|
||||
if b.clicked() {
|
||||
match app.run("album.addPhotos", json!({"id": a.id.0})) {
|
||||
Ok(r) => super::grid::toast_skipped(app, ui.ctx(), &r),
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
if !a.folder && !a.is_smart() {
|
||||
let is_target = app.session.target_album == Some(a.id) || (app.session.target_album.is_none() && a.quick);
|
||||
|
|
|
|||
|
|
@ -1227,5 +1227,99 @@ fn recognise_faces_stays_off_until_a_model_is_chosen() {
|
|||
assert!(!enabled(&mut h), "the box switches recognition off");
|
||||
click(&mut h);
|
||||
assert!(enabled(&mut h), "and on again, with a model chosen");
|
||||
|
||||
/// Issue #684: albums hold library photos. For a browsed (Local) photo the grid's right-click
|
||||
/// Add to Album is off, Photo ▸ Add to Album says to Add to My Photos first with its entries off,
|
||||
/// and a drop onto an album row is refused with that hint; once the photo is in My Photos all
|
||||
/// three work.
|
||||
#[test]
|
||||
fn local_photos_cannot_be_added_to_albums_until_they_join_my_photos() {
|
||||
let mut h = demo([1400.0, 900.0], json!({"view": "photoGrid", "leftPanel": true}));
|
||||
let album = h.app.session.execute("album.create", &json!({"name": "Trip", "addSelected": false})).unwrap()["id"].as_u64().unwrap();
|
||||
let album_id = lightcraft_catalog::AlbumId(album);
|
||||
let dir = std::env::temp_dir().join(format!("lc-ui-albums-local-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let img = lightcraft_raster::Rgba8::from_fn(24, 16, |x, y| [(x * 9) as u8, (y * 12) as u8, 80, 255]);
|
||||
let o = lightcraft_engine::export::ExportOptions { format: lightcraft_engine::export::ExportFormat::Png, ..Default::default() };
|
||||
std::fs::write(dir.join("a.png"), lightcraft_engine::export::encode_image(&img, &o).unwrap()).unwrap();
|
||||
let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": dir.to_string_lossy()}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
let vis = h.app.session.visible_cloned();
|
||||
assert_eq!(vis.len(), 1, "{vis:?}");
|
||||
let local = vis[0];
|
||||
assert!(h.app.session.catalog.photo(local).unwrap().local);
|
||||
let thumb = widget(&h, &format!("thumb:{}", local.0));
|
||||
|
||||
// right-click: Add to Album is there but off, so hovering opens no album entries
|
||||
let r = h.request("ui.click", json!({"x": thumb.center().x, "y": thumb.center().y, "button": "right"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(h.app.session.selection.contains(local));
|
||||
let r = h.request("ui.hoverWidget", json!({"id": "button:contextAddToAlbum"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(!h.app.widgets.iter().any(|(w, _)| w.starts_with("button:contextAlbum:")), "the submenu must not open for a Local photo");
|
||||
let r = h.request("ui.key", json!({"key": "Escape"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
|
||||
// Photo ▸ Add to Album: the hint item first (off), the album and New Album… off
|
||||
let add_to_album = |app: &LightcraftApp| -> Vec<crate::menubar::MenuNode> {
|
||||
let bar = crate::menubar::menu_bar(app);
|
||||
let photo = &bar.iter().find(|(t, _)| t == "Photo").expect("Photo menu").1;
|
||||
photo
|
||||
.iter()
|
||||
.find_map(|n| match n {
|
||||
crate::menubar::MenuNode::Submenu { label, children } if label == "Add to Album" => Some(children.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("Add to Album submenu")
|
||||
};
|
||||
let enabled_of = |nodes: &[crate::menubar::MenuNode], id: &str| -> Option<bool> {
|
||||
nodes.iter().find_map(|n| match n {
|
||||
crate::menubar::MenuNode::Item { id: i, enabled, .. } if i == id => Some(*enabled),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let nodes = add_to_album(&h.app);
|
||||
assert!(matches!(nodes.first(), Some(crate::menubar::MenuNode::Item { id, enabled: false, .. }) if id == "photo.addToLibrary"), "{nodes:?}");
|
||||
assert_eq!(enabled_of(&nodes, "album.addPhotos"), Some(false), "{nodes:?}");
|
||||
assert_eq!(enabled_of(&nodes, "dialog.newAlbum"), Some(false), "{nodes:?}");
|
||||
|
||||
// a drop onto the album row is refused and says why
|
||||
let row = widget(&h, &format!("source:album:{album}"));
|
||||
let r = h.request("ui.drag", json!({"x": thumb.center().x, "y": thumb.center().y, "toX": row.center().x, "toY": row.center().y, "steps": 12}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert!(h.app.session.catalog.album(album_id).unwrap().photos.is_empty(), "nothing invisible was added");
|
||||
assert!(h.app.ui.dragging_photos.is_none(), "the drag ended");
|
||||
let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();
|
||||
assert!(toast.contains("Add to My Photos first"), "{toast:?}");
|
||||
|
||||
// in My Photos: the menu entries come on, the submenu opens, and the album takes the photo
|
||||
h.app.run("photo.addToLibrary", json!({"ids": [local.0]})).unwrap();
|
||||
h.step();
|
||||
let nodes = add_to_album(&h.app);
|
||||
assert!(!matches!(nodes.first(), Some(crate::menubar::MenuNode::Item { id, .. }) if id == "photo.addToLibrary"), "{nodes:?}");
|
||||
assert_eq!(enabled_of(&nodes, "album.addPhotos"), Some(true), "{nodes:?}");
|
||||
assert_eq!(enabled_of(&nodes, "dialog.newAlbum"), Some(true), "{nodes:?}");
|
||||
let thumb = widget(&h, &format!("thumb:{}", local.0));
|
||||
let r = h.request("ui.click", json!({"x": thumb.center().x, "y": thumb.center().y, "button": "right"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
let r = h.request("ui.hoverWidget", json!({"id": "button:contextAddToAlbum"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
let r = h.request("ui.clickWidget", json!({"id": format!("button:contextAlbum:{album}")}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert_eq!(h.app.session.catalog.album(album_id).unwrap().photos, vec![local]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue