diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index be30fa17..785e0fa9 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1912,5 +1912,7 @@ "Drop here for the top level": "Hier ablegen für die oberste Ebene", "Their photos and the keywords below them come together under one keyword.": "Ihre Fotos und die Stichwörter darunter kommen unter einem Stichwort zusammen.", "Select photos to give them keywords.": "Wählen Sie Fotos aus, um ihnen Stichwörter zu geben.", - "Add to All Selected Photos": "Allen ausgewählten Fotos hinzufügen" + "Add to All Selected Photos": "Allen ausgewählten Fotos hinzufügen", + "Will Export": "Wird exportiert", + "No keywords are exported": "Keine Stichwörter werden exportiert" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 1095fac2..dc0726d6 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1757,5 +1757,7 @@ "Drop here for the top level": "Soltar aquí para el nivel superior", "Their photos and the keywords below them come together under one keyword.": "Sus fotos y las palabras clave que contienen quedan bajo una sola palabra clave.", "Select photos to give them keywords.": "Selecciona fotos para darles palabras clave.", - "Add to All Selected Photos": "Añadir a todas las fotos seleccionadas" + "Add to All Selected Photos": "Añadir a todas las fotos seleccionadas", + "Will Export": "Se exportará", + "No keywords are exported": "No se exporta ninguna palabra clave" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index c17f3b3e..5f0cd0c4 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1757,5 +1757,7 @@ "Drop here for the top level": "ここにドロップすると最上位に移動", "Their photos and the keywords below them come together under one keyword.": "写真と下位のキーワードが1つのキーワードにまとめられます。", "Select photos to give them keywords.": "写真を選択するとキーワードを付けられます。", - "Add to All Selected Photos": "選択したすべての写真に追加" + "Add to All Selected Photos": "選択したすべての写真に追加", + "Will Export": "書き出される内容", + "No keywords are exported": "書き出されるキーワードはありません" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 2c26013a..1414b5e0 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1757,5 +1757,7 @@ "Drop here for the top level": "Solte aqui para o nível superior", "Their photos and the keywords below them come together under one keyword.": "Suas fotos e as palavras-chave abaixo delas ficam sob uma só palavra-chave.", "Select photos to give them keywords.": "Selecione fotos para dar palavras-chave a elas.", - "Add to All Selected Photos": "Adicionar a todas as fotos selecionadas" + "Add to All Selected Photos": "Adicionar a todas as fotos selecionadas", + "Will Export": "Será exportado", + "No keywords are exported": "Nenhuma palavra-chave é exportada" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 94d63635..d7029078 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1431,5 +1431,7 @@ "Their photos and the keywords below them come together under one keyword.": "Их фото и вложенные ключевые слова объединятся под одним ключевым словом.", "Select photos to give them keywords.": "Выберите фото, чтобы назначить им ключевые слова.", "Person": "Человек", - "Add to All Selected Photos": "Добавить ко всем выбранным фото" + "Add to All Selected Photos": "Добавить ко всем выбранным фото", + "Will Export": "Будет экспортировано", + "No keywords are exported": "Ключевые слова не экспортируются" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 184b117e..67545963 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1757,5 +1757,7 @@ "Drop here for the top level": "拖放到此处以移至顶层", "Their photos and the keywords below them come together under one keyword.": "它们的照片和下级关键字将合并到同一个关键字下。", "Select photos to give them keywords.": "选择照片以为其添加关键字。", - "Add to All Selected Photos": "添加到所有所选照片" + "Add to All Selected Photos": "添加到所有所选照片", + "Will Export": "将导出", + "No keywords are exported": "没有要导出的关键字" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index aa02f290..448fa70c 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1757,5 +1757,7 @@ "Drop here for the top level": "拖放到此處以移至最上層", "Their photos and the keywords below them come together under one keyword.": "它們的照片與下層關鍵字將合併到同一個關鍵字下。", "Select photos to give them keywords.": "選取照片以加入關鍵字。", - "Add to All Selected Photos": "加入所有選取的照片" + "Add to All Selected Photos": "加入所有選取的照片", + "Will Export": "將匯出", + "No keywords are exported": "沒有要匯出的關鍵字" } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 55138bd7..0e2fed5a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1080,6 +1080,8 @@ mod drop_tests { #[derive(Default)] pub struct Caches { keyword_tree: Option<(u64, std::sync::Arc>)>, + /// The Keywording box's Will Export names, by library revision and selection. + keyword_export: Option<(u64, u64, std::sync::Arc>)>, /// The Keywording box's chips, by library revision and selection. keyword_chips: Option<(u64, u64, std::sync::Arc>)>, /// The Keyword List's tick boxes, by library revision and selection. @@ -1170,6 +1172,22 @@ impl Caches { } } } + /// What exported files carry for the selection (the Keywording box's Will Export). + pub(crate) fn keyword_export( + &mut self, + cat: &lightcraft_catalog::Catalog, + selection: &[lightcraft_catalog::PhotoId], + ) -> std::sync::Arc> { + let key = key_of(selection); + match &self.keyword_export { + Some((r, k, c)) if *r == cat.revision && *k == key => c.clone(), + _ => { + let c = std::sync::Arc::new(panels::keywording::will_export(cat, selection)); + self.keyword_export = Some((cat.revision, key, c.clone())); + c + } + } + } /// The selection's keywords (the Keywording box's chips). pub(crate) fn keyword_chips( &mut self, diff --git a/crates/ui-egui/src/panels/keyword_list.rs b/crates/ui-egui/src/panels/keyword_list.rs index 432f75c9..fb51a14c 100644 --- a/crates/ui-egui/src/panels/keyword_list.rs +++ b/crates/ui-egui/src/panels/keyword_list.rs @@ -593,6 +593,8 @@ mod tests { "No keywords match", "Add to Selected Photos", "Remove from Selected Photos", + "Keywords", + "Will Export", ]; let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales"); for code in ["de", "es", "pt-br", "ru", "ja", "zh-hans", "zh-hant"] { diff --git a/crates/ui-egui/src/panels/keywording.rs b/crates/ui-egui/src/panels/keywording.rs index 09107dc1..c3e47717 100644 --- a/crates/ui-egui/src/panels/keywording.rs +++ b/crates/ui-egui/src/panels/keywording.rs @@ -9,6 +9,47 @@ use crate::LightcraftApp; use crate::theme::Tokens; use crate::widgets::register; +/// The Keywording box's view: the keywords (chips), or what exported files will carry. +pub fn view_switch(app: &mut LightcraftApp, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + for (export, id, label) in [(false, "keywords", "Keywords"), (true, "willExport", "Will Export")] { + let r = ui.selectable_label(app.ui.keywording_will_export == export, crate::i18n::tr(label)); + register(ui.ctx(), format!("keywordView:{id}"), r.rect); + if r.clicked() { + app.ui.keywording_will_export = export; + } + } + }); +} + +/// What exported files will carry for the selection, read only: one name only some of the photos +/// carry is marked with an asterisk. +pub fn export_row(app: &mut LightcraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let selection = app.session.selection.ids.clone(); + let names = app.caches.keyword_export(&app.session.catalog, &selection); + if names.is_empty() { + ui.label(egui::RichText::new(crate::i18n::tr("No keywords are exported")).color(t.text_dim)); + return; + } + ui.horizontal_wrapped(|ui| { + for n in names.iter() { + let partial = !n.on_all(); + let text = format!("{}{}", n.path, if partial { " *" } else { "" }); + let r = egui::Frame::NONE + .stroke(egui::Stroke::new(1.0, t.field_border)) + .corner_radius(10.0) + .inner_margin(egui::Margin::symmetric(8, 2)) + .show(ui, |ui| ui.label(egui::RichText::new(text).color(if partial { t.text_dim } else { t.text_label }))) + .response; + register(ui.ctx(), format!("keywordExport:{}", n.path), r.rect); + if partial { + r.on_hover_text(crate::i18n::tr_format!("On {have} of {of} selected photos", have = n.have, of = n.of)); + } + } + }); +} + /// A chip's menu: it acts on the selected photos (deleting a keyword from the whole library is the /// Keyword List's). fn menu(app: &mut LightcraftApp, ui: &mut egui::Ui, chip: &Chip) { @@ -93,6 +134,20 @@ impl Chip { /// The keywords of the selected `photos`, each once whatever its case, with how many of them have /// it; by name. +/// "Will Export" (Lightroom Classic's Keyword Tags ▸ Will Export): the names exported files carry +/// for the selected `photos`, as the keyword tag options say, each with how many of them carry it; +/// by name. +pub(crate) fn will_export(catalog: &Catalog, photos: &[PhotoId]) -> Vec { + let mut have: std::collections::BTreeMap = Default::default(); + for id in photos { + let Some(p) = catalog.photo(*id) else { continue }; + for name in catalog.export_keywords(&p.meta.keywords).flat { + have.entry(name.to_lowercase()).or_insert_with(|| (name.clone(), 0)).1 += 1; + } + } + have.into_values().map(|(path, have)| Chip { path, have, of: photos.len() }).collect() +} + pub(crate) fn chips(catalog: &Catalog, photos: &[PhotoId]) -> Vec { use lightcraft_catalog::keywords::clean; // by lower-case keyword: the photos with it (each once) @@ -152,4 +207,23 @@ mod tests { let (c, ids) = library(&[&["Beach"], &["beach"], &["BEACH", "beach"]]); assert_eq!(shown(&chips(&c, &ids)), [("Beach", 3, 3)]); } + + /// "Will Export": the names exported files carry for the selection, as the keyword tag options + /// say (a keyword left out goes, its parents and synonyms come), each with how many of the + /// selected photos carry it. + #[test] + fn will_export_shows_what_exported_files_carry() { + use lightcraft_catalog::keywords::KeywordInfo; + let (mut c, ids) = library(&[&["Places|Lisbon", "draft"], &["Places|Lisbon"]]); + let out = KeywordInfo { include_on_export: false, ..KeywordInfo::default() }; + c.apply(Op::SetKeyword { path: "Places".into(), info: Some(out.clone()) }).unwrap(); + c.apply(Op::SetKeyword { path: "draft".into(), info: Some(out) }).unwrap(); + c.apply(Op::SetKeyword { + path: "Places|Lisbon".into(), + info: Some(KeywordInfo { synonyms: vec!["Lisboa".into()], ..KeywordInfo::default() }), + }) + .unwrap(); + assert_eq!(shown(&will_export(&c, &ids)), [("Lisboa", 2, 2), ("Lisbon", 2, 2)]); + assert!(will_export(&c, &[]).is_empty()); + } } diff --git a/crates/ui-egui/src/panels/right.rs b/crates/ui-egui/src/panels/right.rs index d92acaab..f4c5fe89 100644 --- a/crates/ui-egui/src/panels/right.rs +++ b/crates/ui-egui/src/panels/right.rs @@ -787,7 +787,13 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { } ui.data_mut(|d| d.insert_temp(kid, text)); ui.add_space(8.0); - super::keywording::chip_row(app, ui); + super::keywording::view_switch(app, ui); + ui.add_space(4.0); + if app.ui.keywording_will_export { + super::keywording::export_row(app, ui); + } else { + super::keywording::chip_row(app, ui); + } ui.add_space(10.0); keyword_set(app, ui); ui.add_space(8.0); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 20fddc3b..f4dd8ead 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -449,6 +449,9 @@ pub struct UiState { /// The keyword painter: clicking a photo in the grid toggles this keyword on it. #[serde(skip)] pub keyword_painter: Option, + /// The Keywording box shows what exported files will carry (Will Export) instead of the + /// keywords. + pub keywording_will_export: bool, /// The Keyword List's open levels (lower-case paths). pub keyword_list_open: Vec, /// The keyword picked in the Keyword List (− deletes it, Edit edits it). @@ -795,6 +798,7 @@ impl Default for UiState { slideshow: None, second_window: false, keyword_painter: None, + keywording_will_export: false, keyword_list_open: Vec::new(), keyword_list_selected: None, dragging_keyword: None, diff --git a/crates/ui-egui/src/tests_keywording.rs b/crates/ui-egui/src/tests_keywording.rs index c669d709..3de8c53a 100644 --- a/crates/ui-egui/src/tests_keywording.rs +++ b/crates/ui-egui/src/tests_keywording.rs @@ -119,3 +119,18 @@ fn keyword_set_buttons_reflect_the_selection() { assert!(has(&h, &format!("kwSetSome:{}", at("Weddings"))), "only the active one has Weddings"); assert!(!has(&h, &format!("kwSetOn:{}", at("Weddings")))); } + +/// "Will Export" shows the names exported files will carry for the selection instead of the chips: +/// a keyword left out of export isn't there; "Keywords" brings the chips back. +#[test] +fn will_export_shows_what_exports_carry() { + let (mut h, _) = two_selected(); + let r = h.request("engine.execute", json!({"command": "keyword.edit", "params": {"keyword": "Weddings", "includeOnExport": false}}), T); + assert_eq!(r["ok"], true, "{r}"); + ask(&mut h, "ui.clickWidget", json!({"id": "keywordView:willExport"})); + assert!(has(&h, "keywordExport:beach")); + assert!(!has(&h, "keywordExport:Weddings"), "left out of export"); + assert!(!has(&h, "keywordChip:beach"), "the chips give way"); + ask(&mut h, "ui.clickWidget", json!({"id": "keywordView:keywords"})); + assert!(has(&h, "keywordChip:beach") && !has(&h, "keywordExport:beach")); +}