diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 9960fdd54..2ba868c48 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -213,6 +213,8 @@ fn services(originals: Originals, backend: Option, files: Files, frozen // Face models need a folder to live in; the web has none. pick_model_file: None, save_preset_file: None, + pick_keyword_list: None, + save_keyword_list: None, pick_curve_preset_files: None, save_curve_preset_file: None, write: Some(Box::new(download)), diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index 832b158b3..da3373848 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -472,6 +472,22 @@ fn services(ctx: egui::Context, log_file: Option<&std::path::Path>) -> Services .save_file() .map(|p| p.to_string_lossy().to_string()) })), + pick_keyword_list: Some(Box::new(|| { + rfd::FileDialog::new() + .set_title(lightcraft_ui_egui::i18n::tr("Import Keywords")) + .add_filter_nocase(lightcraft_ui_egui::i18n::tr("Keyword Lists"), &["txt", "utf8"]) + .pick_file() + .map(|p| vec![p.to_string_lossy().to_string()]) + .unwrap_or_default() + })), + save_keyword_list: Some(Box::new(|name: &str| { + rfd::FileDialog::new() + .set_title(lightcraft_ui_egui::i18n::tr("Export Keywords")) + .add_filter_nocase(lightcraft_ui_egui::i18n::tr("Keyword Lists"), &["txt"]) + .set_file_name(name) + .save_file() + .map(|p| p.to_string_lossy().to_string()) + })), pick_curve_preset_files: Some(Box::new(|| { rfd::FileDialog::new() .set_title(lightcraft_ui_egui::i18n::tr("Import Point Curve Presets")) diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index a259b6936..a618b92aa 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "„{keyword}“ zu {n} Foto{} hinzugefügt", "On {have} of {of} selected photos": "Auf {have} von {of} ausgewählten Fotos", "Remove “{name}”": "„{name}“ entfernen", - "There is a keyword set “{name}” already": "Es gibt bereits einen Stichwortsatz „{name}“" + "There is a keyword set “{name}” already": "Es gibt bereits einen Stichwortsatz „{name}“", + "Added {n} keyword{}": "{n} Stichwort/Stichwörter hinzugefügt{:.0}", + "; {n} gained synonyms": "; {n} erhielten Synonyme", + "Exported {n} keyword{}": "{n} Stichwort/Stichwörter exportiert{:.0}", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One importiert keine Liste mit ; , < oder > in einem Stichwort: {names}" } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index a82080c9e..30fa6f743 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1921,5 +1921,10 @@ "Keywords (⌥1–⌥9)": "Stichwörter (⌥1–⌥9)", "A keyword set needs a name": "Ein Stichwortsatz braucht einen Namen", "Save as a new set": "Als neuen Satz speichern", - "Recent Keywords is built in: choose another name": "„Letzte Stichwörter“ ist vorgegeben: Wählen Sie einen anderen Namen" + "Recent Keywords is built in: choose another name": "„Letzte Stichwörter“ ist vorgegeben: Wählen Sie einen anderen Namen", + "Import Keywords…": "Stichwörter importieren…", + "Export Keywords…": "Stichwörter exportieren…", + "Import Keywords": "Stichwörter importieren", + "Export Keywords": "Stichwörter exportieren", + "Keyword Lists": "Stichwortlisten" } diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index 04f321003..e4af9a0a4 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "Se añadió “{keyword}” a {n} foto{}", "On {have} of {of} selected photos": "En {have} de {of} fotos seleccionadas", "Remove “{name}”": "Quitar “{name}”", - "There is a keyword set “{name}” already": "Ya hay un conjunto de palabras clave “{name}”" + "There is a keyword set “{name}” already": "Ya hay un conjunto de palabras clave “{name}”", + "Added {n} keyword{}": "Se añadieron {n} palabra{} clave", + "; {n} gained synonyms": "; {n} recibieron sinónimos", + "Exported {n} keyword{}": "Se exportaron {n} palabra{} clave", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One no importa una lista con ; , < o > en una palabra clave: {names}" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 7be7d02c2..173d4fbac 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1766,5 +1766,10 @@ "Keywords (⌥1–⌥9)": "Palabras clave (⌥1–⌥9)", "A keyword set needs a name": "Un conjunto de palabras clave necesita un nombre", "Save as a new set": "Guardar como conjunto nuevo", - "Recent Keywords is built in: choose another name": "“Palabras clave recientes” es fijo: elige otro nombre" + "Recent Keywords is built in: choose another name": "“Palabras clave recientes” es fijo: elige otro nombre", + "Import Keywords…": "Importar palabras clave…", + "Export Keywords…": "Exportar palabras clave…", + "Import Keywords": "Importar palabras clave", + "Export Keywords": "Exportar palabras clave", + "Keyword Lists": "Listas de palabras clave" } diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index cadc6834f..f28f8025e 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "写真{n}枚に「{keyword}」を追加しました{:.0}", "On {have} of {of} selected photos": "選択した写真{of}枚のうち{have}枚", "Remove “{name}”": "「{name}」を削除", - "There is a keyword set “{name}” already": "キーワードセット「{name}」はすでにあります" + "There is a keyword set “{name}” already": "キーワードセット「{name}」はすでにあります", + "Added {n} keyword{}": "{n}件のキーワードを追加しました{:.0}", + "; {n} gained synonyms": "。{n}件に同義語を追加", + "Exported {n} keyword{}": "{n}件のキーワードを書き出しました{:.0}", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One は ; , < > を含むキーワードがあるリストを読み込みません: {names}" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 3c9a6cf5a..392a58318 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1766,5 +1766,10 @@ "Keywords (⌥1–⌥9)": "キーワード (⌥1–⌥9)", "A keyword set needs a name": "キーワードセットには名前が必要です", "Save as a new set": "新しいセットとして保存", - "Recent Keywords is built in: choose another name": "「最近使用したキーワード」は組み込みです。別の名前を選んでください" + "Recent Keywords is built in: choose another name": "「最近使用したキーワード」は組み込みです。別の名前を選んでください", + "Import Keywords…": "キーワードを読み込み…", + "Export Keywords…": "キーワードを書き出し…", + "Import Keywords": "キーワードを読み込み", + "Export Keywords": "キーワードを書き出し", + "Keyword Lists": "キーワードリスト" } diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index f0bbcb2c8..9caa5832c 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "“{keyword}” adicionada a {n} foto{}", "On {have} of {of} selected photos": "Em {have} de {of} fotos selecionadas", "Remove “{name}”": "Remover “{name}”", - "There is a keyword set “{name}” already": "Já existe um conjunto de palavras-chave “{name}”" + "There is a keyword set “{name}” already": "Já existe um conjunto de palavras-chave “{name}”", + "Added {n} keyword{}": "Palavras-chave adicionadas: {n}{:.0}", + "; {n} gained synonyms": "; {n} receberam sinônimos", + "Exported {n} keyword{}": "Palavras-chave exportadas: {n}{:.0}", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — O Capture One não importa uma lista com ; , < ou > em uma palavra-chave: {names}" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index e2c3f5827..169460ecc 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1766,5 +1766,10 @@ "Keywords (⌥1–⌥9)": "Palavras-chave (⌥1–⌥9)", "A keyword set needs a name": "Um conjunto de palavras-chave precisa de um nome", "Save as a new set": "Salvar como novo conjunto", - "Recent Keywords is built in: choose another name": "“Palavras-chave recentes” é fixo: escolha outro nome" + "Recent Keywords is built in: choose another name": "“Palavras-chave recentes” é fixo: escolha outro nome", + "Import Keywords…": "Importar palavras-chave…", + "Export Keywords…": "Exportar palavras-chave…", + "Import Keywords": "Importar palavras-chave", + "Export Keywords": "Exportar palavras-chave", + "Keyword Lists": "Listas de palavras-chave" } diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index 66f9cfd55..9857ebe8c 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "«{keyword}» добавлено к фото ({n}){:.0}", "On {have} of {of} selected photos": "На {have} из {of} выбранных фото", "Remove “{name}”": "Убрать «{name}»", - "There is a keyword set “{name}” already": "Набор ключевых слов «{name}» уже есть" + "There is a keyword set “{name}” already": "Набор ключевых слов «{name}» уже есть", + "Added {n} keyword{}": "Добавлено ключевых слов: {n}{:.0}", + "; {n} gained synonyms": "; синонимы добавлены к {n}", + "Exported {n} keyword{}": "Экспортировано ключевых слов: {n}{:.0}", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One не импортирует список, где в ключевом слове есть ; , < или >: {names}" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 40cae6c3b..e60981012 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1442,5 +1442,10 @@ "Set name": "Название набора", "Save as a new set": "Сохранить как новый набор", "Recent Keywords is built in: choose another name": "«Недавние ключевые слова» — встроенный набор: выберите другое название", - "Recent Keywords": "Недавние ключевые слова" + "Recent Keywords": "Недавние ключевые слова", + "Import Keywords…": "Импорт ключевых слов…", + "Export Keywords…": "Экспорт ключевых слов…", + "Import Keywords": "Импорт ключевых слов", + "Export Keywords": "Экспорт ключевых слов", + "Keyword Lists": "Списки ключевых слов" } diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index b329eef6c..ed682c803 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "已将“{keyword}”添加到 {n} 张照片{:.0}", "On {have} of {of} selected photos": "所选 {of} 张照片中的 {have} 张", "Remove “{name}”": "移除“{name}”", - "There is a keyword set “{name}” already": "已有关键字集“{name}”" + "There is a keyword set “{name}” already": "已有关键字集“{name}”", + "Added {n} keyword{}": "已添加 {n} 个关键字{:.0}", + "; {n} gained synonyms": ";{n} 个获得了同义词", + "Exported {n} keyword{}": "已导出 {n} 个关键字{:.0}", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One 不会导入关键字中含有 ; , < 或 > 的列表:{names}" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 2726ff589..095f27d78 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1766,5 +1766,10 @@ "Keywords (⌥1–⌥9)": "关键字 (⌥1–⌥9)", "A keyword set needs a name": "关键字集需要名称", "Save as a new set": "另存为新关键字集", - "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称" + "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称", + "Import Keywords…": "导入关键字…", + "Export Keywords…": "导出关键字…", + "Import Keywords": "导入关键字", + "Export Keywords": "导出关键字", + "Keyword Lists": "关键字列表" } diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index ba8462202..07b04a79c 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -216,5 +216,9 @@ "Added “{keyword}” to {n} photo{}": "已將「{keyword}」加入 {n} 張照片{:.0}", "On {have} of {of} selected photos": "選取的 {of} 張照片中的 {have} 張", "Remove “{name}”": "移除「{name}」", - "There is a keyword set “{name}” already": "已有關鍵字集「{name}」" + "There is a keyword set “{name}” already": "已有關鍵字集「{name}」", + "Added {n} keyword{}": "已加入 {n} 個關鍵字{:.0}", + "; {n} gained synonyms": ";{n} 個獲得了同義詞", + "Exported {n} keyword{}": "已匯出 {n} 個關鍵字{:.0}", + " — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One 不會匯入關鍵字中含有 ; , < 或 > 的清單:{names}" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index b0c8d19be..e38564f23 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1766,5 +1766,10 @@ "Keywords (⌥1–⌥9)": "關鍵字 (⌥1–⌥9)", "A keyword set needs a name": "關鍵字集需要名稱", "Save as a new set": "另存為新關鍵字集", - "Recent Keywords is built in: choose another name": "「最近使用的關鍵字」為內建集:請另選名稱" + "Recent Keywords is built in: choose another name": "「最近使用的關鍵字」為內建集:請另選名稱", + "Import Keywords…": "匯入關鍵字…", + "Export Keywords…": "匯出關鍵字…", + "Import Keywords": "匯入關鍵字", + "Export Keywords": "匯出關鍵字", + "Keyword Lists": "關鍵字清單" } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index dfaff2c6a..27dd89155 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -42,6 +42,8 @@ mod tests_grid; #[cfg(test)] mod tests_keymap; #[cfg(test)] +mod tests_keyword_files; +#[cfg(test)] mod tests_keyword_list; #[cfg(test)] mod tests_keyword_set; @@ -124,6 +126,10 @@ pub struct Services { pub pick_tracklog: Option, /// Save dialog for an exported `.lcpreset` file. pub save_preset_file: Option, + /// Open dialog for a keyword list file (`.txt`, Photo Supreme's `.utf8`; File ▸ Import Keywords…). + pub pick_keyword_list: Option, + /// Save dialog for an exported keyword list (File ▸ Export Keywords…). + pub save_keyword_list: Option, /// Open dialog for point-curve preset files (`.lccurve`). pub pick_curve_preset_files: Option, /// Save dialog for an exported `.lccurve` file. diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index 7d964856e..34c83048f 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -77,6 +77,8 @@ const LAYOUT: &[(&str, &[&str])] = &[ "---", "file.importPresets", "file.exportPresets", + "file.importKeywords", + "file.exportKeywords", "---", "dialog.export", "app.exportPrevious", diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 73e62d383..9f3a3918f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -164,6 +164,8 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("app.quit", "Quit LightCraft", Some("Cmd+Q"), "File"), ("file.importPresets", "Import Profiles & Presets…", None, "File"), ("file.exportPresets", "Export Presets…", None, "File"), + ("file.importKeywords", "Import Keywords…", None, "File"), + ("file.exportKeywords", "Export Keywords…", None, "File"), // Edit panel ▸ Curve ▸ Point Curve dropdown ("file.importCurvePresets", "Import Point Curve Presets…", None, ""), ("file.exportCurvePresets", "Export Point Curve Presets…", None, ""), @@ -1346,6 +1348,68 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { + // a keyword list file: Lightroom Classic's, Capture One's, Photo Supreme's (.utf8) + let path = match p.get("path").and_then(Value::as_str) { + Some(x) => x.to_string(), + None => { + let req = PickRequest::file(crate::i18n::tr("Import Keywords"), crate::i18n::tr("Keyword Lists"), &["txt", "utf8"]); + match crate::pick::ask(app, id, p, "path", req, |s| s.pick_keyword_list.as_mut().map(|f| f())) { + Picked::Now(v) => match v.into_iter().next() { + Some(x) => x, + None => return Some(Ok(Value::Null)), + }, + Picked::Later => return Some(Ok(Value::Null)), + Picked::Unavailable => return Some(Err("no file dialog on this platform".into())), + } + } + }; + let r = app.session.execute("keyword.import", &json!({"path": path})).map_err(|e| e.to_string()); + match &r { + Ok(v) => { + let (added, updated) = (v["added"].as_u64().unwrap_or(0), v["updated"].as_u64().unwrap_or(0)); + let mut msg = crate::i18n::tr_format!("Added {n} keyword{}", if added == 1 { "" } else { "s" }, n = added); + if updated > 0 { + msg.push_str(&crate::i18n::tr_format!("; {n} gained synonyms", n = updated)); + } + app.toast(&ctx, msg); + } + Err(e) => app.toast(&ctx, e.clone()), + } + return Some(r); + } + "file.exportKeywords" => { + let path = match p.get("path").and_then(Value::as_str) { + Some(x) => x.to_string(), + None => { + let name = "Keywords.txt"; + let req = PickRequest::save(crate::i18n::tr("Export Keywords"), crate::i18n::tr("Keyword Lists"), &["txt"], name); + match crate::pick::ask(app, id, p, "path", req, |s| s.save_keyword_list.as_mut().map(|f| f(name).into_iter().collect())) { + Picked::Now(v) => match v.into_iter().next() { + Some(x) => x, + None => return Some(Ok(Value::Null)), + }, + Picked::Later => return Some(Ok(Value::Null)), + Picked::Unavailable => return Some(Err("no file dialog on this platform".into())), + } + } + }; + let r = app.session.execute("keyword.export", &json!({"path": path})).map_err(|e| e.to_string()); + if let Ok(v) = &r { + let n = v["keywords"].as_u64().unwrap_or(0); + let mut msg = crate::i18n::tr_format!("Exported {n} keyword{}", if n == 1 { "" } else { "s" }, n = n); + // Capture One's importer refuses ; , < > in a list: say which to rename first + let refuses: Vec<&str> = v["captureOneRefuses"].as_array().into_iter().flatten().filter_map(Value::as_str).collect(); + if !refuses.is_empty() { + msg.push_str(&crate::i18n::tr_format!( + " — Capture One won't import a list with ; , < or > in a keyword: {names}", + names = refuses.iter().take(5).copied().collect::>().join(" · ") + )); + } + app.toast(&ctx, msg); + } + return Some(r); + } "file.exportPresets" => { let group = p.get("group").and_then(Value::as_str).map(str::to_string); let path = match p.get("path").and_then(Value::as_str) { diff --git a/crates/ui-egui/src/panels/keyword_list.rs b/crates/ui-egui/src/panels/keyword_list.rs index e315d1bda..b670010ea 100644 --- a/crates/ui-egui/src/panels/keyword_list.rs +++ b/crates/ui-egui/src/panels/keyword_list.rs @@ -605,6 +605,8 @@ mod tests { "Recent Keywords", "Keywords (⌥1–⌥9)", "Save as a new set", + "Import Keywords…", + "Export Keywords…", ]; 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/tests_keyword_files.rs b/crates/ui-egui/src/tests_keyword_files.rs new file mode 100644 index 000000000..3eeed518b --- /dev/null +++ b/crates/ui-egui/src/tests_keyword_files.rs @@ -0,0 +1,76 @@ +//! File ▸ Import Keywords… / Export Keywords…: keyword list files (Lightroom Classic's format, +//! which Capture One and Photo Supreme read and write too). + +use std::time::Duration; + +use serde_json::json; + +use crate::headless::Headless; +use crate::{LightcraftApp, Services}; + +const T: Duration = Duration::from_secs(20); +const SETTLE: Duration = Duration::from_secs(120); + +fn app() -> Headless { + let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); + let mut h = Headless::new(app, [1200.0, 800.0], 1.0); + h.settle(SETTLE); + h +} + +fn temp_dir(tag: &str) -> std::path::PathBuf { + let d = std::env::temp_dir().join(format!("lc-ui-keyword-files-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d +} + +fn run(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value { + let r = h.request("engine.execute", json!({"command": command, "params": params}), T); + h.settle(SETTLE); + r +} + +fn toast(h: &Headless) -> String { + h.app.ui.toast.clone().map(|t| t.0).unwrap_or_default() +} + +/// The File menu offers Import Keywords… and Export Keywords…. +#[test] +fn the_file_menu_offers_keyword_files() { + let mut h = app(); + let r = h.request("ui.menu.list", json!({}), T); + let items = r["result"].to_string(); + for id in ["file.importKeywords", "file.exportKeywords"] { + assert!(items.contains(id), "{id}"); + } +} + +/// Import Keywords… reads a list and says how many keywords it added. +#[test] +fn importing_keywords_says_what_it_added() { + let dir = temp_dir("import"); + let mut h = app(); + let path = dir.join("vocabulary.utf8"); + std::fs::write(&path, "[Places]\r\n\tPortugal\r\n\t\tLisbon\r\n").unwrap(); + let r = run(&mut h, "file.importKeywords", json!({"path": path.to_string_lossy()})); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.app.session.catalog.has_keyword("Places|Portugal|Lisbon")); + assert!(toast(&h).contains("3"), "{}", toast(&h)); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Export Keywords… writes the list, and warns about the keywords Capture One won't import. +#[test] +fn exporting_keywords_warns_about_capture_one() { + let dir = temp_dir("export"); + let mut h = app(); + let id = h.app.session.visible_cloned()[0].0; + run(&mut h, "photo.setMeta", json!({"ids": [id], "addKeywords": ["fish, chips"]})); + let path = dir.join("keywords.txt"); + let r = run(&mut h, "file.exportKeywords", json!({"path": path.to_string_lossy()})); + assert_eq!(r["ok"], true, "{r}"); + assert!(std::fs::read_to_string(&path).unwrap().contains("fish, chips")); + assert!(toast(&h).contains("Capture One"), "{}", toast(&h)); + let _ = std::fs::remove_dir_all(&dir); +}