mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-11 16:31:32 -06:00
ui: first-run onboarding tour and What's New after an update (#2206)
* ui: first-run onboarding tour and What's New after an update The very first launch now opens a short Welcome tour (six pages over the real views: import, the Photo Grid, Detail, Assisted Culling, export and print, and driving the app with agents over MCP), and after the app has been updated the existing What's New dialog opens once on its own. Both are skipped once the running version has been seen. - UiState.seen_version: the last version whose startup dialog was shown (serde default/skip, so older ui.json files read as None = first run). - new startup module: parse_version, startup_dialog (FirstRun only when nothing was ever seen; WhatsNew when the version changed, comparing through parse_version and falling back to string difference when either side is unparseable), and open_at_startup — headless hosts (snapshots, scripted runs, tests: session-local state) never gain a modal. - UiState::record_startup_dialog_seen records on show, so a crash mid-dialog does not re-trigger it on every launch; ui.json saves every few seconds and at exit, as before. - Dialog::Welcome renders the tour with Back / Skip / Next (Get Started on the last page); every string goes through tr(), every menu path names a real command (file.addPhotos, view.photoGrid, view.detail, dialog.cull, dialog.export, dialog.print). - the decision runs once on the first frame (ui-egui, so desktop and web both get it); headless hosts and already-seen versions open nothing. * ui: the integration harnesses are headless hosts The accessibility and docking tests build the app directly and drive it through logic() and ui(). On a fresh UI state the first frame therefore opened the Welcome tour: its modal took the Tab focus the accessibility tests walk (canvas_tool_sliders_are_reachable_with_tab, tab_walks_the_right_panels_top_to_bottom) and the pointer the docking drag aims at (real_drag_float_close_reopen_and_state_reload_keep_photo_tool_mode). Mark both harnesses as headless hosts, as the Headless harness already does: a headless host never gains a first-run modal. * ui: translate the first-run tour into the 13 catalogs The 21 strings of the first-run tour (its six pages, its window title and its Next and Get Started labels) were in no catalog, so every language showed the tour in English. They are now in each of the 13 catalogs under crates/ui-egui/locales/, with the placeholders and punctuation of the English kept: the ellipses, the menu separators, the middle dots, the parentheses around shortcuts and the final full stop. ka.json stays key-sorted; the other catalogs get the keys appended. WELCOME_TITLE, WELCOME_NEXT and WELCOME_START name the three labels that are not part of WELCOME_PAGES, so one test can check all 21 keys: the_welcome_tour_is_translated_in_every_catalog. It fails with the catalogs as they were. The translations were written without a native speaker's review and need one for each language before release. * ui: translate the first-run tour into the four new catalogs main added sv, fi, id and vi after this branch was cut, and the_welcome_tour_is_translated_in_every_catalog iterates Locale::ALL, so the tour's 21 keys were missing there. Same block, same place (after "Visualize HDR"), in each catalog's own terms. * ui: the lifecycle gesture test is a headless host main's egui-037 lifecycle tests (added after this branch was cut) build the app directly and drive it through logic() and ui(), so the first frame opened the Welcome tour there: its modal took the dialog the delivered gesture under test opens (a_delivered_gesture_may_change_the_state_it_acted_on saw the tour where it expected View Options). Mark that one test's app as a headless host, as the accessibility and docking harnesses already are: a headless host never gains a first-run modal. The flag stays scoped to this test because the file's screenshot tests exercise the headless-capture fallback, which a headless host short-circuits. * ui: build the tour's shortcut chords from the platform bindings WELCOME_PAGES hard-coded Cmd+Shift+I, D and Cmd+P in its prose, and the renderer translated those sentences verbatim, so Windows, Linux and FreeBSD users were shown keys the app does not bind. The three pages that name a shortcut now take the command's live binding at render time (shortcuts::shortcut_of) and paint it the way the menus paint theirs (menubar::shortcut_text: a symbol chord on macOS, Ctrl+... elsewhere); no key is written in the source strings any more. A command whose binding was removed shows the menu path alone. The chorded sentences move from the message catalogs into tr_format! messages (placeholders are format!-checked in every language), translated in all 17 catalogs' -formats.json; the catalog keys stay, chordless, as the no-binding fallback, and the_tour_is_translated_in_every_catalog pins both per language. * ui: the tour's Develop page names the Before/After command, not the Y key The page's prose still hard-coded Y as the before/after key: remap or remove that binding and the tour sends users to a command that no longer runs. The body now names View › Before/After Side by Side the way the other pages name their commands, and every catalog translates the new sentence with its own menu label. * ui: record the startup dialog once its input is not blocked * Keep headless UI tests independent of onboarding Assisted-by: OpenAI Codex * Tests: integration harnesses mark the app as a headless host Services has no headless field; the app does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: Find Similar waits for the worker to set the row total It lost that race twice on the Windows gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
eliets2
FredApps
FredApps
parent
fe632e05f9
commit
dcc8a62910
@@ -600,6 +600,9 @@ impl WebApp {
|
||||
let mut saved = serde_json::from_slice::<serde_json::Value>(&ui_written).unwrap_or_default();
|
||||
if let Some(fields) = saved.as_object_mut() {
|
||||
lightcraft_ui_egui::docking::drop_outdated(fields);
|
||||
// a stored ui.json means an installation that has been run: due What's New, never
|
||||
// the first-run tour (the same migration the desktop settings loader runs)
|
||||
lightcraft_ui_egui::startup::migrate_legacy_seen_version(fields);
|
||||
}
|
||||
if let Ok(ui) = serde_json::from_value::<UiState>(saved) {
|
||||
app.ui = ui;
|
||||
|
||||
@@ -67,6 +67,11 @@ fn decode_saved_ui(bytes: &[u8]) -> Result<(UiState, bool), String> {
|
||||
if recovered {
|
||||
fields.remove("docking");
|
||||
}
|
||||
// A settings file from before the startup dialog existed has no `seenVersion` key: the file
|
||||
// is there, so this is an installation that has been run, due What's New — never the
|
||||
// first-run tour. Shared with the browser's loader, which runs the same migration on its
|
||||
// stored `ui.json` (`startup::migrate_legacy_seen_version` carries the full story).
|
||||
lightcraft_ui_egui::startup::migrate_legacy_seen_version(fields);
|
||||
let ui = serde_json::from_value(saved).map_err(|error| error.to_string())?;
|
||||
Ok((ui, recovered))
|
||||
}
|
||||
|
||||
@@ -223,6 +223,42 @@ fn memory_session_leaves_a_damaged_ui_json_alone() {
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
/// A settings file from before the startup dialog existed (no `seenVersion`) belongs to an
|
||||
/// installation that has been run: its first launch after the update shows What's New, never
|
||||
/// the first-run tour. A file this version saved before the dialog was presented carries an
|
||||
/// explicit `null` and is not migrated; only a missing file stays a first run as well.
|
||||
#[test]
|
||||
fn legacy_ui_json_shows_whats_new_not_the_first_run_tour() {
|
||||
let d = dir("legacy-startup");
|
||||
let path = d.join("ui.json");
|
||||
std::fs::write(&path, br#"{"language":"en","settings":{"startupView":"detail"}}"#).unwrap();
|
||||
let (ui, warning, keep) = load_prefs_at(&path, true);
|
||||
assert!(warning.is_none() && !keep);
|
||||
let ui = ui.unwrap();
|
||||
assert_eq!(ui.seen_version.as_deref(), Some(lightcraft_ui_egui::startup::LEGACY_SEEN_VERSION));
|
||||
assert_eq!(
|
||||
lightcraft_ui_egui::startup::startup_dialog(ui.seen_version.as_deref(), env!("CARGO_PKG_VERSION")),
|
||||
Some(lightcraft_ui_egui::startup::StartupDialog::WhatsNew)
|
||||
);
|
||||
// a file that already records the running version is migrated nowhere
|
||||
std::fs::write(&path, format!(r#"{{"seenVersion":"{}"}}"#, env!("CARGO_PKG_VERSION"))).unwrap();
|
||||
let (ui, warning, _) = load_prefs_at(&path, true);
|
||||
assert!(warning.is_none());
|
||||
assert_eq!(ui.unwrap().seen_version.as_deref(), Some(env!("CARGO_PKG_VERSION")));
|
||||
// an explicit `null` is this version's "not presented yet" (the launch chooser saves while
|
||||
// the tour still waits): it stays a first run, not a pre-feature file
|
||||
std::fs::write(&path, br#"{"language":"en","seenVersion":null}"#).unwrap();
|
||||
let (ui, warning, _) = load_prefs_at(&path, true);
|
||||
assert!(warning.is_none());
|
||||
let ui = ui.unwrap();
|
||||
assert_eq!(ui.seen_version, None);
|
||||
assert_eq!(
|
||||
lightcraft_ui_egui::startup::startup_dialog(ui.seen_version.as_deref(), env!("CARGO_PKG_VERSION")),
|
||||
Some(lightcraft_ui_egui::startup::StartupDialog::FirstRun)
|
||||
);
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docking_survives_native_settings_file_reload() {
|
||||
use lightcraft_ui_egui::docking::Panel;
|
||||
|
||||
@@ -321,6 +321,9 @@
|
||||
"Panel move was not applied: {error}": "Přesun panelu nebyl použit: {error}",
|
||||
"{name} is installed": "{name} je nainstalován",
|
||||
"{name} is installed and in use: face recognition is on": "{name} je nainstalován a používán: rozpoznávání obličejů je zapnuto",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Fotografie importujete přes Soubor › Importovat fotografie… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Zobrazení › Zobrazení detailu ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Soubor › Exportovat… · Soubor › Tisk… ({keys}).",
|
||||
"{n} folder{} could not be read": "Složky, které se nepodařilo přečíst: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "Přeskočené soubory (nejde o podporovaný typ fotografie): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -2450,5 +2450,27 @@
|
||||
"Top bar": "Horní lišta",
|
||||
"Tool strip": "Pruh nástrojů",
|
||||
"Bottom bar": "Dolní lišta",
|
||||
"Welcome to LightCraft": "Vítejte v LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Rychlá a nedestruktivní knihovna fotografií a vývojář RAW: originály zůstanou nedotčené a každou úpravu lze později znovu prohlédnout nebo vrátit.",
|
||||
"Bring photos in with File › Import Photos…": "Fotografie importujete přes Soubor › Importovat fotografie…",
|
||||
"Find and organize": "Najděte a uspořádejte",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Mřížka fotografií skládá knihovnu do zarovnaných řádků: hodnoťte, označujte barevnými štítky, filtrujte a řaďte fotografie do alb.",
|
||||
"View › Photo Grid.": "Zobrazení › Mřížka fotografií.",
|
||||
"Develop a photo": "Upravte fotografii",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Otevřete fotografii a upravte ji: expozice, barvy, detaily a masky, to vše nedestruktivně. Pomocí Zobrazení › Před/Po vedle sebe porovnáte stav před a po.",
|
||||
"View › Detail View.": "Zobrazení › Zobrazení detailu.",
|
||||
"Cull a shoot": "Vytřiďte sérii snímků",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Asistované třídění hodnotí ostrost a expozici, najde série a označí rozmazané snímky jako odmítnuté, takže nejlepší fotografie rychle vyniknou.",
|
||||
"Photo › Assisted Culling…": "Fotografie › Asistované třídění…",
|
||||
"Share your work": "Sdílejte svou práci",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Exportovat vytváří JPEG, PNG, TIFF, WebP a další formáty v požadované velikosti; Tisk rozloží fotografii na stránku jako PDF.",
|
||||
"File › Export… · File › Print…": "Soubor › Exportovat… · Soubor › Tisk…",
|
||||
"File › Export…": "Soubor › Exportovat…",
|
||||
"Put agents to work": "Nechte agenty pracovat",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Vše v LightCraft je příkaz, takže AI agenti mohou přes MCP ovládat běžící aplikaci: třídit, upravovat a exportovat podle vašich pokynů.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Aplikaci spusťte s --control 7980 (nebo LIGHTCRAFT_CONTROL_PORT), poté lightcraft-cli mcp --connect připojí jednoho agenta.",
|
||||
"Welcome": "Vítejte",
|
||||
"Next": "Další",
|
||||
"Get Started": "Začít",
|
||||
"Stopped at the scan limit: there may be more photos": "Zastaveno na limitu skenování: je možné, že existují další fotografie"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "Die Bereichsanordnung konnte nicht wiederhergestellt werden: {error}",
|
||||
"Panel move was not applied: {error}": "Das Verschieben des Bereichs wurde nicht ausgeführt: {error}",
|
||||
"{name} is installed": "{name} ist installiert",
|
||||
"{name} is installed and in use: face recognition is on": "{name} ist installiert und in Gebrauch: Gesichtserkennung ist eingeschaltet"
|
||||
"{name} is installed and in use: face recognition is on": "{name} ist installiert und in Gebrauch: Gesichtserkennung ist eingeschaltet",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Fotos holen Sie mit Datei › Fotos importieren… ({keys}) in die Bibliothek.",
|
||||
"View › Detail View ({keys}).": "Ansicht › Detailansicht ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Datei › Exportieren… · Datei › Drucken… ({keys})."
|
||||
}
|
||||
|
||||
@@ -3485,5 +3485,27 @@
|
||||
"Top bar": "Obere Leiste",
|
||||
"Tool strip": "Werkzeugleiste",
|
||||
"Bottom bar": "Untere Leiste",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "G-SYNC-/FreeSync-Flimmern verhindern (gilt ab dem nächsten Start)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "G-SYNC-/FreeSync-Flimmern verhindern (gilt ab dem nächsten Start)",
|
||||
"Welcome to LightCraft": "Willkommen bei LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Eine schnelle, zerstörungsfreie Fotobibliothek und Raw-Entwickler: Ihre Originale bleiben unberührt, und jede Bearbeitung lässt sich jederzeit ansehen oder zurücksetzen.",
|
||||
"Bring photos in with File › Import Photos…": "Fotos holen Sie mit Datei › Fotos importieren…in die Bibliothek.",
|
||||
"Find and organize": "Finden und ordnen",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Das Fotoraster ordnet die Bibliothek in bündigen Zeilen an: bewerten, markieren und farbig kennzeichnen, filtern und Fotos zu Alben sammeln.",
|
||||
"View › Photo Grid.": "Ansicht › Fotoraster.",
|
||||
"Develop a photo": "Ein Foto entwickeln",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Öffnen Sie ein Foto zum Entwickeln: Belichtung, Farbe, Details und Masken, alles zerstörungsfrei. Verwenden Sie Ansicht › Vorher/Nachher nebeneinander, um vorher und nachher zu vergleichen.",
|
||||
"View › Detail View.": "Ansicht › Detailansicht.",
|
||||
"Cull a shoot": "Eine Aufnahmeserie sichten",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Die Unterstützte Fotoauswahl bewertet Schärfe und Belichtung, erkennt Serienaufnahmen und markiert unscharfe Aufnahmen zum Ablehnen, damit die besten Fotos schnell auffallen.",
|
||||
"Photo › Assisted Culling…": "Foto › Unterstützte Fotoauswahl…",
|
||||
"Share your work": "Ihre Arbeit teilen",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Export erstellt JPEG, PNG, TIFF, WebP und weitere Formate in der gewünschten Größe; Drucken ordnet ein Foto als PDF auf einer Seite an.",
|
||||
"File › Export… · File › Print…": "Datei › Exportieren… · Datei › Drucken…",
|
||||
"File › Export…": "Datei › Exportieren…",
|
||||
"Put agents to work": "Agenten an die Arbeit schicken",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Alles in LightCraft ist ein Befehl, daher können KI-Agenten die laufende App über MCP steuern: sortieren, bearbeiten und exportieren, auf Ihren Auftrag hin.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Starten Sie die App mit --control 7980 (oder LIGHTCRAFT_CONTROL_PORT), dann verbindet lightcraft-cli mcp --connect einen Agenten mit ihr.",
|
||||
"Welcome": "Willkommen",
|
||||
"Next": "Weiter",
|
||||
"Get Started": "Loslegen"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "No se pudo restablecer la disposición de paneles: {error}",
|
||||
"Panel move was not applied: {error}": "No se aplicó el movimiento del panel: {error}",
|
||||
"{name} is installed": "{name} está instalado",
|
||||
"{name} is installed and in use: face recognition is on": "{name} está instalado y en uso: el reconocimiento facial está activado"
|
||||
"{name} is installed and in use: face recognition is on": "{name} está instalado y en uso: el reconocimiento facial está activado",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Importa fotos con Archivo › Importar fotos… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Vista › Vista de detalle ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Archivo › Exportar… · Archivo › Imprimir… ({keys})."
|
||||
}
|
||||
|
||||
@@ -2404,5 +2404,27 @@
|
||||
"Top bar": "Barra superior",
|
||||
"Tool strip": "Barra de herramientas",
|
||||
"Bottom bar": "Barra inferior",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Evitar el parpadeo de G-SYNC/FreeSync (se aplica al reiniciar)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Evitar el parpadeo de G-SYNC/FreeSync (se aplica al reiniciar)",
|
||||
"Welcome to LightCraft": "Bienvenido a LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Un gestor de fotos y revelado RAW rápido y no destructivo: tus originales permanecen intactos y cada edición se puede revisar o restablecer.",
|
||||
"Bring photos in with File › Import Photos…": "Importa fotos con Archivo › Importar fotos…",
|
||||
"Find and organize": "Encuentra y organiza",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "La cuadrícula de fotos organiza la biblioteca en filas justificadas: valora, marca y etiqueta por color, filtra y reúne las fotos en álbumes.",
|
||||
"View › Photo Grid.": "Vista › Cuadrícula de fotos.",
|
||||
"Develop a photo": "Revela una foto",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Abre una foto para revelarla: exposición, color, detalle y máscaras, todo de forma no destructiva. Usa Vista › Antes/Después lado a lado para comparar antes y después.",
|
||||
"View › Detail View.": "Vista › Vista de detalle.",
|
||||
"Cull a shoot": "Selecciona una sesión",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "La selección asistida valora el enfoque y la exposición, encuentra las ráfagas y marca las tomas borrosas como rechazadas, para que las mejores destaquen enseguida.",
|
||||
"Photo › Assisted Culling…": "Foto › Selección asistida…",
|
||||
"Share your work": "Comparte tu trabajo",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Exportar genera JPEG, PNG, TIFF, WebP y más, en el tamaño que elijas; Imprimir coloca una foto en una página como PDF.",
|
||||
"File › Export… · File › Print…": "Archivo › Exportar… · Archivo › Imprimir…",
|
||||
"File › Export…": "Archivo › Exportar…",
|
||||
"Put agents to work": "Pon a trabajar a los agentes",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Todo en LightCraft es un comando, así que los agentes de IA pueden manejar la aplicación en marcha a través de MCP: seleccionar, editar y exportar cuando lo pidas.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Inicia la aplicación con --control 7980 (o LIGHTCRAFT_CONTROL_PORT), y lightcraft-cli mcp --connect conecta un agente a ella.",
|
||||
"Welcome": "Bienvenido",
|
||||
"Next": "Siguiente",
|
||||
"Get Started": "Empezar"
|
||||
}
|
||||
|
||||
@@ -318,6 +318,9 @@
|
||||
"Panel move was not applied: {error}": "Paneelin siirtoa ei otettu käyttöön: {error}",
|
||||
"{name} is installed": "{name} on asennettu",
|
||||
"{name} is installed and in use: face recognition is on": "{name} on asennettu ja käytössä: kasvojentunnistus on päällä",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Tuo kuvia valitsemalla Tiedosto › Tuo kuvia… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Näytä › Tarkastelunäkymä ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Tiedosto › Vie… · Tiedosto › Tulosta… ({keys}).",
|
||||
"{n} folder{} could not be read": "Kansiot, joita ei voitu lukea: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "Ohitetut tiedostot (ei tuettu valokuvatyyppi): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -2151,6 +2151,28 @@
|
||||
"Virtual Copy": "Virtuaalikopio",
|
||||
"Visible": "Näkyvä",
|
||||
"Visualize HDR": "Havainnollista HDR",
|
||||
"Welcome to LightCraft": "Tervetuloa LightCraftiin",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Nopea, ei-tuhova kuvakirjasto ja RAW-kehittäjä: alkuperäiset tiedostot säilyvät koskemattomina, ja jokaiseen muokkaukseen voi palata tai sen voi nollata.",
|
||||
"Bring photos in with File › Import Photos…": "Tuo kuvia valitsemalla Tiedosto › Tuo kuvia…",
|
||||
"Find and organize": "Etsi ja järjestä",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Kuvaruudukko asettelee kirjaston tasattuihin riveihin: arvioi, merkitse ja väritä kuvia, suodata ja kokoaa niitä albumeihin.",
|
||||
"View › Photo Grid.": "Näytä › Kuvaruudukko.",
|
||||
"Develop a photo": "Kehitä kuva",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Avaa kuva kehitettäväksi: valotus, väri, yksityiskohdat ja maskit, kaikki ei-tuhovaa. Käytä Näytä › Ennen/jälkeen vierekkäin ennen- ja jälkikuvan vertailuun.",
|
||||
"View › Detail View.": "Näytä › Tarkastelunäkymä.",
|
||||
"Cull a shoot": "Karsi kuvaus",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Avustettu valinta pisteyttää terävyyden ja valotuksen, tunnistaa kuvasarjat ja merkitsee sumeat kuvat hylätyiksi, jotta parhaat nousevat nopeasti esille.",
|
||||
"Photo › Assisted Culling…": "Kuva › Avustettu valinta…",
|
||||
"Share your work": "Jaa työsi",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Vienti tuottaa JPEG-, PNG-, TIFF-, WebP- ja muita tiedostomuotoja haluamassasi koossa; Tulostus somittaa kuvan sivulle PDF:nä.",
|
||||
"File › Export… · File › Print…": "Tiedosto › Vie… · Tiedosto › Tulosta…",
|
||||
"File › Export…": "Tiedosto › Vie…",
|
||||
"Put agents to work": "Pane agentit töihin",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Kaikki LightCraftissa on komento, joten tekoälyagentit voivat ohjata käynnissä olevaa sovellusta MCP:n kautta: karsintaa, kehitystä ja vientiä käskystäsi.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Käynnistä sovellus valitsimella --control 7980 (tai LIGHTCRAFT_CONTROL_PORT), minkä jälkeen lightcraft-cli mcp --connect yhdistää siihen yhden agentin.",
|
||||
"Welcome": "Tervetuloa",
|
||||
"Next": "Seuraava",
|
||||
"Get Started": "Aloitetaan",
|
||||
"Visualize Spots": "Havainnollista täplät",
|
||||
"Visualize Spots (A)": "Havainnollista täplät (A)",
|
||||
"Visualize range": "Havainnollista alue",
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "La disposition des panneaux n'a pas pu être restaurée : {error}",
|
||||
"Panel move was not applied: {error}": "Le déplacement du panneau n'a pas été appliqué : {error}",
|
||||
"{name} is installed": "{name} est installé",
|
||||
"{name} is installed and in use: face recognition is on": "{name} est installé et utilisé : la reconnaissance des visages est activée"
|
||||
"{name} is installed and in use: face recognition is on": "{name} est installé et utilisé : la reconnaissance des visages est activée",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Importez des photos avec Fichier › Importer des photos… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Affichage › Vue de détail ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Fichier › Exporter… · Fichier › Imprimer… ({keys})."
|
||||
}
|
||||
|
||||
@@ -3587,5 +3587,27 @@
|
||||
"Top bar": "Barre supérieure",
|
||||
"Tool strip": "Barre d’outils",
|
||||
"Bottom bar": "Barre inférieure",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Empêcher le scintillement G-SYNC/FreeSync (au prochain démarrage)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Empêcher le scintillement G-SYNC/FreeSync (au prochain démarrage)",
|
||||
"Welcome to LightCraft": "Bienvenue dans LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Une bibliothèque photo rapide et non destructive, et un outil de développement RAW : vos originaux restent intacts, et chaque modification peut être revue ou annulée.",
|
||||
"Bring photos in with File › Import Photos…": "Importez des photos avec Fichier › Importer des photos…",
|
||||
"Find and organize": "Trouver et organiser",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "La grille de photos organise la bibliothèque en rangées alignées : notez, marquez et étiquetez par couleur, filtrez, et regroupez les photos en albums.",
|
||||
"View › Photo Grid.": "Affichage › Grille de photos.",
|
||||
"Develop a photo": "Développer une photo",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Ouvrez une photo pour la développer : exposition, couleur, détails et masques, le tout de façon non destructive. Utilisez Affichage › Avant/Après côte à côte pour comparer avant et après.",
|
||||
"View › Detail View.": "Affichage › Vue de détail.",
|
||||
"Cull a shoot": "Trier une séance",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Le tri assisté évalue la netteté et l'exposition, repère les rafales et marque les photos floues comme rejetées, pour que les meilleures ressortent vite.",
|
||||
"Photo › Assisted Culling…": "Photo › Tri assisté…",
|
||||
"Share your work": "Partager votre travail",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "L'export produit du JPEG, PNG, TIFF, WebP et d'autres formats, à la taille voulue ; Imprimer met une photo en page sur une feuille au format PDF.",
|
||||
"File › Export… · File › Print…": "Fichier › Exporter… · Fichier › Imprimer…",
|
||||
"File › Export…": "Fichier › Exporter…",
|
||||
"Put agents to work": "Mettre les agents au travail",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Tout dans LightCraft est une commande : des agents IA peuvent piloter l'application en cours via MCP, pour trier, modifier et exporter à votre demande.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Démarrez l'application avec --control 7980 (ou LIGHTCRAFT_CONTROL_PORT), puis lightcraft-cli mcp --connect y connecte un agent.",
|
||||
"Welcome": "Bienvenue",
|
||||
"Next": "Suivant",
|
||||
"Get Started": "Commencer"
|
||||
}
|
||||
|
||||
@@ -321,6 +321,9 @@
|
||||
"Creates {stacks} stack{} from {photos} of {scope}": "Membuat {stacks} tumpukan dari {photos} foto ({scope}){:.0}",
|
||||
"{n} selected photo{} · {pages} page{}": "{n} foto dipilih · {pages} halaman{:.0}{:.0}",
|
||||
"{n} photo{} in view · {slides} slide{} · {time}": "{n} foto di tampilan · {slides} slide · {time}{:.0}{:.0}",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Masukkan foto melalui File › Impor foto… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Tampilan › Tampilan Detail ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "File › Ekspor… · File › Cetak… ({keys}).",
|
||||
"{n} folder{} could not be read": "Folder yang tidak bisa dibaca: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "File yang dilewati (bukan jenis foto yang didukung): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -2145,6 +2145,28 @@
|
||||
"Virtual Copy": "Salinan virtual",
|
||||
"Visible": "Terlihat",
|
||||
"Visualize HDR": "Visualisasikan HDR",
|
||||
"Welcome to LightCraft": "Selamat datang di LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Pustaka foto yang cepat dan pengembangan raw yang non-destruktif: berkas asli Anda tetap tak tersentuh, dan setiap penyuntingan dapat ditinjau ulang atau diatur ulang.",
|
||||
"Bring photos in with File › Import Photos…": "Masukkan foto melalui File › Impor foto…",
|
||||
"Find and organize": "Temukan dan rapikan",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Grid foto menyusun pustaka menjadi baris yang rapi: beri peringkat, tandai dan beri label warna, saring, serta kumpulkan foto ke dalam album.",
|
||||
"View › Photo Grid.": "Tampilan › Grid foto.",
|
||||
"Develop a photo": "Kembangkan satu foto",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Buka foto untuk mengembangkannya: eksposur, warna, detail dan masker, semuanya non-destruktif. Gunakan Tampilan › Sebelum/Sesudah berdampingan untuk membandingkan sebelum dan sesudah.",
|
||||
"View › Detail View.": "Tampilan › Tampilan Detail.",
|
||||
"Cull a shoot": "Seleksi hasil pemotretan",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Seleksi foto berbantuan memberi skor fokus dan eksposur, menemukan rangkaian foto, dan menandai foto buram sebagai ditolak, sehingga foto terbaik tampil lebih cepat.",
|
||||
"Photo › Assisted Culling…": "Foto › Seleksi foto berbantuan…",
|
||||
"Share your work": "Bagikan karya Anda",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Ekspor menghasilkan JPEG, PNG, TIFF, WebP dan lainnya, dengan ukuran sesuai keinginan; Cetak menata foto pada halaman sebagai PDF.",
|
||||
"File › Export… · File › Print…": "File › Ekspor… · File › Cetak…",
|
||||
"File › Export…": "File › Ekspor…",
|
||||
"Put agents to work": "Pekerjakan agen",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Semua di LightCraft adalah perintah, sehingga agen AI dapat mengendalikan aplikasi yang berjalan melalui MCP: seleksi, pengembangan, dan ekspor atas perintah Anda.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Jalankan aplikasi dengan --control 7980 (atau LIGHTCRAFT_CONTROL_PORT), lalu lightcraft-cli mcp --connect menghubungkan satu agen kepadanya.",
|
||||
"Welcome": "Selamat Datang",
|
||||
"Next": "Berikutnya",
|
||||
"Get Started": "Mulai",
|
||||
"Visualize Spots": "Visualisasikan bintik",
|
||||
"Visualize Spots (A)": "Visualisasikan bintik (A)",
|
||||
"Visualize range": "Visualisasikan rentang",
|
||||
|
||||
@@ -321,6 +321,9 @@
|
||||
"Panel move was not applied: {error}": "Lo spostamento del pannello non è stato applicato: {error}",
|
||||
"{name} is installed": "{name} è installato",
|
||||
"{name} is installed and in use: face recognition is on": "{name} è installato e in uso: il riconoscimento dei volti è attivo",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Importa le foto con File › Importa foto… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Visualizza › Vista dettaglio ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "File › Esporta… · File › Stampa… ({keys}).",
|
||||
"{n} folder{} could not be read": "Cartelle non leggibili: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "File saltati (non è un tipo di foto supportato): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -2568,5 +2568,27 @@
|
||||
"Top bar": "Barra superiore",
|
||||
"Tool strip": "Barra degli strumenti",
|
||||
"Bottom bar": "Barra inferiore",
|
||||
"Welcome to LightCraft": "Benvenuto in LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Una libreria fotografica veloce e non distruttiva, e uno sviluppatore RAW: gli originali restano intatti e ogni modifica si può rivedere o annullare.",
|
||||
"Bring photos in with File › Import Photos…": "Importa le foto con File › Importa foto…",
|
||||
"Find and organize": "Trova e organizza",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "La Griglia foto dispone la libreria in righe allineate: valuta, contrassegna ed etichetta per colore, filtra e raccogli le foto negli album.",
|
||||
"View › Photo Grid.": "Visualizza › Griglia foto.",
|
||||
"Develop a photo": "Sviluppa una foto",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Apri una foto per svilupparla: esposizione, colore, dettagli e maschere, tutto in modo non distruttivo. Usa Visualizza › Prima/Dopo affiancati per confrontare prima e dopo.",
|
||||
"View › Detail View.": "Visualizza › Vista dettaglio.",
|
||||
"Cull a shoot": "Seleziona una sessione",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "La Selezione assistita valuta messa a fuoco ed esposizione, trova le serie di scatti e segna come scartate le foto mosse, così le migliori emergono subito.",
|
||||
"Photo › Assisted Culling…": "Foto › Selezione assistita…",
|
||||
"Share your work": "Condividi il tuo lavoro",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Esporta crea JPEG, PNG, TIFF, WebP e altri formati, nella dimensione che preferisci; Stampa impagina una foto su una pagina come PDF.",
|
||||
"File › Export… · File › Print…": "File › Esporta… · File › Stampa…",
|
||||
"File › Export…": "File › Esporta…",
|
||||
"Put agents to work": "Metti al lavoro gli agenti",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Tutto in LightCraft è un comando, quindi gli agenti IA possono controllare l'app in esecuzione tramite MCP: selezionare, modificare ed esportare su tua richiesta.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Avvia l'app con --control 7980 (o LIGHTCRAFT_CONTROL_PORT), poi lightcraft-cli mcp --connect vi collega un agente.",
|
||||
"Welcome": "Benvenuto",
|
||||
"Next": "Avanti",
|
||||
"Get Started": "Inizia",
|
||||
"Stopped at the scan limit: there may be more photos": "Fermati al limite di scansione: potrebbero esserci altre foto"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "パネル配置を復元できませんでした:{error}",
|
||||
"Panel move was not applied: {error}": "パネルの移動は適用されませんでした:{error}",
|
||||
"{name} is installed": "{name} をインストールしました",
|
||||
"{name} is installed and in use: face recognition is on": "{name} をインストールして使用中です:顔認識はオンです"
|
||||
"{name} is installed and in use: face recognition is on": "{name} をインストールして使用中です:顔認識はオンです",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "ファイル › 写真を読み込む… ({keys}) で写真を読み込みます。",
|
||||
"View › Detail View ({keys}).": "表示 › 詳細表示 ({keys})。",
|
||||
"File › Export… · File › Print… ({keys}).": "ファイル › 書き出す… · ファイル › 印刷… ({keys})。"
|
||||
}
|
||||
|
||||
@@ -2402,5 +2402,27 @@
|
||||
"Top bar": "上部バー",
|
||||
"Tool strip": "ツールバー",
|
||||
"Bottom bar": "下部バー",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "G-SYNC/FreeSyncのちらつきを防ぐ(次回起動時に適用)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "G-SYNC/FreeSyncのちらつきを防ぐ(次回起動時に適用)",
|
||||
"Welcome to LightCraft": "LightCraft へようこそ",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "高速で非破壊の写真ライブラリと RAW 現像ソフトです。元の写真は変わらず、どの編集もあとから見直したり元に戻したりできます。",
|
||||
"Bring photos in with File › Import Photos…": "ファイル › 写真を読み込む…で写真を読み込みます。",
|
||||
"Find and organize": "見つけて整理",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "写真グリッドは、写真を高さのそろった行に並べます。評価、旗、カラーラベルを付けたり、絞り込んだり、写真をアルバムにまとめたりできます。",
|
||||
"View › Photo Grid.": "表示 › 写真グリッド。",
|
||||
"Develop a photo": "写真を現像する",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "写真を開いて現像します。露出、色、ディテール、マスクをすべて非破壊で調整できます。表示 › 編集前/編集後の左右表示で編集前と編集後を比べられます。",
|
||||
"View › Detail View.": "表示 › 詳細表示。",
|
||||
"Cull a shoot": "撮影分を選別する",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "写真の選別を補助する機能は、ピントと露出を採点し、連写をまとめ、ぶれた写真を不採用にするので、残す写真がすぐ見つかります。",
|
||||
"Photo › Assisted Culling…": "写真 › 写真の選別を補助…",
|
||||
"Share your work": "作品を共有する",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "書き出しは JPEG、PNG、TIFF、WebP などを好みのサイズで出力します。印刷は、写真をページに配置して PDF にします。",
|
||||
"File › Export… · File › Print…": "ファイル › 書き出す… · ファイル › 印刷…",
|
||||
"File › Export…": "ファイル › 書き出す…",
|
||||
"Put agents to work": "エージェントに仕事を任せる",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "LightCraft のすべての操作はコマンドなので、AI エージェントは MCP 経由で起動中のアプリを操作できます。選別、編集、書き出しをあなたの指示どおりに行えます。",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "アプリを --control 7980 (または LIGHTCRAFT_CONTROL_PORT) で起動してから、lightcraft-cli mcp --connect でエージェントを接続できます。",
|
||||
"Welcome": "ようこそ",
|
||||
"Next": "次へ",
|
||||
"Get Started": "始める"
|
||||
}
|
||||
|
||||
@@ -324,6 +324,9 @@
|
||||
"Panel move was not applied: {error}": "პანელის გადატანა არ შესრულდა: {error}",
|
||||
"{name} is installed": "{name} დაინსტალირებულია",
|
||||
"{name} is installed and in use: face recognition is on": "{name} დაინსტალირებულია და გამოიყენება: სახეების ამოცნობა ჩართულია",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "ფოტოების შემოსატანად გამოიყენეთ ფაილი › ფოტოების იმპორტი… ({keys}).",
|
||||
"View › Detail View ({keys}).": "ხედი › დეტალური ხედი ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "ფაილი › ექსპორტი… · ფაილი › დაბეჭდვა… ({keys}).",
|
||||
"{n} folder{} could not be read": "საქაღალდეები, რომლებიც ვერ წაიკითხა: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "გამოტოვებული ფაილები (მხარდაუჭერელი ფოტოს ტიპია): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"A blank template keeps the original names.": "ცარიელი შაბლონი ორიგინალ სახელებს ინარჩუნებს.",
|
||||
"A change that can't be saved to disk (full or unplugged drive) is no longer silent: the command reports": "ცვლილება, რომლის დისკზე შენახვაც ვერ ხერხდება (სავსე ან გამორთული დისკი), აღარ რჩება შეუმჩნეველი: ბრძანება იტყობინება",
|
||||
"A dialog taller than the window no longer runs off it (issue #781): on a 1366×768 screen the Export dialog's title": "ფანჯარაზე მაღალი დიალოგი აღარ გადის მის საზღვრებს გარეთ (ხარვეზი #781): 1366×768 ეკრანზე ექსპორტის დიალოგის სათაური",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "სწრაფი, არადესტრუქციული ფოტობიბლიოთეკა და RAW-დამუშავების ინსტრუმენტი: ორიგინალები უცვლელი რჩება, ყოველი რედაქტირების ნახვა ან გაუქმება კი შესაძლებელია.",
|
||||
"A filter is hiding this view's photos: change it, or clear it (View → Clear Filters)": "ფილტრი მალავს ამ ხედის ფოტოებს: შეცვალეთ ან გაასუფთავეთ იგი (ხედი → ფილტრების გასუფთავება)",
|
||||
"A folder is already being synchronized": "საქაღალდე უკვე სინქრონიზდება",
|
||||
"A folder is being synchronized": "საქაღალდე სინქრონიზდება",
|
||||
@@ -198,6 +199,7 @@
|
||||
"Aspect Ratio": "თანაფარდობა",
|
||||
"Aspect Ratio (a 24 MP photo cropped to a square is 16 MP).": "თანაფარდობა (24 MP ფოტო, კვადრატად ჩამოჭრილი, 16 MP-ია).",
|
||||
"Assisted Culling": "დამხმარე გადარჩევა",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "დამხმარე გადარჩევა აფასებს ფოკუსსა და ექსპოზიციას, პოულობს სერიებს და ბუნდოვან კადრებს უარყოფილებად მონიშნავს, რომ საუკეთესო ფოტოები სწრაფად გამოჩნდეს.",
|
||||
"Assisted Culling…": "დამხმარე გადარჩევა…",
|
||||
"Auto": "ავტომატური",
|
||||
"Auto Advance": "ავტომატური გადასვლა",
|
||||
@@ -277,6 +279,7 @@
|
||||
"Bridge and Photo Supreme (its Formatted Vocabulary File) use the same files, so keyword lists move between them and": "Bridge და Photo Supreme (მისი Formatted Vocabulary File) იმავე ფაილებს იყენებს, ასე რომ საკვანძო სიტყვების სიები მათსა და",
|
||||
"Bright & Airy": "ნათელი და ჰაეროვანი",
|
||||
"Bring All to Front": "ყველას წინა პლანზე გადატანა",
|
||||
"Bring photos in with File › Import Photos…": "ფოტოების შემოსატანად გამოიყენეთ ფაილი › ფოტოების იმპორტი…",
|
||||
"Bring the library up to date with this folder and the folders inside it:": "ბიბლიოთეკის განახლება ამ საქაღალდისა და მასში არსებული საქაღალდეების მიხედვით:",
|
||||
"Browse Folder": "საქაღალდის დათვალიერება",
|
||||
"Browse Folder…": "საქაღალდის დათვალიერება…",
|
||||
@@ -500,6 +503,7 @@
|
||||
"Crop, Rotate, Geometry": "ჩამოჭრა, შემობრუნება, გეომეტრია",
|
||||
"Cropped": "ჩამოჭრილი",
|
||||
"Cross Process": "ჯვარედინი დამუშავება",
|
||||
"Cull a shoot": "სერიის გადარჩევა",
|
||||
"Culling": "გადარჩევა",
|
||||
"Cull": "გადარჩევა",
|
||||
"Create Stacks": "დასტების შექმნა",
|
||||
@@ -594,6 +598,7 @@
|
||||
"Detect Faces": "სახეების ამოცნობა",
|
||||
"Detect Faces (Photo menu) needs it: a small one-time download.": "სახეების ამოცნობას (მენიუ „ფოტო“) ეს სჭირდება: მცირე, ერთჯერადი ჩამოტვირთვა.",
|
||||
"Develop": "დამუშავება",
|
||||
"Develop a photo": "ფოტოს დამუშავება",
|
||||
"Diagonal": "დიაგონალური",
|
||||
"Dialogs": "დიალოგები",
|
||||
"Dialogs fit the window": "დიალოგები ფანჯარაში ეტევა",
|
||||
@@ -690,6 +695,7 @@
|
||||
"Erase": "წაშლა",
|
||||
"Evaluate Face Recognition": "სახეების ამოცნობის შეფასება",
|
||||
"Every EXIF, GPS and XMP field in the file": "ფაილის ყველა EXIF, GPS და XMP ველი",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "LightCraft-ში ყველაფერი ბრძანებაა, ამიტომ AI აგენტებს MCP-ის მეშვეობით შეუძლიათ გაშვებული აპლიკაციის მართვა: გადარჩევა, რედაქტირება და ექსპორტი თქვენი მითითებით.",
|
||||
"Existing previews": "არსებული წინასწარი ხედები",
|
||||
"Existing smart previews are refreshed when their originals are online, and cached renders are redrawn.": "არსებული ჭკვიანი წინასწარი ხედები ახლდება, როცა მათი ორიგინალები ხელმისაწვდომია, ხოლო ქეშირებული რენდერები ხელახლა იხატება.",
|
||||
"Exit": "გასვლა",
|
||||
@@ -716,6 +722,7 @@
|
||||
"Export as JPEG XL (issue #247): Format ▸ JPEG XL in the Export dialog, `format: \"jxl\"` for agents and the CLI": "ექსპორტი JPEG XL-ად (ხარვეზი #247): ფორმატი → JPEG XL ექსპორტის დიალოგში, `format: \"jxl\"` აგენტებისა და CLI-სთვის",
|
||||
"Export can write PSD too (flat, 8- or 16-bit).": "ექსპორტს PSD-ის ჩაწერაც შეუძლია (ერთშრიანი, 8 ან 16 ბიტი).",
|
||||
"Export file names use the Rename Photos tokens ({title}, {seq:2}, {date:%Y-%m-%d}…), plus new {num}, {folder}, {lens}, {iso}, {rating}, {creator}.": "ექსპორტირებული ფაილების სახელები იყენებს ფოტოების გადარქმევის ტეგებს ({title}, {seq:2}, {date:%Y-%m-%d}…) და ახალ {num}, {folder}, {lens}, {iso}, {rating}, {creator}-ს.",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "ექსპორტი ქმნის JPEG, PNG, TIFF, WebP და სხვა ფორმატებს სასურველი ზომით; დაბეჭდვა ფოტოს PDF-ად გვერდზე განათავსებს.",
|
||||
"Export stopped unexpectedly": "ექსპორტი მოულოდნელად შეწყდა",
|
||||
"Export with Preset": "ექსპორტი პრესეტით",
|
||||
"Export with Previous": "ექსპორტი წინა პარამეტრებით",
|
||||
@@ -764,6 +771,8 @@
|
||||
"File extensions match whatever their letter case: `IMG_0001.JPG` and `DSC_0002.NEF` as Nikon writes them, `.Jpg`": "ფაილის გაფართოებები ემთხვევა ასოთა რეგისტრის მიუხედავად: `IMG_0001.JPG` და `DSC_0002.NEF`, როგორც Nikon წერს, `.Jpg`",
|
||||
"File name": "ფაილის სახელი",
|
||||
"File types:": "ფაილის ტიპები:",
|
||||
"File › Export… · File › Print…": "ფაილი › ექსპორტი… · ფაილი › დაბეჭდვა…",
|
||||
"File › Export…": "ფაილი › ექსპორტი…",
|
||||
"File → Import Lightroom Catalog… opens `.lrcat` directly, with originals referenced in place.": "ფაილი → Lightroom-ის კატალოგის იმპორტი… პირდაპირ ხსნის `.lrcat`-ს, ორიგინალებზე მათ ადგილას მიმართვით.",
|
||||
"File ▸ Import Keywords… and Export Keywords… read and write keyword list files as Lightroom Classic does (a keyword": "ფაილი → საკვანძო სიტყვების იმპორტი… და საკვანძო სიტყვების ექსპორტი… კითხულობს და წერს საკვანძო სიტყვების სიის ფაილებს, როგორც Lightroom Classic (თითო საკვანძო სიტყვა",
|
||||
"File ▸ New Library… asks for a name, then where to put it, and opens the new library (issue #902).": "ფაილი → ახალი ბიბლიოთეკა… ითხოვს სახელს, შემდეგ მდებარეობას და ხსნის ახალ ბიბლიოთეკას (ხარვეზი #902).",
|
||||
@@ -789,6 +798,7 @@
|
||||
"Find Missing Photos": "დაკარგული ფოტოების მოძებნა",
|
||||
"Find Missing Photos…": "დაკარგული ფოტოების მოძებნა…",
|
||||
"Find Similar Photos": "მსგავსი ფოტოების მოძებნა",
|
||||
"Find and organize": "მოძებნეთ და დაალაგეთ",
|
||||
"Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps": "ამ საქაღალდეში დისკზე დამატებული ან მისგან გამქრალი ფოტოების და სხვა აპების მიერ შეცვლილი XMP თანმხლები ფაილების მოძებნა",
|
||||
"Find tools and commands…": "ხელსაწყოებისა და ბრძანებების ძებნა…",
|
||||
"Finding faces": "სახეების ძებნა",
|
||||
@@ -854,6 +864,7 @@
|
||||
"Generated demo photo": "სადემონსტრაციო ფოტო",
|
||||
"Geometry": "გეომეტრია",
|
||||
"Get Develop Settings": "დამუშავების პარამეტრების აღება",
|
||||
"Get Started": "დაწყება",
|
||||
"Global Hue": "გლობალური ფერის ტონი",
|
||||
"Global Luminance": "გლობალური სიკაშკაშე",
|
||||
"Global Saturation": "გლობალური გაჯერება",
|
||||
@@ -1314,6 +1325,7 @@
|
||||
"New name": "ახალი სახელი",
|
||||
"New time": "ახალი დრო",
|
||||
"New…": "ახალი…",
|
||||
"Next": "შემდეგი",
|
||||
"Next Appearance Mode": "შემდეგი იერსახის რეჟიმი",
|
||||
"Next History Step": "ისტორიის შემდეგი ნაბიჯი",
|
||||
"Next Keyword Set": "საკვანძო სიტყვების შემდეგი ნაკრები",
|
||||
@@ -1409,6 +1421,7 @@
|
||||
"Open Log Folder": "ჟურნალების საქაღალდის გახსნა",
|
||||
"Open Original in Other Editor (RAW, no copy)": "ორიგინალის გახსნა სხვა რედაქტორში (RAW, ასლის გარეშე)",
|
||||
"Open Recent Library": "ბოლოს გახსნილი ბიბლიოთეკის გახსნა",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "გახსენით ფოტო დასამუშავებლად: ექსპოზიცია, ფერი, დეტალები და ნიღბები, ყველაფერი არადესტრუქციულად. გამოიყენეთ ხედი › მანამდე/შემდეგ გვერდიგვერდ დამუშავებამდე და დამუშავების შემდეგ შედარებისთვის.",
|
||||
"Open in": "გახსნა:",
|
||||
"Open in Detail": "დეტალურ ხედში გახსნა",
|
||||
"Open page": "გვერდის გახსნა",
|
||||
@@ -1487,6 +1500,7 @@
|
||||
"Photo Merge": "ფოტოების გაერთიანება",
|
||||
"Photo Merge of Canon CR2 (and other raws whose colour LightCraft fits from the camera's own JPEG, such as NEF and": "Canon CR2-ის (და სხვა RAW ფაილების, რომელთა ფერსაც LightCraft კამერის საკუთარი JPEG-იდან არგებს, როგორიცაა NEF და",
|
||||
"Photo menu. The filmstrip has the photo context menu too. Adding a file again that is in Recently Deleted no": "„ფოტო“ მენიუში. კადრების ზოლსაც აქვს ფოტოს კონტექსტური მენიუ. ახლახან წაშლილში მყოფი ფაილის ხელახლა დამატება აღარ",
|
||||
"Photo › Assisted Culling…": "ფოტო › დამხმარე გადარჩევა…",
|
||||
"Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "ფოტო → რედაქტირება სხვა პროგრამაში → რედაქტირება PhotoCraft-ში (⇧⌘E, ან მარჯვენა დაწკაპუნება ფოტოზე) არენდერებს ასლს თქვენი ცვლილებებით, ორიგინალთან ერთ დასტაში აერთიანებს და PhotoCraft-ში ხსნის. იქ შენახვისას ასლი აქაც განახლდება. PSD-ის შემთხვევაში PhotoCraft პირდაპირ მასში ინახავს. მდებარეობა ცარიელი დატოვეთ, თუ პორტატულ ასლს არ იყენებთ.",
|
||||
"Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "ფოტო → რედაქტირება გარე რედაქტორში (⇧⌘E) ქმნის 16-ბიტიან TIFF ასლს, ორიგინალთან ერთად დასტაში აერთიანებს და აქ ხსნის (macOS-ზე აპის სახელი, სხვაგან — პროგრამის მისამართი).",
|
||||
"Photos": "ფოტოები",
|
||||
@@ -1577,7 +1591,7 @@
|
||||
"Set Keyword Shortcut…": "საკვანძო სიტყვების მალსახმობის დაყენება…",
|
||||
"Add or Remove Keyword Shortcut": "საკვანძო სიტყვების მალსახმობის დამატება ან მოხსნა",
|
||||
"Keyword Shortcut": "საკვანძო სიტყვების მალსახმობი",
|
||||
"Part of the keyword shortcut": "საკვანძო სიტყვების მალსახმობის ნაწილი",
|
||||
"Part of the keyword shortcut": "საკვანძო სიტყვების მალსახმობის ნაწილი", "Put agents to work": "აგენტების სამუშაოზე დაყენება",
|
||||
"Put the locations you removed from Local back (no files change)": "ლოკალურიდან მოშორებული მდებარეობების დაბრუნება (ფაილები არ იცვლება)",
|
||||
"Quality": "ხარისხი",
|
||||
"Query Photos": "ფოტოების მოთხოვნა",
|
||||
@@ -1903,6 +1917,7 @@
|
||||
"Shadows Saturation": "ჩრდილების გაჯერება",
|
||||
"Shadows Tint": "ჩრდილების ტონირება",
|
||||
"Shadows split": "ჩრდილების გაყოფა",
|
||||
"Share your work": "გააზიარეთ თქვენი ნამუშევარი",
|
||||
"Sharpen": "გამკვეთრება",
|
||||
"Sharpening": "გამკვეთრება",
|
||||
"Sharpness": "სიმკვეთრე",
|
||||
@@ -2096,6 +2111,7 @@
|
||||
"The Hasselblad Lunar's raws (a Sony NEX-7 inside) now develop from their sensor data with every editing tool,": "Hasselblad Lunar-ის (შიგნით Sony NEX-7) RAW ფაილები ახლა სენსორის მონაცემებიდან მუშავდება ყველა რედაქტირების ხელსაწყოთი,",
|
||||
"The Import Photos review opens bigger and can be resized; its photo grid fills it (issue #337). Shift-click checks": "ფოტოების იმპორტის მიმოხილვა უფრო დიდი იხსნება და მისი ზომის შეცვლა შეიძლება; ფოტოების ბადე მას ავსებს (ხარვეზი #337). Shift-დაწკაპუნება",
|
||||
"The Keywords panel ends with a Keyword List, as in Lightroom Classic: every keyword with its photo count, those no": "საკვანძო სიტყვების პანელი საკვანძო სიტყვების სიით სრულდება, როგორც Lightroom Classic-ში: ყველა საკვანძო სიტყვა ფოტოების რაოდენობით, მათ შორის ისეთებიც,",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "ფოტოების ბადე ბიბლიოთეკას სწორი რიგებით აწყობს: შეაფასეთ, მონიშნეთ დროშებით და ფერებით, გაფილტრეთ და ფოტოები ალბომებად შეაგროვეთ.",
|
||||
"The Presets panel builds only the rows in view (and their thumbnails) instead of every preset on every frame; the": "პრესეტების პანელი აგებს მხოლოდ ხილულ რიგებს (და მათ ესკიზებს) ყოველ კადრზე ყველა პრესეტის ნაცვლად;",
|
||||
"The SAM 3 model is installed: Object and Describe masks are ready.": "SAM 3 მოდელი დაინსტალირებულია: „ობიექტის“ და „აღწერის“ ნიღბები მზადაა.",
|
||||
"The Tint slider works the right way round (issues #188, #321): left adds green, right adds magenta, as its track": "ტონირების სლაიდერი სწორი მიმართულებით მუშაობს (ხარვეზები #188, #321): მარცხნივ მწვანეს ამატებს, მარჯვნივ მეწამულს, როგორც მისი ზოლი",
|
||||
@@ -2233,6 +2249,8 @@
|
||||
"Video Duration": "ვიდეოს ხანგრძლივობა",
|
||||
"Videos": "ვიდეოები",
|
||||
"View": "ხედი",
|
||||
"View › Detail View.": "ხედი › დეტალური ხედი.",
|
||||
"View › Photo Grid.": "ხედი › ფოტოების ბადე.",
|
||||
"Viewing and sharing": "ნახვა და გაზიარება",
|
||||
"Vignette": "ვინიეტი",
|
||||
"Vignetting": "ვინიეტირება",
|
||||
@@ -2257,6 +2275,8 @@
|
||||
"We": "ოთხ",
|
||||
"Web / Portfolio": "ვებ / პორტფოლიო",
|
||||
"Wednesday": "ოთხშაბათი",
|
||||
"Welcome": "მოგესალმებით",
|
||||
"Welcome to LightCraft": "კეთილი იყოს მობრძანება LightCraft-ში",
|
||||
"What's New": "რა არის ახალი",
|
||||
"When GPU rendering is on but no graphics processor can be found, LightCraft now says so once at start-up instead": "როცა GPU რენდერი ჩართულია, მაგრამ გრაფიკული პროცესორი ვერ მოიძებნა, LightCraft ახლა ამას გაშვებისას ერთხელ გეუბნებათ, ნაცვლად იმისა,",
|
||||
"When a folder holds several file types, the Import Photos review has a toggle per type (`ARW · 120`, `JPG · 120`):": "როცა საქაღალდეში რამდენიმე ტიპის ფაილია, ფოტოების იმპორტის მიმოხილვას თითო ტიპზე გადამრთველი აქვს (`ARW · 120`, `JPG · 120`):",
|
||||
@@ -2522,6 +2542,7 @@
|
||||
"library open now stays, and LightCraft says \"This is a Lightroom catalog, not a LightCraft library\" and offers": "გახსნილი ბიბლიოთეკა რჩება, LightCraft კი ამბობს: „ეს Lightroom-ის კატალოგია და არა LightCraft-ის ბიბლიოთეკა“ და გთავაზობთ",
|
||||
"library) are written atomically and saved as soon as you open another library, not only at quit. Quitting while": "ბიბლიოთეკა) ატომურად იწერება და ინახება, როგორც კი სხვა ბიბლიოთეკას გახსნით, და არა მხოლოდ გასვლისას. გასვლა, როცა",
|
||||
"light: Silver or Paper). New and existing installs stay dark; following the system is opt-in.": "ღია: ვერცხლისფერი ან ქაღალდი). ახალი და არსებული ინსტალაციები ბნელი რჩება; სისტემის მიყოლა არჩევითია.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "გაუშვით აპლიკაცია --control 7980 (ან LIGHTCRAFT_CONTROL_PORT) პარამეტრით, შემდეგ lightcraft-cli mcp --connect მას ერთ აგენტთან აკავშირებს.",
|
||||
"like a photo that can't be opened, with the reason, instead of rendering forever; its thumbnail stops waiting too.": "როგორც ფოტო, რომლის გახსნაც შეუძლებელია, მიზეზით, უსასრულო რენდერის ნაცვლად; მისი ესკიზიც წყვეტს ლოდინს.",
|
||||
"like the HDR AVIF: the same brightness, with SDR white at 203 cd/m², and content light levels recorded.": "როგორც HDR AVIF: იგივე სიკაშკაშე, SDR თეთრით 203 cd/m²-ზე, და ჩაწერილი შიგთავსის სინათლის დონეებით.",
|
||||
"lit by the chosen white comes out grey and saturated colours move as the camera records them. The eyedropper and": "თეთრით განათებული ნაცრისფერი ნაცრისფერი გამოდის, გაჯერებული ფერები კი ისე იცვლება, როგორც კამერა ჩაწერს. პიპეტი და",
|
||||
|
||||
@@ -321,6 +321,9 @@
|
||||
"Panel move was not applied: {error}": "Przeniesienie panelu nie zostało zastosowane: {error}",
|
||||
"{name} is installed": "{name} jest zainstalowany",
|
||||
"{name} is installed and in use: face recognition is on": "{name} jest zainstalowany i używany: rozpoznawanie twarzy jest włączone",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Zdjęcia importujesz przez Plik › Importuj zdjęcia… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Widok › Widok szczegółów ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Plik › Eksportuj… · Plik › Drukuj… ({keys}).",
|
||||
"{n} folder{} could not be read": "Nie udało się odczytać folderów: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "Pominięte pliki (nieobsługiwany typ zdjęcia): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -2326,5 +2326,27 @@
|
||||
"Top bar": "Górny pasek",
|
||||
"Tool strip": "Pasek narzędzi",
|
||||
"Bottom bar": "Dolny pasek",
|
||||
"Welcome to LightCraft": "Witamy w LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Szybka biblioteka zdjęć i nieniszczący program do obróbki RAW: oryginały pozostają nietknięte, a każdą edycję można później obejrzeć lub cofnąć.",
|
||||
"Bring photos in with File › Import Photos…": "Zdjęcia importujesz przez Plik › Importuj zdjęcia…",
|
||||
"Find and organize": "Znajdź i uporządkuj",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Siatka zdjęć układa bibliotekę w równe rzędy: oceniaj, oznaczaj flagami i kolorami, filtruj i zbieraj zdjęcia w albumach.",
|
||||
"View › Photo Grid.": "Widok › Siatka zdjęć.",
|
||||
"Develop a photo": "Obrób zdjęcie",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Otwórz zdjęcie, by je obrobić: ekspozycja, kolor, szczegóły i maski, wszystko w sposób nieniszczący. Użyj Widok › Przed/Po obok siebie, by porównać przed i po.",
|
||||
"View › Detail View.": "Widok › Widok szczegółów.",
|
||||
"Cull a shoot": "Wybierz zdjęcia z sesji",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Wspomagana selekcja ocenia ostrość i ekspozycję, wykrywa serie i oznacza rozmyte ujęcia jako odrzucone, dzięki czemu najlepsze zdjęcia szybko się wyróżniają.",
|
||||
"Photo › Assisted Culling…": "Zdjęcie › Wspomagana selekcja…",
|
||||
"Share your work": "Udostępnij swoją pracę",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Eksport zapisuje JPEG, PNG, TIFF, WebP i inne formaty w wybranym rozmiarze; Drukuj układa zdjęcie na stronie jako PDF.",
|
||||
"File › Export… · File › Print…": "Plik › Eksportuj… · Plik › Drukuj…",
|
||||
"File › Export…": "Plik › Eksportuj…",
|
||||
"Put agents to work": "Zaprzęgnij agentów do pracy",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Wszystko w LightCraft to polecenia, dlatego agenci AI mogą sterować działającą aplikacją przez MCP: wybierać zdjęcia, edytować je i eksportować na twoje polecenie.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Uruchom aplikację z --control 7980 (lub LIGHTCRAFT_CONTROL_PORT), a lightcraft-cli mcp --connect podłączy do niej jednego agenta.",
|
||||
"Welcome": "Witamy",
|
||||
"Next": "Dalej",
|
||||
"Get Started": "Zaczynamy",
|
||||
"Stopped at the scan limit: there may be more photos": "Zatrzymano na limicie skanowania: możliwe, że jest więcej zdjęć"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "Não foi possível restaurar a disposição dos painéis: {error}",
|
||||
"Panel move was not applied: {error}": "A movimentação do painel não foi aplicada: {error}",
|
||||
"{name} is installed": "{name} está instalado",
|
||||
"{name} is installed and in use: face recognition is on": "{name} está instalado e em uso: o reconhecimento facial está ativado"
|
||||
"{name} is installed and in use: face recognition is on": "{name} está instalado e em uso: o reconhecimento facial está ativado",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Traga fotos com Arquivo › Importar fotos… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Exibir › Exibição de detalhe ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Arquivo › Exportar… · Arquivo › Imprimir… ({keys})."
|
||||
}
|
||||
|
||||
@@ -2248,5 +2248,27 @@
|
||||
"Top bar": "Barra superior",
|
||||
"Tool strip": "Barra de ferramentas",
|
||||
"Bottom bar": "Barra inferior",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Evitar cintilação de G-SYNC/FreeSync (no próximo início)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Evitar cintilação de G-SYNC/FreeSync (no próximo início)",
|
||||
"Welcome to LightCraft": "Bem-vindo ao LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Uma biblioteca de fotos rápida e não destrutiva e um revelador RAW: seus originais permanecem intactos, e cada edição pode ser revista ou desfeita.",
|
||||
"Bring photos in with File › Import Photos…": "Traga fotos com Arquivo › Importar fotos…",
|
||||
"Find and organize": "Encontre e organize",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "A Grade de fotos organiza a biblioteca em linhas justificadas: avalie, sinalize e marque com cores, filtre e reúna as fotos em álbuns.",
|
||||
"View › Photo Grid.": "Exibir › Grade de fotos.",
|
||||
"Develop a photo": "Revele uma foto",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Abra uma foto para revelá-la: exposição, cor, detalhes e máscaras, tudo de forma não destrutiva. Use Exibir › Antes/Depois lado a lado para comparar antes e depois.",
|
||||
"View › Detail View.": "Exibir › Exibição de detalhe.",
|
||||
"Cull a shoot": "Selecione uma sessão",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "A Seleção assistida avalia foco e exposição, encontra rajadas e marca como rejeitadas as fotos borradas, para que as melhores apareçam logo.",
|
||||
"Photo › Assisted Culling…": "Foto › Seleção assistida…",
|
||||
"Share your work": "Compartilhe seu trabalho",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Exportar gera JPEG, PNG, TIFF, WebP e mais, no tamanho que você escolher; Imprimir dispõe uma foto em uma página como PDF.",
|
||||
"File › Export… · File › Print…": "Arquivo › Exportar… · Arquivo › Imprimir…",
|
||||
"File › Export…": "Arquivo › Exportar…",
|
||||
"Put agents to work": "Ponha os agentes para trabalhar",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Tudo no LightCraft é um comando, então agentes de IA podem controlar o aplicativo em execução pelo MCP: selecionar, editar e exportar quando você pedir.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Inicie o aplicativo com --control 7980 (ou LIGHTCRAFT_CONTROL_PORT) e o lightcraft-cli mcp --connect conecta um agente a ele.",
|
||||
"Welcome": "Bem-vindo",
|
||||
"Next": "Próximo",
|
||||
"Get Started": "Começar"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "Не удалось восстановить расположение панелей: {error}",
|
||||
"Panel move was not applied: {error}": "Перемещение панели не применено: {error}",
|
||||
"{name} is installed": "Модель {name} установлена",
|
||||
"{name} is installed and in use: face recognition is on": "Модель {name} установлена и используется: распознавание лиц включено"
|
||||
"{name} is installed and in use: face recognition is on": "Модель {name} установлена и используется: распознавание лиц включено",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Фотографии добавляются через Файл › Импорт фото… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Вид › Режим детализации ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Файл › Экспорт… · Файл › Печать… ({keys})."
|
||||
}
|
||||
|
||||
@@ -2610,5 +2610,27 @@
|
||||
"Top bar": "Верхняя панель",
|
||||
"Tool strip": "Панель инструментов",
|
||||
"Bottom bar": "Нижняя панель",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Предотвращать мерцание G-SYNC/FreeSync (после перезапуска)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Предотвращать мерцание G-SYNC/FreeSync (после перезапуска)",
|
||||
"Welcome to LightCraft": "Добро пожаловать в LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Быстрая библиотека фотографий и неразрушающий проявитель RAW: оригиналы остаются нетронутыми, а каждую правку можно просмотреть или сбросить.",
|
||||
"Bring photos in with File › Import Photos…": "Фотографии добавляются через Файл › Импорт фото…",
|
||||
"Find and organize": "Находите и упорядочивайте",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Сетка фото выстраивает библиотеку ровными рядами: оценивайте, отмечайте флагами и цветовыми метками, фильтруйте и собирайте фотографии в альбомы.",
|
||||
"View › Photo Grid.": "Вид › Сетка фото.",
|
||||
"Develop a photo": "Обработайте фотографию",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Откройте фото для обработки: экспозиция, цвет, детализация и маски, всё неразрушающе. Используйте Вид › До/после рядом, чтобы сравнить до и после.",
|
||||
"View › Detail View.": "Вид › Режим детализации.",
|
||||
"Cull a shoot": "Отберите снимки съёмки",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Автоматический отбор оценивает резкость и экспозицию, находит серии и помечает размытые кадры как отклонённые, чтобы лучшие снимки сразу бросались в глаза.",
|
||||
"Photo › Assisted Culling…": "Фото › Автоматический отбор…",
|
||||
"Share your work": "Поделитесь результатом",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Экспорт сохраняет JPEG, PNG, TIFF, WebP и другие форматы нужного размера; Печать размещает фото на странице в виде PDF.",
|
||||
"File › Export… · File › Print…": "Файл › Экспорт… · Файл › Печать…",
|
||||
"File › Export…": "Файл › Экспорт…",
|
||||
"Put agents to work": "Поручите работу агентам",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Всё в LightCraft — команды, поэтому ИИ-агенты могут управлять запущенным приложением через MCP: отбирать, редактировать и экспортировать по вашему слову.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Запустите приложение с --control 7980 (или LIGHTCRAFT_CONTROL_PORT), и команда lightcraft-cli mcp --connect подключит к нему одного агента.",
|
||||
"Welcome": "Добро пожаловать",
|
||||
"Next": "Далее",
|
||||
"Get Started": "Начать"
|
||||
}
|
||||
|
||||
@@ -321,6 +321,9 @@
|
||||
"Creates {stacks} stack{} from {photos} of {scope}": "Skapar staplar: {stacks} · foton: {photos} av {scope}{:.0}",
|
||||
"{n} selected photo{} · {pages} page{}": "Markerade foton: {n} · sidor: {pages}{:.0}{:.0}",
|
||||
"{n} photo{} in view · {slides} slide{} · {time}": "Foton i vyn: {n} · bilder: {slides} · {time}{:.0}{:.0}",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Importera foton via Fil › Importera foton… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Visa › Detaljvy ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Fil › Exportera… · Fil › Skriv ut… ({keys}).",
|
||||
"{n} folder{} could not be read": "Mappar som inte gick att läsa: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "Hoppade över filer (fotoformat som inte stöds): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -2334,6 +2334,28 @@
|
||||
"Virtual Copy": "Virtuell kopia",
|
||||
"Visible": "Synlig",
|
||||
"Visualize HDR": "Visualisera HDR",
|
||||
"Welcome to LightCraft": "Välkommen till LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Ett snabbt, icke-förstörande fotobibliotek och raw-utvecklarprogram: dina original förblir orörda, och varje redigering kan återbesökas eller återställas.",
|
||||
"Bring photos in with File › Import Photos…": "Importera foton via Fil › Importera foton…",
|
||||
"Find and organize": "Hitta och organisera",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Fotorutnätet lägger ut biblioteket som jämna rader: betygsätt, flagga och märk med färg, filtrera och samla foton i album.",
|
||||
"View › Photo Grid.": "Visa › Fotorutnät.",
|
||||
"Develop a photo": "Utveckla ett foto",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Öppna ett foto för att utveckla det: exponering, färg, detaljer och masker, allt icke-förstörande. Använd Visa › Före/efter sida vid sida för att jämföra före och efter.",
|
||||
"View › Detail View.": "Visa › Detaljvy.",
|
||||
"Cull a shoot": "Gallra en bildserie",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Assisterad gallring poängsätter skärpa och exponering, hittar bildserier och flaggar suddiga bilder som avvisade, så att de bästa kommer fram snabbt.",
|
||||
"Photo › Assisted Culling…": "Foto › Assisterad gallring…",
|
||||
"Share your work": "Dela ditt arbete",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Exportera skapar JPEG, PNG, TIFF, WebP med mera, i den storlek du vill; Skriv ut lägger ett foto på en sida som PDF.",
|
||||
"File › Export… · File › Print…": "Fil › Exportera… · Fil › Skriv ut…",
|
||||
"File › Export…": "Fil › Exportera…",
|
||||
"Put agents to work": "Sätt agenterna i arbete",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Allting i LightCraft är ett kommando, så AI-agenter kan styra det körande programmet över MCP: gallring, utveckling och exportering på ditt ord.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Starta programmet med --control 7980 (eller LIGHTCRAFT_CONTROL_PORT), så kopplar lightcraft-cli mcp --connect en agent till det.",
|
||||
"Welcome": "Välkommen",
|
||||
"Next": "Nästa",
|
||||
"Get Started": "Kom igång",
|
||||
"Visualize range": "Visualisera omfång",
|
||||
"Visualize Spots": "Visualisera fläckar",
|
||||
"Visualize Spots (A)": "Visualisera fläckar (A)",
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "Не вдалося відновити розташування панелей: {error}",
|
||||
"Panel move was not applied: {error}": "Переміщення панелі не застосовано: {error}",
|
||||
"{name} is installed": "{name} установлено",
|
||||
"{name} is installed and in use: face recognition is on": "{name} установлено й використовується: розпізнавання облич увімкнено"
|
||||
"{name} is installed and in use: face recognition is on": "{name} установлено й використовується: розпізнавання облич увімкнено",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Фотографії імпортуйте через Файл › Імпортувати фото… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Вигляд › Детальне подання ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Файл › Експортувати… · Файл › Друк… ({keys})."
|
||||
}
|
||||
|
||||
@@ -2400,5 +2400,27 @@
|
||||
"Top bar": "Верхня панель",
|
||||
"Tool strip": "Панель інструментів",
|
||||
"Bottom bar": "Нижня панель",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Запобігати мерехтінню G-SYNC/FreeSync (після перезапуску)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "Запобігати мерехтінню G-SYNC/FreeSync (після перезапуску)",
|
||||
"Welcome to LightCraft": "Ласкаво просимо до LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Швидка бібліотека фотографій і неруйнівна обробка RAW: оригінали залишаються недоторканими, а кожну зміну можна переглянути або скасувати.",
|
||||
"Bring photos in with File › Import Photos…": "Фотографії імпортуйте через Файл › Імпортувати фото…",
|
||||
"Find and organize": "Знаходьте та впорядковуйте",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Сітка фото розміщує бібліотеку у вирівняних рядках: оцінюйте, позначайте прапорцями й кольорами, фільтруйте та збирайте фото в альбоми.",
|
||||
"View › Photo Grid.": "Вигляд › Сітка фото.",
|
||||
"Develop a photo": "Оброблюйте фотографію",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Відкрийте фото для обробки: експозиція, колір, деталі та маски, усе неруйнівно. Використовуйте Вигляд › До/після поруч, щоб порівняти до і після.",
|
||||
"View › Detail View.": "Вигляд › Детальне подання.",
|
||||
"Cull a shoot": "Відберіть знімки із зйомки",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Допоміжний відбір оцінює різкість і експозицію, знаходить серії та позначає розмиті знімки як відхилені, щоб найкращі фото одразу помітили.",
|
||||
"Photo › Assisted Culling…": "Фото › Допоміжний відбір…",
|
||||
"Share your work": "Поділіться своєю роботою",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Експорт створює JPEG, PNG, TIFF, WebP та інші формати потрібного розміру; Друк розміщує фото на сторінці у форматі PDF.",
|
||||
"File › Export… · File › Print…": "Файл › Експортувати… · Файл › Друк…",
|
||||
"File › Export…": "Файл › Експортувати…",
|
||||
"Put agents to work": "Залучіть агентів до роботи",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Усе в LightCraft є командами, тож ШІ-агенти можуть керувати запущеною програмою через MCP: відбирати, редагувати й експортувати за вашим дорученням.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Запустіть програму з --control 7980 (або LIGHTCRAFT_CONTROL_PORT), і lightcraft-cli mcp --connect під'єднає до неї одного агента.",
|
||||
"Welcome": "Ласкаво просимо",
|
||||
"Next": "Далі",
|
||||
"Get Started": "Почати"
|
||||
}
|
||||
|
||||
@@ -318,6 +318,9 @@
|
||||
"Panel move was not applied: {error}": "Việc di chuyển bảng chưa được áp dụng: {error}",
|
||||
"{name} is installed": "{name} đã được cài đặt",
|
||||
"{name} is installed and in use: face recognition is on": "{name} đã được cài đặt và đang dùng: nhận diện khuôn mặt đã bật",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "Đưa ảnh vào bằng Tệp › Nhập ảnh… ({keys}).",
|
||||
"View › Detail View ({keys}).": "Xem › Chế độ xem chi tiết ({keys}).",
|
||||
"File › Export… · File › Print… ({keys}).": "Tệp › Xuất… · Tệp › In… ({keys}).",
|
||||
"{n} folder{} could not be read": "Thư mục không đọc được: {n}{:.0}",
|
||||
"{n} file{} skipped (not a supported photo type)": "Đã bỏ qua tệp (không phải loại ảnh được hỗ trợ): {n}{:.0}"
|
||||
}
|
||||
|
||||
@@ -1961,6 +1961,28 @@
|
||||
"Virtual Copy": "Bản sao ảo",
|
||||
"Visible": "Hiển thị",
|
||||
"Visualize HDR": "Hình dung HDR",
|
||||
"Welcome to LightCraft": "Chào mừng đến với LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "Thư viện ảnh nhanh và bộ xử lý raw không phá hủy: bản gốc của bạn vẫn nguyên vẹn, và mọi chỉnh sửa đều có thể xem lại hoặc đặt lại.",
|
||||
"Bring photos in with File › Import Photos…": "Đưa ảnh vào bằng Tệp › Nhập ảnh…",
|
||||
"Find and organize": "Tìm và sắp xếp",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "Lưới ảnh trình bày thư viện thành các hàng cân xứng: chấm điểm, gắn cờ và dán nhãn màu, lọc, và gom ảnh vào album.",
|
||||
"View › Photo Grid.": "Xem › Lưới ảnh.",
|
||||
"Develop a photo": "Phát triển một bức ảnh",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "Mở ảnh để phát triển: độ phơi sáng, màu, chi tiết và mặt nạ, tất cả đều không phá hủy. Dùng Xem › Trước/Sau cạnh nhau để so sánh trước và sau.",
|
||||
"View › Detail View.": "Xem › Chế độ xem chi tiết.",
|
||||
"Cull a shoot": "Sàng lọc một buổi chụp",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "Sàng lọc hỗ trợ chấm điểm độ nét và độ phơi sáng, tìm các chuỗi ảnh liên tiếp, và gắn cờ ảnh mờ thành đã loại, để những ảnh đạt nổi lên nhanh chóng.",
|
||||
"Photo › Assisted Culling…": "Ảnh › Sàng lọc hỗ trợ…",
|
||||
"Share your work": "Chia sẻ tác phẩm của bạn",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "Xuất tạo JPEG, PNG, TIFF, WebP và nhiều định dạng khác, với kích thước tùy ý; In sắp ảnh lên trang thành PDF.",
|
||||
"File › Export… · File › Print…": "Tệp › Xuất… · Tệp › In…",
|
||||
"File › Export…": "Tệp › Xuất…",
|
||||
"Put agents to work": "Cho agent vào việc",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "Mọi thứ trong LightCraft đều là lệnh, nên agent AI có thể điều khiển ứng dụng đang chạy qua MCP: sàng lọc, phát triển và xuất ảnh theo lời bạn.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "Khởi chạy ứng dụng với --control 7980 (hoặc LIGHTCRAFT_CONTROL_PORT), rồi lightcraft-cli mcp --connect sẽ gắn một agent vào đó.",
|
||||
"Welcome": "Chào mừng",
|
||||
"Next": "Tiếp theo",
|
||||
"Get Started": "Bắt đầu",
|
||||
"Visualize Spots": "Hiển thị điểm bụi",
|
||||
"Visualize Spots (A)": "Hiển thị điểm bụi (A)",
|
||||
"Visualize range": "Hiển thị dải",
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "无法恢复面板布局:{error}",
|
||||
"Panel move was not applied: {error}": "面板移动未应用:{error}",
|
||||
"{name} is installed": "{name} 已安装",
|
||||
"{name} is installed and in use: face recognition is on": "{name} 已安装并在使用:人脸识别已开启"
|
||||
"{name} is installed and in use: face recognition is on": "{name} 已安装并在使用:人脸识别已开启",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "通过 文件 › 导入照片… ({keys}) 导入照片。",
|
||||
"View › Detail View ({keys}).": "视图 › 详细视图 ({keys})。",
|
||||
"File › Export… · File › Print… ({keys}).": "文件 › 导出… · 文件 › 打印… ({keys})。"
|
||||
}
|
||||
|
||||
@@ -2403,5 +2403,27 @@
|
||||
"Set up name suggestions…": "设置姓名建议…",
|
||||
"Top bar": "顶部栏",
|
||||
"Tool strip": "工具栏",
|
||||
"Bottom bar": "底部栏"
|
||||
"Bottom bar": "底部栏",
|
||||
"Welcome to LightCraft": "欢迎使用 LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "一款快速、非破坏性的照片库和 RAW 显影工具:原片保持不变,每次编辑都可以随时查看或还原。",
|
||||
"Bring photos in with File › Import Photos…": "通过 文件 › 导入照片…导入照片。",
|
||||
"Find and organize": "查找与整理",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "照片网格以等高行排列图库:可评分、标记旗标和颜色标签、筛选,并把照片归入相册。",
|
||||
"View › Photo Grid.": "视图 › 照片网格。",
|
||||
"Develop a photo": "编辑照片",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "打开照片进行编辑:曝光、色彩、细节和蒙版,全部非破坏性。使用 视图 › 修改前/修改后左右排列 对比编辑前后。",
|
||||
"View › Detail View.": "视图 › 详细视图。",
|
||||
"Cull a shoot": "筛选一次拍摄",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "辅助筛选会评估对焦与曝光,找出连拍组,并将模糊的照片标为排除,让保留的照片更快浮现。",
|
||||
"Photo › Assisted Culling…": "照片 › 辅助筛选…",
|
||||
"Share your work": "分享您的作品",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "导出可输出 JPEG、PNG、TIFF、WebP 等格式,并可按需调整尺寸;打印会将照片排版到页面上,输出为 PDF。",
|
||||
"File › Export… · File › Print…": "文件 › 导出… · 文件 › 打印…",
|
||||
"File › Export…": "文件 › 导出…",
|
||||
"Put agents to work": "让智能体开始工作",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "LightCraft 中的一切都是命令,因此 AI 智能体可以通过 MCP 驱动正在运行的应用:按您的指示进行筛选、编辑和导出。",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "先以 --control 7980 (或 LIGHTCRAFT_CONTROL_PORT) 启动应用,lightcraft-cli mcp --connect 即可将一个智能体接入其中。",
|
||||
"Welcome": "欢迎",
|
||||
"Next": "下一步",
|
||||
"Get Started": "开始使用"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,8 @@
|
||||
"Panel layout could not be restored: {error}": "無法還原面板配置:{error}",
|
||||
"Panel move was not applied: {error}": "面板移動未套用:{error}",
|
||||
"{name} is installed": "{name} 已安裝",
|
||||
"{name} is installed and in use: face recognition is on": "{name} 已安裝並在使用中:臉部辨識已開啟"
|
||||
"{name} is installed and in use: face recognition is on": "{name} 已安裝並在使用中:臉部辨識已開啟",
|
||||
"Bring photos in with File › Import Photos… ({keys}).": "透過 檔案 › 匯入照片… ({keys}) 匯入照片。",
|
||||
"View › Detail View ({keys}).": "檢視 › 詳細檢視 ({keys})。",
|
||||
"File › Export… · File › Print… ({keys}).": "檔案 › 匯出… · 檔案 › 列印… ({keys})。"
|
||||
}
|
||||
|
||||
@@ -2404,5 +2404,27 @@
|
||||
"Top bar": "頂部列",
|
||||
"Tool strip": "工具列",
|
||||
"Bottom bar": "底部列",
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "防止 G-SYNC/FreeSync 閃爍(下次啟動時生效)"
|
||||
"Prevent G-SYNC/FreeSync flicker (applies at next launch)": "防止 G-SYNC/FreeSync 閃爍(下次啟動時生效)",
|
||||
"Welcome to LightCraft": "歡迎使用 LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.": "快速且非破壞性的照片庫與 RAW 顯影工具:原始檔保持不變,每次編輯都能隨時檢視或還原。",
|
||||
"Bring photos in with File › Import Photos…": "透過 檔案 › 匯入照片…匯入照片。",
|
||||
"Find and organize": "尋找與整理",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.": "照片格狀檢視以等高列排列圖庫:可評分、標記旗標與色彩標籤、篩選,並將照片歸入相簿。",
|
||||
"View › Photo Grid.": "檢視 › 照片格狀檢視。",
|
||||
"Develop a photo": "編輯照片",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.": "開啟照片進行編輯:曝光、色彩、細節與遮色片,全部為非破壞性。使用 檢視 › 編輯前/後左右檢視 比較編輯前後。",
|
||||
"View › Detail View.": "檢視 › 詳細檢視。",
|
||||
"Cull a shoot": "挑選一次拍攝的照片",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.": "輔助挑片會評估對焦與曝光、找出連拍組,並將模糊的照片標為排除,讓入選的照片更快浮現。",
|
||||
"Photo › Assisted Culling…": "照片 › 輔助挑片…",
|
||||
"Share your work": "分享您的作品",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.": "匯出可輸出 JPEG、PNG、TIFF、WebP 等格式,尺寸可自行設定;列印會將照片排版到頁面上,輸出為 PDF。",
|
||||
"File › Export… · File › Print…": "檔案 › 匯出… · 檔案 › 列印…",
|
||||
"File › Export…": "檔案 › 匯出…",
|
||||
"Put agents to work": "讓代理程式開始工作",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.": "LightCraft 中的所有功能都是指令,因此 AI 代理程式可透過 MCP 操控正在執行的應用程式:依您的指示挑片、編輯與匯出。",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.": "先以 --control 7980 (或 LIGHTCRAFT_CONTROL_PORT) 啟動應用程式,lightcraft-cli mcp --connect 即可將一個代理程式連接到其中。",
|
||||
"Welcome": "歡迎",
|
||||
"Next": "下一步",
|
||||
"Get Started": "開始使用"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::{
|
||||
LightcraftApp, docking, drop_overlay, export_task, focus_scan, import, lightroom_import, merge, monotonic_ms, panels, pick, recheck, shortcuts,
|
||||
slideshow_export, state, sync, tasks, theme, updates_ui, widgets,
|
||||
};
|
||||
use crate::startup;
|
||||
|
||||
impl LightcraftApp {
|
||||
/// Install/refresh fonts once and watch the device and availability caches. Setup, not input.
|
||||
@@ -27,6 +28,24 @@ impl LightcraftApp {
|
||||
} else {
|
||||
self.fonts_ready = true;
|
||||
}
|
||||
// First run ever, or a fresh version: the tour, or What's New. Never headless —
|
||||
// snapshots and scripted runs hold session-local state and must not gain a modal.
|
||||
// Every frame until it shows: another dialog open on the first frame (a delivered file)
|
||||
// keeps its place, and so does the blocking library window (the launch chooser or a
|
||||
// library error — it is not in `ui.dialog`). The startup dialog opens once that window
|
||||
// closes or is dismissed. Choosing it here is only dueing it: the version is recorded
|
||||
// when the dialog is actually presented (`dialogs::show`), so a host that runs `logic`
|
||||
// and quits before a visible frame does not burn the tour.
|
||||
if self.ui.dialog.is_none()
|
||||
&& self.library_problem.as_ref().is_none_or(|p| p.dismissed)
|
||||
&& let Some(dialog) = startup::open_at_startup(self.ui.seen_version.as_deref(), self.headless_host)
|
||||
{
|
||||
self.ui.dialog = Some(dialog);
|
||||
// a drop queued on this same frame (a delivered file, a host drop on the first pass)
|
||||
// arrived before the dialog did: mark it, so it waits behind the dialog and imports
|
||||
// once it closes instead of taking the close-the-dialog refusal on the first UI pass
|
||||
self.defer_dropped_files();
|
||||
}
|
||||
}
|
||||
|
||||
/// 0.36 stored each `PanelState` under the panel's exact `Id`; 0.37 stores it under
|
||||
|
||||
@@ -52,6 +52,152 @@ pub const ABOUT_TABS: &[(&str, &str)] = &[("about", "About"), ("contributors", "
|
||||
/// Help ▸ What's New (docs/whats-new.md).
|
||||
pub const WHATS_NEW: &str = include_str!("../../../docs/whats-new.md");
|
||||
|
||||
/// The tour's window title and the labels of its next and last-page buttons: translated in every
|
||||
/// catalog, with the pages' text (the test that pins it reads these).
|
||||
pub(crate) const WELCOME_TITLE: &str = "Welcome";
|
||||
pub(crate) const WELCOME_NEXT: &str = "Next";
|
||||
pub(crate) const WELCOME_START: &str = "Get Started";
|
||||
|
||||
/// The first-run tour's pages: (title, what it does, where to find it). Every menu path names a
|
||||
/// real command (`file.addPhotos`, `view.photoGrid`, `view.detail`, `view.beforeAfter`,
|
||||
/// `dialog.cull`, `dialog.export`, `dialog.print`); `›` paints (the menu triangle `▸` does not).
|
||||
/// The pages that name a shortcut take the command's live binding at render time
|
||||
/// ([`welcome_body`]) — no key is written here, so no platform is shown a chord its menus do
|
||||
/// not bind.
|
||||
pub(crate) const WELCOME_PAGES: [(&str, &str, &str); 6] = [
|
||||
(
|
||||
"Welcome to LightCraft",
|
||||
"A fast, non-destructive photo library and raw developer: your originals stay untouched, and every edit can be revisited or reset.",
|
||||
"Bring photos in with File › Import Photos…",
|
||||
),
|
||||
(
|
||||
"Find and organize",
|
||||
"The Photo Grid lays the library out as justified rows: rate, flag and colour-label, filter, and gather photos into albums.",
|
||||
"View › Photo Grid.",
|
||||
),
|
||||
(
|
||||
"Develop a photo",
|
||||
"Open a photo to develop it: exposure, colour, detail and masks, all non-destructive. Use View › Before/After Side by Side to compare before and after.",
|
||||
"View › Detail View.",
|
||||
),
|
||||
(
|
||||
"Cull a shoot",
|
||||
"Assisted Culling scores focus and exposure, finds bursts, and flags blurry shots as rejects, so the keepers surface fast.",
|
||||
"Photo › Assisted Culling…",
|
||||
),
|
||||
(
|
||||
"Share your work",
|
||||
"Export renders JPEG, PNG, TIFF, WebP and more, sized as you like; Print lays a photo out on a page as PDF.",
|
||||
"File › Export… · File › Print…",
|
||||
),
|
||||
(
|
||||
"Put agents to work",
|
||||
"Everything in LightCraft is a command, so AI agents can drive the running app over MCP: culling, editing, exporting on your word.",
|
||||
"Start the app with --control 7980 (or LIGHTCRAFT_CONTROL_PORT), then lightcraft-cli mcp --connect attaches one to it.",
|
||||
),
|
||||
];
|
||||
|
||||
/// The tour page that names the desktop-only launch flow: an MCP agent attaches over a `--control`
|
||||
/// port, which only the desktop app listens on. The browser build omits the page rather than
|
||||
/// telling browser users to launch a desktop binary.
|
||||
const WELCOME_DESKTOP_ONLY_PAGE: usize = 5;
|
||||
|
||||
/// The tour's pages for `web_host` (the browser build) against the desktop's: the browser has no
|
||||
/// `--control` port for `lightcraft-cli` to attach to, so its tour omits [`WELCOME_DESKTOP_ONLY_PAGE`].
|
||||
/// The parameter is the pure decision; the running host passes [`welcome_pages_host`]'s answer.
|
||||
pub(crate) fn welcome_pages(web_host: bool) -> &'static [(&'static str, &'static str, &'static str)] {
|
||||
if web_host { &WELCOME_PAGES[..WELCOME_DESKTOP_ONLY_PAGE] } else { &WELCOME_PAGES }
|
||||
}
|
||||
|
||||
/// The pages this build's host shows: the web build (wasm32) hides the desktop-only page.
|
||||
fn welcome_pages_host() -> &'static [(&'static str, &'static str, &'static str)] {
|
||||
welcome_pages(cfg!(target_arch = "wasm32"))
|
||||
}
|
||||
|
||||
/// The command whose shortcut a tour page's opening line names.
|
||||
fn welcome_page_command(page: usize) -> Option<&'static str> {
|
||||
match page {
|
||||
0 => Some("file.addPhotos"),
|
||||
2 => Some("view.detail"),
|
||||
4 => Some("dialog.print"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The tour page that names Print. The browser build's File menu disables Print
|
||||
/// ([`crate::menus::ui_enabled`]), so that host's tour names Export alone in the opening line
|
||||
/// ([`welcome_opening`]) instead of a menu item the host greys out.
|
||||
const WELCOME_PRINT_PAGE: usize = 4;
|
||||
|
||||
/// The opening line a tour page paints under its text: the pages that name a shortcut carry the
|
||||
/// command's live binding (`keys`), the Share page names Export alone on the web host, and the
|
||||
/// rest paint the page's own sentence.
|
||||
pub(crate) fn welcome_opening(page: usize, opening: &str, keys: Option<&str>, web_host: bool) -> String {
|
||||
match (page, keys, web_host) {
|
||||
(WELCOME_PRINT_PAGE, _, true) => crate::i18n::tr("File › Export…").to_string(),
|
||||
(0, Some(keys), _) => crate::i18n::tr_format!("Bring photos in with File › Import Photos… ({keys}).", keys = keys),
|
||||
(2, Some(keys), _) => crate::i18n::tr_format!("View › Detail View ({keys}).", keys = keys),
|
||||
(WELCOME_PRINT_PAGE, Some(keys), _) => crate::i18n::tr_format!("File › Export… · File › Print… ({keys}).", keys = keys),
|
||||
_ => crate::i18n::tr(opening).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The first-run tour: one page at a time, Back / Skip / Next under it. `close` is set when the
|
||||
/// tour is finished (Next on the last page) or skipped.
|
||||
fn welcome_body(app: &LightcraftApp, ui: &mut egui::Ui, t: &Tokens, page: &mut usize, close: &mut bool) {
|
||||
let pages = welcome_pages_host();
|
||||
let last = pages.len() - 1;
|
||||
*page = (*page).min(last);
|
||||
let (title, text, opening) = pages[*page];
|
||||
ui.add_space(2.0);
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(title)).font(t.semibold(16.0)).color(t.text));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(text)).color(t.text_label));
|
||||
ui.add_space(2.0);
|
||||
// The pages that name a shortcut show the command's live binding, painted the way the menus
|
||||
// paint theirs (`menubar::shortcut_text`: `⌘⇧I` on macOS, `Ctrl+Shift+I` elsewhere) — never
|
||||
// a hard-coded key. A command whose binding was removed shows the menu path alone.
|
||||
let mac = ui.ctx().os() == egui::os::OperatingSystem::Mac;
|
||||
let keys = welcome_page_command(*page)
|
||||
.and_then(|id| crate::shortcuts::shortcut_of(&app.ui.settings.keymap, id))
|
||||
.map(|sc| crate::menus::menubar::shortcut_text(sc, mac));
|
||||
let opening = welcome_opening(*page, opening, keys.as_deref(), cfg!(target_arch = "wasm32"));
|
||||
ui.label(egui::RichText::new(opening).color(t.text_dim));
|
||||
ui.add_space(8.0);
|
||||
// progress dots, centred: the active page's is the accent
|
||||
let spacing = 12.0;
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 8.0), egui::Sense::hover());
|
||||
let x0 = rect.center().x - spacing * (last as f32) / 2.0;
|
||||
for i in 0..pages.len() {
|
||||
let c = egui::pos2(x0 + i as f32 * spacing, rect.center().y);
|
||||
ui.painter().circle_filled(c, if i == *page { 2.5 } else { 1.5 }, if i == *page { t.accent } else { t.text_dim });
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
use crate::button::{Button, Kind};
|
||||
let back = Button::new(crate::i18n::tr("Back")).widget("button:welcomeBack").enabled(*page > 0).min_width(72.0);
|
||||
let skip = Button::new(crate::i18n::tr("Skip")).kind(Kind::Ghost).widget("button:welcomeSkip").min_width(72.0);
|
||||
let next_label = if *page == last { crate::i18n::tr(WELCOME_START) } else { crate::i18n::tr(WELCOME_NEXT) };
|
||||
let next = Button::new(next_label).kind(Kind::Primary).widget("button:welcomeNext").min_width(84.0);
|
||||
let gap = ui.spacing().item_spacing.x;
|
||||
let row_w = ui.available_width().max(ui.min_rect().width()).max(1.0);
|
||||
let (skip_w, next_w) = (skip.desired_size(ui).x, next.desired_size(ui).x);
|
||||
ui.allocate_ui_with_layout(egui::vec2(row_w, next.desired_size(ui).y), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
if back.show(ui).clicked() {
|
||||
*page -= 1;
|
||||
}
|
||||
ui.add_space((ui.available_width() - skip_w - next_w - 2.0 * gap).max(0.0));
|
||||
if skip.show(ui).clicked() {
|
||||
*close = true;
|
||||
}
|
||||
if next.show(ui).clicked() {
|
||||
if *page == last {
|
||||
*close = true;
|
||||
} else {
|
||||
*page += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The import review and Settings keep window ids of their own, so the size each is given
|
||||
/// doesn't carry over to the other dialogs.
|
||||
fn window_id(dialog: &Dialog) -> egui::Id {
|
||||
@@ -158,6 +304,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
Dialog::FaceModel { .. } => 460.0,
|
||||
Dialog::LightroomCatalog { .. } => 440.0,
|
||||
Dialog::LightroomDestination { .. } => 480.0,
|
||||
Dialog::Welcome { .. } => 460.0,
|
||||
Dialog::LensDatabase { .. } => 440.0,
|
||||
Dialog::ManageEffects { .. } => 560.0,
|
||||
_ => 380.0,
|
||||
@@ -172,7 +319,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
}
|
||||
ui.spacing_mut().item_spacing.y = 8.0;
|
||||
body_h = dialog_body(ui, (!import && !print).then_some(title.as_str()), body_max, |ui| {
|
||||
dialog_content(app, ui, &t, body_max, &mut confirm, &mut dlg)
|
||||
dialog_content(app, ui, &t, body_max, &mut confirm, &mut close, &mut dlg)
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
// the confirm on the right with Cancel to its left (macOS and GNOME's order), filled with
|
||||
@@ -217,6 +364,14 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
if let Some(band) = title_bar {
|
||||
title_bar_handle(ctx, band, w.response.rect);
|
||||
}
|
||||
// The startup dialog is recorded once it was presented, not when the logic tick picked
|
||||
// it: a host that runs `logic` and quits before a visible frame does not burn the tour,
|
||||
// and a crash while it is up still does not reopen it on every launch. A frame whose
|
||||
// input a notice owns is not a presentation: the dialog is drawn but cannot be used, so
|
||||
// the recording waits for a pass where the dialog actually takes input.
|
||||
if !raw_blocked && matches!(at_start, Dialog::Welcome { .. } | Dialog::WhatsNew) {
|
||||
app.ui.record_startup_dialog_seen();
|
||||
}
|
||||
}
|
||||
// (while the shortcuts editor records a key, it takes Esc itself to cancel; an open popup in
|
||||
// the dialog, such as a date picker's calendar, takes it to close itself)
|
||||
@@ -304,6 +459,7 @@ fn dialog_title(app: &LightcraftApp, dlg: &Dialog) -> String {
|
||||
Dialog::FaceModel { info, .. } if info["download"].is_string() => "Download Face Model",
|
||||
Dialog::FaceModel { .. } => "Add Face Model",
|
||||
Dialog::WhatsNew => "What's New",
|
||||
Dialog::Welcome { .. } => WELCOME_TITLE,
|
||||
Dialog::Cull { .. } => "Assisted Culling",
|
||||
Dialog::SmartRules { id: None, .. } => "New Smart Album",
|
||||
Dialog::SmartRules { .. } => "Edit Smart Album",
|
||||
@@ -448,6 +604,7 @@ fn fixed_ok_label(app: &LightcraftApp, dlg: &Dialog, informational: bool) -> &'s
|
||||
Dialog::RemoveFolder { .. } => "Remove",
|
||||
Dialog::LightroomCatalog { .. } => "Import Lightroom Catalog…",
|
||||
d @ Dialog::LightroomDestination { .. } => crate::lightroom_destination::ok_label(d),
|
||||
Dialog::Welcome { .. } => "",
|
||||
Dialog::LensDatabase { .. } => "Download",
|
||||
Dialog::SynchronizeFolder { .. } if informational => "Close",
|
||||
Dialog::SynchronizeFolder { .. } => "Synchronize",
|
||||
@@ -1919,11 +2076,12 @@ fn about_body(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, dlg: &mut
|
||||
|
||||
/// The body of the open dialog: what each kind of dialog shows between its title and its buttons. `confirm` is set
|
||||
/// by a dialog that is confirmed from its body (a text field's Return).
|
||||
fn dialog_content(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, body_max: f32, confirm: &mut bool, dlg: &mut Dialog) {
|
||||
fn dialog_content(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, body_max: f32, confirm: &mut bool, close: &mut bool, dlg: &mut Dialog) {
|
||||
match dlg {
|
||||
Dialog::AutoStack { .. } => auto_stack_body(app, ui, t, dlg),
|
||||
Dialog::Cull { .. } => cull_body(app, ui, t, dlg),
|
||||
Dialog::WhatsNew => whats_new_body(ui, t, dlg),
|
||||
Dialog::Welcome { page } => welcome_body(app, ui, t, page, close),
|
||||
Dialog::DenoiseModel { info, accepted } => crate::panels::denoise::model_dialog(app, ui, t, info, accepted),
|
||||
Dialog::FaceModel { info, accepted, .. } => crate::panels::faces::model_dialog(app, ui, t, info, accepted),
|
||||
Dialog::LightroomResult { .. } => lightroom_result_body(ui, dlg),
|
||||
|
||||
@@ -177,7 +177,7 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result<serde_jso
|
||||
Dialog::AutoStack { gap } => app.run("stack.auto", json!({"gap": gap})),
|
||||
Dialog::DenoiseModel { info, accepted } => crate::panels::denoise::install(app, info, *accepted),
|
||||
Dialog::FaceModel { path, info, accepted } => crate::panels::faces::install(app, path, info, *accepted),
|
||||
Dialog::AllMetadata { .. } | Dialog::SystemInfo { .. } | Dialog::WhatsNew => Ok(serde_json::Value::Null),
|
||||
Dialog::AllMetadata { .. } | Dialog::SystemInfo { .. } | Dialog::WhatsNew | Dialog::Welcome { .. } => Ok(serde_json::Value::Null),
|
||||
Dialog::Cull { reject_below, pick_best, burst_gap, similarity } => {
|
||||
app.ui.cull_burst = Some((*burst_gap, *similarity));
|
||||
app.run("photo.analyze", cull_params(*reject_below, *pick_best, *burst_gap, *similarity))
|
||||
|
||||
@@ -208,6 +208,7 @@ pub(super) fn only_shows(dlg: &Dialog) -> bool {
|
||||
| Dialog::SystemInfo { .. }
|
||||
| Dialog::AllMetadata { .. }
|
||||
| Dialog::WhatsNew
|
||||
| Dialog::Welcome { .. }
|
||||
| Dialog::ManageEffects { .. }
|
||||
| Dialog::ViewOptions
|
||||
) || matches!(dlg, Dialog::SynchronizeFolder { counts: Some(c), .. } if c.up_to_date())
|
||||
|
||||
@@ -136,6 +136,33 @@ fn catalogs_agree_on_placeholders() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The tour's text (its pages, and its window and button labels) is in every catalog: a language
|
||||
/// missing one of these keys would show that line in English on the first launch. The pages that
|
||||
/// name a shortcut carry the live chord through a format message (`menubar::shortcut_text` paints
|
||||
/// it), so their `{keys}` forms are pinned in every `*-formats.json` too.
|
||||
#[test]
|
||||
fn the_welcome_tour_is_translated_in_every_catalog() {
|
||||
use crate::dialogs::{WELCOME_NEXT, WELCOME_PAGES, WELCOME_START, WELCOME_TITLE};
|
||||
let keys: Vec<&str> = WELCOME_PAGES
|
||||
.iter()
|
||||
.flat_map(|(title, text, opening)| [*title, *text, *opening])
|
||||
.chain([WELCOME_TITLE, WELCOME_NEXT, WELCOME_START])
|
||||
.collect();
|
||||
let format_keys = [
|
||||
"Bring photos in with File \u{203a} Import Photos\u{2026} ({keys}).",
|
||||
"View \u{203a} Detail View ({keys}).",
|
||||
"File \u{203a} Export\u{2026} \u{b7} File \u{203a} Print\u{2026} ({keys}).",
|
||||
];
|
||||
for language in Locale::ALL.iter().copied().filter(|language| *language != Locale::En) {
|
||||
let catalog = language.catalog();
|
||||
let missing: Vec<&str> = keys.iter().copied().filter(|key| !catalog.contains_key(*key)).collect();
|
||||
assert!(missing.is_empty(), "{}: no translation for {missing:?}", language.code());
|
||||
let own_formats = formats_catalog(language.code());
|
||||
let missing_formats: Vec<&str> = format_keys.iter().copied().filter(|key| !own_formats.contains_key(*key)).collect();
|
||||
assert!(missing_formats.is_empty(), "{}: no translation for {missing_formats:?}", language.code());
|
||||
}
|
||||
}
|
||||
|
||||
/// Locale tags are matched leniently, so a system locale reaches a shipped language.
|
||||
/// The message box for a desktop start that failed (issue #260) names the error and the log
|
||||
/// file, in the UI language.
|
||||
@@ -679,6 +706,9 @@ fn german_ukrainian_italian_and_finnish_buttons_fit_the_bottom_bar_and_export_di
|
||||
let ctx = fonts_ctx(&[]);
|
||||
let services = crate::Services { pick_folder: Some(Box::new(|| None)), ..Default::default() };
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
// a headless host never gains the first-run tour: this harness drives `ui` directly and
|
||||
// measures the export dialog's own layout
|
||||
app.headless_host = true;
|
||||
app.ui.language = language;
|
||||
let mut bounds = Vec::new();
|
||||
for export in [false, true] {
|
||||
@@ -738,6 +768,7 @@ fn painted_text(ctx: &egui::Context, app: &mut crate::LightcraftApp, language: L
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
app.headless_host = true;
|
||||
app.ui.language = language;
|
||||
let mut text = String::new();
|
||||
for frame in 0..4 {
|
||||
@@ -827,6 +858,8 @@ fn the_ui_works_without_craft_fonts() {
|
||||
}
|
||||
});
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default());
|
||||
// a headless host never gains the first-run tour: this pass just warms every language up
|
||||
app.headless_host = true;
|
||||
for language in Locale::ALL {
|
||||
app.ui.language = *language;
|
||||
for frame in 0..3 {
|
||||
|
||||
@@ -46,6 +46,7 @@ pub mod shortcuts;
|
||||
pub mod sidebar;
|
||||
pub mod softpaint;
|
||||
pub mod space_hand;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
pub mod sync;
|
||||
pub mod tasks;
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
//! Startup dialog selection for first-run onboarding and What's New.
|
||||
//!
|
||||
//! On first launch (`seen == None`) show `FirstRun`. After an update (the stored previous
|
||||
//! version differs from the running version) show `WhatsNew`. Both are skipped when the
|
||||
//! version was already seen.
|
||||
//!
|
||||
//! `UiState::record_startup_dialog_seen` uses record-on-show semantics (documented there): the
|
||||
//! version is recorded when the dialog is presented in a UI pass, not when the logic tick
|
||||
//! picks it.
|
||||
//!
|
||||
//! A settings file from before this existed migrates to [`LEGACY_SEEN_VERSION`], so an
|
||||
//! existing installation is due What's New, never the first-run tour.
|
||||
|
||||
use crate::state::Dialog;
|
||||
|
||||
/// What to show at startup before the main UI takes over.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StartupDialog {
|
||||
/// First launch ever: show the onboarding walkthrough.
|
||||
FirstRun,
|
||||
/// The app was updated since the last launch: show What's New.
|
||||
WhatsNew,
|
||||
}
|
||||
|
||||
/// Parse "1.2.3" / "1.2.3-rc.1" / "0.8.0" into (major, minor, patch), ignoring any
|
||||
/// prerelease suffix. Returns `None` when not parseable.
|
||||
pub fn parse_version(version: &str) -> Option<(u64, u64, u64)> {
|
||||
let core = version.split('-').next()?.trim();
|
||||
let parts: Vec<&str> = core.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let major = parts[0].parse::<u64>().ok()?;
|
||||
let minor = parts[1].parse::<u64>().ok()?;
|
||||
let patch = parts[2].parse::<u64>().ok()?;
|
||||
Some((major, minor, patch))
|
||||
}
|
||||
|
||||
/// The version a settings file from before the startup dialog existed migrates to: the file's
|
||||
/// existence means an installation that has been run, so it is due What's New, never the
|
||||
/// first-run tour. `None` stays reserved for no settings at all.
|
||||
pub const LEGACY_SEEN_VERSION: &str = "0.0.0";
|
||||
|
||||
/// The legacy `seenVersion` migration both settings loaders share — the desktop settings file
|
||||
/// (`lightcraft`'s `prefs`) and the browser's stored `ui.json` — applied to the parsed JSON
|
||||
/// object before it deserializes into [`crate::state::UiState`]. A saved file from before the
|
||||
/// startup dialog existed has no `seenVersion` key, and the file's existence means an
|
||||
/// installation that has been run, due What's New — never the first-run tour. A file this
|
||||
/// version saved before the dialog was presented carries an explicit `null` instead (that is
|
||||
/// why `None` serializes) and is not migrated, and no file at all (no object) stays a first
|
||||
/// run.
|
||||
pub fn migrate_legacy_seen_version(fields: &mut serde_json::Map<String, serde_json::Value>) {
|
||||
if !fields.contains_key("seenVersion") {
|
||||
fields.insert("seenVersion".into(), serde_json::json!(LEGACY_SEEN_VERSION));
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog to show for `seen` (the stored last-seen version, `None` = first run) against
|
||||
/// `current` (the running version).
|
||||
///
|
||||
/// `FirstRun` only when `seen == None`. `WhatsNew` when the version changed. `None` when
|
||||
/// identical.
|
||||
///
|
||||
/// Comparison goes through [`parse_version`]. When both sides parse, any numeric difference
|
||||
/// (and a prerelease-vs-release string difference with the same numbers) is a change →
|
||||
/// `WhatsNew`. When either side is unparseable, fall back to the string comparison: show
|
||||
/// `WhatsNew` when the strings differ, `None` when identical — never `FirstRun` once
|
||||
/// something was seen.
|
||||
pub fn startup_dialog(seen: Option<&str>, current: &str) -> Option<StartupDialog> {
|
||||
let Some(seen) = seen else {
|
||||
return Some(StartupDialog::FirstRun);
|
||||
};
|
||||
if seen == current {
|
||||
return None;
|
||||
}
|
||||
// `seen` differs from `current`. `parse_version` confirms a real version-like change
|
||||
// when both sides parse; when either side is unparseable we fall back to the string
|
||||
// difference already established and show WhatsNew conservatively.
|
||||
match (parse_version(seen), parse_version(current)) {
|
||||
(Some(sv), Some(cv)) if sv != cv => Some(StartupDialog::WhatsNew),
|
||||
_ => Some(StartupDialog::WhatsNew),
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog the first frame opens for `seen` (the stored last-seen version): [`Dialog::Welcome`]
|
||||
/// on the first run ever, [`Dialog::WhatsNew`] after an update, none once the running version was
|
||||
/// seen. A headless host (snapshots, scripted runs, tests: their UI state is session-local, with
|
||||
/// no stored version behind it) never gains a modal.
|
||||
pub fn open_at_startup(seen: Option<&str>, headless: bool) -> Option<Dialog> {
|
||||
if headless {
|
||||
return None;
|
||||
}
|
||||
match startup_dialog(seen, env!("CARGO_PKG_VERSION")) {
|
||||
Some(StartupDialog::FirstRun) => Some(Dialog::Welcome { page: 0 }),
|
||||
Some(StartupDialog::WhatsNew) => Some(Dialog::WhatsNew),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_version_plain() {
|
||||
assert_eq!(parse_version("1.2.3"), Some((1, 2, 3)));
|
||||
assert_eq!(parse_version("0.8.0"), Some((0, 8, 0)));
|
||||
assert_eq!(parse_version("10.20.30"), Some((10, 20, 30)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_version_prerelease() {
|
||||
assert_eq!(parse_version("1.2.3-rc.1"), Some((1, 2, 3)));
|
||||
assert_eq!(parse_version("0.8.0-beta.2"), Some((0, 8, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_version_garbage() {
|
||||
assert_eq!(parse_version(""), None);
|
||||
assert_eq!(parse_version("not-a-version"), None);
|
||||
assert_eq!(parse_version("1.2"), None);
|
||||
assert_eq!(parse_version("1.2.3.4"), None);
|
||||
assert_eq!(parse_version("abc.2.3"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialog_first_run() {
|
||||
assert_eq!(startup_dialog(None, "0.8.0"), Some(StartupDialog::FirstRun));
|
||||
assert_eq!(startup_dialog(None, "1.0.0"), Some(StartupDialog::FirstRun));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialog_equal() {
|
||||
assert_eq!(startup_dialog(Some("0.8.0"), "0.8.0"), None);
|
||||
assert_eq!(startup_dialog(Some("1.2.3"), "1.2.3"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialog_version_changed() {
|
||||
assert_eq!(startup_dialog(Some("0.7.9"), "0.8.0"), Some(StartupDialog::WhatsNew));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialog_prerelease_vs_release() {
|
||||
assert_eq!(startup_dialog(Some("1.2.3-rc.1"), "1.2.3"), Some(StartupDialog::WhatsNew));
|
||||
assert_eq!(startup_dialog(Some("1.2.3"), "1.2.3-rc.1"), Some(StartupDialog::WhatsNew));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialog_unparseable_differing() {
|
||||
assert_eq!(startup_dialog(Some("garbage"), "0.8.0"), Some(StartupDialog::WhatsNew));
|
||||
assert_eq!(startup_dialog(Some("0.8.0"), "garbage"), Some(StartupDialog::WhatsNew));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialog_unparseable_identical() {
|
||||
assert_eq!(startup_dialog(Some("garbage"), "garbage"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_state_serde_round_trip_with_seen_version() {
|
||||
let state = crate::state::UiState { seen_version: Some("0.8.0".to_string()), ..Default::default() };
|
||||
let json = serde_json::to_string(&state).expect("serialize");
|
||||
let back: crate::state::UiState = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(back.seen_version, Some("0.8.0".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_state_serde_serializes_an_unset_marker_as_null() {
|
||||
// A file this version saved before the startup dialog was presented carries an explicit
|
||||
// `null`, so the desktop loader can tell it apart from a pre-feature file (which lacks
|
||||
// the key and migrates): null stays `None` — the tour, not What's New.
|
||||
let state = crate::state::UiState::default();
|
||||
let json = serde_json::to_string(&state).expect("serialize");
|
||||
assert!(json.contains(r#""seenVersion":null"#), "{json}");
|
||||
let back: crate::state::UiState = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(back.seen_version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_frame_opens_the_tour_or_whats_new_but_never_headless() {
|
||||
assert!(matches!(super::open_at_startup(None, false), Some(crate::state::Dialog::Welcome { page: 0 })));
|
||||
assert!(matches!(super::open_at_startup(Some("0.0.1"), false), Some(crate::state::Dialog::WhatsNew)));
|
||||
assert_eq!(super::open_at_startup(Some(env!("CARGO_PKG_VERSION")), false), None);
|
||||
// session-local state: nothing stored behind it, so no modal
|
||||
assert_eq!(super::open_at_startup(None, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_state_serde_older_json_without_field() {
|
||||
// Older ui.json without seen_version deserializes fine (None = first run).
|
||||
let json = r#"{"language":"en","view":"detail","left_panel":false}"#;
|
||||
let back: crate::state::UiState = serde_json::from_str(json).expect("deserialize");
|
||||
assert_eq!(back.seen_version, None);
|
||||
}
|
||||
|
||||
/// A stored UiState serialized like both loaders receive it, with `seenVersion` rewritten.
|
||||
fn saved_ui(seen_version: serde_json::Value) -> serde_json::Value {
|
||||
let mut saved = serde_json::to_value(crate::state::UiState::default()).expect("serialize");
|
||||
saved.as_object_mut().expect("object").insert("seenVersion".into(), seen_version);
|
||||
saved
|
||||
}
|
||||
|
||||
/// A settings file from before the startup dialog existed has no `seenVersion` key: the
|
||||
/// shared migration the desktop settings loader and the browser loader both run marks the
|
||||
/// installation as run (`LEGACY_SEEN_VERSION`), so it is due What's New, never the tour.
|
||||
#[test]
|
||||
fn legacy_settings_without_a_seen_version_get_whats_new() {
|
||||
let mut saved = saved_ui(serde_json::Value::Null);
|
||||
saved.as_object_mut().unwrap().remove("seenVersion");
|
||||
// without the migration it deserializes to a first run — exactly what the migration
|
||||
// exists to prevent for an existing installation
|
||||
let unmigrated: crate::state::UiState = serde_json::from_value(saved.clone()).unwrap();
|
||||
assert_eq!(unmigrated.seen_version, None);
|
||||
super::migrate_legacy_seen_version(saved.as_object_mut().unwrap());
|
||||
let ui: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
||||
assert_eq!(ui.seen_version.as_deref(), Some(LEGACY_SEEN_VERSION));
|
||||
assert_eq!(startup_dialog(ui.seen_version.as_deref(), env!("CARGO_PKG_VERSION")), Some(StartupDialog::WhatsNew));
|
||||
}
|
||||
|
||||
/// A file this version saved before the dialog was presented carries an explicit `null`
|
||||
/// (that is why `None` serializes): it is not migrated, so the tour still runs.
|
||||
#[test]
|
||||
fn an_explicit_null_seen_version_stays_a_first_run() {
|
||||
let mut saved = saved_ui(serde_json::Value::Null);
|
||||
super::migrate_legacy_seen_version(saved.as_object_mut().unwrap());
|
||||
assert_eq!(saved["seenVersion"], serde_json::Value::Null);
|
||||
let ui: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
||||
assert_eq!(ui.seen_version, None);
|
||||
assert_eq!(startup_dialog(ui.seen_version.as_deref(), env!("CARGO_PKG_VERSION")), Some(StartupDialog::FirstRun));
|
||||
}
|
||||
|
||||
/// A version already seen is left exactly as stored: no What's New, no tour.
|
||||
#[test]
|
||||
fn a_previously_seen_version_is_not_migrated() {
|
||||
let mut saved = saved_ui(serde_json::json!("0.14.0"));
|
||||
super::migrate_legacy_seen_version(saved.as_object_mut().unwrap());
|
||||
let ui: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
||||
assert_eq!(ui.seen_version.as_deref(), Some("0.14.0"));
|
||||
assert_eq!(startup_dialog(Some("0.14.0"), "0.15.0"), Some(StartupDialog::WhatsNew));
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,12 @@ pub struct UiState {
|
||||
pub status: String,
|
||||
#[serde(skip)]
|
||||
pub dialog: Option<Dialog>,
|
||||
/// The last app version whose startup dialog was shown; `None` = the dialog was never
|
||||
/// presented (never run, pre-feature settings, or a file saved before it was shown — that is
|
||||
/// why `None` serializes, as `null`, instead of being omitted: a settings file from this
|
||||
/// version without a seen version must not read as a pre-feature file).
|
||||
#[serde(default)]
|
||||
pub seen_version: Option<String>,
|
||||
/// The Print dialog's page, selection and drag.
|
||||
#[serde(skip)]
|
||||
pub print: crate::print_view::PrintUi,
|
||||
@@ -518,6 +524,7 @@ impl Default for UiState {
|
||||
#[cfg(test)]
|
||||
find_missing_test_delay: None,
|
||||
rechecked_library: None,
|
||||
seen_version: None,
|
||||
lens_download_checked: false,
|
||||
lens_refreshed: None,
|
||||
lens_lookup_ids: Vec::new(),
|
||||
@@ -617,6 +624,16 @@ impl UiState {
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Record that the startup dialog for the running version has been presented.
|
||||
///
|
||||
/// Record-on-show semantics: `seen_version` is set when the dialog is laid out in a UI pass
|
||||
/// (`dialogs::show`), not when the logic tick picks it — a host that quits before a visible
|
||||
/// frame does not burn the tour — and a crash while the dialog is up does not re-trigger it
|
||||
/// on every subsequent launch.
|
||||
pub fn record_startup_dialog_seen(&mut self) {
|
||||
self.seen_version = Some(env!("CARGO_PKG_VERSION").to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of ids read leniently: what isn't a list is none, and what isn't text in it is left out.
|
||||
|
||||
@@ -165,6 +165,12 @@ pub enum Dialog {
|
||||
},
|
||||
/// Help ▸ What's New.
|
||||
WhatsNew,
|
||||
/// First launch: a short tour of the main views, one page at a time.
|
||||
Welcome {
|
||||
/// The page on show (0-based; clamped when rendered).
|
||||
#[serde(default)]
|
||||
page: usize,
|
||||
},
|
||||
/// Photo ▸ Assisted Culling: reject photos below this focus score (0 = none), pick the best
|
||||
/// of each burst; shots at most `burst_gap` seconds apart and at least `similarity` alike are
|
||||
/// a burst.
|
||||
|
||||
@@ -97,6 +97,7 @@ mod tool_overlay;
|
||||
mod unsaved;
|
||||
mod view_cost;
|
||||
mod wb_selector;
|
||||
mod welcome;
|
||||
mod zoom;
|
||||
mod zoom_keys;
|
||||
|
||||
|
||||
@@ -568,6 +568,32 @@ fn a_queued_drop_waits_for_a_dialog_opened_before_the_launch_list_closes() {
|
||||
assert!(h.app.dropped_files.is_empty(), "the deferred drop was consumed only when safe");
|
||||
}
|
||||
|
||||
/// A drop delivered on the very first frame is queued by `raw_input_hook` before the same frame's
|
||||
/// `logic` assigns the startup dialog. The dialog did not exist when the drop arrived, so the
|
||||
/// drop must not take the "close the open dialog" refusal on the first UI pass that can see it:
|
||||
/// it waits behind the startup dialog and is imported once the tour closes.
|
||||
#[test]
|
||||
fn a_drop_delivered_on_the_first_frame_waits_behind_the_startup_dialog() {
|
||||
let dir = Scratch::new("startup-drop");
|
||||
write_png(&dir.path("trip/a.png"), 1);
|
||||
let mut session = lightcraft_engine::Session::new().with_fs();
|
||||
session.open_library(dir.path("library"), false).unwrap();
|
||||
let mut h = Headless::new(LightcraftApp::new(session, services()), [1300.0, 900.0], 1.0);
|
||||
// a window host on its first launch, with the drop riding the first frame's raw input
|
||||
h.app.headless_host = false;
|
||||
h.app.drop_pending = vec![dir.path("trip/a.png")];
|
||||
h.step();
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", h.app.ui.dialog);
|
||||
assert_eq!(h.app.dropped_files.len(), 1, "the first-frame drop is queued");
|
||||
h.step();
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::Welcome { .. })), "the tour stays: {:?}", h.app.ui.dialog);
|
||||
assert_eq!(h.app.dropped_files.len(), 1, "the first-frame drop waits behind the startup dialog instead of being refused");
|
||||
assert!(h.app.import.is_none(), "nothing starts behind the dialog");
|
||||
h.app.ui.dialog = None;
|
||||
assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the drop opens its review once the tour is closed");
|
||||
assert!(h.app.dropped_files.is_empty(), "the queued drop was consumed only when safe");
|
||||
}
|
||||
|
||||
/// A drop queued behind the launch list may be released while a Synchronize Folder runs. It
|
||||
/// waits for that run to finish instead of taking `import::start_paths`'s "A folder is being
|
||||
/// synchronized" refusal and being lost. A drop made while no barrier was up still gets that
|
||||
|
||||
@@ -104,6 +104,12 @@ fn a_search_from_the_menu_reads_off_the_ui_thread_and_the_window_goes_on() {
|
||||
// nothing has been read yet: the command and the frames waited for none of it
|
||||
assert!(t0.elapsed() < FRAMES, "the window waited for the files: {:?}", t0.elapsed());
|
||||
assert_eq!(h.app.session.filter, Default::default(), "no answer before the files are read");
|
||||
// the worker sets the row's total when it starts, which a loaded machine can leave for later
|
||||
let waited = Instant::now();
|
||||
while rows(&h)[0].total == 0 && waited.elapsed() < T {
|
||||
h.step();
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
let row = &rows(&h)[0];
|
||||
assert_eq!(row.label, LABEL);
|
||||
assert_eq!(row.total, PHOTOS);
|
||||
|
||||
@@ -2029,6 +2029,9 @@ fn a_discarded_layout_does_not_publish_its_widget_registry() {
|
||||
fn a_gesture_replies_only_after_the_completed_pass_consumed_its_event() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = app();
|
||||
// a headless host never gains the first-run tour: its modal would sit over the passes this
|
||||
// test drives (screenshot tests elsewhere in this file need the host not headless)
|
||||
app.headless_host = true;
|
||||
frame(&ctx, raw(egui::vec2(1000.0, 700.0)), |ui| app.logic(ui.ctx()));
|
||||
app.logic(&ctx);
|
||||
assert!(app.fonts_ready);
|
||||
@@ -2057,6 +2060,10 @@ fn a_gesture_replies_only_after_the_completed_pass_consumed_its_event() {
|
||||
fn a_delivered_gesture_may_change_the_state_it_acted_on() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = app();
|
||||
// a headless host never gains the first-run tour: its modal would take the dialog this
|
||||
// delivered gesture opens (screenshot tests elsewhere in this file need the host not
|
||||
// headless, so the flag is scoped to this test)
|
||||
app.headless_host = true;
|
||||
frame(&ctx, raw(egui::vec2(1000.0, 700.0)), |ui| app.logic(ui.ctx()));
|
||||
app.logic(&ctx);
|
||||
assert!(app.fonts_ready);
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
//! The first-run Welcome tour: Back / Next walk its pages, Skip and the last page's Get Started
|
||||
//! close it. (The auto-open at launch is decided by `startup::open_at_startup`, unit-tested there:
|
||||
//! a headless host never gains the modal, so the tour is placed by hand here.)
|
||||
|
||||
use crate::headless::Headless;
|
||||
use crate::state::Dialog;
|
||||
use crate::{LightcraftApp, Services};
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
const T: Duration = Duration::from_secs(20);
|
||||
|
||||
/// The app with the tour open on `page` (the last page of [`crate::panels::dialogs::WELCOME_PAGES`]
|
||||
/// is `WELCOME_PAGES.len() - 1`).
|
||||
fn tour(page: usize) -> Headless {
|
||||
let services = Services { png: None, ..Default::default() };
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
app.ui.dialog = Some(Dialog::Welcome { page });
|
||||
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
// fonts land on the second frame, and the dialog window settles its place a frame or two later;
|
||||
// clicks must aim at the settled layout
|
||||
h.step();
|
||||
h.settle(Duration::from_secs(5));
|
||||
h
|
||||
}
|
||||
|
||||
fn page_of(h: &Headless) -> Option<usize> {
|
||||
match &h.app.ui.dialog {
|
||||
Some(Dialog::Welcome { page }) => Some(*page),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Click an automation id and let the injected press/release play out (they can straddle a frame).
|
||||
fn click(h: &mut Headless, id: &str) {
|
||||
let r = h.request("ui.clickWidget", json!({"id": id}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn welcome_pages_walk_back_next_and_skip_closes() {
|
||||
let mut h = tour(0);
|
||||
// Back is disabled on the first page: the click lands on nothing and the tour stays.
|
||||
click(&mut h, "button:welcomeBack");
|
||||
assert_eq!(page_of(&h), Some(0), "{:?}", h.app.ui.dialog);
|
||||
click(&mut h, "button:welcomeNext");
|
||||
assert_eq!(page_of(&h), Some(1), "{:?}", h.app.ui.dialog);
|
||||
click(&mut h, "button:welcomeBack");
|
||||
assert_eq!(page_of(&h), Some(0), "{:?}", h.app.ui.dialog);
|
||||
click(&mut h, "button:welcomeSkip");
|
||||
assert!(h.app.ui.dialog.is_none(), "{:?}", h.app.ui.dialog);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn welcome_last_page_gets_started_closes() {
|
||||
let last = crate::panels::dialogs::WELCOME_PAGES.len() - 1;
|
||||
let mut h = tour(last);
|
||||
click(&mut h, "button:welcomeNext");
|
||||
assert!(h.app.ui.dialog.is_none(), "{:?}", h.app.ui.dialog);
|
||||
}
|
||||
|
||||
/// The web build omits the tour page that names the desktop-only launch flow (`--control`,
|
||||
/// `lightcraft-cli mcp --connect`): a browser has no control port for an agent to attach to.
|
||||
/// The host is a compile-time fact (wasm32), so this harness — native, a desktop host — tests
|
||||
/// the pure decision ([`crate::dialogs::welcome_pages`]) for both answers.
|
||||
#[test]
|
||||
fn the_web_tour_omits_the_desktop_only_mcp_page() {
|
||||
let all = crate::dialogs::WELCOME_PAGES.as_slice();
|
||||
assert_eq!(crate::dialogs::welcome_pages(false), all, "the desktop shows every page");
|
||||
let web = crate::dialogs::welcome_pages(true);
|
||||
assert_eq!(web, &all[..all.len() - 1], "the web tour drops the last page");
|
||||
// the dropped page is the desktop-only one: its opening names the launch flow
|
||||
let (_, _, opening) = all.last().unwrap();
|
||||
assert!(opening.contains("--control 7980") && opening.contains("lightcraft-cli"), "{opening}");
|
||||
}
|
||||
|
||||
/// The first frame decides the launch dialog, but records nothing until the dialog is presented:
|
||||
/// a window host with no stored version gets the tour; a headless host (the harness, snapshots,
|
||||
/// scripted runs) gets nothing; a version already seen gets nothing; a version that changed gets
|
||||
/// What's New.
|
||||
#[test]
|
||||
fn the_first_frame_opens_the_tour_for_a_window_host_only() {
|
||||
let ctx = egui::Context::default();
|
||||
let logic = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| app.logic(ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let fresh = || LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
|
||||
let mut window = fresh();
|
||||
logic(&mut window);
|
||||
assert!(matches!(window.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", window.ui.dialog);
|
||||
assert_eq!(window.ui.seen_version, None, "chosen, not yet presented");
|
||||
|
||||
let mut headless = fresh();
|
||||
headless.headless_host = true;
|
||||
logic(&mut headless);
|
||||
assert!(headless.ui.dialog.is_none(), "{:?}", headless.ui.dialog);
|
||||
assert_eq!(headless.ui.seen_version, None);
|
||||
|
||||
let mut seen = fresh();
|
||||
seen.ui.seen_version = Some(env!("CARGO_PKG_VERSION").to_string());
|
||||
logic(&mut seen);
|
||||
assert!(seen.ui.dialog.is_none(), "{:?}", seen.ui.dialog);
|
||||
|
||||
let mut updated = fresh();
|
||||
updated.ui.seen_version = Some("0.0.1".to_string());
|
||||
logic(&mut updated);
|
||||
assert!(matches!(updated.ui.dialog, Some(Dialog::WhatsNew)), "{:?}", updated.ui.dialog);
|
||||
}
|
||||
|
||||
/// The version is recorded when the dialog is presented, not when the logic tick picks it: a
|
||||
/// hidden host that runs `logic` and quits before a visible frame must not burn the tour.
|
||||
#[test]
|
||||
fn the_startup_dialog_is_recorded_when_presented_not_when_chosen() {
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
app.logic(ui.ctx());
|
||||
// the presentation pass (`ui` waits for the fonts, so frame one only runs `logic`)
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let mut held = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
|
||||
frame(&mut held);
|
||||
assert!(matches!(held.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version, None, "not presented yet, not recorded");
|
||||
|
||||
frame(&mut held);
|
||||
assert!(matches!(held.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version.as_deref(), Some(env!("CARGO_PKG_VERSION")));
|
||||
|
||||
// shown once: closing it does not bring it back
|
||||
held.ui.dialog = None;
|
||||
frame(&mut held);
|
||||
assert!(held.ui.dialog.is_none(), "{:?}", held.ui.dialog);
|
||||
}
|
||||
|
||||
/// Another dialog open on the first frame keeps its place: the tour is not chosen that frame and
|
||||
/// nothing is recorded. Once that dialog closes, the tour opens and is recorded then — shown, not
|
||||
/// merely due.
|
||||
#[test]
|
||||
fn the_tour_waits_for_a_dialog_that_is_already_open() {
|
||||
let ctx = egui::Context::default();
|
||||
let logic = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| app.logic(ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let present = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let mut held = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
held.ui.dialog = Some(Dialog::ViewOptions);
|
||||
|
||||
logic(&mut held);
|
||||
assert!(matches!(held.ui.dialog, Some(Dialog::ViewOptions)), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version, None, "not shown, not recorded");
|
||||
|
||||
held.ui.dialog = None;
|
||||
logic(&mut held);
|
||||
assert!(matches!(held.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version, None, "chosen behind the other dialog's turn, not yet presented");
|
||||
present(&mut held);
|
||||
assert_eq!(held.ui.seen_version.as_deref(), Some(env!("CARGO_PKG_VERSION")));
|
||||
|
||||
// shown once: closing it does not bring it back
|
||||
held.ui.dialog = None;
|
||||
logic(&mut held);
|
||||
assert!(held.ui.dialog.is_none(), "{:?}", held.ui.dialog);
|
||||
}
|
||||
|
||||
/// The launch chooser or a library error is the blocking library window, and it is not in
|
||||
/// `ui.dialog`: it must still hold the startup dialog back, or the chooser's New Library name
|
||||
/// prompt (a `ui.dialog` of its own) would take the tour's place with the version already
|
||||
/// recorded. Once the window closes or is dismissed, the tour opens and is recorded then.
|
||||
#[test]
|
||||
fn the_tour_waits_for_the_launch_window_too() {
|
||||
let ctx = egui::Context::default();
|
||||
let logic = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| app.logic(ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let present = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let mut held = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
held.library_problem = Some(crate::panels::library_problem::LibraryProblem::new("", "the last library did not open"));
|
||||
|
||||
logic(&mut held);
|
||||
assert!(held.ui.dialog.is_none(), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version, None, "not shown behind the library window, not recorded");
|
||||
|
||||
held.library_problem = None;
|
||||
logic(&mut held);
|
||||
assert!(matches!(held.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version, None, "chosen, not yet presented");
|
||||
present(&mut held);
|
||||
assert_eq!(held.ui.seen_version.as_deref(), Some(env!("CARGO_PKG_VERSION")));
|
||||
|
||||
// shown once: closing it does not bring it back
|
||||
held.ui.dialog = None;
|
||||
logic(&mut held);
|
||||
assert!(held.ui.dialog.is_none(), "{:?}", held.ui.dialog);
|
||||
}
|
||||
|
||||
/// A dismissed library window (Continue Without Saving) shows the banner instead of the window:
|
||||
/// it no longer blocks, so the startup dialog opens over it as usual.
|
||||
#[test]
|
||||
fn the_tour_opens_once_the_library_window_is_dismissed() {
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut LightcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let mut held = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
let mut problem = crate::panels::library_problem::LibraryProblem::new("", "the last library did not open");
|
||||
problem.dismissed = true;
|
||||
held.library_problem = Some(problem);
|
||||
|
||||
frame(&mut held);
|
||||
assert!(matches!(held.ui.dialog, Some(Dialog::Welcome { page: 0 })), "{:?}", held.ui.dialog);
|
||||
assert_eq!(held.ui.seen_version, None, "chosen this frame, presented once the fonts are in");
|
||||
frame(&mut held);
|
||||
assert_eq!(held.ui.seen_version.as_deref(), Some(env!("CARGO_PKG_VERSION")));
|
||||
}
|
||||
|
||||
/// The browser build's File menu disables Print (`menus::ui_enabled` turns `dialog.print` off on
|
||||
/// wasm), so the Share page's opening line names Export alone instead of a menu item the host
|
||||
/// greys out. The desktop keeps both menu paths and the live Print binding.
|
||||
#[test]
|
||||
fn the_web_tour_names_export_alone_not_the_desktop_print_menu() {
|
||||
use crate::panels::dialogs::welcome_opening;
|
||||
assert_eq!(welcome_opening(4, "File › Export… · File › Print…", Some("Ctrl+P"), true), "File › Export…");
|
||||
assert_eq!(welcome_opening(4, "File › Export… · File › Print…", Some("Ctrl+P"), false), "File › Export… · File › Print… (Ctrl+P).");
|
||||
assert_eq!(welcome_opening(4, "File › Export… · File › Print…", None, false), "File › Export… · File › Print…");
|
||||
}
|
||||
@@ -11,6 +11,8 @@ use lightcraft_ui_egui::{
|
||||
|
||||
fn harness(view: ViewMode, right: RightPanel) -> Harness<'static, LightcraftApp> {
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
// a headless host never gains the first-run tour: its modal would take the Tab focus under test
|
||||
app.headless_host = true;
|
||||
app.ui.left_panel = true;
|
||||
app.ui.view = view;
|
||||
app.ui.right = right;
|
||||
|
||||
@@ -6,7 +6,8 @@ use egui_kittest::{Harness, kittest::NodeT};
|
||||
use lightcraft_ui_egui::{LightcraftApp, Services};
|
||||
|
||||
fn harness() -> Harness<'static, LightcraftApp> {
|
||||
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
app.headless_host = true;
|
||||
let mut h = Harness::builder().with_size(vec2(1500.0, 950.0)).build_ui_state(
|
||||
|ui, app: &mut LightcraftApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
|
||||
@@ -8,6 +8,8 @@ use std::time::{Duration, Instant};
|
||||
|
||||
fn harness(state: Option<UiState>, width: f32, scale: f32) -> Harness<'static, LightcraftApp> {
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
// a headless host never gains the first-run tour: its modal would take the pointer the drags aim at
|
||||
app.headless_host = true;
|
||||
if let Some(state) = state {
|
||||
app.ui = state;
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,7 @@ impl Drop for Scratch {
|
||||
fn harness(places: Vec<(String, String)>, kept: Vec<String>) -> Harness<'static, LightcraftApp> {
|
||||
let services = Services { local_places: Some(places), pick_folder: Some(Box::new(|| None)), ..Default::default() };
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::new(), services);
|
||||
app.headless_host = true;
|
||||
app.ui.left_panel = true;
|
||||
app.ui.local_roots = kept;
|
||||
let mut h = Harness::builder().with_size(vec2(1000.0, 800.0)).build_ui_state(
|
||||
|
||||
@@ -13,6 +13,7 @@ use lightcraft_ui_egui::{
|
||||
|
||||
fn sidebar() -> Harness<'static, LightcraftApp> {
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
app.headless_host = true;
|
||||
app.ui.left_panel = true;
|
||||
app.ui.view = ViewMode::PhotoGrid;
|
||||
app.ui.right = RightPanel::None;
|
||||
@@ -51,6 +52,7 @@ fn a_month_triangle_names_its_year() {
|
||||
session.catalog.apply(lightcraft_catalog::Op::SetCaptured { id, captured: Some(captured.into()) }).expect("the capture time is set");
|
||||
}
|
||||
let mut app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
|
||||
app.headless_host = true;
|
||||
app.ui.left_panel = true;
|
||||
app.ui.view = ViewMode::PhotoGrid;
|
||||
app.ui.right = RightPanel::None;
|
||||
@@ -85,6 +87,7 @@ fn a_keyword_list_triangle_names_its_keyword() {
|
||||
session.execute("keyword.create", ¶ms).expect("a keyword is created");
|
||||
}
|
||||
let mut app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
|
||||
app.headless_host = true;
|
||||
app.ui.left_panel = false;
|
||||
app.ui.view = ViewMode::PhotoGrid;
|
||||
app.ui.right = RightPanel::Keywords;
|
||||
|
||||
@@ -12,6 +12,7 @@ const NAME: &str = "Thumbnail size";
|
||||
|
||||
fn grid() -> Harness<'static, LightcraftApp> {
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
app.headless_host = true;
|
||||
app.ui.view = ViewMode::PhotoGrid;
|
||||
app.ui.right = RightPanel::None;
|
||||
let mut h = Harness::builder().with_size(vec2(1500.0, 950.0)).build_ui_state(
|
||||
|
||||
Reference in New Issue
Block a user