mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 13:07:43 -06:00
LR-LIB-PEOPLE: preserve later person navigation on current main
Port the original Escape return and last-person shortcut behavior, keeping current translations and a no-wrap chip. The existing headless regressions and all seven quality gates pass with the hosted fonts on the refreshed recognition dependency chain. Generated with Codex.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1642,5 +1642,6 @@
|
||||
"≤ at most": "≤ no máximo",
|
||||
"≥ at least": "≥ no mínimo",
|
||||
"日本語": "日本語",
|
||||
"繁體中文(台灣)": "繁體中文(台灣)"
|
||||
"繁體中文(台灣)": "繁體中文(台灣)",
|
||||
"Back to this person": "Back to this person"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -318,8 +318,12 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
Ok(json!({"show": app.ui.face_boxes}))
|
||||
}
|
||||
"view.people" => {
|
||||
// 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<Re
|
||||
return Some(Err("view.person: missing `name`".into()));
|
||||
};
|
||||
app.ui.view = ViewMode::People;
|
||||
app.ui.person_from = None;
|
||||
app.ui.person_page = Some(name.to_string());
|
||||
Ok(json!({"person": name}))
|
||||
}
|
||||
@@ -381,7 +386,14 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
} else if matches!(app.ui.view, ViewMode::Compare | ViewMode::Survey) {
|
||||
app.ui.view = ViewMode::Detail;
|
||||
} else if app.ui.view == ViewMode::Detail {
|
||||
app.ui.view = ViewMode::PhotoGrid;
|
||||
// a photo opened from a person's page goes back to that page, any other to the grid
|
||||
match app.ui.person_from.take() {
|
||||
Some((name, photo)) if app.session.active().is_some_and(|a| a.0 == photo) => {
|
||||
app.ui.view = ViewMode::People;
|
||||
app.ui.person_page = Some(name);
|
||||
}
|
||||
_ => app.ui.view = ViewMode::PhotoGrid,
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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<String>,
|
||||
/// "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(),
|
||||
|
||||
Reference in New Issue
Block a user