diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 2a4721864..520c73f9f 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1797,5 +1797,6 @@ "How to install the model": "So installierst du das Modell", "Install the SAM 3 Model": "SAM-3-Modell installieren", "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Diese Version kann das Modell noch nicht herunterladen. Zum Installieren von Hand lege model.safetensors, vocab.json und merges.txt aus Metas facebook/sam3 in den obigen Ordner: Objekt und Beschreiben funktionieren, sobald sie dort liegen.", - "Open Log Folder": "Protokollordner öffnen" + "Open Log Folder": "Protokollordner öffnen", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index c0986c191..1c8b00135 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1642,5 +1642,6 @@ "Captured from": "Capturadas desde", "Captured until": "Capturadas hasta", "Only": "Solo", - "Open Log Folder": "Abrir carpeta de registros" + "Open Log Folder": "Abrir carpeta de registros", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 31c61a7a7..afc2fe44b 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1642,5 +1642,6 @@ "How to install the model": "モデルのインストール方法", "Install the SAM 3 Model": "SAM 3モデルのインストール", "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "このビルドではまだモデルをダウンロードできません。手動でインストールするには、Meta の facebook/sam3 の model.safetensors、vocab.json、merges.txt を上のフォルダに置いてください。置いた時点で「オブジェクト」と「説明で選択」が使えるようになります。", - "Open Log Folder": "ログフォルダーを開く" + "Open Log Folder": "ログフォルダーを開く", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 03ae0773f..be7d22baa 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1642,5 +1642,6 @@ "≤ at most": "≤ no máximo", "≥ at least": "≥ no mínimo", "日本語": "日本語", - "繁體中文(台灣)": "繁體中文(台灣)" + "繁體中文(台灣)": "繁體中文(台灣)", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 530cdfcbb..f1d33491c 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1312,5 +1312,6 @@ "of the following rules:": "из следующих условий:", "saved in": "сохранено в", "this photo": "это фото", - "− harder border · + softer border": "− жёстче край · + мягче край" + "− harder border · + softer border": "− жёстче край · + мягче край", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index cea313d01..3d88dcc25 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1642,5 +1642,6 @@ "How to install the model": "如何安装模型", "Install the SAM 3 Model": "安装 SAM 3 模型", "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。", - "Open Log Folder": "打开日志文件夹" + "Open Log Folder": "打开日志文件夹", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 597ecbf9a..085ccadc5 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1642,5 +1642,6 @@ "How to install the model": "如何安裝模型", "Install the SAM 3 Model": "安裝 SAM 3 模型", "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚無法下載模型。如需手動安裝,請將 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的資料夾:放入後即可使用「物件」與「描述」。", - "Open Log Folder": "開啟記錄資料夾" + "Open Log Folder": "開啟記錄資料夾", + "Back to this person": "Back to this person" } diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index b9ad67286..cddfb43b2 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -715,10 +715,30 @@ mod tests { h.step(); h.step(); assert_eq!((h.app.ui.view, h.app.session.active()), (ViewMode::Detail, Some(ids[1]))); - // back to the page, then each way out of it - h.request("engine.execute", json!({"command": "view.person", "params": {"name": "Jane Doe"}}), t); + // Escape from that photo goes back to the page, not to the grid + h.request("ui.key", json!({"key": "escape"}), t); h.step(); assert_eq!((h.app.ui.view, h.app.ui.person_page.as_deref()), (ViewMode::People, Some("Jane Doe"))); + // the People button from a photo shows everyone, with the person last left one click away next to the title + h.request("ui.clickWidget", json!({"id": format!("person-face:{}:0", ids[1].0)}), t); + h.step(); + h.step(); + assert_eq!(h.app.ui.view, ViewMode::Detail); + open_people(&mut h); + assert_eq!((h.app.ui.view, h.app.ui.person_page.clone()), (ViewMode::People, None)); + h.step(); + let r = h.request("ui.clickWidget", json!({"id": "people:last"}), t); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + h.step(); + assert_eq!((h.app.ui.view, h.app.ui.person_page.as_deref()), (ViewMode::People, Some("Jane Doe"))); + // a photo reached any other way goes back to the grid on Escape + h.request("engine.execute", json!({"command": "view.detail"}), t); + h.request("ui.key", json!({"key": "escape"}), t); + h.step(); + assert_eq!(h.app.ui.view, ViewMode::PhotoGrid); + h.request("engine.execute", json!({"command": "view.person", "params": {"name": "Jane Doe"}}), t); + h.step(); let r = h.request("ui.clickWidget", json!({"id": "person:back"}), t); assert_eq!(r["ok"], true, "{r}"); h.step(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 3e807fe1a..d4c716b40 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -318,8 +318,12 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { + // Everyone; the page just left stays one click away (the chip next to the title) + if let Some(name) = app.ui.person_page.take() { + app.ui.last_person = Some(name); + } + app.ui.person_from = None; app.ui.view = ViewMode::People; - app.ui.person_page = None; Ok(json!({"people": app.session.catalog.people().len()})) } "view.person" => { @@ -328,6 +332,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option Option { + app.ui.view = ViewMode::People; + app.ui.person_page = Some(name); + } + _ => app.ui.view = ViewMode::PhotoGrid, + } } Ok(Value::Null) } diff --git a/crates/ui-egui/src/panels/people.rs b/crates/ui-egui/src/panels/people.rs index 7a7307efd..6fdf032f3 100644 --- a/crates/ui-egui/src/panels/people.rs +++ b/crates/ui-egui/src/panels/people.rs @@ -7,7 +7,7 @@ //! every face is cut from the same kind of box (the detector's, once the scan has looked at it), so faces are shown //! equally close. Only the rows on screen ask for a face render (the engine caches them, memory and disk). -use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; +use egui::{Align2, Color32, Rect, RichText, Sense, Stroke, StrokeKind, pos2, vec2}; use lightcraft_catalog::Person; use serde_json::json; @@ -65,7 +65,19 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let thumb = app.ui.thumb_size; let people = app.caches.people(&app.session.catalog, &app.session.filter); let (head, _) = ui.allocate_exact_size(vec2(ui.available_width(), HEADER_H), Sense::hover()); - ui.painter().text(pos2(head.left() + PAD, head.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Named People"), t.semibold(15.0), t.text); + let title = + ui.painter().text(pos2(head.left() + PAD, head.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Named People"), t.semibold(15.0), t.text); + // the person page last left (a face opened from it, or its back button): one click returns to it + if let Some(last) = app.ui.last_person.clone().filter(|n| people.iter().any(|p| &p.name == n)) { + let label = fit(ui.painter(), &format!("{last} ›"), t.font(13.0), 240.0); + let width = ui.painter().layout_no_wrap(label.clone(), t.font(13.0), Color32::WHITE).size().x + 24.0; + let r = Rect::from_min_size(pos2(title.right() + 14.0, head.center().y - 12.0), vec2(width, 24.0)); + let b = ui.put(r, egui::Button::new(RichText::new(label).font(t.font(13.0))).wrap_mode(egui::TextWrapMode::Extend)); + register(ui.ctx(), "people:last", b.rect); + if b.on_hover_text(crate::i18n::tr("Back to this person")).clicked() { + let _ = app.run("view.person", json!({"name": last})); + } + } ui.painter().text(pos2(head.right() - PAD, head.center().y), Align2::RIGHT_CENTER, people.len().to_string(), t.font(13.0), t.text_dim); // until face recognition is set up: what it takes, and a button that does the next step super::faces::setup_banner(app, ui); diff --git a/crates/ui-egui/src/panels/person.rs b/crates/ui-egui/src/panels/person.rs index 73472d430..9ebf09a64 100644 --- a/crates/ui-egui/src/panels/person.rs +++ b/crates/ui-egui/src/panels/person.rs @@ -208,6 +208,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, name: &str) { Hit::Open => { let _ = app.run("library.select", json!({"ids": [f.photo], "active": f.photo})); let _ = app.run("view.detail", json!({})); + // Escape comes back to this page; the People button to everyone, with this person next to its title + app.ui.person_from = Some((shown_name.clone(), f.photo)); + app.ui.last_person = Some(shown_name.clone()); + app.ui.person_page = None; } Hit::Confirm => { let _ = app.run("faces.setName", json!({"id": f.photo, "index": f.index, "name": shown_name})); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 20e69b008..66084cf12 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -338,6 +338,12 @@ pub struct UiState { /// The person whose page the People view shows (their faces and the faces that look like them); `None`: everyone. #[serde(skip)] pub person_page: Option, + /// The person page a photo in the loupe was opened from, and that photo: Escape goes back to the page (not the grid). + #[serde(skip)] + pub person_from: Option<(String, u64)>, + /// The last person page left: the People view offers a way back to it next to its title. + #[serde(skip)] + pub last_person: Option, /// "More" faces the user hid with ×, for this session: (photo, region index). #[serde(skip)] pub dismissed_faces: std::collections::HashSet<(u64, usize)>, @@ -687,6 +693,8 @@ impl Default for UiState { collapsed_sidebar: Vec::new(), name_edit: None, person_page: None, + person_from: None, + last_person: None, dismissed_faces: Default::default(), unnamed_selected: Default::default(), unnamed_name: String::new(),