Merge pull request #114 from arata-1972/codex/lightcraft-japanese
Extend Japanese localization across editing, import and export UI
This commit is contained in:
commit
2b2ade7a23
|
|
@ -29,7 +29,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
|||
| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering |
|
||||
| **Classic modules** | ~30% | Geotagging from GPX track logs, soft proofing (partial), slideshow (basic) | **Map view, Book, Print, Slideshow module, Web, publish services**: ~40 tracker rows ⬜ |
|
||||
| **HDR & video** | 0% | | HDR edit/display/export; video play/trim/edit/export |
|
||||
| **Platform & robustness** | ~70% | macOS native; Windows/Linux builds; web via WASM; no-panic lints workspace-wide, `unsafe` confined to `crates/sysmem`; failed saves are reported; GPU errors fall back to CPU | Windows installer UI unverified on Windows (PR #79); GPU path proven only on Apple + user reports; no localisation; accessibility partial; headless UI tests time out under machine load |
|
||||
| **Platform & robustness** | ~70% | macOS native; Windows/Linux builds; web via WASM; no-panic lints workspace-wide, `unsafe` confined to `crates/sysmem`; failed saves are reported; GPU errors fall back to CPU | Windows installer UI unverified on Windows (PR #79); GPU path proven only on Apple + user reports; Japanese/English UI (see docs/localization-ja.md); remaining technical errors and other languages; accessibility partial; headless UI tests time out under machine load |
|
||||
|
||||
### By kind of user
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ hardening (#78), copyright metadata (#51), GPX geotagging (#60), import tag help
|
|||
| M13 | Merge | HDR merge (deghost), panorama (projections, boundary warp, fill edges), HDR panorama | 10–15 | ✅ |
|
||||
| M14 | Video | import/playback/trim via FilmCraft crates, global edits + presets on video, video export | 6–10 | ⬜ |
|
||||
| M15 | Classic modules | Map, Book, Slideshow, Print, Web; smart collections, stacks, virtual copies, publish services, tethering | 25–40 | 🚧 (smart albums, stacks, virtual copies, compare/survey ✅; Map/Book/Slideshow/Print/Web ⬜) |
|
||||
| M16 | 1.0 polish | preferences, shortcut editor, accessibility, localization, packaging (dmg/msi/AppImage/web), hardening | 10–20 | 🚧 (settings, keyboard shortcuts sheet, packaging basics ✅; accessibility, localisation ⬜) |
|
||||
| M16 | 1.0 polish | preferences, shortcut editor, accessibility, localization, packaging (dmg/msi/AppImage/web), hardening | 10–20 | 🚧 (settings, keyboard shortcuts sheet, packaging basics ✅; Japanese/English localisation 🟡; accessibility and other locales ⬜) |
|
||||
|
||||
## Parity estimate (feature count updated 2026-10-05; effort estimate from 2026-10-02)
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,9 @@ impl PrefsWriter {
|
|||
|
||||
fn services() -> Services {
|
||||
Services {
|
||||
pick_folder: Some(Box::new(|| rfd::FileDialog::new().set_title("Open Library").pick_folder().map(|p| p.to_string_lossy().to_string()))),
|
||||
pick_folder: Some(Box::new(|| {
|
||||
rfd::FileDialog::new().set_title(lightcraft_ui_egui::i18n::tr("Open Library")).pick_folder().map(|p| p.to_string_lossy().to_string())
|
||||
})),
|
||||
open_with: Some(Box::new(|path: &str, app: &str| {
|
||||
// spawned, never waited for: the editor runs alongside
|
||||
let app = app.trim();
|
||||
|
|
@ -254,7 +256,7 @@ fn services() -> Services {
|
|||
pick_files: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(
|
||||
"Photos",
|
||||
lightcraft_ui_egui::i18n::tr("Photos"),
|
||||
&[
|
||||
"jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "cr3", "nef", "nrw", "arw", "raf", "orf", "rw2", "pef", "psd",
|
||||
"jxl", "gif", "bmp",
|
||||
|
|
@ -268,8 +270,11 @@ fn services() -> Services {
|
|||
})),
|
||||
pick_preset_files: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.set_title("Import Presets")
|
||||
.add_filter("Presets & Profiles", &["lcpreset", "xmp", "lrtemplate", "zip", "dng", "lmp", "mplumpack", "cube"])
|
||||
.set_title(lightcraft_ui_egui::i18n::tr("Import Presets"))
|
||||
.add_filter(
|
||||
lightcraft_ui_egui::i18n::tr("Presets & Profiles"),
|
||||
&["lcpreset", "xmp", "lrtemplate", "zip", "dng", "lmp", "mplumpack", "cube"],
|
||||
)
|
||||
.pick_files()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
|
|
@ -278,24 +283,24 @@ fn services() -> Services {
|
|||
})),
|
||||
pick_tracklog: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.set_title("Auto-Tag from Tracklog")
|
||||
.add_filter("GPS Track Log", &["gpx"])
|
||||
.set_title(lightcraft_ui_egui::i18n::tr("Auto-Tag from Tracklog"))
|
||||
.add_filter(lightcraft_ui_egui::i18n::tr("GPS Track Log"), &["gpx"])
|
||||
.pick_file()
|
||||
.map(|p| vec![p.to_string_lossy().to_string()])
|
||||
.unwrap_or_default()
|
||||
})),
|
||||
save_preset_file: Some(Box::new(|name: &str| {
|
||||
rfd::FileDialog::new()
|
||||
.set_title("Export Presets")
|
||||
.add_filter("LightCraft Preset", &["lcpreset"])
|
||||
.set_title(lightcraft_ui_egui::i18n::tr("Export Presets"))
|
||||
.add_filter(lightcraft_ui_egui::i18n::tr("LightCraft Preset"), &["lcpreset"])
|
||||
.set_file_name(name)
|
||||
.save_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
})),
|
||||
pick_curve_preset_files: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.set_title("Import Point Curve Presets")
|
||||
.add_filter("Point Curve Presets", &["lccurve", "json"])
|
||||
.set_title(lightcraft_ui_egui::i18n::tr("Import Point Curve Presets"))
|
||||
.add_filter(lightcraft_ui_egui::i18n::tr("Point Curve Presets"), &["lccurve", "json"])
|
||||
.pick_files()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
|
|
@ -304,8 +309,8 @@ fn services() -> Services {
|
|||
})),
|
||||
save_curve_preset_file: Some(Box::new(|name: &str| {
|
||||
rfd::FileDialog::new()
|
||||
.set_title("Export Point Curve Presets")
|
||||
.add_filter("Point Curve Presets", &["lccurve"])
|
||||
.set_title(lightcraft_ui_egui::i18n::tr("Export Point Curve Presets"))
|
||||
.add_filter(lightcraft_ui_egui::i18n::tr("Point Curve Presets"), &["lccurve"])
|
||||
.set_file_name(name)
|
||||
.save_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
|
|
@ -434,6 +439,7 @@ fn main() -> eframe::Result {
|
|||
if let Some(ui) = prefs {
|
||||
app.ui = ui;
|
||||
}
|
||||
lightcraft_ui_egui::i18n::set_language(app.ui.language);
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.notices.extend(prefs_warning);
|
||||
// what's on disk now: only changes are written
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ fn yields_to_text(sc: &str) -> bool {
|
|||
|
||||
/// `&` marks a mnemonic in muda labels.
|
||||
fn label_text(s: &str) -> String {
|
||||
s.replace('&', "&&")
|
||||
lightcraft_ui_egui::i18n::tr(s).replace('&', "&&")
|
||||
}
|
||||
|
||||
fn structure_of(bar: &[(String, Vec<MenuNode>)]) -> String {
|
||||
|
|
@ -171,7 +171,7 @@ fn structure_of(bar: &[(String, Vec<MenuNode>)]) -> String {
|
|||
out.push(';');
|
||||
}
|
||||
}
|
||||
let mut s = String::new();
|
||||
let mut s = format!("{};", lightcraft_ui_egui::i18n::is_japanese());
|
||||
for (t, items) in bar {
|
||||
s.push_str(t);
|
||||
walk(items, &mut s);
|
||||
|
|
@ -210,30 +210,30 @@ impl NativeMenu {
|
|||
|
||||
// the application menu
|
||||
let app_menu = Submenu::new("LightCraft", true);
|
||||
let about = MenuItem::with_id("app.about", "About LightCraft", true, None);
|
||||
let settings = MenuItem::with_id(SETTINGS, app.ui.language.tr("Settings…"), true, accelerator(SETTINGS_KEY));
|
||||
let quit = MenuItem::with_id(QUIT, "Quit LightCraft", true, accelerator("Cmd+Q"));
|
||||
let about = MenuItem::with_id("app.about", lightcraft_ui_egui::i18n::tr("About LightCraft"), true, None);
|
||||
let settings = MenuItem::with_id(SETTINGS, lightcraft_ui_egui::i18n::tr("Settings…"), true, accelerator(SETTINGS_KEY));
|
||||
let quit = MenuItem::with_id(QUIT, lightcraft_ui_egui::i18n::tr("Quit LightCraft"), true, accelerator("Cmd+Q"));
|
||||
let _ = app_menu.append_items(&[
|
||||
&about,
|
||||
&PredefinedMenuItem::separator(),
|
||||
&settings,
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::services(None),
|
||||
&PredefinedMenuItem::services(Some(lightcraft_ui_egui::i18n::tr("Services"))),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::hide(Some("Hide LightCraft")),
|
||||
&PredefinedMenuItem::hide_others(None),
|
||||
&PredefinedMenuItem::show_all(None),
|
||||
&PredefinedMenuItem::hide(Some(lightcraft_ui_egui::i18n::tr("Hide LightCraft"))),
|
||||
&PredefinedMenuItem::hide_others(Some(lightcraft_ui_egui::i18n::tr("Hide Others"))),
|
||||
&PredefinedMenuItem::show_all(Some(lightcraft_ui_egui::i18n::tr("Show All"))),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&quit,
|
||||
]);
|
||||
let _ = self.menu.append(&app_menu);
|
||||
|
||||
for (title, nodes) in &bar {
|
||||
let sub = Submenu::new(title, true);
|
||||
if title == app.ui.language.tr("Window") {
|
||||
let sub = Submenu::new(label_text(title), true);
|
||||
if title == "Window" {
|
||||
let _ = sub.append_items(&[
|
||||
&PredefinedMenuItem::minimize(None),
|
||||
&PredefinedMenuItem::maximize(Some("Zoom")),
|
||||
&PredefinedMenuItem::minimize(Some(lightcraft_ui_egui::i18n::tr("Minimize"))),
|
||||
&PredefinedMenuItem::maximize(Some(lightcraft_ui_egui::i18n::tr("Zoom"))),
|
||||
&PredefinedMenuItem::separator(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -245,15 +245,21 @@ impl NativeMenu {
|
|||
.collect();
|
||||
let nodes = tidy_separators(nodes);
|
||||
self.append_nodes(&sub, &nodes);
|
||||
if title == app.ui.language.tr("File") {
|
||||
if title == "File" {
|
||||
// ⌘W, the system's own item
|
||||
let _ = sub.append_items(&[&PredefinedMenuItem::separator(), &PredefinedMenuItem::close_window(None)]);
|
||||
let _ = sub.append_items(&[
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::close_window(Some(lightcraft_ui_egui::i18n::tr("Close Window"))),
|
||||
]);
|
||||
}
|
||||
if title == app.ui.language.tr("Window") {
|
||||
let _ = sub.append_items(&[&PredefinedMenuItem::separator(), &PredefinedMenuItem::bring_all_to_front(None)]);
|
||||
if title == "Window" {
|
||||
let _ = sub.append_items(&[
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::bring_all_to_front(Some(lightcraft_ui_egui::i18n::tr("Bring All to Front"))),
|
||||
]);
|
||||
sub.set_as_windows_menu_for_nsapp();
|
||||
}
|
||||
if title == app.ui.language.tr("Help") {
|
||||
if title == "Help" {
|
||||
sub.set_as_help_menu_for_nsapp();
|
||||
}
|
||||
let _ = self.menu.append(&sub);
|
||||
|
|
@ -282,12 +288,23 @@ impl NativeMenu {
|
|||
let accel = shortcut.as_deref().filter(|s| !CONTEXTUAL.contains(s)).and_then(accelerator);
|
||||
let handle = match checked {
|
||||
Some(c) => {
|
||||
let it = CheckMenuItem::with_id(key.clone(), label_text(label), *enabled, *c, accel);
|
||||
let it = CheckMenuItem::with_id(
|
||||
key.clone(),
|
||||
lightcraft_ui_egui::menubar::display_item_label(id, params, label).replace('&', "&&"),
|
||||
*enabled,
|
||||
*c,
|
||||
accel,
|
||||
);
|
||||
let _ = sub.append(&it);
|
||||
Handle::Check(it)
|
||||
}
|
||||
None => {
|
||||
let it = MenuItem::with_id(key.clone(), label_text(label), *enabled, accel);
|
||||
let it = MenuItem::with_id(
|
||||
key.clone(),
|
||||
lightcraft_ui_egui::menubar::display_item_label(id, params, label).replace('&', "&&"),
|
||||
*enabled,
|
||||
accel,
|
||||
);
|
||||
let _ = sub.append(&it);
|
||||
Handle::Plain(it)
|
||||
}
|
||||
|
|
@ -325,6 +342,7 @@ impl NativeMenu {
|
|||
/// Per frame: run chosen items, then sync labels / enabled / checked and the text-focus
|
||||
/// accelerators with the app state.
|
||||
pub fn update(&mut self, app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
lightcraft_ui_egui::i18n::set_language(app.ui.language);
|
||||
while let Ok(key) = self.rx.try_recv() {
|
||||
if key == QUIT {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
|
|
@ -352,8 +370,8 @@ impl NativeMenu {
|
|||
let Some(it) = items.get_mut(&MenuNode::key(id, params)) else { continue };
|
||||
if it.label != *label {
|
||||
match &it.handle {
|
||||
Handle::Plain(h) => h.set_text(label_text(label)),
|
||||
Handle::Check(h) => h.set_text(label_text(label)),
|
||||
Handle::Plain(h) => h.set_text(lightcraft_ui_egui::menubar::display_item_label(id, params, label).replace('&', "&&")),
|
||||
Handle::Check(h) => h.set_text(lightcraft_ui_egui::menubar::display_item_label(id, params, label).replace('&', "&&")),
|
||||
}
|
||||
it.label = label.clone();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ covers a whole directory), or if a licence file referenced in the Licence column
|
|||
| `docs/images/*.jpg` (UI) | LightCraft application screenshots | LightCraft contributors | original work (captured with `docs/showcase/`) | MIT OR Apache-2.0 | 2026-09-30 | n/a |
|
||||
| `docs/brand/` (artcraft-logo and artcraft-mark, SVG and PNG) | ArtCraft wordmark and mark | ArtCraft Team | original work (https://getartcraft.com/) | ArtCraft trademark, see `docs/brand/LICENSE-brand.txt` (not open source) | 2026-10-01 | none |
|
||||
|
||||
| `assets/fonts/BIZUDPGothic-*.ttf` | BIZ UDPGothic (Regular, Bold) | Morisawa / BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic/tree/18934af56b9c003ca58c54bffbf226848cb11032 | SIL Open Font License 1.1 (`assets/fonts/OFL-BIZUDGothic.txt`) | 2026-10-05 | none |
|
||||
|
||||
Notes
|
||||
- Fonts authored or published by Adobe (Source Sans/Serif/Code, Source Han, …) are not used, even though some are
|
||||
OFL-licensed: the asset rule excludes anything from Adobe. Source Sans 3 was removed on 2026-09-30 and replaced by Inter.
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -25,3 +25,6 @@ log = { workspace = true }
|
|||
|
||||
[dev-dependencies]
|
||||
lightcraft-codecs = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
//! Compile translated format strings so Rust checks both languages' placeholders.
|
||||
use std::{collections::BTreeMap, env, fs, path::PathBuf};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed=locales/ja-formats.json");
|
||||
let messages: BTreeMap<String, String> = serde_json::from_str(&fs::read_to_string("locales/ja-formats.json")?)?;
|
||||
let mut source = String::from("macro_rules! tr_format {\n");
|
||||
for (english, japanese) in messages {
|
||||
let en = serde_json::to_string(&english)?;
|
||||
let ja = serde_json::to_string(&japanese)?;
|
||||
source.push_str(&format!(
|
||||
"({en} $(, $($args:tt)*)?) => {{ if $crate::i18n::is_japanese() {{ format!({ja} $(, $($args)*)?) }} else {{ format!({en} $(, $($args)*)?) }} }};\n"
|
||||
));
|
||||
}
|
||||
source.push_str("}\npub(crate) use tr_format;\n");
|
||||
fs::write(PathBuf::from(env::var("OUT_DIR")?).join("ja-formats.rs"), source)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
" · {skipped} skipped (file exists)": "・{skipped}件は同名ファイルのためスキップ",
|
||||
" · {} duplicate{} skipped": "・重複{}枚をスキップ{:.0}",
|
||||
" · {} failed: {}": "・{}件失敗:{}",
|
||||
" · {} moved": "・{}件移動",
|
||||
" · {} not readable": "・{}件は読み込み不可",
|
||||
", {failed} file{} not readable": "、{failed}件を読み込めません{:.0}",
|
||||
"Add {local_n} to My Photos": "{local_n}枚をマイフォトに追加",
|
||||
"Add {} Photo{}": "{}枚の写真を追加{:.0}",
|
||||
"Add “{}” to Favorites": "「{}」をお気に入りに追加",
|
||||
"Added {n} photo{} to My Photos": "マイフォトに{n}枚を追加しました{:.0}",
|
||||
"Added {n} photo{} to “{}”": "「{1}」に{n}枚を追加しました{0:.0}",
|
||||
"All {n} selected photos move by the same amount.": "選択中の{n}枚を同じ時間だけずらします",
|
||||
"Auto Sync: edits apply to {n} photos": "自動同期:{n}枚に編集を適用",
|
||||
"Automatic ({} MB)": "自動({}MB)",
|
||||
"Click {}:1": "クリック時 {}:1",
|
||||
"Create {n} Virtual Copies": "{n}枚の仮想コピーを作成",
|
||||
"Decoder: {reason}": "デコーダー:{reason}",
|
||||
"Delete \"{label}\"": "「{label}」を削除",
|
||||
"Delete \"{label}\" (and the mask)": "「{label}」とマスクを削除",
|
||||
"Delete {n} Photos": "{n}枚の写真を削除",
|
||||
"Delete “{current}”": "「{current}」を削除",
|
||||
"Duplicate \"{label}\"": "「{label}」を複製",
|
||||
"Example: {example}": "例:{example}",
|
||||
"Exported {n} photo{}": "{n}枚を書き出しました{:.0}",
|
||||
"Exported {ok} of {total} photo{}": "{total}枚中{ok}枚を書き出しました{:.0}",
|
||||
"Exported {} point curve preset{}": "{}件のポイントカーブを書き出しました{:.0}",
|
||||
"Exported {} preset{}": "{}件のプリセットを書き出しました{:.0}",
|
||||
"Exporting {} of {total}": "書き出し中:{total}枚中{}枚",
|
||||
"Filter bar — {active} active filter{}": "フィルターバー:{active}件適用中{:.0}",
|
||||
"Healed {n} dust spot{}": "{n}か所のほこりを修復しました{:.0}",
|
||||
"Imported {n} point curve preset{}": "{n}件のポイントカーブを読み込みました{:.0}",
|
||||
"Imported {n} preset{}": "{n}件のプリセットを読み込みました{:.0}",
|
||||
"Imported {n} preset{}, {f} file{} not readable": "{n}件のプリセットを読み込みました{:.0}。{f}件は読み込めません{:.0}",
|
||||
"Imported {} photo{}": "{}枚を読み込みました{:.0}",
|
||||
"Matches: {}": "一致条件:{}",
|
||||
"Merging… {stage} {:.0}%": "合成中… {stage} {:.0}%",
|
||||
"Move {what} to Recently Deleted?": "{what}を「最近削除した写真」に移動しますか?",
|
||||
"Named ({named_n})": "名前付き({named_n})",
|
||||
"Paste into {n} photo{} — only settings that were copied are pasted": "{n}枚に貼り付け{:.0}(コピーされた設定だけを適用)",
|
||||
"Preset: {name}": "プリセット:{name}",
|
||||
"Preview only — editing the camera's embedded JPEG ({})": "プレビューのみ:カメラ内蔵のJPEGを編集中({})",
|
||||
"Preview only — {}": "プレビューのみ:{}",
|
||||
"Profile: {}": "プロファイル:{}",
|
||||
"Proof Copy ({})": "校正用コピー({})",
|
||||
"Proof Preview · {}": "校正プレビュー・{}",
|
||||
"Quick Develop · {n} photos": "クイック現像・{n}枚",
|
||||
"Rated {}": "評価:{}",
|
||||
"Reading photos… {done} of {total}": "写真を読み込み中:{total}枚中{done}枚",
|
||||
"Redo {}": "やり直す:{}",
|
||||
"Remove “{}” from Favorites": "「{}」をお気に入りから削除",
|
||||
"Rename {n} Photos…": "{n}枚の写真の名前を変更…",
|
||||
"Rename “{name}”": "「{name}」の名前を変更",
|
||||
"Rendering on the CPU: {why}": "CPUで描画:{why}",
|
||||
"Reset {} Channel": "{}チャンネルをリセット",
|
||||
"Saved export preset “{}”": "書き出しプリセット「{}」を保存しました",
|
||||
"Show {hidden} hidden location{}": "非表示の{hidden}か所を表示{:.0}",
|
||||
"Showing {n} of {}": "{}枚中{n}枚を表示",
|
||||
"Smart album: {}": "スマートアルバム:{}",
|
||||
"Undo {}": "取り消す:{}",
|
||||
"Version {} — a clean-room, pure-Rust photo library and raw developer.": "バージョン{} — Rust製の写真管理・RAW現像アプリ",
|
||||
"Version: {}": "バージョン:{}",
|
||||
"none (CPU rendering): {why}": "なし(CPU描画):{why}",
|
||||
"the {n} selected photos": "選択中の{n}枚",
|
||||
"the {} photos in view": "表示中の{}枚",
|
||||
"these {count} photos": "この{count}枚の写真",
|
||||
"{count} smart preview{} · {:.1} MB": "スマートプレビュー{count}枚{:.0}・{:.1}MB",
|
||||
"{f} folders (and their subfolders)": "{f}個のフォルダとそのサブフォルダ",
|
||||
"{label} Grading": "{label}のカラーグレーディング",
|
||||
"{label} {d:+} on every selected photo": "選択したすべての写真の{label}を{d:+}調整",
|
||||
"{label}, {n} photos": "{label}、{n}枚",
|
||||
"{matching} of {t} photos": "{t}枚中{matching}枚",
|
||||
"{matching} photos": "{matching}枚",
|
||||
"{mode} spot {} of {}": "{mode}:{1}か所中{0}か所",
|
||||
"{msg} ({failed} failed)": "{msg}({failed}件失敗)",
|
||||
"{name} Label": "{name}のラベル",
|
||||
"{name} Off": "{name}を無効にする",
|
||||
"{name} On": "{name}を有効にする",
|
||||
"{name} — double-click to reset it": "{name}:ダブルクリックでリセット",
|
||||
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "{n}件の変更{:.0}をディスクに保存できません:{e}\n自動で再試行します。終了すると変更が失われます{:.0}",
|
||||
"{n} found · {sel} selected": "{n}枚検出・{sel}枚選択",
|
||||
"{n} photos": "{n}枚",
|
||||
"{n} photos in {} are hidden by the filters above": "{}の{n}枚は上のフィルターで非表示になっています",
|
||||
"{n} photo{}": "{n}枚{:.0}",
|
||||
"{n} photo{} match · updates automatically as photos change": "{n}枚が一致{:.0}・写真の変更に応じて自動更新",
|
||||
"{n} photo{} now · updates automatically as photos change": "現在{n}枚{:.0}・写真の変更に応じて自動更新",
|
||||
"{sel_n} selected · {counted}": "{sel_n}枚選択・{counted}",
|
||||
"{stage}… {:.0}%": "{stage}… {:.0}%",
|
||||
"{what} merge added": "{what}合成を追加しました",
|
||||
"{what} merge failed: {e}": "{what}合成に失敗:{e}",
|
||||
"{} and higher": "{}以上",
|
||||
"{} files": "{}個のファイル",
|
||||
"{} files and {f} folder{}": "{}個のファイルと{f}個のフォルダ{:.0}",
|
||||
"{} of 4 guides — drag along lines that should be vertical or horizontal.": "ガイド4本中{}本:垂直または水平にする線に沿ってドラッグ",
|
||||
"{} photos": "{}枚",
|
||||
"{} photo{} not readable": "{}枚を読み込めません{:.0}",
|
||||
"{} spot(s) on this photo": "この写真に{}か所のスポット",
|
||||
"· {dups} already in the library": "・{dups}枚は読み込み済み",
|
||||
"· {n} photo{}": "・{n}枚{:.0}",
|
||||
"… and {more} more": "…ほか{more}件",
|
||||
"Import cancelled · {} photo{} added": "読み込みを中止しました・{}枚を追加{:.0}",
|
||||
"Reading photos… {} of {}": "写真を確認中:{1}枚中{0}枚",
|
||||
"Adding photos… {} of {}": "写真を追加中:{1}枚中{0}枚"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -75,12 +75,13 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let ok = files.iter().filter(|f| f.get("path").is_some()).count();
|
||||
let failed: Vec<&Value> = files.iter().filter(|f| f.get("error").is_some()).collect();
|
||||
let skipped = files.iter().filter(|f| f.get("skipped").is_some()).count();
|
||||
let mut m = format!("Exported {ok} of {total} photo{}", if total == 1 { "" } else { "s" });
|
||||
let mut m =
|
||||
crate::i18n::tr_format!("Exported {ok} of {total} photo{}", if total == 1 { "" } else { "s" }, ok = ok, total = total);
|
||||
if skipped > 0 {
|
||||
m += &format!(" · {skipped} skipped (file exists)");
|
||||
m += &crate::i18n::tr_format!(" · {skipped} skipped (file exists)", skipped = skipped);
|
||||
}
|
||||
if let Some(first) = failed.first() {
|
||||
m += &format!(" · {} failed: {}", failed.len(), first["error"].as_str().unwrap_or("error"));
|
||||
m += &crate::i18n::tr_format!(" · {} failed: {}", failed.len(), first["error"].as_str().unwrap_or("error"));
|
||||
}
|
||||
if cancelled {
|
||||
m += " · cancelled";
|
||||
|
|
@ -96,14 +97,17 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let (done, current) = task.progress.lock().map(|g| g.clone()).unwrap_or_default();
|
||||
let (total, cancel) = (task.total, task.cancel.clone());
|
||||
let t = Tokens::get(ctx);
|
||||
egui::Window::new("Exporting")
|
||||
egui::Window::new(crate::i18n::tr("Exporting"))
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.anchor(Align2::LEFT_BOTTOM, [16.0, -56.0])
|
||||
.fixed_size([300.0, 64.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(format!("Exporting {} of {total}", (done + 1).min(total))).color(t.text));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::tr_format!("Exporting {} of {total}", (done + 1).min(total), total = total))
|
||||
.color(t.text),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let stopping = cancel.load(Ordering::Relaxed);
|
||||
if crate::widgets::text_button(ui, "exportCancel", if stopping { "Stopping…" } else { "Cancel" }, false).clicked() {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
//! Interface translations. Command ids, document text and file names remain stable.
|
||||
//! Untranslated labels fall back to English so coverage can grow incrementally.
|
||||
//! Presentation-only localization. Command ids, user names and catalog data remain unchanged.
|
||||
use std::{cell::Cell, collections::BTreeMap, sync::OnceLock};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/ja-formats.rs"));
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Language {
|
||||
#[default]
|
||||
#[serde(rename = "en")]
|
||||
En,
|
||||
#[serde(rename = "ja")]
|
||||
Ja,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub const ALL: [Self; 2] = [Self::En, Self::Ja];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::En => "English",
|
||||
Self::Ja => "日本語",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(code: &str) -> Option<Self> {
|
||||
match code {
|
||||
"en" => Some(Self::En),
|
||||
|
|
@ -26,129 +29,134 @@ impl Language {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tr(self, text: &str) -> &str {
|
||||
if self == Self::Ja
|
||||
&& let Some((_, japanese)) = JAPANESE.iter().find(|(english, _)| *english == text)
|
||||
{
|
||||
return japanese;
|
||||
}
|
||||
text
|
||||
pub fn tr(self, source: &str) -> &str {
|
||||
if self == Self::Ja { japanese().get(source).map(String::as_str).unwrap_or(source) } else { source }
|
||||
}
|
||||
}
|
||||
|
||||
const JAPANESE: &[(&str, &str)] = &[
|
||||
("Photo", "写真"),
|
||||
("Import Photos…", "写真を読み込み…"),
|
||||
("Detail", "詳細"),
|
||||
("Photo Grid", "写真グリッド"),
|
||||
("Square Grid", "正方形グリッド"),
|
||||
("Object", "オブジェクト"),
|
||||
("Effect", "効果"),
|
||||
("Settings…", "環境設定…"),
|
||||
("Image", "画像"),
|
||||
("Layer", "レイヤー"),
|
||||
("Type", "書式"),
|
||||
("Select", "選択"),
|
||||
("Filter", "フィルター"),
|
||||
("Window", "ウィンドウ"),
|
||||
("Language", "表示言語"),
|
||||
("Save As…", "別名で保存…"),
|
||||
("Exit", "終了"),
|
||||
("New…", "新規…"),
|
||||
("New", "新規"),
|
||||
("Horizontal", "横書き"),
|
||||
("Vertical", "縦書き"),
|
||||
("Orientation", "組み方向"),
|
||||
("Layers", "レイヤー"),
|
||||
("History", "履歴"),
|
||||
("Properties", "プロパティ"),
|
||||
("Color", "カラー"),
|
||||
("Brush Settings", "ブラシ設定"),
|
||||
("Tools", "ツール"),
|
||||
("Options", "オプション"),
|
||||
("Zoom In", "ズームイン"),
|
||||
("Zoom Out", "ズームアウト"),
|
||||
("Fit on Screen", "画面に合わせる"),
|
||||
("Copy", "コピー"),
|
||||
("Cut", "切り取り"),
|
||||
("Paste", "貼り付け"),
|
||||
("Select All", "すべて選択"),
|
||||
("Deselect", "選択を解除"),
|
||||
("Export", "書き出し"),
|
||||
("Export As…", "形式を指定して書き出し…"),
|
||||
("Search…", "検索…"),
|
||||
("Theme", "テーマ"),
|
||||
("Menu", "メニュー"),
|
||||
("File", "ファイル"),
|
||||
("Edit", "編集"),
|
||||
("Pages", "ページ"),
|
||||
("View", "表示"),
|
||||
("Help", "ヘルプ"),
|
||||
("Preferences", "環境設定"),
|
||||
("Preferences…", "環境設定…"),
|
||||
("Interface language", "表示言語"),
|
||||
("Open…", "開く…"),
|
||||
("New blank PDF", "空白の PDF を作成"),
|
||||
("Create PDF from file…", "ファイルから PDF を作成…"),
|
||||
("Create PDF from images…", "画像から PDF を作成…"),
|
||||
("Create PDF from clipboard", "クリップボードから PDF を作成"),
|
||||
("Combine files…", "ファイルを結合…"),
|
||||
("Save", "保存"),
|
||||
("Save as…", "別名で保存…"),
|
||||
("Close file", "ファイルを閉じる"),
|
||||
("Close all", "すべて閉じる"),
|
||||
("Revert", "保存済みの状態に戻す"),
|
||||
("Print…", "印刷…"),
|
||||
("Document properties…", "文書のプロパティ…"),
|
||||
("Undo", "取り消し"),
|
||||
("Redo", "やり直し"),
|
||||
("Find…", "検索…"),
|
||||
("Advanced search…", "高度な検索…"),
|
||||
("Copy pages", "ページをコピー"),
|
||||
("Cut pages", "ページを切り取り"),
|
||||
("Paste pages", "ページを貼り付け"),
|
||||
("Fit visible", "表示範囲に合わせる"),
|
||||
("Marquee zoom", "範囲指定ズーム"),
|
||||
("Take a snapshot", "スナップショットを作成"),
|
||||
("Full screen mode", "全画面表示"),
|
||||
("Read mode", "閲覧モード"),
|
||||
("Switch light / dark theme", "明るい/暗いテーマを切り替え"),
|
||||
("Comments panel", "コメントパネル"),
|
||||
("Form fields panel", "フォームフィールドパネル"),
|
||||
("Clear form", "フォームをクリア"),
|
||||
("Find tools and commands…", "ツールとコマンドを検索…"),
|
||||
("Zoom", "ズーム"),
|
||||
("Actual size", "実際のサイズ"),
|
||||
("Zoom to page level", "ページ全体を表示"),
|
||||
("Fit to width", "幅に合わせる"),
|
||||
("Display theme", "表示テーマ"),
|
||||
("Side panels", "サイドパネル"),
|
||||
("OK", "OK"),
|
||||
];
|
||||
thread_local! {
|
||||
static LANGUAGE: Cell<Language> = const { Cell::new(Language::En) };
|
||||
}
|
||||
|
||||
pub fn default_language() -> Language {
|
||||
if std::env::var("LIGHTCRAFT_LANGUAGE").as_deref() == Ok("ja") { Language::Ja } else { Language::En }
|
||||
}
|
||||
|
||||
pub fn set_language(language: Language) {
|
||||
LANGUAGE.with(|value| value.set(language));
|
||||
}
|
||||
|
||||
pub fn is_japanese() -> bool {
|
||||
LANGUAGE.with(|value| value.get() == Language::Ja)
|
||||
}
|
||||
|
||||
fn japanese() -> &'static BTreeMap<String, String> {
|
||||
static MESSAGES: OnceLock<BTreeMap<String, String>> = OnceLock::new();
|
||||
MESSAGES.get_or_init(|| {
|
||||
serde_json::from_str(include_str!("../locales/ja.json")).unwrap_or_else(|error| {
|
||||
log::error!("Invalid Japanese message catalog: {error}");
|
||||
BTreeMap::new()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Translate a built-in display label, preserving unknown labels verbatim.
|
||||
/// Never call this on editable user text, filenames or command identifiers.
|
||||
pub fn tr(source: &str) -> &str {
|
||||
if is_japanese() { japanese().get(source).map(String::as_str).unwrap_or(source) } else { source }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn language_setting_persists() {
|
||||
let state = crate::state::UiState { language: Language::Ja, ..Default::default() };
|
||||
let saved = serde_json::to_string(&state).unwrap();
|
||||
let restored: crate::state::UiState = serde_json::from_str(&saved).unwrap();
|
||||
assert_eq!(restored.language, Language::Ja);
|
||||
assert_eq!(crate::state::UiState::default().language, Language::En);
|
||||
fn catalog_is_valid_and_contains_core_workflows() {
|
||||
let messages: BTreeMap<String, String> = serde_json::from_str(include_str!("../locales/ja.json")).unwrap();
|
||||
for key in ["Import Photos…", "Export…", "Exposure", "White Balance", "Settings", "Language"] {
|
||||
assert!(messages.get(key).is_some_and(|value| !value.is_empty() && value != key), "{key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translations_are_unique_and_preserve_unknown_text() {
|
||||
for (i, (en, ja)) in JAPANESE.iter().enumerate() {
|
||||
assert!(!ja.is_empty());
|
||||
assert!(JAPANESE.iter().take(i).all(|(other, _)| en != other));
|
||||
assert_eq!(Language::En.tr(en), *en);
|
||||
fn language_switches_and_unknown_text_survives() {
|
||||
set_language(Language::Ja);
|
||||
assert_eq!(tr("Exposure"), "露出");
|
||||
assert_eq!(tr("my-photo.jpg"), "my-photo.jpg");
|
||||
assert_eq!(tr("develop.set"), "develop.set");
|
||||
assert_eq!(crate::menubar::display_item_label("album.addPhotos", &serde_json::json!({"id": 1}), "Color"), "Color");
|
||||
assert_eq!(crate::menubar::display_item_label("app.export", &serde_json::json!({"preset": "Color"}), "Color"), "Color");
|
||||
assert_eq!(crate::menubar::display_item_label("view.photoGrid", &serde_json::Value::Null, "Color"), "カラー");
|
||||
set_language(Language::En);
|
||||
assert_eq!(tr("Exposure"), "Exposure");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translated_formats_preserve_counts_and_remove_english_plural_suffixes() {
|
||||
set_language(Language::Ja);
|
||||
assert_eq!(tr_format!("{n} photo{}", "s", n = 12), "12枚");
|
||||
assert_eq!(tr_format!("Exported {ok} of {total} photo{}", "s", ok = 4, total = 12), "12枚中4枚を書き出しました");
|
||||
set_language(Language::En);
|
||||
assert_eq!(tr_format!("{n} photo{}", "s", n = 12), "12 photos");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_round_trip_and_old_settings_remain_readable() {
|
||||
let old: crate::state::UiState = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(old.language, Language::En);
|
||||
let settings = crate::state::UiState { language: Language::Ja, ..old };
|
||||
let saved = serde_json::to_string(&settings).unwrap();
|
||||
let restored: crate::state::UiState = serde_json::from_str(&saved).unwrap();
|
||||
assert_eq!(restored.language, Language::Ja);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_is_painted_and_both_font_weights_cover_the_catalog() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default());
|
||||
app.ui.language = Language::Ja;
|
||||
app.ui.left_panel = true;
|
||||
let mut text = String::new();
|
||||
fn collect(shape: &egui::epaint::Shape, text: &mut String) {
|
||||
match shape {
|
||||
egui::epaint::Shape::Text(shape) => {
|
||||
text.push_str(&shape.galley.job.text);
|
||||
text.push('\n');
|
||||
}
|
||||
egui::epaint::Shape::Vec(shapes) => shapes.iter().for_each(|shape| collect(shape, text)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(Language::Ja.tr("File"), "ファイル");
|
||||
assert_eq!(Language::Ja.tr("日本語の文書.pdf"), "日本語の文書.pdf");
|
||||
assert_eq!(Language::parse("xx"), None);
|
||||
for frame in 0..4 {
|
||||
let input = crate::headless::HeadlessView::raw_input(egui::vec2(1600.0, 1000.0), 1.0, frame as f64 / 60.0, vec![]);
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
// This assertion inspects shapes without a renderer; discard texture uploads explicitly.
|
||||
out.textures_delta.clear();
|
||||
text.clear();
|
||||
for shape in out.shapes {
|
||||
collect(&shape.shape, &mut text);
|
||||
}
|
||||
}
|
||||
assert!(text.contains("マイフォト"), "{text}");
|
||||
assert!(text.contains("すべての写真"), "{text}");
|
||||
ctx.fonts_mut(|fonts| {
|
||||
for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] {
|
||||
let font = egui::FontId::new(13.0, family);
|
||||
for message in japanese().values() {
|
||||
for ch in message.chars().filter(|ch| !ch.is_whitespace()) {
|
||||
assert!(fonts.has_glyph(&font, ch), "Missing glyph {ch} in {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Locale affects presentation only: command ids remain the same.
|
||||
let ids = |app: &crate::LightcraftApp| crate::menus::menu_entries(app).into_iter().map(|entry| entry.id).collect::<Vec<_>>();
|
||||
let japanese_ids = ids(&app);
|
||||
set_language(Language::En);
|
||||
assert_eq!(japanese_ids, ids(&app));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,19 +412,21 @@ pub fn scan_progress(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let text = if total == 0 {
|
||||
if task.browse { "Reading folder…" } else { "Looking for photos…" }.to_string()
|
||||
} else {
|
||||
format!("Reading photos… {done} of {total}")
|
||||
crate::i18n::tr_format!("Reading photos… {done} of {total}", done = done, total = total)
|
||||
};
|
||||
let mut cancel = false;
|
||||
egui::Window::new("Scanning").title_bar(false).resizable(false).anchor(Align2::CENTER_BOTTOM, [0.0, -80.0]).fixed_size([340.0, 80.0]).show(
|
||||
ctx,
|
||||
|ui| {
|
||||
egui::Window::new(crate::i18n::tr("Scanning"))
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.anchor(Align2::CENTER_BOTTOM, [0.0, -80.0])
|
||||
.fixed_size([340.0, 80.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label(egui::RichText::new(text).color(t.text));
|
||||
ui.add(egui::ProgressBar::new(done as f32 / total.max(1) as f32).desired_width(320.0));
|
||||
let r = ui.button("Cancel");
|
||||
let r = ui.button(crate::i18n::tr("Cancel"));
|
||||
register(ui.ctx(), "button:scanCancel", r.rect);
|
||||
cancel = r.clicked();
|
||||
},
|
||||
);
|
||||
});
|
||||
if cancel {
|
||||
// the worker stops at its next file; don't wait for it (a NAS read can take a while)
|
||||
task.progress.cancel.store(true, Ordering::Relaxed);
|
||||
|
|
@ -581,7 +583,7 @@ fn commit_batch(app: &mut LightcraftApp, task: &mut ImportTask, prepared: lightc
|
|||
/// The import is done (or cancelled): one undo step, select the first photo, say what happened.
|
||||
fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) {
|
||||
let steps = app.session.undo.len().saturating_sub(task.undo0);
|
||||
let label = format!("Add {} Photo{}", task.imported, if task.imported == 1 { "" } else { "s" });
|
||||
let label = crate::i18n::tr_format!("Add {} Photo{}", task.imported, if task.imported == 1 { "" } else { "s" });
|
||||
app.session.merge_undo(steps, &label);
|
||||
if task.auto {
|
||||
if task.imported > 0 {
|
||||
|
|
@ -591,7 +593,7 @@ fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) {
|
|||
}
|
||||
if task.browse {
|
||||
if task.failed > 0 {
|
||||
app.toast(ctx, format!("{} photo{} not readable", task.failed, if task.failed == 1 { "" } else { "s" }));
|
||||
app.toast(ctx, crate::i18n::tr_format!("{} photo{} not readable", task.failed, if task.failed == 1 { "" } else { "s" }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -600,21 +602,21 @@ fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) {
|
|||
}
|
||||
let plural = |n: usize| if n == 1 { "" } else { "s" };
|
||||
let mut msg = if task.cancelled {
|
||||
format!("Import cancelled · {} photo{} added", task.imported, plural(task.imported))
|
||||
crate::i18n::tr_format!("Import cancelled · {} photo{} added", task.imported, plural(task.imported))
|
||||
} else {
|
||||
format!("Imported {} photo{}", task.imported, plural(task.imported))
|
||||
crate::i18n::tr_format!("Imported {} photo{}", task.imported, plural(task.imported))
|
||||
};
|
||||
if task.params["mode"] == "move" {
|
||||
msg.push_str(&format!(" · {} moved", task.moved));
|
||||
msg.push_str(&crate::i18n::tr_format!(" · {} moved", task.moved));
|
||||
if task.kept > 0 {
|
||||
msg.push_str(&format!(" · {} original{} left at the source", task.kept, plural(task.kept)));
|
||||
}
|
||||
}
|
||||
if task.duplicates > 0 {
|
||||
msg.push_str(&format!(" · {} duplicate{} skipped", task.duplicates, if task.duplicates == 1 { "" } else { "s" }));
|
||||
msg.push_str(&crate::i18n::tr_format!(" · {} duplicate{} skipped", task.duplicates, if task.duplicates == 1 { "" } else { "s" }));
|
||||
}
|
||||
if task.failed > 0 {
|
||||
msg.push_str(&format!(" · {} not readable", task.failed));
|
||||
msg.push_str(&crate::i18n::tr_format!(" · {} not readable", task.failed));
|
||||
}
|
||||
app.toast(ctx, msg);
|
||||
}
|
||||
|
|
@ -626,21 +628,25 @@ pub fn progress(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
let t = Tokens::get(ctx);
|
||||
let frac = task.done as f32 / task.total.max(1) as f32;
|
||||
let text = if task.cancelled {
|
||||
"Stopping…".to_string()
|
||||
crate::i18n::tr("Stopping…").to_string()
|
||||
} else if task.browse {
|
||||
crate::i18n::tr_format!("Reading photos… {} of {}", task.done, task.total)
|
||||
} else {
|
||||
format!("{} photos… {} of {}", if task.browse { "Reading" } else { "Adding" }, task.done, task.total)
|
||||
crate::i18n::tr_format!("Adding photos… {} of {}", task.done, task.total)
|
||||
};
|
||||
let mut cancel = false;
|
||||
egui::Window::new("Importing").title_bar(false).resizable(false).anchor(Align2::CENTER_BOTTOM, [0.0, -80.0]).fixed_size([340.0, 80.0]).show(
|
||||
ctx,
|
||||
|ui| {
|
||||
egui::Window::new(crate::i18n::tr("Importing"))
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.anchor(Align2::CENTER_BOTTOM, [0.0, -80.0])
|
||||
.fixed_size([340.0, 80.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label(egui::RichText::new(text).color(t.text));
|
||||
ui.add(egui::ProgressBar::new(frac).desired_width(320.0));
|
||||
let r = ui.add_enabled(!task.cancelled, egui::Button::new("Cancel"));
|
||||
let r = ui.add_enabled(!task.cancelled, egui::Button::new(crate::i18n::tr("Cancel")));
|
||||
register(ui.ctx(), "button:importCancel", r.rect);
|
||||
cancel = r.clicked();
|
||||
},
|
||||
);
|
||||
});
|
||||
if cancel && let Some(task) = app.import.as_mut() {
|
||||
task.cancelled = true;
|
||||
if let Some(run) = &task.run {
|
||||
|
|
@ -656,15 +662,15 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
let dups = d.candidates.iter().filter(|c| c.duplicate.is_some()).count();
|
||||
let sel = d.selected_paths().len();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(format!("{n} found · {sel} selected")).color(t.text));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("{n} found · {sel} selected", n = n, sel = sel)).color(t.text));
|
||||
if dups > 0 {
|
||||
ui.label(egui::RichText::new(format!("· {dups} already in the library")).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("· {dups} already in the library", dups = dups)).color(t.text_dim));
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::widgets::text_button(ui, "importNone", "Uncheck All", false).clicked() {
|
||||
if crate::widgets::text_button(ui, "importNone", crate::i18n::tr("Uncheck All"), false).clicked() {
|
||||
d.checked.iter_mut().for_each(|c| *c = false);
|
||||
}
|
||||
if crate::widgets::text_button(ui, "importAll", "Check All", false).clicked() {
|
||||
if crate::widgets::text_button(ui, "importAll", crate::i18n::tr("Check All"), false).clicked() {
|
||||
for i in 0..n {
|
||||
d.checked[i] = d.importable(i);
|
||||
}
|
||||
|
|
@ -702,25 +708,32 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
register(ui.ctx(), "label:importSource", r.rect);
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new("Importing scans the source for photos; it doesn't add the folder to Local (use Local → Browse Folder… to work in a folder without importing).")
|
||||
egui::RichText::new(crate::i18n::tr("Importing scans the source for photos; it doesn't add the folder to Local (use Local → Browse Folder… to work in a folder without importing)."))
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
);
|
||||
}
|
||||
field(ui, "Transfer", |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
if crate::widgets::text_button(ui, "importAdd", "Add in place", !d.copy).on_hover_text("Reference the files where they are").clicked() {
|
||||
if crate::widgets::text_button(ui, "importAdd", crate::i18n::tr("Add in place"), !d.copy)
|
||||
.on_hover_text(crate::i18n::tr("Reference the files where they are"))
|
||||
.clicked()
|
||||
{
|
||||
d.copy = false;
|
||||
d.move_files = false;
|
||||
}
|
||||
let can_copy = app.session.library.as_ref().is_some_and(|l| l.on_disk) || app.services.pick_folder.is_some();
|
||||
let r = ui.add_enabled_ui(can_copy, |ui| crate::widgets::text_button(ui, "importCopy", "Copy", d.copy && !d.move_files)).inner;
|
||||
if r.on_hover_text("Copy the files (into the library's Originals/, or a folder you choose)").clicked() {
|
||||
let r =
|
||||
ui.add_enabled_ui(can_copy, |ui| crate::widgets::text_button(ui, "importCopy", crate::i18n::tr("Copy"), d.copy && !d.move_files)).inner;
|
||||
if r.on_hover_text(crate::i18n::tr("Copy the files (into the library's Originals/, or a folder you choose)")).clicked() {
|
||||
d.copy = true;
|
||||
d.move_files = false;
|
||||
}
|
||||
let r = ui.add_enabled_ui(can_copy, |ui| crate::widgets::text_button(ui, "importMove", "Move", d.copy && d.move_files)).inner;
|
||||
if r.on_hover_text("Move the files (into the library's Originals/, or a folder you choose), removing them from the source").clicked() {
|
||||
let r =
|
||||
ui.add_enabled_ui(can_copy, |ui| crate::widgets::text_button(ui, "importMove", crate::i18n::tr("Move"), d.copy && d.move_files)).inner;
|
||||
if r.on_hover_text(crate::i18n::tr("Move the files (into the library's Originals/, or a folder you choose), removing them from the source"))
|
||||
.clicked()
|
||||
{
|
||||
d.copy = true;
|
||||
d.move_files = true;
|
||||
}
|
||||
|
|
@ -745,12 +758,12 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
let shown = if d.destination.trim().is_empty() { "Library Originals".to_string() } else { d.destination.clone() };
|
||||
ui.label(egui::RichText::new(shown).color(Tokens::get(ui.ctx()).text_label));
|
||||
if app.services.pick_folder.is_some()
|
||||
&& crate::widgets::text_button(ui, "importDest", "Choose…", false).clicked()
|
||||
&& crate::widgets::text_button(ui, "importDest", crate::i18n::tr("Choose…"), false).clicked()
|
||||
&& let Some(f) = app.services.pick_folder.as_mut().and_then(|f| f())
|
||||
{
|
||||
d.destination = f;
|
||||
}
|
||||
if !d.destination.is_empty() && crate::widgets::text_button(ui, "importDestReset", "Library", false).clicked() {
|
||||
if !d.destination.is_empty() && crate::widgets::text_button(ui, "importDestReset", crate::i18n::tr("Library"), false).clicked() {
|
||||
d.destination.clear();
|
||||
}
|
||||
});
|
||||
|
|
@ -797,14 +810,16 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
None => unknown_tags_warning(ui, &d.folder_template),
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new("Each / starts a folder level; tags are filled in per photo (a level with missing metadata is \"unknown\").")
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
egui::RichText::new(crate::i18n::tr(
|
||||
"Each / starts a folder level; tags are filled in per photo (a level with missing metadata is \"unknown\").",
|
||||
))
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
);
|
||||
}
|
||||
if !d.move_files {
|
||||
field(ui, "Raw files", |ui| {
|
||||
let r = ui.checkbox(&mut d.dng, "Copy as DNG");
|
||||
let r = ui.checkbox(&mut d.dng, crate::i18n::tr("Copy as DNG"));
|
||||
register(ui.ctx(), "check:importDng", r.rect);
|
||||
});
|
||||
}
|
||||
|
|
@ -822,10 +837,12 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
unknown_tags_warning(ui, &d.rename);
|
||||
if let Some(example) = example_destination(app, d) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let r = ui.label(egui::RichText::new(format!("Example: {example}")).color(t.text_dim));
|
||||
let r = ui.label(egui::RichText::new(crate::i18n::tr_format!("Example: {example}", example = example)).color(t.text_dim));
|
||||
register(ui.ctx(), "label:importExample", r.rect);
|
||||
ui.label(
|
||||
egui::RichText::new("Folders and {date} use the capture time; a photo without one uses today's date.").color(t.text_dim).small(),
|
||||
egui::RichText::new(crate::i18n::tr("Folders and {date} use the capture time; a photo without one uses today's date."))
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -842,11 +859,11 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
None => "None".into(),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("import-album").selected_text(cur).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(d.album.is_none() && d.new_album.is_empty(), "None").clicked() {
|
||||
if ui.selectable_label(d.album.is_none() && d.new_album.is_empty(), crate::i18n::tr("None")).clicked() {
|
||||
d.album = None;
|
||||
d.new_album.clear();
|
||||
}
|
||||
if ui.selectable_label(d.album.is_none() && !d.new_album.is_empty(), "New album…").clicked() {
|
||||
if ui.selectable_label(d.album.is_none() && !d.new_album.is_empty(), crate::i18n::tr("New album…")).clicked() {
|
||||
d.album = None;
|
||||
if d.new_album.is_empty() {
|
||||
d.new_album = "Imported Photos".into();
|
||||
|
|
@ -867,7 +884,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
field(ui, "Preset", |ui| {
|
||||
let cur = app.session.presets.iter().find(|p| p.id == d.preset).map(|p| p.name.clone()).unwrap_or_else(|| "None".into());
|
||||
egui::ComboBox::from_id_salt("import-preset").selected_text(cur).height(300.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(d.preset.is_empty(), "None").clicked() {
|
||||
if ui.selectable_label(d.preset.is_empty(), crate::i18n::tr("None")).clicked() {
|
||||
d.preset.clear();
|
||||
}
|
||||
for p in &app.session.presets {
|
||||
|
|
@ -881,7 +898,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
field(ui, "Metadata", |ui| {
|
||||
let cur = if d.metadata_preset.is_empty() { "None".to_string() } else { d.metadata_preset.clone() };
|
||||
egui::ComboBox::from_id_salt("import-metadata").selected_text(cur).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(d.metadata_preset.is_empty(), "None").clicked() {
|
||||
if ui.selectable_label(d.metadata_preset.is_empty(), crate::i18n::tr("None")).clicked() {
|
||||
d.metadata_preset.clear();
|
||||
}
|
||||
for m in &app.session.metadata_presets {
|
||||
|
|
@ -893,7 +910,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
|||
});
|
||||
}
|
||||
field(ui, "Keywords", |ui| {
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.keywords).hint_text("comma, separated").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.keywords).hint_text(crate::i18n::tr("comma, separated")).desired_width(f32::INFINITY));
|
||||
register(ui.ctx(), "field:importKeywords", r.rect);
|
||||
});
|
||||
}
|
||||
|
|
@ -966,7 +983,8 @@ fn candidate_cell(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDial
|
|||
pub(crate) fn tag_toggle(ui: &mut egui::Ui, key: &str) -> bool {
|
||||
let id = egui::Id::new((key, "tags-open"));
|
||||
let mut open = ui.ctx().data(|d| d.get_temp::<bool>(id)).unwrap_or(false);
|
||||
let r = crate::widgets::text_button(ui, &format!("{key}Tags"), "Tags", open).on_hover_text("Show the template tags; click one to insert it");
|
||||
let r = crate::widgets::text_button(ui, &format!("{key}Tags"), crate::i18n::tr("Tags"), open)
|
||||
.on_hover_text(crate::i18n::tr("Show the template tags; click one to insert it"));
|
||||
if r.clicked() {
|
||||
open = !open;
|
||||
ui.ctx().data_mut(|d| d.insert_temp(id, open));
|
||||
|
|
@ -1010,7 +1028,9 @@ pub(crate) fn tag_help(ui: &mut egui::Ui, key: &str, text: &mut String, edit_id:
|
|||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::new().fill(t.inset).corner_radius(4.0).inner_margin(6.0).show(ui, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("Click a tag to insert it. Examples are for IMG_0042.CR3 taken 14 Jan 2026, 05:58:48.").color(t.text_dim).small(),
|
||||
egui::RichText::new(crate::i18n::tr("Click a tag to insert it. Examples are for IMG_0042.CR3 taken 14 Jan 2026, 05:58:48."))
|
||||
.color(t.text_dim)
|
||||
.small(),
|
||||
);
|
||||
egui::ScrollArea::vertical().id_salt((key, "tags")).max_height(170.0).show(ui, |ui| {
|
||||
egui::Grid::new((key, "tag-grid")).num_columns(3).spacing([10.0, 2.0]).show(ui, |ui| {
|
||||
|
|
@ -1088,9 +1108,9 @@ pub fn source_summary(sources: &[String]) -> String {
|
|||
many => {
|
||||
let folders = many.iter().filter(|p| std::path::Path::new(p.as_str()).is_dir()).count();
|
||||
match folders {
|
||||
0 => format!("{} files", many.len()),
|
||||
f if f == many.len() => format!("{f} folders (and their subfolders)"),
|
||||
f => format!("{} files and {f} folder{}", many.len() - f, if f == 1 { "" } else { "s" }),
|
||||
0 => crate::i18n::tr_format!("{} files", many.len()),
|
||||
f if f == many.len() => crate::i18n::tr_format!("{f} folders (and their subfolders)", f = f),
|
||||
f => crate::i18n::tr_format!("{} files and {f} folder{}", many.len() - f, if f == 1 { "" } else { "s" }, f = f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,6 +480,7 @@ impl LightcraftApp {
|
|||
|
||||
/// Per-frame logic before layout (control channel, renders, shortcuts, drops).
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
i18n::set_language(self.ui.language);
|
||||
let t0 = now_ms();
|
||||
panels::library_problem::logic(self);
|
||||
self.logic_inner(ctx);
|
||||
|
|
@ -657,6 +658,7 @@ impl LightcraftApp {
|
|||
|
||||
/// Lay out the whole window.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
i18n::set_language(self.ui.language);
|
||||
let ctx = ui.ctx().clone();
|
||||
if !self.fonts_ready {
|
||||
ctx.request_repaint();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ impl MenuNode {
|
|||
}
|
||||
}
|
||||
|
||||
/// User-defined names in parameterized menus are data, not message keys.
|
||||
pub fn display_item_label<'a>(id: &str, params: &Value, label: &'a str) -> &'a str {
|
||||
if matches!(id, "metadata.applyPreset" | "album.addPhotos" | "label.applySet") || (id == "app.export" && params.get("preset").is_some()) {
|
||||
label
|
||||
} else {
|
||||
crate::i18n::tr(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level menus in order (the macOS app menu is added by the host).
|
||||
pub const MENUS: &[&str] = &["File", "Edit", "View", "Photo", "Window", "Help"];
|
||||
|
||||
|
|
@ -307,11 +316,15 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option<bool> {
|
|||
fn live_label(app: &LightcraftApp, id: &str, label: &str) -> String {
|
||||
let n = app.session.selection.ids.len();
|
||||
match id {
|
||||
"edit.undo" => app.session.undo.last().map(|e| format!("Undo {}", e.label)).unwrap_or_else(|| "Undo".into()),
|
||||
"edit.redo" => app.session.redo.last().map(|e| format!("Redo {}", e.label)).unwrap_or_else(|| "Redo".into()),
|
||||
"photo.delete" if n > 1 => format!("Delete {n} Photos"),
|
||||
"photo.virtualCopy" if n > 1 => format!("Create {n} Virtual Copies"),
|
||||
"dialog.rename" if n > 1 => format!("Rename {n} Photos…"),
|
||||
"edit.undo" => {
|
||||
app.session.undo.last().map(|e| crate::i18n::tr_format!("Undo {}", crate::i18n::tr(&e.label))).unwrap_or_else(|| "Undo".into())
|
||||
}
|
||||
"edit.redo" => {
|
||||
app.session.redo.last().map(|e| crate::i18n::tr_format!("Redo {}", crate::i18n::tr(&e.label))).unwrap_or_else(|| "Redo".into())
|
||||
}
|
||||
"photo.delete" if n > 1 => crate::i18n::tr_format!("Delete {n} Photos", n = n),
|
||||
"photo.virtualCopy" if n > 1 => crate::i18n::tr_format!("Create {n} Virtual Copies", n = n),
|
||||
"dialog.rename" if n > 1 => crate::i18n::tr_format!("Rename {n} Photos…", n = n),
|
||||
_ => label.to_string(),
|
||||
}
|
||||
}
|
||||
|
|
@ -476,14 +489,21 @@ fn expanded(app: &LightcraftApp, name: &str) -> Option<Vec<MenuNode>> {
|
|||
.map(|(f, label)| item("library.selectBy", json!({"flag": f}), label, None, true, None))
|
||||
.collect();
|
||||
v.push(MenuNode::Separator);
|
||||
v.extend(
|
||||
(1..=5u8).map(|r| item("library.selectBy", json!({"rating": r}), format!("{} and higher", "★".repeat(r as usize)), None, true, None)),
|
||||
);
|
||||
v.extend((1..=5u8).map(|r| {
|
||||
item("library.selectBy", json!({"rating": r}), crate::i18n::tr_format!("{} and higher", "★".repeat(r as usize)), None, true, None)
|
||||
}));
|
||||
v.push(item("library.selectBy", json!({"rating": 0, "ratingOp": "eq"}), "Unrated", None, true, None));
|
||||
v.push(MenuNode::Separator);
|
||||
v.extend(lightcraft_catalog::ColorLabel::ALL.iter().map(|l| {
|
||||
let name = format!("{l:?}");
|
||||
item("library.selectBy", json!({"label": name.to_lowercase()}), format!("{name} Label"), None, true, None)
|
||||
item(
|
||||
"library.selectBy",
|
||||
json!({"label": name.to_lowercase()}),
|
||||
crate::i18n::tr_format!("{name} Label", name = name),
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
}));
|
||||
v
|
||||
}
|
||||
|
|
@ -608,22 +628,6 @@ pub fn menu_bar(app: &LightcraftApp) -> Vec<(String, Vec<MenuNode>)> {
|
|||
}
|
||||
bar.push((title.to_string(), tidy(items)));
|
||||
}
|
||||
fn translate(nodes: &mut [MenuNode], language: crate::i18n::Language) {
|
||||
for node in nodes {
|
||||
match node {
|
||||
MenuNode::Submenu { label, children } => {
|
||||
*label = language.tr(label).to_string();
|
||||
translate(children, language);
|
||||
}
|
||||
MenuNode::Item { label, .. } => *label = language.tr(label).to_string(),
|
||||
MenuNode::Separator => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (title, nodes) in &mut bar {
|
||||
*title = app.ui.language.tr(title).to_string();
|
||||
translate(nodes, app.ui.language);
|
||||
}
|
||||
bar
|
||||
}
|
||||
|
||||
|
|
@ -677,9 +681,9 @@ pub fn shortcut_text(sc: &str, mac: bool) -> String {
|
|||
const TITLE_GAP: f32 = 24.0;
|
||||
|
||||
/// Width of the in-window menu bar's titles.
|
||||
pub fn bar_width(ui: &egui::Ui, language: crate::i18n::Language) -> f32 {
|
||||
pub fn bar_width(ui: &egui::Ui) -> f32 {
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
MENUS.iter().map(|m| ui.painter().layout_no_wrap(language.tr(m).to_string(), t.font(13.0), t.text).size().x + TITLE_GAP).sum::<f32>()
|
||||
MENUS.iter().map(|m| ui.painter().layout_no_wrap(crate::i18n::tr(m).to_string(), t.font(13.0), t.text).size().x + TITLE_GAP).sum::<f32>()
|
||||
}
|
||||
|
||||
/// The in-window menu bar (hosts without a native one): one dropdown per menu, or a single
|
||||
|
|
@ -688,8 +692,10 @@ pub fn show_in_window(app: &mut LightcraftApp, ui: &mut egui::Ui, max_width: f32
|
|||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
let bar = menu_bar(app);
|
||||
let font = t.font(13.0);
|
||||
let widths: Vec<f32> =
|
||||
bar.iter().map(|(title, _)| ui.painter().layout_no_wrap(title.clone(), font.clone(), t.text).size().x + TITLE_GAP).collect();
|
||||
let widths: Vec<f32> = bar
|
||||
.iter()
|
||||
.map(|(title, _)| ui.painter().layout_no_wrap(crate::i18n::tr(title).to_string(), font.clone(), t.text).size().x + TITLE_GAP)
|
||||
.collect();
|
||||
let total: f32 = widths.iter().sum();
|
||||
let mut clicked: Option<(String, Value)> = None;
|
||||
let start = ui.cursor().left();
|
||||
|
|
@ -697,18 +703,18 @@ pub fn show_in_window(app: &mut LightcraftApp, ui: &mut egui::Ui, max_width: f32
|
|||
if total <= max_width {
|
||||
let saved = ui.spacing().item_spacing.x;
|
||||
ui.spacing_mut().item_spacing.x = TITLE_GAP;
|
||||
for (index, (title, items)) in bar.iter().enumerate() {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(title).font(font.clone()).color(t.text_label)).frame(false));
|
||||
crate::widgets::register(ui.ctx(), format!("menu:{}", MENUS.get(index).copied().unwrap_or(title.as_str())), r.rect);
|
||||
for (title, items) in &bar {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(crate::i18n::tr(title)).font(font.clone()).color(t.text_label)).frame(false));
|
||||
crate::widgets::register(ui.ctx(), format!("menu:{title}"), r.rect);
|
||||
egui::Popup::menu(&r).show(|ui| nodes_ui(ui, items, mac, &mut clicked));
|
||||
}
|
||||
ui.spacing_mut().item_spacing.x = saved;
|
||||
} else {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(app.ui.language.tr("Menu")).font(font.clone()).color(t.text_label)).frame(false));
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(crate::i18n::tr("Menu")).font(font.clone()).color(t.text_label)).frame(false));
|
||||
crate::widgets::register(ui.ctx(), "menu:all", r.rect);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
for (title, items) in &bar {
|
||||
ui.menu_button(title, |ui| nodes_ui(ui, items, mac, &mut clicked));
|
||||
ui.menu_button(crate::i18n::tr(title), |ui| nodes_ui(ui, items, mac, &mut clicked));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -726,11 +732,11 @@ fn nodes_ui(ui: &mut egui::Ui, nodes: &[MenuNode], mac: bool, clicked: &mut Opti
|
|||
ui.separator();
|
||||
}
|
||||
MenuNode::Submenu { label, children } => {
|
||||
ui.menu_button(format!(" {label}"), |ui| nodes_ui(ui, children, mac, clicked));
|
||||
ui.menu_button(format!(" {}", crate::i18n::tr(label)), |ui| nodes_ui(ui, children, mac, clicked));
|
||||
}
|
||||
MenuNode::Item { id, params, label, shortcut, enabled, checked } => {
|
||||
// a gutter for check marks, like native menus
|
||||
let mut b = egui::Button::new(format!(" {label}"));
|
||||
let mut b = egui::Button::new(format!(" {}", display_item_label(id, params, label)));
|
||||
if let Some(sc) = shortcut {
|
||||
b = b.shortcut_text(shortcut_text(sc, mac));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,10 +151,10 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
|||
fn panel(app: &mut LightcraftApp, ctx: &egui::Context, p: RightPanel, name: &str) {
|
||||
if app.ui.right == p {
|
||||
app.ui.right = RightPanel::None;
|
||||
app.toast(ctx, format!("{name} Off"));
|
||||
app.toast(ctx, crate::i18n::tr_format!("{name} Off", name = name));
|
||||
} else {
|
||||
app.ui.right = p;
|
||||
app.toast(ctx, format!("{name} On"));
|
||||
app.toast(ctx, crate::i18n::tr_format!("{name} On", name = name));
|
||||
if p.is_edit_tool() && !matches!(app.ui.view, ViewMode::Detail) {
|
||||
app.ui.view = ViewMode::Detail;
|
||||
}
|
||||
|
|
@ -874,7 +874,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
(
|
||||
"GPU".to_string(),
|
||||
gpu.unwrap_or_else(|| match lightcraft_engine::gpu::unavailable_reason() {
|
||||
Some(why) => format!("none (CPU rendering): {why}"),
|
||||
Some(why) => crate::i18n::tr_format!("none (CPU rendering): {why}", why = why),
|
||||
None => "none (CPU rendering)".into(),
|
||||
}),
|
||||
),
|
||||
|
|
@ -1095,8 +1095,14 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
let failed = v["failed"].as_array().map_or(0, Vec::len);
|
||||
let mut msg = match (n, failed) {
|
||||
(0, 0) => "No new presets".to_string(),
|
||||
(n, 0) => format!("Imported {n} preset{}", if n == 1 { "" } else { "s" }),
|
||||
(n, f) => format!("Imported {n} preset{}, {f} file{} not readable", if n == 1 { "" } else { "s" }, if f == 1 { "" } else { "s" }),
|
||||
(n, 0) => crate::i18n::tr_format!("Imported {n} preset{}", if n == 1 { "" } else { "s" }, n = n),
|
||||
(n, f) => crate::i18n::tr_format!(
|
||||
"Imported {n} preset{}, {f} file{} not readable",
|
||||
if n == 1 { "" } else { "s" },
|
||||
if f == 1 { "" } else { "s" },
|
||||
f = f,
|
||||
n = n
|
||||
),
|
||||
};
|
||||
// settings with no counterpart here (the other editor's profiles, masks…)
|
||||
let mut skipped: Vec<&str> = v["imported"]
|
||||
|
|
@ -1141,7 +1147,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
}
|
||||
let r = app.session.execute("preset.export", ¶ms).map_err(|e| e.to_string());
|
||||
if let Ok(v) = &r {
|
||||
app.toast(&ctx, format!("Exported {} preset{}", v["count"], if v["count"] == 1 { "" } else { "s" }));
|
||||
app.toast(&ctx, crate::i18n::tr_format!("Exported {} preset{}", v["count"], if v["count"] == 1 { "" } else { "s" }));
|
||||
}
|
||||
return Some(r);
|
||||
}
|
||||
|
|
@ -1160,9 +1166,9 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
if let Ok(v) = &r {
|
||||
let n = v["imported"].as_array().map_or(0, Vec::len);
|
||||
let failed = v["failed"].as_array().map_or(0, Vec::len);
|
||||
let mut msg = format!("Imported {n} point curve preset{}", if n == 1 { "" } else { "s" });
|
||||
let mut msg = crate::i18n::tr_format!("Imported {n} point curve preset{}", if n == 1 { "" } else { "s" }, n = n);
|
||||
if failed > 0 {
|
||||
msg += &format!(", {failed} file{} not readable", if failed == 1 { "" } else { "s" });
|
||||
msg += &crate::i18n::tr_format!(", {failed} file{} not readable", if failed == 1 { "" } else { "s" }, failed = failed);
|
||||
}
|
||||
app.toast(&ctx, msg);
|
||||
}
|
||||
|
|
@ -1183,7 +1189,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
}
|
||||
let r = app.session.execute("curve.exportPresets", ¶ms).map_err(|e| e.to_string());
|
||||
if let Ok(v) = &r {
|
||||
app.toast(&ctx, format!("Exported {} point curve preset{}", v["count"], if v["count"] == 1 { "" } else { "s" }));
|
||||
app.toast(&ctx, crate::i18n::tr_format!("Exported {} point curve preset{}", v["count"], if v["count"] == 1 { "" } else { "s" }));
|
||||
}
|
||||
return Some(r);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,18 +257,18 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
match r.map_err(lightcraft_engine::EngineError::Other).and_then(|out| app.session.finish_merge(&t.job, out)) {
|
||||
Ok(v) => {
|
||||
app.merge.last_result = Some(v);
|
||||
app.toast(ctx, format!("{what} merge added"));
|
||||
app.toast(ctx, crate::i18n::tr_format!("{what} merge added", what = what));
|
||||
}
|
||||
Err(e) => {
|
||||
app.ui.status = e.to_string();
|
||||
app.toast(ctx, format!("{what} merge failed: {e}"));
|
||||
app.toast(ctx, crate::i18n::tr_format!("{what} merge failed: {e}", e = e, what = what));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let (f, stage) = t.progress.lock().map(|g| g.clone()).unwrap_or_default();
|
||||
let now = ctx.input(|i| i.time);
|
||||
app.ui.toast = Some((format!("Merging… {stage} {:.0}%", f * 100.0), now + 0.5));
|
||||
app.ui.toast = Some((crate::i18n::tr_format!("Merging… {stage} {:.0}%", f * 100.0, stage = stage), now + 0.5));
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
|
@ -299,7 +299,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, opts: &mut MergeDialog)
|
|||
ui.painter().text(
|
||||
rect.center_bottom() - egui::vec2(0.0, 14.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
format!("{stage}… {:.0}%", f * 100.0),
|
||||
crate::i18n::tr_format!("{stage}… {:.0}%", f * 100.0, stage = stage),
|
||||
t.font(12.0),
|
||||
t.text,
|
||||
);
|
||||
|
|
@ -330,7 +330,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, opts: &mut MergeDialog)
|
|||
ui.vertical(|ui| {
|
||||
ui.set_width(220.0);
|
||||
if opts.is_pano() {
|
||||
ui.label(egui::RichText::new("Projection").color(t.text_label));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Projection")).color(t.text_label));
|
||||
for (v, l) in [("auto", "Auto Select"), ("spherical", "Spherical"), ("cylindrical", "Cylindrical"), ("perspective", "Perspective")] {
|
||||
if ui.radio(opts.projection == v, l).clicked() {
|
||||
opts.projection = v.to_string();
|
||||
|
|
@ -341,13 +341,13 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, opts: &mut MergeDialog)
|
|||
if num(ui, &BOUNDARY_WARP, &mut bw) {
|
||||
opts.boundary_warp = bw.round();
|
||||
}
|
||||
ui.checkbox(&mut opts.fill_edges, "Fill Edges");
|
||||
ui.checkbox(&mut opts.auto_crop, "Auto Crop");
|
||||
ui.checkbox(&mut opts.fill_edges, crate::i18n::tr("Fill Edges"));
|
||||
ui.checkbox(&mut opts.auto_crop, crate::i18n::tr("Auto Crop"));
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
if opts.is_hdr() {
|
||||
ui.checkbox(&mut opts.align, "Auto Align");
|
||||
ui.label(egui::RichText::new("Deghost Amount").color(t.text_label));
|
||||
ui.checkbox(&mut opts.align, crate::i18n::tr("Auto Align"));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Deghost Amount")).color(t.text_label));
|
||||
ui.horizontal(|ui| {
|
||||
for (i, (v, l)) in [("none", "None"), ("low", "Low"), ("medium", "Med"), ("high", "High")].iter().enumerate() {
|
||||
if crate::widgets::text_button(ui, &format!("mergeDeghost-{i}"), l, opts.deghost == *v).clicked() {
|
||||
|
|
@ -356,7 +356,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, opts: &mut MergeDialog)
|
|||
}
|
||||
});
|
||||
if opts.command == "merge.hdr" {
|
||||
ui.add_enabled_ui(opts.deghost != "none", |ui| ui.checkbox(&mut opts.show_overlay, "Show Deghost Overlay"));
|
||||
ui.add_enabled_ui(opts.deghost != "none", |ui| ui.checkbox(&mut opts.show_overlay, crate::i18n::tr("Show Deghost Overlay")));
|
||||
}
|
||||
if opts.command == "merge.hdrPanorama" {
|
||||
let mut b = opts.bracket as f64;
|
||||
|
|
@ -366,10 +366,10 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, opts: &mut MergeDialog)
|
|||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
ui.checkbox(&mut opts.auto_settings, "Auto Settings");
|
||||
ui.checkbox(&mut opts.stack, "Create Stack");
|
||||
ui.checkbox(&mut opts.auto_settings, crate::i18n::tr("Auto Settings"));
|
||||
ui.checkbox(&mut opts.stack, crate::i18n::tr("Create Stack"));
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(format!("{} photos", app.merge.ids.len())).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("{} photos", app.merge.ids.len())).color(t.text_dim));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,13 @@ fn centre(app: &mut LightcraftApp, ui: &mut egui::Ui, full: Rect, from: f32, to:
|
|||
let cresp = ui.interact(copy_r, egui::Id::new("copy-settings"), Sense::click());
|
||||
register(ui.ctx(), "button:copySettings", copy_r);
|
||||
ui.painter().rect_filled(copy_r, 15.0, if cresp.hovered() { t.hover } else { t.canvas });
|
||||
ui.painter().text(copy_r.center(), Align2::CENTER_CENTER, label, t.font(13.0), if active.is_some() { t.text_label } else { t.text_disabled });
|
||||
ui.painter().text(
|
||||
copy_r.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
crate::i18n::tr(label),
|
||||
t.font(13.0),
|
||||
if active.is_some() { t.text_label } else { t.text_disabled },
|
||||
);
|
||||
if cresp.clicked() && active.is_some() {
|
||||
let _ = if has_clip { app.run("develop.paste", json!({})) } else { app.run("develop.copy", json!({})) };
|
||||
let msg = if has_clip { "Settings pasted" } else { "Edit settings copied" };
|
||||
|
|
@ -168,7 +174,7 @@ fn right_side(app: &mut LightcraftApp, ui: &mut egui::Ui, full: Rect) -> f32 {
|
|||
Zoom::Fill => "Fill".to_string(),
|
||||
Zoom::Percent(p) => format!("{p}%"),
|
||||
};
|
||||
let zr = crate::widgets::dropdown(&mut child, "zoom", &zoom_label, t.font(13.0), t.text_label);
|
||||
let zr = crate::widgets::dropdown(&mut child, "zoom", crate::i18n::tr(&zoom_label), t.font(13.0), t.text_label);
|
||||
egui::Popup::menu(&zr).show(|ui| {
|
||||
for (label, z) in [
|
||||
("Fit", Zoom::Fit),
|
||||
|
|
@ -185,7 +191,13 @@ fn right_side(app: &mut LightcraftApp, ui: &mut egui::Ui, full: Rect) -> f32 {
|
|||
}
|
||||
});
|
||||
child.add_space(6.0);
|
||||
let cz = crate::widgets::dropdown(&mut child, "clickZoom", &format!("Click {}:1", app.ui.click_zoom / 100), t.font(13.0), t.text_label);
|
||||
let cz = crate::widgets::dropdown(
|
||||
&mut child,
|
||||
"clickZoom",
|
||||
&crate::i18n::tr_format!("Click {}:1", app.ui.click_zoom / 100),
|
||||
t.font(13.0),
|
||||
t.text_label,
|
||||
);
|
||||
egui::Popup::menu(&cz).show(|ui| {
|
||||
for pct in crate::state::CLICK_ZOOMS {
|
||||
if ui.selectable_label(app.ui.click_zoom == pct, format!("{}:1", pct / 100)).clicked() {
|
||||
|
|
@ -216,7 +228,7 @@ fn sort_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
use lightcraft_catalog::GroupBy;
|
||||
use lightcraft_catalog::SortKey::*;
|
||||
let cur = app.session.sort;
|
||||
ui.label(egui::RichText::new("Sort by").weak());
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Sort by")).weak());
|
||||
for (label, key, k) in [
|
||||
("Capture Date", CaptureDate, "captureDate"),
|
||||
("Import Date", ImportDate, "importDate"),
|
||||
|
|
@ -225,19 +237,19 @@ fn sort_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
("Rating", Rating, "rating"),
|
||||
("File Size", FileSize, "fileSize"),
|
||||
] {
|
||||
if ui.selectable_label(cur.key == key, label).clicked() {
|
||||
if ui.selectable_label(cur.key == key, crate::i18n::tr(label)).clicked() {
|
||||
let _ = app.run("library.sort", json!({"key": k}));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.selectable_label(cur.ascending, "Ascending").clicked() {
|
||||
if ui.selectable_label(cur.ascending, crate::i18n::tr("Ascending")).clicked() {
|
||||
let _ = app.run("library.sort", json!({"ascending": true}));
|
||||
}
|
||||
if ui.selectable_label(!cur.ascending, "Descending").clicked() {
|
||||
if ui.selectable_label(!cur.ascending, crate::i18n::tr("Descending")).clicked() {
|
||||
let _ = app.run("library.sort", json!({"ascending": false}));
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Group by date").weak());
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Group by date")).weak());
|
||||
for (label, g, k) in [
|
||||
("Automatic", GroupBy::Auto, "auto"),
|
||||
("Day", GroupBy::Day, "day"),
|
||||
|
|
@ -245,7 +257,7 @@ fn sort_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
("Year", GroupBy::Year, "year"),
|
||||
("None", GroupBy::None, "none"),
|
||||
] {
|
||||
if ui.selectable_label(cur.group == g, label).clicked() {
|
||||
if ui.selectable_label(cur.group == g, crate::i18n::tr(label)).clicked() {
|
||||
let _ = app.run("library.sort", json!({"group": k}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const TAIL: f32 = 170.0;
|
|||
/// "14 of 120 photos" when a filter narrows the source, else "14 photos".
|
||||
pub fn count_text(matching: usize, total: Option<usize>, filtering: bool) -> String {
|
||||
match total {
|
||||
Some(t) if filtering && t != matching => format!("{matching} of {t} photos"),
|
||||
_ => format!("{matching} photos"),
|
||||
Some(t) if filtering && t != matching => crate::i18n::tr_format!("{matching} of {t} photos", matching = matching, t = t),
|
||||
_ => crate::i18n::tr_format!("{matching} photos", matching = matching),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, chips: &[FilterChip]) {
|
|||
let text_r = Rect::from_min_max(r.min, pos2(r.right() - 22.0, r.max.y));
|
||||
ui.painter().with_clip_rect(text_r).text(pos2(r.left() + 10.0, r.center().y), egui::Align2::LEFT_CENTER, &c.label, font.clone(), t.text);
|
||||
let xr = Rect::from_center_size(pos2(r.right() - 12.0, r.center().y), vec2(18.0, 18.0));
|
||||
let resp = ui.interact(xr, egui::Id::new(("filter-chip-x", i)), Sense::click()).on_hover_text("Remove this filter");
|
||||
let resp = ui.interact(xr, egui::Id::new(("filter-chip-x", i)), Sense::click()).on_hover_text(crate::i18n::tr("Remove this filter"));
|
||||
register(ui.ctx(), format!("chip:{i}"), xr);
|
||||
let col = if resp.hovered() { t.text } else { t.text_dim };
|
||||
let m = 3.5;
|
||||
|
|
@ -80,7 +80,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, chips: &[FilterChip]) {
|
|||
let more = crate::widgets::text_button(&mut child, "chipsMore", &format!("+{} more", hidden.len()), false);
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
for c in hidden {
|
||||
if ui.button(format!("{} ✕", c.label)).on_hover_text("Remove this filter").clicked() {
|
||||
if ui.button(format!("{} ✕", c.label)).on_hover_text(crate::i18n::tr("Remove this filter")).clicked() {
|
||||
let _ = app.run("library.filter", c.clear.clone());
|
||||
if c.clear.get("text").is_some() {
|
||||
app.ui.search.clear();
|
||||
|
|
@ -89,7 +89,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, chips: &[FilterChip]) {
|
|||
}
|
||||
});
|
||||
}
|
||||
if crate::widgets::text_button(&mut child, "chipsClearAll", "Clear all", false).clicked() {
|
||||
if crate::widgets::text_button(&mut child, "chipsClearAll", crate::i18n::tr("Clear all"), false).clicked() {
|
||||
app.ui.search.clear();
|
||||
let _ = app.run("library.clearFilter", json!({}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,10 +263,10 @@ pub fn show_compare(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
app.ui.zoom = if app.ui.zoom == Zoom::Fit { Zoom::Percent(100) } else { Zoom::Fit };
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Swap").clicked() {
|
||||
if ui.button(crate::i18n::tr("Swap")).clicked() {
|
||||
let _ = swap(app);
|
||||
}
|
||||
if ui.button("Make Candidate the Select").clicked() {
|
||||
if ui.button(crate::i18n::tr("Make Candidate the Select")).clicked() {
|
||||
let _ = make_select(app);
|
||||
}
|
||||
ui.separator();
|
||||
|
|
@ -321,7 +321,7 @@ pub fn show_survey(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
// remove from the survey (deselect) on hover
|
||||
if resp.hovered() && photos.len() > 1 {
|
||||
let xr = Rect::from_min_size(pos2(img.right() - 26.0, img.top() + 6.0), vec2(20.0, 20.0));
|
||||
let xresp = ui.interact(xr, egui::Id::new(("survey-x", id.0)), Sense::click()).on_hover_text("Remove from survey");
|
||||
let xresp = ui.interact(xr, egui::Id::new(("survey-x", id.0)), Sense::click()).on_hover_text(crate::i18n::tr("Remove from survey"));
|
||||
ui.painter().circle_filled(xr.center(), 10.0, Color32::from_black_alpha(if xresp.hovered() { 230 } else { 160 }));
|
||||
paint(ui.painter(), xr.shrink(4.0), Icon::Close, t.text);
|
||||
if xresp.clicked() {
|
||||
|
|
@ -331,8 +331,11 @@ pub fn show_survey(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
resp.context_menu(|ui| super::grid::context_menu(app, ui, *id));
|
||||
}
|
||||
let n = photos.len();
|
||||
let msg =
|
||||
if app.session.selection.ids.len() > SURVEY_MAX { format!("Showing {n} of {}", app.session.selection.ids.len()) } else { String::new() };
|
||||
let msg = if app.session.selection.ids.len() > SURVEY_MAX {
|
||||
crate::i18n::tr_format!("Showing {n} of {}", app.session.selection.ids.len(), n = n)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if !msg.is_empty() {
|
||||
ui.painter().text(pos2(canvas.right() - 16.0, canvas.top() + 10.0), Align2::RIGHT_TOP, msg, t.font(12.0), t.text_dim);
|
||||
}
|
||||
|
|
@ -357,7 +360,7 @@ pub fn show_reference(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
app.image_rect = Some(img);
|
||||
pan(app, &resp, img);
|
||||
lresp.context_menu(|ui| {
|
||||
if ui.button("Clear Reference").clicked() {
|
||||
if ui.button(crate::i18n::tr("Clear Reference")).clicked() {
|
||||
app.ui.reference = None;
|
||||
app.ui.view = ViewMode::Detail;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
// soft proofing: a paper-white surround and the proof's name, as Lightroom shows it
|
||||
if app.ui.soft_proof && !fullscreen {
|
||||
p.rect_filled(canvas, 0.0, Color32::from_gray(238));
|
||||
let label = format!("Proof Preview · {}", app.ui.proof.space.label());
|
||||
let label = crate::i18n::tr_format!("Proof Preview · {}", app.ui.proof.space.label());
|
||||
p.text(pos2(canvas.right() - 16.0, canvas.top() + 14.0), Align2::RIGHT_CENTER, label, t.font(12.5), Color32::from_gray(60));
|
||||
}
|
||||
let shown;
|
||||
|
|
@ -275,11 +275,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let br = fit_rect(areas[0], aspect, app.ui.zoom, native, ppp, app.ui.pan);
|
||||
draw(Slot::Before, br);
|
||||
shown = draw(Slot::Main, img_rect);
|
||||
p.text(pos2(br.left(), br.bottom() + 14.0), Align2::LEFT_CENTER, "Before", t.font(12.0), t.text_dim);
|
||||
p.text(pos2(img_rect.left(), img_rect.bottom() + 14.0), Align2::LEFT_CENTER, "After", t.font(12.0), t.text_dim);
|
||||
p.text(pos2(br.left(), br.bottom() + 14.0), Align2::LEFT_CENTER, crate::i18n::tr("Before"), t.font(12.0), t.text_dim);
|
||||
p.text(pos2(img_rect.left(), img_rect.bottom() + 14.0), Align2::LEFT_CENTER, crate::i18n::tr("After"), t.font(12.0), t.text_dim);
|
||||
} else if show_before {
|
||||
shown = draw(Slot::Before, img_rect);
|
||||
p.text(pos2(img_rect.left() + 8.0, img_rect.top() + 14.0), Align2::LEFT_CENTER, "Before", t.font(12.0), t.text);
|
||||
p.text(pos2(img_rect.left() + 8.0, img_rect.top() + 14.0), Align2::LEFT_CENTER, crate::i18n::tr("Before"), t.font(12.0), t.text);
|
||||
} else if let Some((key, label)) = &hover_key
|
||||
&& let Some(tex) = app.renderer.textures.get(&Slot::Hover).filter(|t| t.photo == id && t.key == *key)
|
||||
{
|
||||
|
|
@ -294,11 +294,17 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
if shown == "none" {
|
||||
match app.renderer.failure(Slot::Main) {
|
||||
Some(e) => {
|
||||
p.text(canvas.center() - vec2(0.0, 10.0), Align2::CENTER_CENTER, "This photo can't be opened", t.semibold(14.0), t.text);
|
||||
p.text(
|
||||
canvas.center() - vec2(0.0, 10.0),
|
||||
Align2::CENTER_CENTER,
|
||||
crate::i18n::tr("This photo can't be opened"),
|
||||
t.semibold(14.0),
|
||||
t.text,
|
||||
);
|
||||
p.text(canvas.center() + vec2(0.0, 12.0), Align2::CENTER_CENTER, e, t.font(12.0), t.text_dim);
|
||||
}
|
||||
None => {
|
||||
p.text(canvas.center(), Align2::CENTER_CENTER, "Rendering…", t.font(13.0), t.text_dim);
|
||||
p.text(canvas.center(), Align2::CENTER_CENTER, crate::i18n::tr("Rendering…"), t.font(13.0), t.text_dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -354,7 +360,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
preview_only_pill(ui, canvas, why);
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
ui.menu_button("Zoom", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Zoom"), |ui| {
|
||||
for (label, cmd) in [("Fit", "view.zoomFit"), ("100%", "view.zoom100"), ("Zoom In", "view.zoomIn"), ("Zoom Out", "view.zoomOut")] {
|
||||
if ui.button(label).clicked() {
|
||||
let _ = app.run(cmd, json!({}));
|
||||
|
|
@ -419,7 +425,7 @@ fn info_overlay(app: &LightcraftApp, p: &egui::Painter, canvas: Rect, photo: &li
|
|||
fn preview_only_pill(ui: &mut egui::Ui, canvas: Rect, reason: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let p = ui.painter_at(canvas);
|
||||
let text = format!("Preview only — editing the camera's embedded JPEG ({})", crate::widgets::preview_only_variant(reason));
|
||||
let text = crate::i18n::tr_format!("Preview only — editing the camera's embedded JPEG ({})", crate::widgets::preview_only_variant(reason));
|
||||
let g = p.layout_no_wrap(text, t.font(12.0), Color32::WHITE);
|
||||
let size = vec2(g.size().x + 40.0, 26.0);
|
||||
let r = Rect::from_min_size(pos2(canvas.center().x - size.x / 2.0, canvas.top() + 12.0), size);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
.to_string();
|
||||
let frame = egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::symmetric(16, 12));
|
||||
let shown = egui::Window::new(title)
|
||||
let shown = egui::Window::new(crate::i18n::tr(&title)).id(egui::Id::new("lightcraft-dialog"))
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.frame(frame)
|
||||
|
|
@ -114,7 +114,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.spacing_mut().item_spacing.y = 8.0;
|
||||
match &mut dlg {
|
||||
Dialog::AutoStack { gap } => {
|
||||
ui.label(egui::RichText::new("Stack photos taken within this time of each other:").color(t.text_label));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Stack photos taken within this time of each other:")).color(t.text_label));
|
||||
ui.add(
|
||||
egui::Slider::new(gap, 0.0..=86400.0)
|
||||
.logarithmic(true)
|
||||
|
|
@ -130,15 +130,15 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
Dialog::Cull { reject_below, pick_best } => {
|
||||
let n = app.session.selection.ids.len();
|
||||
let scope = if n > 1 { format!("the {n} selected photos") } else { format!("the {} photos in view", app.session.visible_cloned().len()) };
|
||||
let scope = if n > 1 { crate::i18n::tr_format!("the {n} selected photos", n = n) } else { crate::i18n::tr_format!("the {} photos in view", app.session.visible_cloned().len()) };
|
||||
ui.label(egui::RichText::new(format!("Scores {scope} for focus and exposure and finds similar shots taken within seconds of each other (bursts).")).color(t.text_label));
|
||||
ui.add_space(6.0);
|
||||
let r = ui.add(egui::Slider::new(reject_below, 0.0..=80.0).text("Reject below focus").step_by(1.0));
|
||||
crate::widgets::register(ui.ctx(), "field:cullReject", r.rect);
|
||||
ui.label(egui::RichText::new(if *reject_below > 0.0 { "Blurry photos below the score are flagged as rejects." } else { "0: nothing is rejected." }).color(t.text_dim));
|
||||
let r = ui.checkbox(pick_best, "Pick the sharpest photo of each burst");
|
||||
let r = ui.checkbox(pick_best, crate::i18n::tr("Pick the sharpest photo of each burst"));
|
||||
crate::widgets::register(ui.ctx(), "check:cullPick", r.rect);
|
||||
ui.label(egui::RichText::new("Scores stay on the photos: filter or make smart albums with Focus and Best of Similar Shots.").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Scores stay on the photos: filter or make smart albums with Focus and Best of Similar Shots.")).color(t.text_dim));
|
||||
}
|
||||
Dialog::WhatsNew => {
|
||||
egui::ScrollArea::vertical().max_height(460.0).auto_shrink([false, true]).show(ui, |ui| {
|
||||
|
|
@ -167,14 +167,14 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.end_row();
|
||||
}
|
||||
});
|
||||
if ui.button("Copy to Clipboard").clicked() {
|
||||
if ui.button(crate::i18n::tr("Copy to Clipboard")).clicked() {
|
||||
let text: String = rows.iter().map(|(k, v)| format!("{k}: {v}\n")).collect();
|
||||
ui.ctx().copy_text(text);
|
||||
}
|
||||
}
|
||||
Dialog::AllMetadata { title, rows, search } => {
|
||||
ui.label(egui::RichText::new(title.as_str()).color(t.text_label));
|
||||
let r = ui.add(egui::TextEdit::singleline(search).hint_text("Filter fields").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(search).hint_text(crate::i18n::tr("Filter fields")).desired_width(f32::INFINITY));
|
||||
crate::widgets::register(ui.ctx(), "field:metadataSearch", r.rect);
|
||||
let q = search.trim().to_lowercase();
|
||||
let keep = |n: &str, v: &str| q.is_empty() || n.to_lowercase().contains(&q) || v.to_lowercase().contains(&q);
|
||||
|
|
@ -217,7 +217,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
}
|
||||
Dialog::SmartRules { name, rules, .. } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text("Name").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text(crate::i18n::tr("Name")).desired_width(f32::INFINITY));
|
||||
crate::widgets::register(ui.ctx(), "field:smartName", r.rect);
|
||||
ui.add_space(6.0);
|
||||
egui::ScrollArea::vertical().max_height(360.0).auto_shrink([false, true]).show(ui, |ui| {
|
||||
|
|
@ -230,22 +230,22 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.label(
|
||||
egui::RichText::new(match problems.first() {
|
||||
Some(p) => p.clone(),
|
||||
None => format!("{n} photo{} match · updates automatically as photos change", if n == 1 { "" } else { "s" }),
|
||||
None => crate::i18n::tr_format!("{n} photo{} match · updates automatically as photos change", if n == 1 { "" } else { "s" }, n = n),
|
||||
})
|
||||
.color(t.text_dim),
|
||||
);
|
||||
}
|
||||
Dialog::NewSmartAlbum { name } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text("Name").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text(crate::i18n::tr("Name")).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
confirm = true;
|
||||
}
|
||||
let rules = app.session.view_rules();
|
||||
let n = app.session.catalog.query(&rules, &Default::default()).len();
|
||||
ui.label(egui::RichText::new(format!("Matches: {}", rules.describe())).color(t.text_label));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("Matches: {}", rules.describe())).color(t.text_label));
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{n} photo{} now · updates automatically as photos change", if n == 1 { "" } else { "s" }))
|
||||
egui::RichText::new(crate::i18n::tr_format!("{n} photo{} now · updates automatically as photos change", if n == 1 { "" } else { "s" }, n = n))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
}
|
||||
|
|
@ -299,7 +299,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
);
|
||||
}
|
||||
if n > 1 {
|
||||
ui.label(egui::RichText::new(format!("All {n} selected photos move by the same amount.")).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("All {n} selected photos move by the same amount.", n = n)).color(t.text_dim));
|
||||
}
|
||||
}
|
||||
Dialog::LabelNames { names, save_as } => {
|
||||
|
|
@ -325,17 +325,17 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
}
|
||||
field(ui, "Save as set", |ui| {
|
||||
let te = ui.add(egui::TextEdit::singleline(save_as).hint_text("Optional name").desired_width(f32::INFINITY));
|
||||
let te = ui.add(egui::TextEdit::singleline(save_as).hint_text(crate::i18n::tr("Optional name")).desired_width(f32::INFINITY));
|
||||
crate::widgets::register(ui.ctx(), "field:labelSetName", te.rect);
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new("Names appear in the label menu, the filter bar and the Info panel, and are written to XMP. Empty = the colour's name.")
|
||||
egui::RichText::new(crate::i18n::tr("Names appear in the label menu, the filter bar and the Info panel, and are written to XMP. Empty = the colour's name."))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
}
|
||||
Dialog::Rename { template, start } => {
|
||||
let n = app.session.targets(&json!({})).len();
|
||||
ui.label(egui::RichText::new(format!("{n} photo{}", if n == 1 { "" } else { "s" })).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("{n} photo{}", if n == 1 { "" } else { "s" }, n = n)).color(t.text_dim));
|
||||
let template_id = egui::Id::new("rename-template");
|
||||
let tags_open = field(ui, "Template", |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
|
|
@ -360,7 +360,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
field(ui, "Start at", |ui| ui.add(egui::DragValue::new(start).range(0..=999_999)));
|
||||
ui.label(
|
||||
egui::RichText::new("Tags: see Tags beside the template. Files are renamed on disk (with their XMP sidecars); existing names get -1, -2…")
|
||||
egui::RichText::new(crate::i18n::tr("Tags: see Tags beside the template. Files are renamed on disk (with their XMP sidecars); existing names get -1, -2…"))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
let (preview, total) = rename_preview(app, ui.ctx(), template, *start as usize);
|
||||
|
|
@ -381,12 +381,12 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
let more = total.saturating_sub(RENAME_PREVIEW_ROWS);
|
||||
if more > 0 {
|
||||
ui.label(egui::RichText::new(format!("… and {more} more")).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("… and {more} more", more = more)).color(t.text_dim));
|
||||
}
|
||||
}
|
||||
Dialog::RenameKeyword { from, to } => {
|
||||
let n = app.session.catalog.photos().filter(|p| p.meta.keywords.iter().any(|k| lightcraft_catalog::keywords::is_under(k, from))).count();
|
||||
let r = ui.add(egui::TextEdit::singleline(to).hint_text("New name").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(to).hint_text(crate::i18n::tr("New name")).desired_width(f32::INFINITY));
|
||||
crate::widgets::register(ui.ctx(), "field:keywordName", r.rect);
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
|
|
@ -402,7 +402,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
Dialog::MergeKeywords { from, into } => {
|
||||
ui.label(egui::RichText::new(format!("Replace {} with:", from.iter().map(|f| format!("“{f}”")).collect::<Vec<_>>().join(", "))).color(t.text_label));
|
||||
let r = ui.add(egui::TextEdit::singleline(into).hint_text("Keyword").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(into).hint_text(crate::i18n::tr("Keyword")).desired_width(f32::INFINITY));
|
||||
crate::widgets::register(ui.ctx(), "field:keywordInto", r.rect);
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
|
|
@ -431,23 +431,23 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
Dialog::NewAlbum { name, .. } | Dialog::RenameAlbum { name, .. } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text("Name").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text(crate::i18n::tr("Name")).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
confirm = true;
|
||||
}
|
||||
}
|
||||
Dialog::CreatePreset { name, group, groups } => {
|
||||
ui.add(egui::TextEdit::singleline(name).hint_text("Preset name").desired_width(f32::INFINITY));
|
||||
ui.add(egui::TextEdit::singleline(group).hint_text("Group").desired_width(f32::INFINITY));
|
||||
ui.label(egui::RichText::new("Settings to include").color(t.text_dim));
|
||||
ui.add(egui::TextEdit::singleline(name).hint_text(crate::i18n::tr("Preset name")).desired_width(f32::INFINITY));
|
||||
ui.add(egui::TextEdit::singleline(group).hint_text(crate::i18n::tr("Group")).desired_width(f32::INFINITY));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Settings to include")).color(t.text_dim));
|
||||
group_checklist(ui, "presetInclude", groups);
|
||||
}
|
||||
Dialog::CopySettings { groups } => group_checklist(ui, "copyGroup", groups),
|
||||
Dialog::PasteSettings { groups } => {
|
||||
let n = app.session.targets(&json!({})).len();
|
||||
ui.label(
|
||||
egui::RichText::new(format!("Paste into {n} photo{} — only settings that were copied are pasted", if n == 1 { "" } else { "s" }))
|
||||
egui::RichText::new(crate::i18n::tr_format!("Paste into {n} photo{} — only settings that were copied are pasted", if n == 1 { "" } else { "s" }, n = n))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
group_checklist(ui, "pasteGroup", groups);
|
||||
|
|
@ -455,11 +455,11 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
Dialog::Export { opts, full_size, resize, preset_name, limit_kb, dir } => {
|
||||
use lightcraft_engine::export::{Anchor as P, ExportFormat as F, MetadataPolicy as M, SharpenAmount as A, SharpenFor as S};
|
||||
let n = app.session.selection.ids.len().max(1);
|
||||
ui.label(egui::RichText::new(format!("{n} photo{}", if n == 1 { "" } else { "s" })).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("{n} photo{}", if n == 1 { "" } else { "s" }, n = n)).color(t.text_dim));
|
||||
// Preset: load a built-in or saved set of options into the dialog
|
||||
field(ui, "Preset", |ui| {
|
||||
let mut chosen = None;
|
||||
let c = egui::ComboBox::from_id_salt("exportPreset").width(220.0).selected_text("Choose…").show_ui(ui, |ui| {
|
||||
let c = egui::ComboBox::from_id_salt("exportPreset").width(220.0).selected_text(crate::i18n::tr("Choose…")).show_ui(ui, |ui| {
|
||||
let mut builtin = true;
|
||||
for (p, b) in app.session.all_export_presets() {
|
||||
if builtin && !b {
|
||||
|
|
@ -488,7 +488,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui,
|
||||
"Format",
|
||||
"exportFormat",
|
||||
&[(F::Jpeg, "JPEG"), (F::Png, "PNG"), (F::Tiff, "TIFF"), (F::Webp, "WebP"), (F::Avif, "AVIF"), (F::Dng, "DNG"), (F::Original, "Original")],
|
||||
&[(F::Jpeg, "JPEG"), (F::Png, "PNG"), (F::Tiff, "TIFF"), (F::Webp, "WebP"), (F::Avif, "AVIF"), (F::Dng, "DNG"), (F::Original, "Original files")],
|
||||
&mut opts.format,
|
||||
);
|
||||
if !opts.format.is_rendered() {
|
||||
|
|
@ -512,7 +512,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
if !rendered {
|
||||
} else if opts.format == F::Avif {
|
||||
ui.label(egui::RichText::new("Color space: sRGB (AVIF)").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Color space: sRGB (AVIF)")).color(t.text_dim));
|
||||
} else {
|
||||
use lightcraft_engine::export::OutputSpace as C;
|
||||
choices(
|
||||
|
|
@ -571,10 +571,10 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
&mut opts.metadata,
|
||||
);
|
||||
if !matches!(opts.metadata, M::None | M::Copyright) {
|
||||
ui.checkbox(&mut opts.remove_location, "Remove location info");
|
||||
ui.checkbox(&mut opts.remove_location, crate::i18n::tr("Remove location info"));
|
||||
}
|
||||
let mut wm_on = opts.watermark.is_some();
|
||||
if ui.checkbox(&mut wm_on, "Watermark").changed() {
|
||||
if ui.checkbox(&mut wm_on, crate::i18n::tr("Watermark")).changed() {
|
||||
opts.watermark = wm_on.then(|| lightcraft_engine::export::Watermark { text: "© ".into(), ..Default::default() });
|
||||
}
|
||||
if let Some(wm) = &mut opts.watermark {
|
||||
|
|
@ -592,7 +592,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
field(ui, "Graphic", |ui| {
|
||||
trailing_button_row(ui, |ui| {
|
||||
if app.services.pick_files.is_some()
|
||||
&& crate::widgets::text_button(ui, "exportWmChoose", "Choose…", false).clicked()
|
||||
&& crate::widgets::text_button(ui, "exportWmChoose", crate::i18n::tr("Choose…"), false).clicked()
|
||||
&& let Some(f) = app.services.pick_files.as_mut().and_then(|pick| pick().into_iter().next())
|
||||
{
|
||||
wm.image = f;
|
||||
|
|
@ -605,7 +605,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
wm.image_width = (width / 100.0) as f32;
|
||||
}
|
||||
} else {
|
||||
choices(ui, app.ui.language.tr("Orientation"), "exportWmOrientation", &[(false, app.ui.language.tr("Horizontal")), (true, app.ui.language.tr("Vertical"))], &mut wm.vertical);
|
||||
choices(ui, app.ui.language.tr("Text direction"), "exportWmOrientation", &[(false, app.ui.language.tr("Horizontal text")), (true, app.ui.language.tr("Vertical text"))], &mut wm.vertical);
|
||||
field(ui, "Text", |ui| {
|
||||
ui.add(egui::TextEdit::multiline(&mut wm.text).desired_rows(2).hint_text("© Your Name").desired_width(f32::INFINITY))
|
||||
});
|
||||
|
|
@ -626,7 +626,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
wm.opacity = (op / 100.0) as f32;
|
||||
}
|
||||
if !graphic {
|
||||
ui.checkbox(&mut wm.shadow, "Shadow");
|
||||
ui.checkbox(&mut wm.shadow, crate::i18n::tr("Shadow"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -659,7 +659,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
field(ui, "Folder", |ui| {
|
||||
trailing_button_row(ui, |ui| {
|
||||
if app.services.pick_folder.is_some()
|
||||
&& crate::widgets::text_button(ui, "exportChooseFolder", "Choose…", false).clicked()
|
||||
&& crate::widgets::text_button(ui, "exportChooseFolder", crate::i18n::tr("Choose…"), false).clicked()
|
||||
&& let Some(pick) = app.services.pick_folder.as_mut()
|
||||
&& let Some(d) = pick()
|
||||
{
|
||||
|
|
@ -669,7 +669,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
});
|
||||
field(ui, "Subfolder", |ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut opts.subfolder).hint_text("none").desired_width(f32::INFINITY))
|
||||
ui.add(egui::TextEdit::singleline(&mut opts.subfolder).hint_text(crate::i18n::tr("none")).desired_width(f32::INFINITY))
|
||||
});
|
||||
use lightcraft_engine::export::Conflict as K;
|
||||
choices(
|
||||
|
|
@ -683,17 +683,17 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
field(ui, "Save preset", |ui| {
|
||||
trailing_button_row(ui, |ui| {
|
||||
let named = !preset_name.trim().is_empty();
|
||||
if crate::widgets::text_button(ui, "exportSavePreset", "Save", false).clicked() && named {
|
||||
if crate::widgets::text_button(ui, "exportSavePreset", crate::i18n::tr("Save"), false).clicked() && named {
|
||||
let params = export_dialog_params(opts, *full_size, resize, *limit_kb);
|
||||
match app.run("export.savePreset", json!({"name": preset_name.trim(), "params": params})) {
|
||||
Ok(_) => {
|
||||
app.toast(ui.ctx(), format!("Saved export preset “{}”", preset_name.trim()));
|
||||
app.toast(ui.ctx(), crate::i18n::tr_format!("Saved export preset “{}”", preset_name.trim()));
|
||||
preset_name.clear();
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
ui.add(egui::TextEdit::singleline(preset_name).hint_text("Preset name").desired_width(ui.available_width()));
|
||||
ui.add(egui::TextEdit::singleline(preset_name).hint_text(crate::i18n::tr("Preset name")).desired_width(ui.available_width()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -701,14 +701,14 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
Dialog::Import { opts } => crate::import::body(app, ui, opts),
|
||||
Dialog::Settings { tab } => crate::panels::settings::body(app, ui, tab),
|
||||
Dialog::ConfirmDelete { count } => {
|
||||
let what = if *count == 1 { "this photo".to_string() } else { format!("these {count} photos") };
|
||||
ui.label(format!("Move {what} to Recently Deleted?"));
|
||||
ui.label(egui::RichText::new("They can be restored from Recently Deleted until it is emptied.").color(t.text_dim));
|
||||
let what = if *count == 1 { "this photo".to_string() } else { crate::i18n::tr_format!("these {count} photos", count = count) };
|
||||
ui.label(crate::i18n::tr_format!("Move {what} to Recently Deleted?", what = what));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("They can be restored from Recently Deleted until it is emptied.")).color(t.text_dim));
|
||||
}
|
||||
Dialog::About => {
|
||||
ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text));
|
||||
ui.label(format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION")));
|
||||
ui.label("MIT OR Apache-2.0. Font: Inter (OFL). Icons: original.");
|
||||
ui.label(crate::i18n::tr_format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION")));
|
||||
ui.label("MIT OR Apache-2.0. Fonts: Inter / BIZ UDPGothic (OFL). Icons: original.");
|
||||
ui.add_space(10.0);
|
||||
let discord = egui::Button::new(egui::RichText::new("Join the ArtCraft Discord").font(t.semibold(15.0)).color(egui::Color32::WHITE))
|
||||
.fill(t.accent)
|
||||
|
|
@ -735,7 +735,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
egui::Grid::new("shortcuts").striped(true).show(ui, |ui| {
|
||||
for (id, label, sc, _) in crate::menus::UI_COMMANDS {
|
||||
if let Some(sc) = sc {
|
||||
ui.label(*label);
|
||||
ui.label(crate::i18n::tr(label));
|
||||
ui.label(*sc);
|
||||
ui.label(egui::RichText::new(*id).color(t.text_dim));
|
||||
ui.end_row();
|
||||
|
|
@ -743,7 +743,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
for c in lightcraft_engine::command_specs() {
|
||||
if let Some(sc) = c.shortcut {
|
||||
ui.label(c.label);
|
||||
ui.label(crate::i18n::tr(c.label));
|
||||
ui.label(sc);
|
||||
ui.label(egui::RichText::new(c.id).color(t.text_dim));
|
||||
ui.end_row();
|
||||
|
|
@ -756,7 +756,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
.map(|c| c.1)
|
||||
.or_else(|| lightcraft_engine::find_command(id).map(|c| c.label))
|
||||
.unwrap_or(id);
|
||||
ui.label(label);
|
||||
ui.label(crate::i18n::tr(label));
|
||||
ui.label(*sc);
|
||||
ui.label(egui::RichText::new(*id).color(t.text_dim));
|
||||
ui.end_row();
|
||||
|
|
@ -768,7 +768,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
let informational = matches!(dlg, Dialog::About | Dialog::Shortcuts | Dialog::Settings { .. });
|
||||
if !informational && ui.button("Cancel").clicked() {
|
||||
if !informational && ui.button(crate::i18n::tr("Cancel")).clicked() {
|
||||
close = true;
|
||||
}
|
||||
let add_label;
|
||||
|
|
@ -784,7 +784,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
_ if informational => "Close",
|
||||
_ => "OK",
|
||||
};
|
||||
if ui.button(ok).clicked() {
|
||||
if ui.button(crate::i18n::tr(ok)).clicked() {
|
||||
if informational {
|
||||
close = true;
|
||||
} else {
|
||||
|
|
@ -983,15 +983,15 @@ fn export_size(ui: &mut egui::Ui, full: &mut bool, r: &mut lightcraft_engine::ex
|
|||
(R::Megapixels, "Megapixels"),
|
||||
(R::Percent, "Percentage"),
|
||||
];
|
||||
let r_full = ui.checkbox(full, "Full size");
|
||||
let r_full = ui.checkbox(full, crate::i18n::tr("Full size"));
|
||||
crate::widgets::register(ui.ctx(), "check:exportFullSize", r_full.rect);
|
||||
if !*full {
|
||||
field(ui, "Resize to", |ui| {
|
||||
let cur = MODES.iter().find(|m| m.0 == r.mode).map_or("Long Edge", |m| m.1);
|
||||
let before = r.mode;
|
||||
let c = egui::ComboBox::from_id_salt("exportResizeMode").width(150.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
let c = egui::ComboBox::from_id_salt("exportResizeMode").width(150.0).selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
for (m, l) in MODES {
|
||||
ui.selectable_value(&mut r.mode, m, l);
|
||||
ui.selectable_value(&mut r.mode, m, crate::i18n::tr(l));
|
||||
}
|
||||
});
|
||||
crate::widgets::register(ui.ctx(), "combo:exportResizeMode", c.response.rect);
|
||||
|
|
@ -1024,7 +1024,7 @@ fn export_size(ui: &mut egui::Ui, full: &mut bool, r: &mut lightcraft_engine::ex
|
|||
r.height = h as u32;
|
||||
}
|
||||
}
|
||||
let c = ui.checkbox(&mut r.dont_enlarge, "Don't enlarge");
|
||||
let c = ui.checkbox(&mut r.dont_enlarge, crate::i18n::tr("Don't enlarge"));
|
||||
crate::widgets::register(ui.ctx(), "check:exportDontEnlarge", c.rect);
|
||||
}
|
||||
let mut p = *ppi as f64;
|
||||
|
|
@ -1053,7 +1053,7 @@ fn field<R>(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui) -> R
|
|||
ui.horizontal(|ui| {
|
||||
ui.allocate_ui_with_layout(egui::vec2(LABEL_W, 24.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.set_min_width(LABEL_W);
|
||||
ui.label(egui::RichText::new(label).color(t.text_label));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(label)).color(t.text_label));
|
||||
});
|
||||
add(ui)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -87,20 +87,21 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
// header: Edit + Auto / B&W / HDR
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 40.0), Sense::hover());
|
||||
ui.painter().text(pos2(hr.left() + 24.0, hr.bottom() - 10.0), Align2::LEFT_CENTER, "Edit", t.semibold(15.0), t.text);
|
||||
ui.painter().text(pos2(hr.left() + 24.0, hr.bottom() - 10.0), Align2::LEFT_CENTER, crate::i18n::tr("Edit"), t.semibold(15.0), t.text);
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 8, bottom: 14 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
if text_button(ui, "auto", "Auto", false).clicked() {
|
||||
if text_button(ui, "auto", crate::i18n::tr("Auto"), false).clicked() {
|
||||
let _ = app.run("develop.auto", json!({}));
|
||||
app.toast(ui.ctx(), "Auto settings applied");
|
||||
}
|
||||
let bw = crate::is_bw(&d);
|
||||
if text_button(ui, "bw", "B&W", bw).clicked() {
|
||||
if text_button(ui, "bw", crate::i18n::tr("B&W"), bw).clicked() {
|
||||
let _ = app.run("develop.treatment", json!({}));
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if text_button(ui, "reset", "Reset", false).on_hover_text("Reset all edits (Cmd+Shift+R)").clicked() {
|
||||
if text_button(ui, "reset", crate::i18n::tr("Reset"), false).on_hover_text(crate::i18n::tr("Reset all edits (Cmd+Shift+R)")).clicked()
|
||||
{
|
||||
let _ = app.run("develop.reset", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -118,9 +119,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if app.session.auto_sync && n > 1 {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 0, bottom: 10 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(format!("Auto Sync: edits apply to {n} photos")).color(t.accent).size(12.0));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("Auto Sync: edits apply to {n} photos", n = n)).color(t.accent).size(12.0));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if text_button(ui, "autoSyncOff", "Turn Off", false).clicked() {
|
||||
if text_button(ui, "autoSyncOff", crate::i18n::tr("Turn Off"), false).clicked() {
|
||||
let _ = app.run("develop.autoSync", json!({"on": false}));
|
||||
}
|
||||
});
|
||||
|
|
@ -131,9 +132,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
// profile row
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 14, bottom: 14 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Profile").font(t.font(13.0)).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Profile")).font(t.font(13.0)).color(t.text_dim));
|
||||
let name = lightcraft_engine::presets::profile(&d.profile.id).map(|p| p.name).unwrap_or("Color");
|
||||
let r = crate::widgets::dropdown(ui, "profile", name, t.font(15.0), t.text_label);
|
||||
let r =
|
||||
crate::widgets::dropdown(ui, "profile", if name == "Color" { crate::i18n::tr("Color") } else { name }, t.font(15.0), t.text_label);
|
||||
egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::widgets::icon_button(ui, "profileBrowser", Icon::ProfileGrid, vec2(28.0, 28.0), false, true, "Browse Profiles").clicked() {
|
||||
|
|
@ -154,7 +156,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
ui.add_space(6.0);
|
||||
let open = app.ui.flyout_open("curve");
|
||||
if flyout_row(ui, "curve", "Curve", Icon::Curve, open).clicked() {
|
||||
if flyout_row(ui, "curve", crate::i18n::tr("Curve"), Icon::Curve, open).clicked() {
|
||||
app.ui.toggle_flyout("curve");
|
||||
}
|
||||
if open {
|
||||
|
|
@ -166,8 +168,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
// White balance row
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 2, bottom: 2 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("White Balance").font(t.font(13.0)).color(t.text_dim));
|
||||
let r = crate::widgets::dropdown(ui, "wbMode", d.wb.mode.label(), t.font(14.0), t.text_label);
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("White Balance")).font(t.font(13.0)).color(t.text_dim));
|
||||
let r = crate::widgets::dropdown(ui, "wbMode", crate::i18n::tr(d.wb.mode.label()), t.font(14.0), t.text_label);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
for m in WbMode::ALL {
|
||||
if m == WbMode::Custom {
|
||||
|
|
@ -210,7 +212,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
if !crate::is_bw(d) {
|
||||
let open = app.ui.flyout_open("pointColor");
|
||||
if flyout_row(ui, "pointColor", "Point Color", Icon::Picker, open).clicked() {
|
||||
if flyout_row(ui, "pointColor", crate::i18n::tr("Point Color"), Icon::Picker, open).clicked() {
|
||||
app.ui.toggle_flyout("pointColor");
|
||||
}
|
||||
if open {
|
||||
|
|
@ -218,7 +220,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
}
|
||||
let open = app.ui.flyout_open("grading");
|
||||
if flyout_row(ui, "grading", "Color Grading", Icon::Presets, open).clicked() {
|
||||
if flyout_row(ui, "grading", crate::i18n::tr("Color Grading"), Icon::Presets, open).clicked() {
|
||||
app.ui.toggle_flyout("grading");
|
||||
}
|
||||
if open {
|
||||
|
|
@ -230,7 +232,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
for c in ["effects.texture", "effects.clarity", "effects.dehaze"] {
|
||||
control(app, ui, d, c, true);
|
||||
}
|
||||
sub_title(ui, "Vignette");
|
||||
sub_title(ui, crate::i18n::tr("Vignette"));
|
||||
{
|
||||
use lightcraft_develop::VignetteStyle as V;
|
||||
let styles = [
|
||||
|
|
@ -255,7 +257,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let enabled = c == "vignette.amount" || d.vignette.amount != 0.0;
|
||||
control(app, ui, d, c, enabled);
|
||||
}
|
||||
sub_title(ui, "Grain");
|
||||
sub_title(ui, crate::i18n::tr("Grain"));
|
||||
for c in ["grain.amount", "grain.size", "grain.roughness"] {
|
||||
control(app, ui, d, c, c == "grain.amount" || d.grain.amount != 0.0);
|
||||
}
|
||||
|
|
@ -265,7 +267,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
for c in ["detail.sharpenAmount", "detail.sharpenRadius", "detail.sharpenDetail", "detail.sharpenMasking"] {
|
||||
control(app, ui, d, c, c == "detail.sharpenAmount" || d.detail.sharpen_amount > 0.0);
|
||||
}
|
||||
sub_title(ui, "Noise Reduction");
|
||||
sub_title(ui, crate::i18n::tr("Noise Reduction"));
|
||||
for c in ["detail.nrLuminance", "detail.nrDetail", "detail.nrContrast", "detail.nrColor", "detail.nrColorDetail", "detail.nrColorSmoothness"]
|
||||
{
|
||||
control(app, ui, d, c, true);
|
||||
|
|
@ -276,16 +278,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let has_lens = app.session.catalog.photo(id).is_some_and(|p| p.embedded_lens.is_some());
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 4, bottom: 4 }).show(ui, |ui| {
|
||||
let mut ca = d.optics.remove_ca;
|
||||
if ui.checkbox(&mut ca, "Remove Chromatic Aberration").changed() {
|
||||
if ui.checkbox(&mut ca, crate::i18n::tr("Remove Chromatic Aberration")).changed() {
|
||||
let _ = app.run("develop.merge", json!({"settings": {"optics": {"remove_ca": ca}}, "label": "Remove CA"}));
|
||||
}
|
||||
let mut lp = d.optics.lens_profile;
|
||||
if ui.checkbox(&mut lp, "Enable Lens Corrections").changed() {
|
||||
if ui.checkbox(&mut lp, crate::i18n::tr("Enable Lens Corrections")).changed() {
|
||||
let _ = app.run("develop.merge", json!({"settings": {"optics": {"lens_profile": lp}}, "label": "Lens Corrections"}));
|
||||
}
|
||||
if lp && !has_lens {
|
||||
ui.label(
|
||||
egui::RichText::new("No lens data embedded in this file (DNG lens opcodes only).")
|
||||
egui::RichText::new(crate::i18n::tr("No lens data embedded in this file (DNG lens opcodes only)."))
|
||||
.size(11.0)
|
||||
.color(Tokens::get(ui.ctx()).text_dim),
|
||||
);
|
||||
|
|
@ -296,14 +298,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
control(app, ui, d, c, has_lens);
|
||||
}
|
||||
}
|
||||
sub_title(ui, "Manual");
|
||||
sub_title(ui, crate::i18n::tr("Manual"));
|
||||
for c in ["optics.distortion", "optics.vignetting"] {
|
||||
control(app, ui, d, c, true);
|
||||
}
|
||||
control(app, ui, d, "optics.vignettingMidpoint", d.optics.vignetting != 0.0);
|
||||
ui.add_space(6.0);
|
||||
let open = app.ui.flyout_open("defringe");
|
||||
if flyout_row(ui, "defringe", "Defringe", Icon::Picker, open).clicked() {
|
||||
if flyout_row(ui, "defringe", crate::i18n::tr("Defringe"), Icon::Picker, open).clicked() {
|
||||
app.ui.toggle_flyout("defringe");
|
||||
}
|
||||
if open {
|
||||
|
|
@ -315,7 +317,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let on = d.optics.defringe_green_amount > 0.0;
|
||||
control(app, ui, d, "optics.defringeGreenHueLo", on);
|
||||
control(app, ui, d, "optics.defringeGreenHueHi", on);
|
||||
sub_title(ui, "Lateral Chromatic Aberration");
|
||||
sub_title(ui, crate::i18n::tr("Lateral Chromatic Aberration"));
|
||||
control(app, ui, d, "optics.caRed", true);
|
||||
control(app, ui, d, "optics.caBlue", true);
|
||||
}
|
||||
|
|
@ -323,7 +325,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
});
|
||||
// Lightroom Classic's Calibration panel (the cloud app hides it): last, like there.
|
||||
section(app, ui, &d, "calibration", "Calibration", |app, ui, d| {
|
||||
sub_title(ui, "Shadows");
|
||||
sub_title(ui, crate::i18n::tr("Shadows"));
|
||||
control(app, ui, d, "calibration.shadowsTint", true);
|
||||
for (title, k) in [("Red Primary", "red"), ("Green Primary", "green"), ("Blue Primary", "blue")] {
|
||||
sub_title(ui, title);
|
||||
|
|
@ -383,14 +385,18 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
ui.separator();
|
||||
if let Some(p) = profile(cur) {
|
||||
let fav = app.session.profile_favorites.iter().any(|f| f == p.id);
|
||||
let label = if fav { format!("Remove “{}” from Favorites", p.name) } else { format!("Add “{}” to Favorites", p.name) };
|
||||
let label = if fav {
|
||||
crate::i18n::tr_format!("Remove “{}” from Favorites", p.name)
|
||||
} else {
|
||||
crate::i18n::tr_format!("Add “{}” to Favorites", p.name)
|
||||
};
|
||||
let r = ui.button(label);
|
||||
register(ui.ctx(), "profileMenu:toggleFavorite", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("profile.favorite", json!({"id": p.id}));
|
||||
}
|
||||
}
|
||||
let r = ui.button("Browse…");
|
||||
let r = ui.button(crate::i18n::tr("Browse…"));
|
||||
register(ui.ctx(), "profileMenu:browse", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("panel.profiles", json!({}));
|
||||
|
|
@ -402,7 +408,7 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
let mut s = d.clone();
|
||||
s.profile.id = p.id.to_string();
|
||||
s.profile.amount = 100.0;
|
||||
app.hover_preview = Some(crate::HoverPreview { label: format!("Profile: {}", p.name), settings: s });
|
||||
app.hover_preview = Some(crate::HoverPreview { label: crate::i18n::tr_format!("Profile: {}", p.name), settings: s });
|
||||
}
|
||||
if let Some(id) = pick.0 {
|
||||
let _ = app.run("develop.profile", json!({"id": id}));
|
||||
|
|
@ -412,7 +418,7 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
pub fn sub_title(ui: &mut egui::Ui, title: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 30.0), Sense::hover());
|
||||
ui.painter().text(pos2(r.left() + 24.0, r.center().y + 4.0), Align2::LEFT_CENTER, title, t.semibold(13.0), t.text_label);
|
||||
ui.painter().text(pos2(r.left() + 24.0, r.center().y + 4.0), Align2::LEFT_CENTER, crate::i18n::tr(title), t.semibold(13.0), t.text_label);
|
||||
}
|
||||
|
||||
fn section(
|
||||
|
|
@ -531,16 +537,17 @@ fn soft_proofing(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 8, bottom: 6 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("Soft Proofing").color(t.text).strong());
|
||||
ui.label(RichText::new(crate::i18n::tr("Soft Proofing")).color(t.text).strong());
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let mut dest = app.ui.proof.dest_warning;
|
||||
let r = ui
|
||||
.toggle_value(&mut dest, RichText::new("■").color(Color32::from_rgb(255, 40, 40)))
|
||||
.on_hover_text("Show destination gamut warning");
|
||||
.on_hover_text(crate::i18n::tr("Show destination gamut warning"));
|
||||
register(ui.ctx(), "button:proofDestWarning", r.rect);
|
||||
let mut disp = app.ui.proof.display_warning;
|
||||
let r2 =
|
||||
ui.toggle_value(&mut disp, RichText::new("■").color(Color32::from_rgb(40, 90, 255))).on_hover_text("Show display gamut warning");
|
||||
let r2 = ui
|
||||
.toggle_value(&mut disp, RichText::new("■").color(Color32::from_rgb(40, 90, 255)))
|
||||
.on_hover_text(crate::i18n::tr("Show display gamut warning"));
|
||||
register(ui.ctx(), "button:proofDisplayWarning", r2.rect);
|
||||
if dest != app.ui.proof.dest_warning || disp != app.ui.proof.display_warning {
|
||||
let _ = app.run("view.softProof", json!({"destWarning": dest, "displayWarning": disp}));
|
||||
|
|
@ -548,7 +555,7 @@ fn soft_proofing(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("Profile:").color(t.text_label));
|
||||
ui.label(RichText::new(crate::i18n::tr("Profile:")).color(t.text_label));
|
||||
let cur = app.ui.proof.space;
|
||||
egui::ComboBox::from_id_salt("proof-space").width(170.0).selected_text(cur.label()).show_ui(ui, |ui| {
|
||||
for s in OutputSpace::ALL {
|
||||
|
|
@ -558,10 +565,10 @@ fn soft_proofing(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
});
|
||||
});
|
||||
let r = ui.button("Create Proof Copy");
|
||||
let r = ui.button(crate::i18n::tr("Create Proof Copy"));
|
||||
register(ui.ctx(), "button:createProofCopy", r.rect);
|
||||
if r.clicked() {
|
||||
let name = format!("Proof Copy ({})", app.ui.proof.space.label());
|
||||
let name = crate::i18n::tr_format!("Proof Copy ({})", app.ui.proof.space.label());
|
||||
if app.run("photo.virtualCopy", json!({"ids": [id.0], "name": name})).is_ok() {
|
||||
app.toast(ui.ctx(), "Proof copy created");
|
||||
}
|
||||
|
|
@ -605,7 +612,7 @@ fn curve_editor(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, d: &Dev
|
|||
"green" => "Green channel",
|
||||
_ => "Blue channel",
|
||||
};
|
||||
let resp = resp.on_hover_text(format!("{name} — double-click to reset it"));
|
||||
let resp = resp.on_hover_text(crate::i18n::tr_format!("{name} — double-click to reset it", name = name));
|
||||
if resp.double_clicked() {
|
||||
app.ui.curve_channel = ch.into();
|
||||
let _ = app.run("curve.reset", json!({"channel": ch}));
|
||||
|
|
@ -796,7 +803,7 @@ fn curve_footer(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 2, bottom: 4 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("Point Curve").font(t.font(12.0)).color(t.text_label));
|
||||
ui.label(RichText::new(crate::i18n::tr("Point Curve")).font(t.font(12.0)).color(t.text_label));
|
||||
let current = matching_preset(&app.session, &d.curve);
|
||||
let r = crate::widgets::dropdown(ui, "curvePreset", current.as_deref().unwrap_or("Custom"), t.font(12.0), t.text);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
|
|
@ -816,7 +823,7 @@ fn curve_footer(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
}
|
||||
if !p.builtin {
|
||||
r.context_menu(|ui| {
|
||||
if ui.button("Delete Preset").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Preset")).clicked() {
|
||||
let _ = app.run("curve.deletePreset", json!({"name": p.name}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -825,28 +832,29 @@ fn curve_footer(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
}
|
||||
ui.separator();
|
||||
let shaped = current.as_deref() != Some("Linear");
|
||||
let r = ui.add_enabled(shaped, egui::Button::new("Save Point Curve…"));
|
||||
let r = ui.add_enabled(shaped, egui::Button::new(crate::i18n::tr("Save Point Curve…")));
|
||||
register(ui.ctx(), "curvePresetMenu:save", r.rect);
|
||||
if r.clicked() {
|
||||
crate::panels::dialogs::prompt(app, "Save Point Curve Preset", "Preset name", "", "curve.savePreset", json!({}), "name");
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(app.services.pick_curve_preset_files.is_some(), egui::Button::new("Import Presets…")).clicked() {
|
||||
if ui.add_enabled(app.services.pick_curve_preset_files.is_some(), egui::Button::new(crate::i18n::tr("Import Presets…"))).clicked() {
|
||||
let _ = app.run("file.importCurvePresets", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
let can_export = app.services.save_curve_preset_file.is_some() && !app.session.curve_presets.is_empty();
|
||||
if ui.add_enabled(can_export, egui::Button::new("Export Presets…")).clicked() {
|
||||
if ui.add_enabled(can_export, egui::Button::new(crate::i18n::tr("Export Presets…"))).clicked() {
|
||||
let _ = app.run("file.exportCurvePresets", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if presets.iter().any(|p| !p.builtin) {
|
||||
ui.label(RichText::new("Right-click a preset to delete it").font(t.font(11.0)).color(t.text_dim));
|
||||
ui.label(RichText::new(crate::i18n::tr("Right-click a preset to delete it")).font(t.font(11.0)).color(t.text_dim));
|
||||
}
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let r = text_button(ui, "curveReset", "Reset", false)
|
||||
.on_hover_text("Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)");
|
||||
let r = text_button(ui, "curveReset", crate::i18n::tr("Reset"), false).on_hover_text(crate::i18n::tr(
|
||||
"Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)",
|
||||
));
|
||||
if r.clicked() {
|
||||
let _ = app.run("curve.reset", json!({"channel": "all"}));
|
||||
}
|
||||
|
|
@ -857,14 +865,15 @@ fn curve_footer(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
|
||||
/// Right-click menu of the curve graph.
|
||||
fn curve_reset_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, ch: &str) {
|
||||
let label = if ch == "parametric" { "Reset Parametric Curve".to_string() } else { format!("Reset {} Channel", channel_label(ch)) };
|
||||
let label =
|
||||
if ch == "parametric" { "Reset Parametric Curve".to_string() } else { crate::i18n::tr_format!("Reset {} Channel", channel_label(ch)) };
|
||||
let r = ui.button(label);
|
||||
register(ui.ctx(), "curveMenu:resetChannel", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("curve.reset", json!({"channel": ch}));
|
||||
ui.close();
|
||||
}
|
||||
let r = ui.button("Reset All Curves");
|
||||
let r = ui.button(crate::i18n::tr("Reset All Curves"));
|
||||
register(ui.ctx(), "curveMenu:resetAll", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("curve.reset", json!({"channel": "all"}));
|
||||
|
|
@ -925,7 +934,7 @@ fn mixer(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) {
|
|||
let tool = format!("tat:{k}");
|
||||
let active = app.ui.tool == tool;
|
||||
if text_button(ui, &format!("tatMixer-{k}"), label, active)
|
||||
.on_hover_text("Targeted adjustment: drag up/down on the photo to adjust the colours there")
|
||||
.on_hover_text(crate::i18n::tr("Targeted adjustment: drag up/down on the photo to adjust the colours there"))
|
||||
.clicked()
|
||||
{
|
||||
app.ui.tool = if active { String::new() } else { tool };
|
||||
|
|
@ -937,7 +946,10 @@ fn mixer(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) {
|
|||
if crate::is_bw(d) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
if crate::widgets::text_button(ui, "bwAuto", "Auto", false).on_hover_text("Set the mix from the photo's colours").clicked() {
|
||||
if crate::widgets::text_button(ui, "bwAuto", crate::i18n::tr("Auto"), false)
|
||||
.on_hover_text(crate::i18n::tr("Set the mix from the photo's colours"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("develop.autoBwMix", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -994,25 +1006,25 @@ fn point_color(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
|||
});
|
||||
if n == 0 {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 2, bottom: 8 }).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("Pick a colour on the photo with the eyedropper.").size(12.0).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Pick a colour on the photo with the eyedropper.")).size(12.0).color(t.text_dim));
|
||||
});
|
||||
return;
|
||||
}
|
||||
for k in ["hueShift", "satShift", "lumShift", "variance"] {
|
||||
control(app, ui, d, &format!("pointColor.{sel}.{k}"), true);
|
||||
}
|
||||
sub_title(ui, "Range");
|
||||
sub_title(ui, crate::i18n::tr("Range"));
|
||||
for k in ["range", "hueRange", "satRange", "lumRange"] {
|
||||
control(app, ui, d, &format!("pointColor.{sel}.{k}"), true);
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 4, bottom: 8 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let mut v = app.ui.point_color_visualize;
|
||||
if ui.checkbox(&mut v, "Visualize range").changed() {
|
||||
if ui.checkbox(&mut v, crate::i18n::tr("Visualize range")).changed() {
|
||||
app.ui.point_color_visualize = v;
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if text_button(ui, "pointColorDelete", "Delete", false).clicked() {
|
||||
if text_button(ui, "pointColorDelete", crate::i18n::tr("Delete"), false).clicked() {
|
||||
let _ = app.run("pointColor.delete", json!({"index": sel}));
|
||||
}
|
||||
});
|
||||
|
|
@ -1047,9 +1059,9 @@ fn grading(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) {
|
|||
let dot = c + vec2(a.cos(), -a.sin()) * rad * (wh.sat as f32 / 100.0);
|
||||
ui.painter().circle_filled(dot, 5.0, Color32::WHITE);
|
||||
ui.painter().circle_stroke(dot, 5.0, Stroke::new(1.0, Color32::BLACK));
|
||||
ui.painter().text(pos2(c.x, c.y + rad + 12.0), Align2::CENTER_CENTER, label, t.font(12.0), t.text_label);
|
||||
ui.painter().text(pos2(c.x, c.y + rad + 12.0), Align2::CENTER_CENTER, crate::i18n::tr(label), t.font(12.0), t.text_label);
|
||||
if resp.drag_started() {
|
||||
let _ = app.run("develop.beginInteraction", json!({"label": format!("{label} Grading")}));
|
||||
let _ = app.run("develop.beginInteraction", json!({"label": crate::i18n::tr_format!("{label} Grading", label = label)}));
|
||||
}
|
||||
if (resp.dragged() || resp.clicked())
|
||||
&& let Some(q) = resp.interact_pointer_pos()
|
||||
|
|
@ -1093,7 +1105,7 @@ fn paint_wheel(p: &egui::Painter, c: Pos2, rad: f32) {
|
|||
fn quick_develop(app: &mut LightcraftApp, ui: &mut egui::Ui, n: usize) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 0, bottom: 10 }).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new(format!("Quick Develop · {n} photos")).color(t.text_label).size(12.5));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("Quick Develop · {n} photos", n = n)).color(t.text_label).size(12.5));
|
||||
ui.add_space(4.0);
|
||||
let rows: [(&str, &str, f64, f64); 9] = [
|
||||
("Exposure", "light.exposure", 1.0 / 3.0, 1.0),
|
||||
|
|
@ -1108,11 +1120,11 @@ fn quick_develop(app: &mut LightcraftApp, ui: &mut egui::Ui, n: usize) {
|
|||
];
|
||||
egui::Grid::new("quick-develop").num_columns(5).spacing([4.0, 3.0]).show(ui, |ui| {
|
||||
for (label, ctl, small, big) in rows {
|
||||
ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(label)).size(11.5).color(t.text_dim));
|
||||
for (txt, d) in [("◀◀", -big), ("◀", -small), ("▶", small), ("▶▶", big)] {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(txt).size(10.0)).min_size(egui::vec2(28.0, 18.0)));
|
||||
crate::widgets::register(ui.ctx(), format!("button:quick-{ctl}-{txt}"), r.rect);
|
||||
if r.on_hover_text(format!("{label} {d:+} on every selected photo")).clicked() {
|
||||
if r.on_hover_text(crate::i18n::tr_format!("{label} {d:+} on every selected photo", d = d, label = label)).clicked() {
|
||||
let _ = app.run("develop.quickAdjust", json!({"control": ctl, "delta": d}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ fn filter(app: &mut LightcraftApp, patch: Value) {
|
|||
/// A small label before a group of controls.
|
||||
fn caption(ui: &mut egui::Ui, text: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new(text).font(t.font(12.0)).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(text)).font(t.font(12.0)).color(t.text_dim));
|
||||
}
|
||||
|
||||
/// A square toggle with an icon or a colour swatch.
|
||||
|
|
@ -59,13 +59,13 @@ fn toggle(ui: &mut egui::Ui, id: &str, on: bool, tip: &str, draw: impl FnOnce(&e
|
|||
t.icon
|
||||
},
|
||||
);
|
||||
resp.on_hover_text(tip)
|
||||
resp.on_hover_text(crate::i18n::tr(tip))
|
||||
}
|
||||
|
||||
/// A dropdown showing `current`; `items` are (label, filter patch, selected).
|
||||
fn picker(app: &mut LightcraftApp, ui: &mut egui::Ui, id: &str, current: &str, active: bool, items: Vec<(String, Value, bool)>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let r = crate::widgets::dropdown(ui, &format!("filter-{id}"), current, t.font(12.5), if active { t.text } else { t.text_label });
|
||||
let r = crate::widgets::dropdown(ui, &format!("filter-{id}"), crate::i18n::tr(current), t.font(12.5), if active { t.text } else { t.text_label });
|
||||
if active {
|
||||
ui.painter().rect_stroke(r.rect.expand(2.0), 4.0, Stroke::new(1.0, t.accent.gamma_multiply(0.7)), StrokeKind::Outside);
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ fn picker(app: &mut LightcraftApp, ui: &mut egui::Ui, id: &str, current: &str, a
|
|||
egui::Popup::menu(&r).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for (label, patch, sel) in &items {
|
||||
if ui.selectable_label(*sel, label).clicked() {
|
||||
if ui.selectable_label(*sel, crate::i18n::tr(label)).clicked() {
|
||||
chosen = Some(patch.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -260,19 +260,19 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
// save / clear
|
||||
ui.add_space(14.0);
|
||||
let filtering = app.session.filter != Default::default();
|
||||
let save = ui.add_enabled(filtering, egui::Button::new(egui::RichText::new("Save as Smart Album…").font(t.font(12.5))));
|
||||
let save = ui.add_enabled(filtering, egui::Button::new(egui::RichText::new(crate::i18n::tr("Save as Smart Album…")).font(t.font(12.5))));
|
||||
register(ui.ctx(), "button:filterSave", save.rect);
|
||||
if save.clicked() {
|
||||
let _ = app.run("dialog.newSmartAlbum", json!({}));
|
||||
}
|
||||
let clear = ui.add_enabled(filtering, egui::Button::new(egui::RichText::new("Clear").font(t.font(12.5))));
|
||||
let clear = ui.add_enabled(filtering, egui::Button::new(egui::RichText::new(crate::i18n::tr("Clear")).font(t.font(12.5))));
|
||||
register(ui.ctx(), "button:filterClear", clear.rect);
|
||||
if clear.clicked() {
|
||||
app.ui.search.clear();
|
||||
let _ = app.run("library.clearFilter", json!({}));
|
||||
}
|
||||
// filter presets: apply one, save the current filter, delete
|
||||
let presets = ui.button(egui::RichText::new("Presets ▾").font(t.font(12.5)));
|
||||
let presets = ui.button(egui::RichText::new(crate::i18n::tr("Presets ▾")).font(t.font(12.5)));
|
||||
register(ui.ctx(), "button:filterPresets", presets.rect);
|
||||
egui::Popup::menu(&presets).show(|ui| {
|
||||
let names: Vec<String> = app.session.filter_presets.iter().map(|f| f.name.clone()).collect();
|
||||
|
|
@ -282,16 +282,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let _ = app.run("filter.applyPreset", json!({"name": n}));
|
||||
}
|
||||
r.context_menu(|ui| {
|
||||
if ui.button("Delete Preset").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Preset")).clicked() {
|
||||
let _ = app.run("filter.deletePreset", json!({"name": n}));
|
||||
}
|
||||
});
|
||||
}
|
||||
if names.is_empty() {
|
||||
ui.label(egui::RichText::new("No filter presets yet").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("No filter presets yet")).color(t.text_dim));
|
||||
}
|
||||
ui.separator();
|
||||
if ui.add_enabled(filtering, egui::Button::new("Save Current Filter as Preset…")).clicked() {
|
||||
if ui.add_enabled(filtering, egui::Button::new(crate::i18n::tr("Save Current Filter as Preset…"))).clicked() {
|
||||
crate::panels::dialogs::prompt(app, "Save Filter Preset", "Preset name", "", "filter.savePreset", json!({}), "name");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ fn show_inner(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let chips = lightcraft_engine::filter_chips(&app.session.filter, &app.session.catalog);
|
||||
let total = app.session.source_total();
|
||||
let counted = super::chips::count_text(ids.len(), total, !chips.is_empty());
|
||||
let cnt = if sel_n > 1 { format!("{sel_n} selected · {counted}") } else { counted };
|
||||
let cnt = if sel_n > 1 { crate::i18n::tr_format!("{sel_n} selected · {counted}", counted = counted, sel_n = sel_n) } else { counted };
|
||||
match app.session.browse.clone().filter(|_| app.session.source == lightcraft_engine::LibrarySource::Folder) {
|
||||
Some(b) => {
|
||||
let local = app.caches.grid.local(&app.session.catalog, &ids, generation);
|
||||
|
|
@ -163,7 +163,9 @@ fn show_inner(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
if ids.is_empty() {
|
||||
if !chips.is_empty() {
|
||||
let body = match total {
|
||||
Some(n) if n > 0 => format!("{n} photos in {} are hidden by the filters above", app.session.source.label(&app.session.catalog)),
|
||||
Some(n) if n > 0 => {
|
||||
crate::i18n::tr_format!("{n} photos in {} are hidden by the filters above", app.session.source.label(&app.session.catalog), n = n)
|
||||
}
|
||||
_ => "Remove a filter above, or choose Clear all".to_string(),
|
||||
};
|
||||
super::empty_message(ui, ui.max_rect(), "No photos match the active filters", &body);
|
||||
|
|
@ -412,7 +414,13 @@ fn group_header(app: &mut LightcraftApp, ui: &mut egui::Ui, run: &DateRun, ids:
|
|||
let gw = g.size().x;
|
||||
p.galley(pos2(x, r.center().y - g.size().y / 2.0), g, t.text);
|
||||
let n = run.count;
|
||||
p.text(pos2(x + gw + 10.0, r.center().y), Align2::LEFT_CENTER, format!("· {n} photo{}", if n == 1 { "" } else { "s" }), t.font(12.5), t.text_dim);
|
||||
p.text(
|
||||
pos2(x + gw + 10.0, r.center().y),
|
||||
Align2::LEFT_CENTER,
|
||||
crate::i18n::tr_format!("· {n} photo{}", if n == 1 { "" } else { "s" }, n = n),
|
||||
t.font(12.5),
|
||||
t.text_dim,
|
||||
);
|
||||
if resp.clicked() {
|
||||
let group: Vec<u64> = ids[run.start..(run.start + run.count).min(ids.len())].iter().map(|p| p.0).collect();
|
||||
let m = ui.input(|i| i.modifiers);
|
||||
|
|
@ -571,7 +579,7 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square
|
|||
if let Some(why) = &photo.preview_only {
|
||||
// a raw shown from its embedded JPEG: a small amber "Preview" pill at the image's bottom
|
||||
// left, above the badge bar (always shown: it changes what editing does)
|
||||
let g = p.layout_no_wrap("Preview".into(), t.semibold(9.5), Color32::WHITE);
|
||||
let g = p.layout_no_wrap(crate::i18n::tr("Preview").into(), t.semibold(9.5), Color32::WHITE);
|
||||
let at = pos2(img_rect.left() + 6.0, img_rect.bottom() - 30.0 - 16.0);
|
||||
let br = Rect::from_min_size(at, vec2(g.size().x + 24.0, 16.0));
|
||||
p.rect_filled(br, 8.0, Color32::from_black_alpha(170));
|
||||
|
|
@ -579,7 +587,7 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square
|
|||
p.galley(pos2(br.left() + 17.0, br.center().y - g.size().y / 2.0), g, Color32::WHITE);
|
||||
register(ui.ctx(), format!("badge:previewOnly:{}", id.0), br);
|
||||
ui.interact(br, egui::Id::new(("preview-only-badge", id.0)), Sense::hover())
|
||||
.on_hover_text(format!("Preview only — {}", crate::widgets::preview_only_explanation(why)));
|
||||
.on_hover_text(crate::i18n::tr_format!("Preview only — {}", crate::widgets::preview_only_explanation(why)));
|
||||
}
|
||||
if photo.flag == Flag::Reject {
|
||||
p.rect_filled(img_rect, 0.0, Color32::from_black_alpha(110));
|
||||
|
|
@ -656,18 +664,18 @@ fn folder_header(app: &mut LightcraftApp, ui: &mut egui::Ui, hr: Rect, b: &light
|
|||
ui.add_space(10.0);
|
||||
// counted once per view (cached); the ids are only gathered on a click
|
||||
if local_n > 0 {
|
||||
let label = format!("Add {local_n} to My Photos");
|
||||
let label = crate::i18n::tr_format!("Add {local_n} to My Photos", local_n = local_n);
|
||||
if crate::widgets::text_button(ui, "addToLibrary", &label, false).clicked() {
|
||||
let local: Vec<u64> = ids.iter().filter(|id| app.session.catalog.photo(**id).is_some_and(|p| p.local)).map(|id| id.0).collect();
|
||||
let n = local.len();
|
||||
match app.run("photo.addToLibrary", json!({"ids": local})) {
|
||||
Ok(_) => app.toast(ui.ctx(), format!("Added {n} photo{} to My Photos", if n == 1 { "" } else { "s" })),
|
||||
Ok(_) => app.toast(ui.ctx(), crate::i18n::tr_format!("Added {n} photo{} to My Photos", if n == 1 { "" } else { "s" }, n = n)),
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sub = b.subfolders;
|
||||
let c = ui.checkbox(&mut sub, "Include subfolders");
|
||||
let c = ui.checkbox(&mut sub, crate::i18n::tr("Include subfolders"));
|
||||
register(ui.ctx(), "check:includeSubfolders", c.rect);
|
||||
if c.changed() {
|
||||
let _ = app.run("library.browse", json!({"path": b.path, "subfolders": sub}));
|
||||
|
|
@ -690,7 +698,11 @@ pub fn drag_feedback(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
egui::Area::new(egui::Id::new("drag-photos")).order(egui::Order::Tooltip).interactable(false).fixed_pos(pos + vec2(14.0, 10.0)).show(ctx, |ui| {
|
||||
egui::Frame::NONE.fill(t.accent).corner_radius(10.0).inner_margin(egui::Margin::symmetric(9, 3)).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new(format!("{n} photo{}", if n == 1 { "" } else { "s" })).color(Color32::WHITE).font(t.semibold(12.0)));
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::tr_format!("{n} photo{}", if n == 1 { "" } else { "s" }, n = n))
|
||||
.color(Color32::WHITE)
|
||||
.font(t.semibold(12.0)),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -712,11 +724,11 @@ pub fn label_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.selectable_label(current.is_none(), "None").clicked() {
|
||||
if ui.selectable_label(current.is_none(), crate::i18n::tr("None")).clicked() {
|
||||
let _ = app.run("photo.label", json!({"label": "none"}));
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Edit Label Names…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Edit Label Names…")).clicked() {
|
||||
let _ = app.run("dialog.labelNames", json!({}));
|
||||
}
|
||||
}
|
||||
|
|
@ -755,10 +767,10 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if !app.session.selection.contains(id) {
|
||||
let _ = app.run("library.select", json!({"ids": [id.0]}));
|
||||
}
|
||||
if ui.button("Open in Detail").clicked() {
|
||||
if ui.button(crate::i18n::tr("Open in Detail")).clicked() {
|
||||
let _ = app.run("view.detail", json!({}));
|
||||
}
|
||||
if ui.button("Find Similar Photos").clicked() {
|
||||
if ui.button(crate::i18n::tr("Find Similar Photos")).clicked() {
|
||||
match app.run("library.findSimilar", json!({"id": id.0})) {
|
||||
Ok(r) => {
|
||||
let n = r["photos"].as_array().map_or(0, Vec::len);
|
||||
|
|
@ -768,26 +780,26 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
if ui.button("Set as Reference Photo").clicked() {
|
||||
if ui.button(crate::i18n::tr("Set as Reference Photo")).clicked() {
|
||||
let _ = app.run("photo.setReference", json!({"id": id.0}));
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Set Rating", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Set Rating"), |ui| {
|
||||
for r in 0..=5 {
|
||||
if ui.button(if r == 0 { "No Stars".to_string() } else { "★".repeat(r) }).clicked() {
|
||||
let _ = app.run("photo.rate", json!({"rating": r}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button("Set Flag", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Set Flag"), |ui| {
|
||||
for (l, f) in [("Pick", "pick"), ("Reject", "reject"), ("Unflagged", "none")] {
|
||||
if ui.button(l).clicked() {
|
||||
let _ = app.run("photo.flag", json!({"flag": f}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button("Set Color Label", |ui| label_menu(app, ui));
|
||||
ui.menu_button("Add to Album", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Set Color Label"), |ui| label_menu(app, ui));
|
||||
ui.menu_button(crate::i18n::tr("Add to Album"), |ui| {
|
||||
let albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.id.0, a.name.clone())).collect();
|
||||
for (aid, name) in albums {
|
||||
if ui.button(name).clicked() {
|
||||
|
|
@ -799,23 +811,23 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if let lightcraft_engine::LibrarySource::Album(aid) = app.session.source
|
||||
&& app.session.catalog.album(aid).is_some_and(|a| !a.folder && !a.is_smart())
|
||||
{
|
||||
if ui.button("Remove from Album").clicked() {
|
||||
if ui.button(crate::i18n::tr("Remove from Album")).clicked() {
|
||||
let _ = app.run("album.removePhotos", json!({"id": aid.0}));
|
||||
}
|
||||
if ui.button("Set as Album Cover").clicked() {
|
||||
if ui.button(crate::i18n::tr("Set as Album Cover")).clicked() {
|
||||
let _ = app.run("album.setCover", json!({"id": aid.0, "photo": id.0}));
|
||||
}
|
||||
}
|
||||
if ui.button("Rename…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
let _ = app.run("dialog.rename", json!({}));
|
||||
}
|
||||
if ui.button("Create Virtual Copy").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Virtual Copy")).clicked() {
|
||||
let _ = app.run("photo.virtualCopy", json!({}));
|
||||
}
|
||||
if ui.button("Create Version").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Version")).clicked() {
|
||||
let _ = app.run("version.create", json!({}));
|
||||
}
|
||||
ui.menu_button("Stack", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Stack"), |ui| {
|
||||
let stacked = app.session.catalog.stack_of(id).is_some();
|
||||
let several = app.session.selection.ids.len() > 1;
|
||||
for (label, cmd, on) in [
|
||||
|
|
@ -833,24 +845,24 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Auto-Stack by Capture Time…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Auto-Stack by Capture Time…")).clicked() {
|
||||
let _ = app.run("dialog.autoStack", json!({}));
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui.button("Copy Edit Settings").clicked() {
|
||||
if ui.button(crate::i18n::tr("Copy Edit Settings")).clicked() {
|
||||
let _ = app.run("develop.copy", json!({}));
|
||||
}
|
||||
if ui.add_enabled(app.session.clipboard.is_some(), egui::Button::new("Paste Edit Settings")).clicked() {
|
||||
if ui.add_enabled(app.session.clipboard.is_some(), egui::Button::new(crate::i18n::tr("Paste Edit Settings"))).clicked() {
|
||||
let _ = app.run("develop.paste", json!({}));
|
||||
}
|
||||
if ui.add_enabled(app.session.clipboard.is_some(), egui::Button::new("Paste Selected Settings…")).clicked() {
|
||||
if ui.add_enabled(app.session.clipboard.is_some(), egui::Button::new(crate::i18n::tr("Paste Selected Settings…"))).clicked() {
|
||||
let _ = app.run("dialog.pasteSettings", json!({}));
|
||||
}
|
||||
if ui.button("Reset Edits").clicked() {
|
||||
if ui.button(crate::i18n::tr("Reset Edits")).clicked() {
|
||||
let _ = app.run("develop.reset", json!({}));
|
||||
}
|
||||
ui.menu_button("Photo Merge", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Photo Merge"), |ui| {
|
||||
let n = app.session.targets(&json!({})).len();
|
||||
for (id, label) in [("dialog.mergeHdr", "HDR…"), ("dialog.mergePanorama", "Panorama…"), ("dialog.mergeHdrPanorama", "HDR Panorama…")] {
|
||||
if ui.add_enabled(n >= 2, egui::Button::new(label)).clicked() {
|
||||
|
|
@ -859,33 +871,33 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui.button("Rotate Left").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rotate Left")).clicked() {
|
||||
let _ = app.run("photo.rotateLeft", json!({}));
|
||||
}
|
||||
if ui.button("Rotate Right").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rotate Right")).clicked() {
|
||||
let _ = app.run("photo.rotateRight", json!({}));
|
||||
}
|
||||
ui.separator();
|
||||
// the original moved or its drive is gone: point the photo at the file again
|
||||
// (a cached answer, checked off the UI thread)
|
||||
let missing = matches!(&app.session.catalog.photo(id).map(|p| p.source.clone()), Some(lightcraft_catalog::Source::File { path }) if app.session.media.availability.is_offline(path));
|
||||
if missing && ui.button("Locate Missing File…").clicked() {
|
||||
if missing && ui.button(crate::i18n::tr("Locate Missing File…")).clicked() {
|
||||
let _ = app.run("photo.locate", json!({}));
|
||||
}
|
||||
if ui.add_enabled(crate::menus::ui_enabled(app, "app.showInFinder"), egui::Button::new("Show in Finder")).clicked() {
|
||||
if ui.add_enabled(crate::menus::ui_enabled(app, "app.showInFinder"), egui::Button::new(crate::i18n::tr("Show in Finder"))).clicked() {
|
||||
let _ = app.run("app.showInFinder", json!({}));
|
||||
}
|
||||
if ui.button("Export…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Export…")).clicked() {
|
||||
let _ = app.run("dialog.export", json!({}));
|
||||
}
|
||||
ui.menu_button("Export with Preset", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Export with Preset"), |ui| {
|
||||
for (p, _) in app.session.all_export_presets() {
|
||||
if ui.button(&p.name).clicked() {
|
||||
match app.run("app.export", json!({"preset": p.name, "background": true})) {
|
||||
Ok(r) if r.get("background").is_some() => {}
|
||||
Ok(r) => {
|
||||
let n = r["files"].as_array().map_or(0, Vec::len);
|
||||
app.toast(ui.ctx(), format!("Exported {n} photo{}", if n == 1 { "" } else { "s" }));
|
||||
app.toast(ui.ctx(), crate::i18n::tr_format!("Exported {n} photo{}", if n == 1 { "" } else { "s" }, n = n));
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
|
|
@ -893,7 +905,7 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui.button("Delete Photo").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Photo")).clicked() {
|
||||
let _ = app.run("photo.delete", json!({}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ fn row(
|
|||
selected: bool,
|
||||
indent: f32,
|
||||
) -> egui::Response {
|
||||
let label = if matches!(id, "all" | "recentlyAdded" | "picks" | "missing" | "recentlyDeleted") { crate::i18n::tr(label) } else { label };
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 29.0), Sense::click());
|
||||
register(ui.ctx(), format!("source:{id}"), r);
|
||||
let name = match count {
|
||||
Some(n) => format!("{label}, {n} photos"),
|
||||
Some(n) => crate::i18n::tr_format!("{label}, {n} photos", label = label, n = n),
|
||||
None => label.to_string(),
|
||||
};
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, selected, &name));
|
||||
|
|
@ -61,7 +62,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let resized = super::resizable_side(ui, true, "left_panel", frame, width, crate::state::LEFT_WIDTH, 0.0, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 44.0), Sense::hover());
|
||||
ui.painter().text(pos2(hr.left() + 18.0, hr.center().y), Align2::LEFT_CENTER, "My Photos", t.semibold(15.0), t.text);
|
||||
ui.painter().text(pos2(hr.left() + 18.0, hr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("My Photos"), t.semibold(15.0), t.text);
|
||||
let counts = app.caches.counts(&app.session.catalog);
|
||||
let (total, picks, deleted) = (counts.total, counts.picks, counts.deleted);
|
||||
egui::ScrollArea::vertical().id_salt("left-scroll").auto_shrink([false, false]).show(ui, |ui| {
|
||||
|
|
@ -85,7 +86,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
ui.add_space(10.0);
|
||||
// Albums header
|
||||
let (ar, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
|
||||
ui.painter().text(pos2(ar.left() + 18.0, ar.center().y), Align2::LEFT_CENTER, "Albums", t.semibold(13.5), t.text_label);
|
||||
ui.painter().text(pos2(ar.left() + 18.0, ar.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Albums"), t.semibold(13.5), t.text_label);
|
||||
let mut hdr = ui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(ar.right() - 50.0, ar.top()), ar.right_bottom()))
|
||||
|
|
@ -93,20 +94,20 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
);
|
||||
let plus = icon_button(&mut hdr, "albumNew", Icon::Plus, vec2(26.0, 26.0), false, true, "Create Album");
|
||||
egui::Popup::menu(&plus).show(|ui| {
|
||||
if ui.button("Create Album…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Album…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: false });
|
||||
}
|
||||
if ui.button("Create Smart Album…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Smart Album…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::SmartRules {
|
||||
id: None,
|
||||
name: String::new(),
|
||||
rules: lightcraft_catalog::RuleSet { rules: vec![crate::panels::rules_editor::new_rule()], ..Default::default() },
|
||||
});
|
||||
}
|
||||
if ui.button("Create Smart Album from Filter…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Smart Album from Filter…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::NewSmartAlbum { name: String::new() });
|
||||
}
|
||||
if ui.button("Create Folder…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Folder…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: true });
|
||||
}
|
||||
});
|
||||
|
|
@ -116,7 +117,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
local_section(app, ui);
|
||||
// By date
|
||||
let (dr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
|
||||
ui.painter().text(pos2(dr.left() + 18.0, dr.center().y), Align2::LEFT_CENTER, "By Date", t.semibold(13.5), t.text_label);
|
||||
ui.painter().text(pos2(dr.left() + 18.0, dr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("By Date"), t.semibold(13.5), t.text_label);
|
||||
for g in app.caches.date_groups(&app.session.catalog).iter() {
|
||||
// year → month → day; a click filters by that prefix, the triangle opens a level
|
||||
if date_row(app, ui, &g.year, &g.year, g.count, 0.0) {
|
||||
|
|
@ -193,7 +194,7 @@ fn local_section(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (lr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
|
||||
ui.painter().text(pos2(lr.left() + 18.0, lr.center().y), Align2::LEFT_CENTER, "Local", t.semibold(13.5), t.text_label);
|
||||
ui.painter().text(pos2(lr.left() + 18.0, lr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Local"), t.semibold(13.5), t.text_label);
|
||||
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).unwrap_or_default();
|
||||
let mut builtin: Vec<(String, String)> = Vec::new();
|
||||
if !home.is_empty() {
|
||||
|
|
@ -232,9 +233,9 @@ fn local_section(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
let hidden = app.ui.hidden_locations.len();
|
||||
if hidden > 0 {
|
||||
let label = format!("Show {hidden} hidden location{}", if hidden == 1 { "" } else { "s" });
|
||||
let label = crate::i18n::tr_format!("Show {hidden} hidden location{}", if hidden == 1 { "" } else { "s" }, hidden = hidden);
|
||||
if row(app, ui, "local:restoreHidden", Icon::Folder, &label, None, false, 0.0)
|
||||
.on_hover_text("Put the locations you removed from Local back (no files change)")
|
||||
.on_hover_text(crate::i18n::tr("Put the locations you removed from Local back (no files change)"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("local.restoreHidden", json!({}));
|
||||
|
|
@ -432,20 +433,26 @@ fn folder_tree(
|
|||
}
|
||||
resp.context_menu(|ui| {
|
||||
if (transient || indent > 0.0)
|
||||
&& ui.button(if transient { "Keep in Local" } else { "Add to Local" }).on_hover_text("List this folder in Local from now on").clicked()
|
||||
&& ui
|
||||
.button(if transient { "Keep in Local" } else { "Add to Local" })
|
||||
.on_hover_text(crate::i18n::tr("List this folder in Local from now on"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("local.addRoot", json!({"path": path}));
|
||||
ui.close();
|
||||
}
|
||||
if indent == 0.0
|
||||
&& ui.button("Remove from Local").on_hover_text("Hides this shortcut only; the folder and its photos stay as they are").clicked()
|
||||
&& ui
|
||||
.button(crate::i18n::tr("Remove from Local"))
|
||||
.on_hover_text(crate::i18n::tr("Hides this shortcut only; the folder and its photos stay as they are"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("local.hide", json!({"path": path}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Rename Folder…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename Folder…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::TextPrompt {
|
||||
title: format!("Rename “{name}”"),
|
||||
title: crate::i18n::tr_format!("Rename “{name}”", name = name),
|
||||
hint: "Folder name (renamed on disk; its photos follow)".into(),
|
||||
value: name.to_string(),
|
||||
command: "folder.rename".into(),
|
||||
|
|
@ -453,7 +460,7 @@ fn folder_tree(
|
|||
key: "name".into(),
|
||||
});
|
||||
}
|
||||
if app.services.pick_folder.is_some() && ui.button("Move Folder To…").clicked() {
|
||||
if app.services.pick_folder.is_some() && ui.button(crate::i18n::tr("Move Folder To…")).clicked() {
|
||||
let into = app.services.pick_folder.as_mut().and_then(|f| f());
|
||||
if let Some(into) = into {
|
||||
match app.run("folder.move", json!({"path": path, "into": into})) {
|
||||
|
|
@ -463,7 +470,7 @@ fn folder_tree(
|
|||
}
|
||||
}
|
||||
if app.services.reveal.is_some()
|
||||
&& ui.button("Show in Finder").clicked()
|
||||
&& ui.button(crate::i18n::tr("Show in Finder")).clicked()
|
||||
&& let Some(f) = app.services.reveal.as_mut()
|
||||
{
|
||||
let _ = f(path);
|
||||
|
|
@ -537,7 +544,7 @@ fn albums_tree(app: &mut LightcraftApp, ui: &mut egui::Ui, all: &[Album], parent
|
|||
drop_target(app, ui, &resp, a);
|
||||
}
|
||||
if let Some(rules) = &a.smart {
|
||||
resp = resp.on_hover_text(format!("Smart album: {}", rules.describe()));
|
||||
resp = resp.on_hover_text(crate::i18n::tr_format!("Smart album: {}", rules.describe()));
|
||||
}
|
||||
if resp.clicked() {
|
||||
let _ = app.run("library.source", json!({"kind": "album", "id": a.id.0}));
|
||||
|
|
@ -560,7 +567,7 @@ fn drop_target(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response
|
|||
if ui.input(|i| i.pointer.any_released()) {
|
||||
let n = ids.len();
|
||||
match app.run("album.addPhotos", json!({"id": a.id.0, "ids": ids})) {
|
||||
Ok(_) => app.toast(ui.ctx(), format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, a.name)),
|
||||
Ok(_) => app.toast(ui.ctx(), crate::i18n::tr_format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, a.name, n = n)),
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
app.ui.dragging_photos = None;
|
||||
|
|
@ -569,27 +576,27 @@ fn drop_target(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response
|
|||
|
||||
fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
resp.context_menu(|ui| {
|
||||
if !a.folder && !a.is_smart() && ui.button("Add Selected Photos").clicked() {
|
||||
if !a.folder && !a.is_smart() && ui.button(crate::i18n::tr("Add Selected Photos")).clicked() {
|
||||
let _ = app.run("album.addPhotos", json!({"id": a.id.0}));
|
||||
}
|
||||
if !a.folder && !a.is_smart() {
|
||||
let is_target = app.session.target_album == Some(a.id) || (app.session.target_album.is_none() && a.quick);
|
||||
if !is_target && ui.button("Set as Target Album (B adds to it)").clicked() {
|
||||
if !is_target && ui.button(crate::i18n::tr("Set as Target Album (B adds to it)")).clicked() {
|
||||
let _ = app.run("album.setTarget", json!({"id": if a.quick { serde_json::Value::Null } else { json!(a.id.0) }}));
|
||||
}
|
||||
if is_target && !a.quick && ui.button("Stop Using as Target Album").clicked() {
|
||||
if is_target && !a.quick && ui.button(crate::i18n::tr("Stop Using as Target Album")).clicked() {
|
||||
let _ = app.run("album.setTarget", json!({"id": null}));
|
||||
}
|
||||
}
|
||||
if a.quick && ui.button("Clear Quick Collection").clicked() {
|
||||
if a.quick && ui.button(crate::i18n::tr("Clear Quick Collection")).clicked() {
|
||||
let _ = app.run("album.clearQuick", json!({}));
|
||||
}
|
||||
if a.is_smart() && ui.button("Edit Smart Album…").clicked() {
|
||||
if a.is_smart() && ui.button(crate::i18n::tr("Edit Smart Album…")).clicked() {
|
||||
// older smart albums keep their filter fields; the editor works on the rule set
|
||||
let rules = a.smart.as_ref().and_then(|f| f.rule_set.clone()).unwrap_or_default();
|
||||
app.ui.dialog = Some(crate::state::Dialog::SmartRules { id: Some(a.id.0), name: a.name.clone(), rules });
|
||||
}
|
||||
if a.is_smart() && ui.button("Update Rules from Current Filter").clicked() {
|
||||
if a.is_smart() && ui.button(crate::i18n::tr("Update Rules from Current Filter")).clicked() {
|
||||
let _ = app.run("album.setRules", json!({"id": a.id.0, "fromView": true}));
|
||||
}
|
||||
if !a.folder {
|
||||
|
|
@ -599,12 +606,12 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
|||
let _ = app.run("library.selectAll", json!({}));
|
||||
};
|
||||
let has_photos = app.session.catalog.album_count(a.id) > 0;
|
||||
if ui.add_enabled(has_photos, egui::Button::new("Export Album…")).clicked() {
|
||||
if ui.add_enabled(has_photos, egui::Button::new(crate::i18n::tr("Export Album…"))).clicked() {
|
||||
show_all(app);
|
||||
let _ = app.run("dialog.export", json!({}));
|
||||
}
|
||||
ui.add_enabled_ui(has_photos, |ui| {
|
||||
ui.menu_button("Export Album with Preset", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Export Album with Preset"), |ui| {
|
||||
for (p, _) in app.session.all_export_presets() {
|
||||
if ui.button(&p.name).clicked() {
|
||||
show_all(app);
|
||||
|
|
@ -621,8 +628,8 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
|||
let mut folders: Vec<(u64, String)> =
|
||||
app.session.catalog.albums().filter(|f| f.folder && !is_within(app, f.id, a.id)).map(|f| (f.id.0, f.name.clone())).collect();
|
||||
folders.sort_by_key(|(_, n)| n.to_lowercase());
|
||||
ui.menu_button("Move to", |ui| {
|
||||
if ui.add_enabled(a.parent.is_some(), egui::Button::new("Top Level")).clicked() {
|
||||
ui.menu_button(crate::i18n::tr("Move to"), |ui| {
|
||||
if ui.add_enabled(a.parent.is_some(), egui::Button::new(crate::i18n::tr("Top Level"))).clicked() {
|
||||
let _ = app.run("album.move", json!({"id": a.id.0, "parent": null}));
|
||||
}
|
||||
for (fid, name) in &folders {
|
||||
|
|
@ -631,10 +638,10 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
|||
}
|
||||
}
|
||||
});
|
||||
if ui.button("Rename…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::RenameAlbum { id: a.id.0, name: a.name.clone() });
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete")).clicked() {
|
||||
let _ = app.run("album.delete", json!({"id": a.id.0}));
|
||||
}
|
||||
});
|
||||
|
|
@ -668,7 +675,7 @@ fn keywords_section(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
ui.add_space(10.0);
|
||||
let (kr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
|
||||
ui.painter().text(pos2(kr.left() + 18.0, kr.center().y), Align2::LEFT_CENTER, "Keywords", t.semibold(13.5), t.text_label);
|
||||
ui.painter().text(pos2(kr.left() + 18.0, kr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Keywords"), t.semibold(13.5), t.text_label);
|
||||
keyword_rows(app, ui, &tree, 0.0);
|
||||
}
|
||||
|
||||
|
|
@ -704,20 +711,20 @@ fn keyword_rows(app: &mut LightcraftApp, ui: &mut egui::Ui, nodes: &[KeywordNode
|
|||
}
|
||||
resp.context_menu(|ui| {
|
||||
let has_sel = app.session.active().is_some();
|
||||
if ui.add_enabled(has_sel, egui::Button::new("Add to Selected Photos")).clicked() {
|
||||
if ui.add_enabled(has_sel, egui::Button::new(crate::i18n::tr("Add to Selected Photos"))).clicked() {
|
||||
let _ = app.run("photo.setMeta", json!({"addKeywords": [n.path]}));
|
||||
}
|
||||
if ui.add_enabled(has_sel, egui::Button::new("Remove from Selected Photos")).clicked() {
|
||||
if ui.add_enabled(has_sel, egui::Button::new(crate::i18n::tr("Remove from Selected Photos"))).clicked() {
|
||||
let _ = app.run("photo.setMeta", json!({"removeKeywords": [n.path]}));
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Rename Keyword…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename Keyword…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: n.path.clone(), to: n.path.clone() });
|
||||
}
|
||||
if ui.button("Merge into…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Merge into…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::MergeKeywords { from: vec![n.path.clone()], into: String::new() });
|
||||
}
|
||||
if ui.button("Delete Keyword").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Keyword")).clicked() {
|
||||
let _ = app.run("keyword.delete", json!({"keyword": n.path}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
});
|
||||
let mut choice = None;
|
||||
let frame = egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::symmetric(18, 14));
|
||||
egui::Window::new("Your library couldn't be opened")
|
||||
egui::Window::new(crate::i18n::tr("Your library couldn't be opened"))
|
||||
.id(egui::Id::new("library-problem"))
|
||||
.order(egui::Order::Foreground)
|
||||
.collapsible(false)
|
||||
|
|
@ -107,9 +107,9 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.set_width(440.0);
|
||||
ui.spacing_mut().item_spacing.y = 8.0;
|
||||
if problem.path.is_empty() {
|
||||
ui.label(RichText::new("LightCraft couldn't find where to keep your library.").color(t.text));
|
||||
ui.label(RichText::new(crate::i18n::tr("LightCraft couldn't find where to keep your library.")).color(t.text));
|
||||
} else {
|
||||
ui.label(RichText::new("LightCraft couldn't open the library at").color(t.text_label));
|
||||
ui.label(RichText::new(crate::i18n::tr("LightCraft couldn't open the library at")).color(t.text_label));
|
||||
ui.label(RichText::new(&problem.path).font(t.semibold(12.5)).color(t.text));
|
||||
}
|
||||
ui.add(egui::Label::new(RichText::new(&problem.error).color(t.caution)).wrap());
|
||||
|
|
@ -124,7 +124,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
let mut button = |ui: &mut egui::Ui, id: &str, label: &str, enabled: bool, c: Choice| {
|
||||
let r = ui.add_enabled(enabled, egui::Button::new(label).min_size(vec2(0.0, 26.0)));
|
||||
let r = ui.add_enabled(enabled, egui::Button::new(crate::i18n::tr(label)).min_size(vec2(0.0, 26.0)));
|
||||
register(ui.ctx(), format!("button:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
choice = Some(c);
|
||||
|
|
@ -166,12 +166,14 @@ pub fn banner(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
.show(ui, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
let r = ui.label(
|
||||
RichText::new("Temporary session — your library isn't open, and nothing you do here is saved.")
|
||||
RichText::new(crate::i18n::tr("Temporary session — your library isn't open, and nothing you do here is saved."))
|
||||
.font(t.semibold(12.5))
|
||||
.color(t.canvas),
|
||||
);
|
||||
register(ui.ctx(), "indicator:temporarySession", r.rect);
|
||||
let b = ui.add(egui::Button::new(RichText::new("Open Library…").color(t.canvas)).fill(egui::Color32::from_black_alpha(40)));
|
||||
let b = ui.add(
|
||||
egui::Button::new(RichText::new(crate::i18n::tr("Open Library…")).color(t.canvas)).fill(egui::Color32::from_black_alpha(40)),
|
||||
);
|
||||
register(ui.ctx(), "button:libraryReopen", b.rect);
|
||||
reopen = b.clicked();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let d = app.session.develop_of(id).unwrap_or_default();
|
||||
header(ui, "Masking");
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 0, bottom: 10 }).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("Create New Mask").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Create New Mask")).color(t.text_dim));
|
||||
ui.add_space(6.0);
|
||||
let tiles: [(&str, &str, Icon); 8] = [
|
||||
("subject", "Subject", Icon::Subject),
|
||||
|
|
@ -123,7 +123,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
// mask list
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 16, right: 16, top: 8, bottom: 8 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Masks").font(t.semibold(13.0)).color(t.text_label));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Masks")).font(t.semibold(13.0)).color(t.text_label));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let on = app.ui.mask_overlay;
|
||||
if icon_button(ui, "maskOverlay", Icon::Eye, vec2(24.0, 24.0), on, true, "Show overlay (O)").clicked() {
|
||||
|
|
@ -132,7 +132,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
});
|
||||
});
|
||||
if d.masks.is_empty() {
|
||||
ui.label(egui::RichText::new("No masks yet. Choose a mask type above.").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("No masks yet. Choose a mask type above.")).color(t.text_dim));
|
||||
}
|
||||
let count = d.masks.len();
|
||||
for (index, m) in d.masks.iter().enumerate() {
|
||||
|
|
@ -254,7 +254,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
resp.context_menu(|ui| component_row_menu(app, ui, m.id, i, &label, m.components.len()));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let more = ui.small_button("…").on_hover_text("Component options");
|
||||
let more = ui.small_button("…").on_hover_text(crate::i18n::tr("Component options"));
|
||||
register(ui.ctx(), format!("button:componentMenu{i}"), more.rect);
|
||||
egui::Popup::menu(&more).show(|ui| component_row_menu(app, ui, m.id, i, &label, m.components.len()));
|
||||
});
|
||||
|
|
@ -265,13 +265,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
// wraps: the five actions are wider than a narrow panel
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 6.0;
|
||||
let add = text_button(ui, "maskAddComp", "Add", false);
|
||||
let add = text_button(ui, "maskAddComp", crate::i18n::tr("Add"), false);
|
||||
egui::Popup::menu(&add).show(|ui| component_menu(app, ui, "add"));
|
||||
let sub = text_button(ui, "maskSubComp", "Subtract", false);
|
||||
let sub = text_button(ui, "maskSubComp", crate::i18n::tr("Subtract"), false);
|
||||
egui::Popup::menu(&sub).show(|ui| component_menu(app, ui, "subtract"));
|
||||
let int = text_button(ui, "maskIntComp", "Intersect", false);
|
||||
let int = text_button(ui, "maskIntComp", crate::i18n::tr("Intersect"), false);
|
||||
egui::Popup::menu(&int).show(|ui| component_menu(app, ui, "intersect"));
|
||||
if text_button(ui, "maskInvert", "Invert", m.invert).clicked() {
|
||||
if text_button(ui, "maskInvert", crate::i18n::tr("Invert"), m.invert).clicked() {
|
||||
let _ = app.run("mask.invert", json!({}));
|
||||
}
|
||||
if icon_button(ui, "maskDelete", Icon::Trash, vec2(26.0, 24.0), false, true, "Delete mask").clicked() {
|
||||
|
|
@ -335,11 +335,11 @@ fn mask_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: u32, name: &str, vi
|
|||
run(ui, "Move Up", index > 0, "mask.move", json!({"id": id, "delta": -1}));
|
||||
run(ui, "Move Down", index + 1 < count, "mask.move", json!({"id": id, "delta": 1}));
|
||||
ui.separator();
|
||||
if ui.button("Rename…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
app.ui.renaming_mask = Some((id, name.to_string()));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Delete Mask").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Mask")).clicked() {
|
||||
let _ = app.run("mask.delete", json!({"id": id}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -354,7 +354,7 @@ fn overlay_options(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let view = MaskView::parse(&app.ui.mask_overlay_mode).unwrap_or_default();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Overlay").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Overlay")).color(t.text_dim));
|
||||
let r = crate::widgets::dropdown(ui, "maskOverlayMode", view.label(), t.font(12.5), t.text_label);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
for v in MaskView::ALL {
|
||||
|
|
@ -396,7 +396,7 @@ fn overlay_options(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
app.ui.mask_overlay_opacity = v as f32;
|
||||
}
|
||||
let mut pins = app.ui.mask_pins;
|
||||
if ui.checkbox(&mut pins, "Show Pins").changed() {
|
||||
if ui.checkbox(&mut pins, crate::i18n::tr("Show Pins")).changed() {
|
||||
let _ = app.run("view.maskPins", json!({"show": pins}));
|
||||
}
|
||||
}
|
||||
|
|
@ -410,7 +410,7 @@ fn range_controls(app: &mut LightcraftApp, ui: &mut egui::Ui, comp: usize, shape
|
|||
};
|
||||
match shape {
|
||||
MaskShape::LuminanceRange { lo, hi, lo_feather, hi_feather } => {
|
||||
ui.label(egui::RichText::new("Select Luminance Range").color(t.text_dim).size(11.5));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Select Luminance Range")).color(t.text_dim).size(11.5));
|
||||
let (lo, hi) = (*lo, *hi);
|
||||
let w = ui.available_width().min(240.0);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(w, 18.0), Sense::click_and_drag());
|
||||
|
|
@ -471,7 +471,7 @@ fn range_controls(app: &mut LightcraftApp, ui: &mut egui::Ui, comp: usize, shape
|
|||
app.run("mask.update", json!({"component": comp, "shape": MaskShape::LuminanceRange { lo, hi, lo_feather: f, hi_feather: f }}))
|
||||
});
|
||||
let mut map = app.ui.mask_overlay && app.ui.mask_overlay_mode == "colorOnBw";
|
||||
let r = ui.checkbox(&mut map, "Show Luminance Map");
|
||||
let r = ui.checkbox(&mut map, crate::i18n::tr("Show Luminance Map"));
|
||||
register(ui.ctx(), format!("check:lumMap{comp}"), r.rect);
|
||||
if r.changed() {
|
||||
// the luminance map: the photo in black & white with the selected range tinted
|
||||
|
|
@ -494,8 +494,8 @@ fn range_controls(app: &mut LightcraftApp, ui: &mut egui::Ui, comp: usize, shape
|
|||
.size(11.5),
|
||||
);
|
||||
let picking = app.ui.tool == "colorRange";
|
||||
if text_button(ui, &format!("colorPick{comp}"), "Pick", picking)
|
||||
.on_hover_text("Click the photo to pick a colour; ⇧-click adds more (up to 5)")
|
||||
if text_button(ui, &format!("colorPick{comp}"), crate::i18n::tr("Pick"), picking)
|
||||
.on_hover_text(crate::i18n::tr("Click the photo to pick a colour; ⇧-click adds more (up to 5)"))
|
||||
.clicked()
|
||||
{
|
||||
app.ui.tool = if picking { String::new() } else { "colorRange".into() };
|
||||
|
|
@ -534,22 +534,26 @@ fn component_row_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, mask: u32, k:
|
|||
}
|
||||
};
|
||||
run(ui, "Invert", json!({"action": "invert"}));
|
||||
run(ui, &format!("Duplicate \"{label}\""), json!({"action": "duplicate"}));
|
||||
run(ui, &crate::i18n::tr_format!("Duplicate \"{label}\"", label = label), json!({"action": "duplicate"}));
|
||||
if k > 0 {
|
||||
ui.menu_button("Mode", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Mode"), |ui| {
|
||||
for (op, text) in [("add", "Add"), ("subtract", "Subtract"), ("intersect", "Intersect")] {
|
||||
run(ui, text, json!({"action": "op", "op": op}));
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.menu_button("Intersect with", |ui| component_menu(app, ui, "intersect"));
|
||||
ui.menu_button("Subtract", |ui| component_menu(app, ui, "subtract"));
|
||||
ui.menu_button(crate::i18n::tr("Intersect with"), |ui| component_menu(app, ui, "intersect"));
|
||||
ui.menu_button(crate::i18n::tr("Subtract"), |ui| component_menu(app, ui, "subtract"));
|
||||
ui.separator();
|
||||
if ui.button("Rename…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
app.ui.renaming_component = Some((mask, k, label.to_string()));
|
||||
ui.close();
|
||||
}
|
||||
let del = if count == 1 { format!("Delete \"{label}\" (and the mask)") } else { format!("Delete \"{label}\"") };
|
||||
let del = if count == 1 {
|
||||
crate::i18n::tr_format!("Delete \"{label}\" (and the mask)", label = label)
|
||||
} else {
|
||||
crate::i18n::tr_format!("Delete \"{label}\"", label = label)
|
||||
};
|
||||
if ui.button(del).clicked() {
|
||||
let _ = app.run("mask.component", json!({"id": mask, "component": k, "action": "delete"}));
|
||||
ui.close();
|
||||
|
|
@ -584,16 +588,20 @@ fn brush_settings(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 6, bottom: 0 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Brush").font(t.semibold(13.0)));
|
||||
if text_button(ui, "brushAdd", "Add", !app.ui.brush_erase).clicked() {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Brush")).font(t.semibold(13.0)));
|
||||
if text_button(ui, "brushAdd", crate::i18n::tr("Add"), !app.ui.brush_erase).clicked() {
|
||||
app.ui.brush_erase = false;
|
||||
}
|
||||
if text_button(ui, "brushErase", "Erase", app.ui.brush_erase).clicked() {
|
||||
if text_button(ui, "brushErase", crate::i18n::tr("Erase"), app.ui.brush_erase).clicked() {
|
||||
app.ui.brush_erase = true;
|
||||
}
|
||||
});
|
||||
let mut auto = app.ui.brush_auto_mask;
|
||||
if ui.checkbox(&mut auto, "Auto Mask").on_hover_text("Paint only areas like the one under the brush").changed() {
|
||||
if ui
|
||||
.checkbox(&mut auto, crate::i18n::tr("Auto Mask"))
|
||||
.on_hover_text(crate::i18n::tr("Paint only areas like the one under the brush"))
|
||||
.changed()
|
||||
{
|
||||
app.ui.brush_auto_mask = auto;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,6 +85,6 @@ pub fn toast(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
pub fn empty_message(ui: &egui::Ui, rect: Rect, title: &str, body: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let p = ui.painter();
|
||||
p.text(rect.center() - vec2(0.0, 12.0), Align2::CENTER_CENTER, title, t.semibold(18.0), t.text_label);
|
||||
p.text(rect.center() + vec2(0.0, 14.0), Align2::CENTER_CENTER, body, t.font(13.0), t.text_dim);
|
||||
p.text(rect.center() - vec2(0.0, 12.0), Align2::CENTER_CENTER, crate::i18n::tr(title), t.semibold(18.0), t.text_label);
|
||||
p.text(rect.center() + vec2(0.0, 14.0), Align2::CENTER_CENTER, crate::i18n::tr(body), t.font(13.0), t.text_dim);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::hover());
|
||||
ui.painter().text(pos2(hr.left() + 24.0, hr.center().y + 2.0), Align2::LEFT_CENTER, "Presets", t.semibold(15.0), t.text);
|
||||
ui.painter().text(pos2(hr.left() + 24.0, hr.center().y + 2.0), Align2::LEFT_CENTER, crate::i18n::tr("Presets"), t.semibold(15.0), t.text);
|
||||
let mut hdr = ui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(hr.right() - 80.0, hr.top()), hr.right_bottom()))
|
||||
|
|
@ -38,14 +38,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
let more = icon_button(&mut hdr, "presetMore", Icon::More, vec2(28.0, 28.0), false, true, "More preset options");
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
let r = ui.checkbox(&mut app.ui.preset_thumbs, "Show Thumbnails");
|
||||
let r = ui.checkbox(&mut app.ui.preset_thumbs, crate::i18n::tr("Show Thumbnails"));
|
||||
register(ui.ctx(), "presetMenu:thumbnails", r.rect);
|
||||
ui.separator();
|
||||
if ui.button("Import Presets…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Import Presets…")).clicked() {
|
||||
let _ = app.run("file.importPresets", json!({}));
|
||||
}
|
||||
let any_user = app.session.presets.iter().any(|p| !p.builtin);
|
||||
if ui.add_enabled(any_user, egui::Button::new("Export User Presets…")).clicked() {
|
||||
if ui.add_enabled(any_user, egui::Button::new(crate::i18n::tr("Export User Presets…"))).clicked() {
|
||||
let _ = app.run("file.exportPresets", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -99,7 +99,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
ui.data_mut(|d| d.insert_temp(open_id, !open));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Export Group…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Export Group…")).clicked() {
|
||||
let _ = app.run("file.exportPresets", json!({"group": g}));
|
||||
}
|
||||
});
|
||||
|
|
@ -141,12 +141,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
if resp.hovered()
|
||||
&& let Some(s) = look
|
||||
{
|
||||
app.hover_preview = Some(HoverPreview { label: format!("Preset: {name}"), settings: s });
|
||||
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Preset: {name}", name = name), settings: s });
|
||||
}
|
||||
if resp.clicked() {
|
||||
let _ = app.run("preset.apply", json!({"id": pid, "amount": 100}));
|
||||
ui.data_mut(|d| d.insert_temp(amt_id, (pid.clone(), 100.0)));
|
||||
app.toast(ui.ctx(), format!("Preset: {name}"));
|
||||
app.toast(ui.ctx(), crate::i18n::tr_format!("Preset: {name}", name = name));
|
||||
}
|
||||
let (builtin, group) = (pr.builtin, pr.group.clone());
|
||||
resp.context_menu(|ui| {
|
||||
|
|
@ -157,13 +157,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
return;
|
||||
}
|
||||
ui.separator();
|
||||
if ui.add_enabled(app.session.active().is_some(), egui::Button::new("Update with Current Settings")).clicked() {
|
||||
if ui
|
||||
.add_enabled(app.session.active().is_some(), egui::Button::new(crate::i18n::tr("Update with Current Settings")))
|
||||
.clicked()
|
||||
{
|
||||
match app.run("preset.update", json!({"id": pid})) {
|
||||
Ok(_) => app.toast(ui.ctx(), format!("Updated “{name}”")),
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
if ui.button("Rename…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
crate::panels::dialogs::prompt(
|
||||
app,
|
||||
"Rename Preset",
|
||||
|
|
@ -174,7 +177,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
"name",
|
||||
);
|
||||
}
|
||||
ui.menu_button("Move to Group", |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Move to Group"), |ui| {
|
||||
let mut groups: Vec<String> = app.session.presets.iter().filter(|p| !p.builtin).map(|p| p.group.clone()).collect();
|
||||
groups.sort();
|
||||
groups.dedup();
|
||||
|
|
@ -183,7 +186,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let _ = app.run("preset.move", json!({"id": pid, "group": g}));
|
||||
}
|
||||
}
|
||||
if ui.button("New Group…").clicked() {
|
||||
if ui.button(crate::i18n::tr("New Group…")).clicked() {
|
||||
crate::panels::dialogs::prompt(
|
||||
app,
|
||||
"Move Preset to New Group",
|
||||
|
|
@ -196,7 +199,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
});
|
||||
ui.separator();
|
||||
if ui.button("Delete Preset").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Preset")).clicked() {
|
||||
let _ = app.run("preset.delete", json!({"id": pid}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if icon_button(&mut back, "profilesBack", Icon::ChevronLeft, vec2(28.0, 28.0), false, true, "Back to Edit").clicked() {
|
||||
app.ui.right = RightPanel::Edit;
|
||||
}
|
||||
ui.painter().text(pos2(hr.left() + 46.0, hr.center().y + 2.0), Align2::LEFT_CENTER, "Profiles", t.semibold(15.0), t.text);
|
||||
ui.painter().text(pos2(hr.left() + 46.0, hr.center().y + 2.0), Align2::LEFT_CENTER, crate::i18n::tr("Profiles"), t.semibold(15.0), t.text);
|
||||
divider(ui);
|
||||
// the applied profile and its amount
|
||||
let (cr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
|
||||
let name = profile(&d.profile.id).map(|p| p.name).unwrap_or("Color");
|
||||
ui.painter().text(pos2(cr.left() + 24.0, cr.center().y), Align2::LEFT_CENTER, "Profile", t.font(13.0), t.text_dim);
|
||||
ui.painter().text(pos2(cr.left() + 24.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Profile"), t.font(13.0), t.text_dim);
|
||||
ui.painter().text(pos2(cr.left() + 84.0, cr.center().y), Align2::LEFT_CENTER, name, t.font(14.0), t.text_label);
|
||||
if d.profile.id != "lc.color" {
|
||||
super::edit::control(app, ui, &d, "profile.amount", true);
|
||||
|
|
@ -126,7 +126,7 @@ fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id:
|
|||
paint(painter, star.shrink(4.0), if fav { Icon::StarFilled } else { Icon::Star }, if fav || over_star { t.star } else { Color32::WHITE });
|
||||
}
|
||||
if resp.hovered() && !applied && !over_star {
|
||||
app.hover_preview = Some(HoverPreview { label: format!("Profile: {}", p.name), settings: s.clone() });
|
||||
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", p.name), settings: s.clone() });
|
||||
}
|
||||
let resp = resp.on_hover_text(format!("{} ({})", p.name, p.group));
|
||||
if resp.clicked() {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ pub fn header(ui: &mut egui::Ui, title: &str) {
|
|||
pub fn label_row(ui: &mut egui::Ui, label: &str, value: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::hover());
|
||||
ui.painter().text(pos2(r.left() + 24.0, r.center().y), Align2::LEFT_CENTER, label, t.font(12.5), t.text_dim);
|
||||
ui.painter().text(pos2(r.left() + 24.0, r.center().y), Align2::LEFT_CENTER, crate::i18n::tr(label), t.font(12.5), t.text_dim);
|
||||
ui.painter().text(pos2(r.left() + 110.0, r.center().y), Align2::LEFT_CENTER, value, t.font(12.5), t.text_label);
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
header(ui, "Crop");
|
||||
padded(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Aspect Ratio");
|
||||
ui.label(crate::i18n::tr("Aspect Ratio"));
|
||||
let cur = d.crop.aspect.map(|(w, h)| format!("{} × {}", w as f64 / 100.0, h as f64 / 100.0)).unwrap_or_else(|| "Free".into());
|
||||
let r = ui.add(egui::Button::new(cur).frame(false));
|
||||
register(ui.ctx(), "button:cropAspect", r.rect);
|
||||
|
|
@ -92,25 +92,25 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
});
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if text_button(ui, "cropRotateLeft", "Rotate Left", false).clicked() {
|
||||
if text_button(ui, "cropRotateLeft", crate::i18n::tr("Rotate Left"), false).clicked() {
|
||||
let _ = app.run("photo.rotateLeft", json!({}));
|
||||
}
|
||||
if text_button(ui, "cropRotateRight", "Rotate Right", false).clicked() {
|
||||
if text_button(ui, "cropRotateRight", crate::i18n::tr("Rotate Right"), false).clicked() {
|
||||
let _ = app.run("photo.rotateRight", json!({}));
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if text_button(ui, "cropFlipH", "Flip H", d.crop.flip_h).clicked() {
|
||||
if text_button(ui, "cropFlipH", crate::i18n::tr("Flip H"), d.crop.flip_h).clicked() {
|
||||
let _ = app.run("photo.flipHorizontal", json!({}));
|
||||
}
|
||||
if text_button(ui, "cropFlipV", "Flip V", d.crop.flip_v).clicked() {
|
||||
if text_button(ui, "cropFlipV", crate::i18n::tr("Flip V"), d.crop.flip_v).clicked() {
|
||||
let _ = app.run("photo.flipVertical", json!({}));
|
||||
}
|
||||
if text_button(ui, "cropSwapAspect", "Swap Aspect (X)", false).clicked() {
|
||||
if text_button(ui, "cropSwapAspect", crate::i18n::tr("Swap Aspect (X)"), false).clicked() {
|
||||
let _ = app.run("crop.rotateAspect", json!({}));
|
||||
}
|
||||
if text_button(ui, "cropReset", "Reset", false).clicked() {
|
||||
if text_button(ui, "cropReset", crate::i18n::tr("Reset"), false).clicked() {
|
||||
let _ = app.run("crop.reset", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -123,16 +123,19 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
padded(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let on = app.ui.tool == "straighten";
|
||||
if text_button(ui, "straightenTool", "Straighten Tool", on).on_hover_text("Drag along the horizon; double-click for Auto").clicked() {
|
||||
if text_button(ui, "straightenTool", crate::i18n::tr("Straighten Tool"), on)
|
||||
.on_hover_text(crate::i18n::tr("Drag along the horizon; double-click for Auto"))
|
||||
.clicked()
|
||||
{
|
||||
app.ui.tool = if on { String::new() } else { "straighten".into() };
|
||||
}
|
||||
if text_button(ui, "straightenAuto", "Auto", false).clicked() {
|
||||
if text_button(ui, "straightenAuto", crate::i18n::tr("Auto"), false).clicked() {
|
||||
let _ = app.run("crop.autoStraighten", json!({}));
|
||||
}
|
||||
});
|
||||
});
|
||||
padded(ui, |ui| {
|
||||
ui.label("Overlay");
|
||||
ui.label(crate::i18n::tr("Overlay"));
|
||||
{
|
||||
use crate::state::CropOverlay as O;
|
||||
let opts = [
|
||||
|
|
@ -149,7 +152,8 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if let Some(i) = crate::widgets::segmented(ui, "cropOverlay", &items, active, 4) {
|
||||
app.ui.crop_overlay = opts[i].0;
|
||||
}
|
||||
if matches!(app.ui.crop_overlay, O::Triangle | O::Spiral) && text_button(ui, "cropOverlayOrient", "Flip Overlay (⇧O)", false).clicked()
|
||||
if matches!(app.ui.crop_overlay, O::Triangle | O::Spiral)
|
||||
&& text_button(ui, "cropOverlayOrient", crate::i18n::tr("Flip Overlay (⇧O)"), false).clicked()
|
||||
{
|
||||
let _ = app.run("view.cropOverlayOrientation", json!({}));
|
||||
}
|
||||
|
|
@ -158,7 +162,7 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
divider(ui);
|
||||
header(ui, "Geometry");
|
||||
padded(ui, |ui| {
|
||||
ui.label("Upright");
|
||||
ui.label(crate::i18n::tr("Upright"));
|
||||
{
|
||||
use lightcraft_develop::Upright;
|
||||
let modes = [
|
||||
|
|
@ -179,30 +183,36 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
let guided = d.geometry.upright == lightcraft_develop::Upright::Guided;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if !guided && d.geometry.upright != lightcraft_develop::Upright::Off && text_button(ui, "uprightUpdate", "Update", false).clicked() {
|
||||
if !guided
|
||||
&& d.geometry.upright != lightcraft_develop::Upright::Off
|
||||
&& text_button(ui, "uprightUpdate", crate::i18n::tr("Update"), false).clicked()
|
||||
{
|
||||
let mode = serde_json::to_value(d.geometry.upright).unwrap_or_default();
|
||||
let _ = app.run("geometry.upright", json!({"mode": mode}));
|
||||
}
|
||||
if guided {
|
||||
let drawing = app.ui.tool == "guidedUpright";
|
||||
if text_button(ui, "uprightDraw", "Draw Guides", drawing).clicked() {
|
||||
if text_button(ui, "uprightDraw", crate::i18n::tr("Draw Guides"), drawing).clicked() {
|
||||
app.ui.tool = if drawing { String::new() } else { "guidedUpright".into() };
|
||||
}
|
||||
if !d.geometry.guides.is_empty() && text_button(ui, "uprightClear", "Clear Guides", false).clicked() {
|
||||
if !d.geometry.guides.is_empty() && text_button(ui, "uprightClear", crate::i18n::tr("Clear Guides"), false).clicked() {
|
||||
let _ = app.run("geometry.guides", json!({"guides": []}));
|
||||
}
|
||||
}
|
||||
});
|
||||
if guided {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{} of 4 guides — drag along lines that should be vertical or horizontal.", d.geometry.guides.len()))
|
||||
.size(11.0)
|
||||
.color(Tokens::get(ui.ctx()).text_dim),
|
||||
egui::RichText::new(crate::i18n::tr_format!(
|
||||
"{} of 4 guides — drag along lines that should be vertical or horizontal.",
|
||||
d.geometry.guides.len()
|
||||
))
|
||||
.size(11.0)
|
||||
.color(Tokens::get(ui.ctx()).text_dim),
|
||||
);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
let mut c = d.geometry.constrain_crop;
|
||||
if ui.checkbox(&mut c, "Constrain Crop").changed() {
|
||||
if ui.checkbox(&mut c, crate::i18n::tr("Constrain Crop")).changed() {
|
||||
let _ = app.run("develop.merge", json!({"settings": {"geometry": {"constrain_crop": c}}, "label": "Constrain Crop"}));
|
||||
}
|
||||
});
|
||||
|
|
@ -225,10 +235,10 @@ fn remove(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("{} spot(s) on this photo", d.spots.len()));
|
||||
ui.label(crate::i18n::tr_format!("{} spot(s) on this photo", d.spots.len()));
|
||||
ui.add_space(4.0);
|
||||
if text_button(ui, "findDust", "Find Dust Spots", false)
|
||||
.on_hover_text("Heal the small, soft dark spots sensor dust leaves on smooth areas")
|
||||
if text_button(ui, "findDust", crate::i18n::tr("Find Dust Spots"), false)
|
||||
.on_hover_text(crate::i18n::tr("Heal the small, soft dark spots sensor dust leaves on smooth areas"))
|
||||
.clicked()
|
||||
{
|
||||
match app.run("spot.findDust", json!({})) {
|
||||
|
|
@ -236,14 +246,18 @@ fn remove(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let n = r["added"].as_u64().unwrap_or(0);
|
||||
app.toast(
|
||||
ui.ctx(),
|
||||
if n == 0 { "No dust spots found".to_string() } else { format!("Healed {n} dust spot{}", if n == 1 { "" } else { "s" }) },
|
||||
if n == 0 {
|
||||
"No dust spots found".to_string()
|
||||
} else {
|
||||
crate::i18n::tr_format!("Healed {n} dust spot{}", if n == 1 { "" } else { "s" }, n = n)
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Paint over a distraction on the photo to remove it.").color(Tokens::get(ui.ctx()).text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Paint over a distraction on the photo to remove it.")).color(Tokens::get(ui.ctx()).text_dim));
|
||||
});
|
||||
// brush settings; with a spot selected they edit that spot too
|
||||
let sel = app.session.active_spot.and_then(|i| d.spots.get(i).map(|sp| (i, sp.clone())));
|
||||
|
|
@ -255,7 +269,7 @@ fn remove(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
lightcraft_develop::SpotMode::Clone => "Clone",
|
||||
lightcraft_develop::SpotMode::Remove => "Remove",
|
||||
};
|
||||
super::edit::sub_title(ui, &format!("{mode} spot {} of {}", i + 1, d.spots.len()));
|
||||
super::edit::sub_title(ui, &crate::i18n::tr_format!("{mode} spot {} of {}", i + 1, d.spots.len(), mode = mode));
|
||||
}
|
||||
let plain = |id: &'static str, label: &'static str, min: f64, max: f64, default: f64| lightcraft_develop::ControlSpec {
|
||||
id,
|
||||
|
|
@ -290,10 +304,10 @@ fn remove(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if sel.is_some() {
|
||||
padded(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if text_button(ui, "spotRefresh", "Refresh Source (/)", false).clicked() {
|
||||
if text_button(ui, "spotRefresh", crate::i18n::tr("Refresh Source (/)"), false).clicked() {
|
||||
let _ = app.run("spot.refreshSource", json!({}));
|
||||
}
|
||||
if text_button(ui, "spotDelete", "Delete (⌫)", false).clicked() {
|
||||
if text_button(ui, "spotDelete", crate::i18n::tr("Delete (⌫)"), false).clicked() {
|
||||
let _ = app.run("spot.delete", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -302,7 +316,7 @@ fn remove(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
// Visualize Spots (A): a black/white high-pass view that makes dust and specks stand out
|
||||
padded(ui, |ui| {
|
||||
let mut v = app.ui.visualize_spots;
|
||||
if ui.checkbox(&mut v, "Visualize Spots (A)").changed() {
|
||||
if ui.checkbox(&mut v, crate::i18n::tr("Visualize Spots (A)")).changed() {
|
||||
app.ui.visualize_spots = v;
|
||||
}
|
||||
});
|
||||
|
|
@ -322,7 +336,7 @@ fn remove(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
app.ui.spots_threshold = v as f32;
|
||||
}
|
||||
padded(ui, |ui| {
|
||||
if !d.spots.is_empty() && text_button(ui, "removeClear", "Delete all spots", false).clicked() {
|
||||
if !d.spots.is_empty() && text_button(ui, "removeClear", crate::i18n::tr("Delete all spots"), false).clicked() {
|
||||
for i in (0..d.spots.len()).rev() {
|
||||
let _ = app.run("spot.delete", json!({"index": i}));
|
||||
}
|
||||
|
|
@ -343,7 +357,8 @@ fn red_eye(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Drag over an eye on the photo; the pupil inside is found automatically.").color(Tokens::get(ui.ctx()).text_dim),
|
||||
egui::RichText::new(crate::i18n::tr("Drag over an eye on the photo; the pupil inside is found automatically."))
|
||||
.color(Tokens::get(ui.ctx()).text_dim),
|
||||
);
|
||||
});
|
||||
let n = d.red_eye.len();
|
||||
|
|
@ -367,16 +382,16 @@ fn red_eye(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
padded(ui, |ui| {
|
||||
if eye.pet {
|
||||
let mut on = eye.catchlight.is_some();
|
||||
if ui.checkbox(&mut on, "Add Catchlight").changed() {
|
||||
if ui.checkbox(&mut on, crate::i18n::tr("Add Catchlight")).changed() {
|
||||
let _ = app.run("redeye.catchlight", json!({"index": i, "on": on}));
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if text_button(ui, "eyeDelete", "Delete", false).clicked() {
|
||||
if text_button(ui, "eyeDelete", crate::i18n::tr("Delete"), false).clicked() {
|
||||
let _ = app.run("redeye.delete", json!({"index": i}));
|
||||
}
|
||||
if n > 1 && text_button(ui, "eyeDeleteAll", "Delete all", false).clicked() {
|
||||
if n > 1 && text_button(ui, "eyeDeleteAll", crate::i18n::tr("Delete all"), false).clicked() {
|
||||
for k in (0..n).rev() {
|
||||
let _ = app.run("redeye.delete", json!({"index": k}));
|
||||
}
|
||||
|
|
@ -515,7 +530,10 @@ fn info(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let pretty = format!("{:.5}° {}, {:.5}° {}", la.abs(), if la >= 0.0 { "N" } else { "S" }, lo.abs(), if lo >= 0.0 { "E" } else { "W" });
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(pretty).size(11.0).color(t.text_dim));
|
||||
if text_button(ui, "showOnMap", "Show on Map", false).on_hover_text("Open the place in OpenStreetMap").clicked() {
|
||||
if text_button(ui, "showOnMap", crate::i18n::tr("Show on Map"), false)
|
||||
.on_hover_text(crate::i18n::tr("Open the place in OpenStreetMap"))
|
||||
.clicked()
|
||||
{
|
||||
let url = format!("https://www.openstreetmap.org/?mlat={la:.6}&mlon={lo:.6}#map=15/{la:.6}/{lo:.6}");
|
||||
if let Err(e) = crate::links::open(app, &url) {
|
||||
app.toast(ui.ctx(), e);
|
||||
|
|
@ -556,7 +574,10 @@ fn info(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
if text_button(ui, "allMetadata", "All Metadata…", false).on_hover_text("Every EXIF, GPS and XMP field in the file").clicked() {
|
||||
if text_button(ui, "allMetadata", crate::i18n::tr("All Metadata…"), false)
|
||||
.on_hover_text(crate::i18n::tr("Every EXIF, GPS and XMP field in the file"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("dialog.allMetadata", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -616,10 +637,10 @@ fn human_size(bytes: u64) -> String {
|
|||
/// Copyright Status: Unknown / Copyrighted / Public Domain (`xmpRights:Marked`).
|
||||
fn copyright_status(app: &mut LightcraftApp, ui: &mut egui::Ui, current: lightcraft_catalog::CopyrightStatus) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new("Copyright Status").size(11.5).color(t.text_dim));
|
||||
let r = egui::ComboBox::from_id_salt("info-copyright-status").selected_text(current.label()).show_ui(ui, |ui| {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Copyright Status")).size(11.5).color(t.text_dim));
|
||||
let r = egui::ComboBox::from_id_salt("info-copyright-status").selected_text(crate::i18n::tr(current.label())).show_ui(ui, |ui| {
|
||||
for st in lightcraft_catalog::CopyrightStatus::ALL {
|
||||
if ui.selectable_label(st == current, st.label()).clicked() && st != current {
|
||||
if ui.selectable_label(st == current, crate::i18n::tr(st.label())).clicked() && st != current {
|
||||
let _ = app.run("photo.setMeta", json!({"copyrightStatus": st.id()}));
|
||||
}
|
||||
}
|
||||
|
|
@ -654,7 +675,7 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
padded(ui, |ui| {
|
||||
let kid = egui::Id::new("kw-input");
|
||||
let mut text = ui.data_mut(|d| d.get_temp::<String>(kid).unwrap_or_default());
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut text).hint_text("Add keyword").desired_width(f32::INFINITY));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut text).hint_text(crate::i18n::tr("Add keyword")).desired_width(f32::INFINITY));
|
||||
register(ui.ctx(), "field:keyword", r.rect);
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !text.trim().is_empty() {
|
||||
let kws: Vec<String> = text.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
|
|
@ -672,17 +693,17 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let _ = app.run("photo.setMeta", json!({"removeKeywords": [k]}));
|
||||
}
|
||||
r.context_menu(|ui| {
|
||||
if ui.button("Remove from Photo").clicked() {
|
||||
if ui.button(crate::i18n::tr("Remove from Photo")).clicked() {
|
||||
let _ = app.run("photo.setMeta", json!({"removeKeywords": [k]}));
|
||||
}
|
||||
if ui.button("Show Photos with Keyword").clicked() {
|
||||
if ui.button(crate::i18n::tr("Show Photos with Keyword")).clicked() {
|
||||
let _ = app.run("library.filter", json!({"keyword": k}));
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Rename Keyword…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename Keyword…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: k.clone(), to: k.clone() });
|
||||
}
|
||||
if ui.button("Delete Keyword").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete Keyword")).clicked() {
|
||||
let _ = app.run("keyword.delete", json!({"keyword": k}));
|
||||
}
|
||||
});
|
||||
|
|
@ -696,12 +717,15 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let pid = egui::Id::new("kw-painter");
|
||||
let painting = app.ui.keyword_painter.clone();
|
||||
let mut k: String = ui.data(|d| d.get_temp(pid)).unwrap_or_else(|| painting.clone().unwrap_or_default());
|
||||
let r = ui.add_enabled(painting.is_none(), egui::TextEdit::singleline(&mut k).hint_text("Keyword to paint").desired_width(130.0));
|
||||
let r = ui.add_enabled(
|
||||
painting.is_none(),
|
||||
egui::TextEdit::singleline(&mut k).hint_text(crate::i18n::tr("Keyword to paint")).desired_width(130.0),
|
||||
);
|
||||
register(ui.ctx(), "field:keywordPainter", r.rect);
|
||||
ui.data_mut(|d| d.insert_temp(pid, k.clone()));
|
||||
let label = if painting.is_some() { "Stop" } else { "Paint" };
|
||||
if text_button(ui, "keywordPaint", label, painting.is_some())
|
||||
.on_hover_text("Click photos in the grid to toggle the keyword; Esc stops")
|
||||
.on_hover_text(crate::i18n::tr("Click photos in the grid to toggle the keyword; Esc stops"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("tool.keywordPainter", json!({"keyword": if painting.is_some() { serde_json::Value::Null } else { json!(k) }}));
|
||||
|
|
@ -714,7 +738,7 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let last = typed.rsplit(',').next().unwrap_or("").trim().to_string();
|
||||
let suggestions = (*app.caches.suggestions(&app.session.catalog, &p.meta.keywords, &last, 12)).clone();
|
||||
if !suggestions.is_empty() {
|
||||
ui.label(egui::RichText::new("Suggestions").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Suggestions")).color(t.text_dim));
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for k in suggestions {
|
||||
let r = ui
|
||||
|
|
@ -739,7 +763,7 @@ fn keyword_set(app: &mut LightcraftApp, ui: &mut egui::Ui, have: &[String]) {
|
|||
let sets = lightcraft_engine::cmd::keywords::keyword_sets_json(&app.session);
|
||||
let current = sets["current"].as_str().unwrap_or_default().to_string();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Keyword Set").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Keyword Set")).color(t.text_dim));
|
||||
egui::ComboBox::from_id_salt("kw-set").selected_text(¤t).show_ui(ui, |ui| {
|
||||
for set in sets["sets"].as_array().into_iter().flatten() {
|
||||
let name = set["name"].as_str().unwrap_or_default();
|
||||
|
|
@ -748,7 +772,7 @@ fn keyword_set(app: &mut LightcraftApp, ui: &mut egui::Ui, have: &[String]) {
|
|||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Save Current Keywords as Set…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Save Current Keywords as Set…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::TextPrompt {
|
||||
title: "Save Keyword Set".into(),
|
||||
hint: "Set name".into(),
|
||||
|
|
@ -758,14 +782,16 @@ fn keyword_set(app: &mut LightcraftApp, ui: &mut egui::Ui, have: &[String]) {
|
|||
key: "name".into(),
|
||||
});
|
||||
}
|
||||
if current != lightcraft_engine::cmd::keywords::RECENT && ui.button(format!("Delete “{current}”")).clicked() {
|
||||
if current != lightcraft_engine::cmd::keywords::RECENT
|
||||
&& ui.button(crate::i18n::tr_format!("Delete “{current}”", current = current)).clicked()
|
||||
{
|
||||
let _ = app.run("keyword.deleteSet", json!({"name": current}));
|
||||
}
|
||||
});
|
||||
});
|
||||
let kws: Vec<String> = sets["keywords"].as_array().into_iter().flatten().filter_map(|k| k.as_str().map(str::to_string)).collect();
|
||||
if kws.is_empty() {
|
||||
ui.label(egui::RichText::new("Keywords you add appear here; ⌥1–⌥9 apply them.").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Keywords you add appear here; ⌥1–⌥9 apply them.")).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
let bw = ((ui.available_width() - 8.0) / 3.0).floor().max(40.0);
|
||||
|
|
@ -795,7 +821,7 @@ fn versions(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
header(ui, "Versions");
|
||||
let t = Tokens::get(ui.ctx());
|
||||
padded(ui, |ui| {
|
||||
if text_button(ui, "versionCreate", "Create Version", false).clicked() {
|
||||
if text_button(ui, "versionCreate", crate::i18n::tr("Create Version"), false).clicked() {
|
||||
let _ = app.run("version.create", json!({}));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
|
|
@ -804,7 +830,7 @@ fn versions(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let mut auto: bool = ui.data(|d| d.get_temp(tab_id)).unwrap_or(false);
|
||||
let named_n = p.versions.iter().filter(|v| !v.auto).count();
|
||||
let items = [("Named", "named"), ("Auto", "auto")];
|
||||
let labels = [format!("Named ({named_n})"), format!("Auto ({})", p.versions.len() - named_n)];
|
||||
let labels = [crate::i18n::tr_format!("Named ({named_n})", named_n = named_n), format!("Auto ({})", p.versions.len() - named_n)];
|
||||
let items: Vec<(&str, &str)> = items.iter().zip(&labels).map(|((_, k), l)| (l.as_str(), *k)).collect();
|
||||
if let Some(i) = crate::widgets::segmented(ui, "versionsTab", &items, Some(auto as usize), 2) {
|
||||
auto = i == 1;
|
||||
|
|
@ -847,7 +873,8 @@ fn versions(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
// resting on a version shows it in the loupe (nothing is changed)
|
||||
if resp.hovered() && !current {
|
||||
app.hover_preview = Some(crate::HoverPreview { label: format!("Version: {}", v.name), settings: (*v.settings).clone() });
|
||||
app.hover_preview =
|
||||
Some(crate::HoverPreview { label: crate::i18n::tr_format!("Version: {}", v.name), settings: (*v.settings).clone() });
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
crate::panels::dialogs::prompt(app, "Rename Version", "Version name", &v.name, "version.rename", json!({"index": i}), "name");
|
||||
|
|
@ -856,20 +883,20 @@ fn versions(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
}
|
||||
resp.on_hover_text(if current { "The photo has these settings" } else { "Click to restore · double-click to rename" }).context_menu(
|
||||
|ui| {
|
||||
if ui.button("Restore").clicked() {
|
||||
if ui.button(crate::i18n::tr("Restore")).clicked() {
|
||||
let _ = app.run("version.restore", json!({"index": i}));
|
||||
}
|
||||
if ui.button("Update with Current Settings").clicked() {
|
||||
if ui.button(crate::i18n::tr("Update with Current Settings")).clicked() {
|
||||
let _ = app.run("version.update", json!({"index": i}));
|
||||
}
|
||||
if ui.button("Rename…").clicked() {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
crate::panels::dialogs::prompt(app, "Rename Version", "Version name", &v.name, "version.rename", json!({"index": i}), "name");
|
||||
}
|
||||
if ui.button("Set as Before").clicked() {
|
||||
if ui.button(crate::i18n::tr("Set as Before")).clicked() {
|
||||
let _ = app.run("beforeAfter.setBefore", json!({"source": "version", "name": v.name}));
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Delete").clicked() {
|
||||
if ui.button(crate::i18n::tr("Delete")).clicked() {
|
||||
let _ = app.run("version.delete", json!({"index": i}));
|
||||
}
|
||||
},
|
||||
|
|
@ -899,7 +926,7 @@ fn activity(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
padded(ui, |ui| {
|
||||
if p.history.is_empty() {
|
||||
ui.label(egui::RichText::new("No edits yet.").color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("No edits yet.")).color(t.text_dim));
|
||||
}
|
||||
for (i, h) in p.history.iter().enumerate().rev() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click());
|
||||
|
|
@ -913,15 +940,15 @@ fn activity(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
let _ = app.run("history.restore", json!({"index": i}));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Copy History Step Settings to Before").clicked() {
|
||||
if ui.button(crate::i18n::tr("Copy History Step Settings to Before")).clicked() {
|
||||
let _ = app.run("beforeAfter.setBefore", json!({"source": "history", "index": i}));
|
||||
}
|
||||
if ui.button("Create Version from Step").clicked() {
|
||||
if ui.button(crate::i18n::tr("Create Version from Step")).clicked() {
|
||||
let _ = app.run("history.restore", json!({"index": i}));
|
||||
let _ = app.run("version.create", json!({"name": h.label}));
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Clear History").clicked() {
|
||||
if ui.button(crate::i18n::tr("Clear History")).clicked() {
|
||||
let _ = app.run("history.clear", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,11 +98,11 @@ fn value_editor(ui: &mut egui::Ui, field: &str, op: &str, v: &mut Value, salt: &
|
|||
let Value::Array(a) = v else { return };
|
||||
if k == Kind::Number {
|
||||
number_value(ui, &mut a[0], field);
|
||||
ui.label("and");
|
||||
ui.label(crate::i18n::tr("and"));
|
||||
number_value(ui, &mut a[1], field);
|
||||
} else {
|
||||
text_value(ui, &mut a[0], 86.0, "2026-01-01", &format!("{salt}-a"));
|
||||
ui.label("and");
|
||||
ui.label(crate::i18n::tr("and"));
|
||||
text_value(ui, &mut a[1], 86.0, "2026-12", &format!("{salt}-b"));
|
||||
}
|
||||
}
|
||||
|
|
@ -134,9 +134,9 @@ fn value_editor(ui: &mut egui::Ui, field: &str, op: &str, v: &mut Value, salt: &
|
|||
pub fn edit(ui: &mut egui::Ui, rs: &mut RuleSet, salt: &str, depth: usize) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("Match").color(t.text_label));
|
||||
ui.label(RichText::new(crate::i18n::tr("Match")).color(t.text_label));
|
||||
match_combo(ui, salt, &mut rs.mode);
|
||||
ui.label(RichText::new("of the following rules:").color(t.text_label));
|
||||
ui.label(RichText::new(crate::i18n::tr("of the following rules:")).color(t.text_label));
|
||||
});
|
||||
let mut remove = None;
|
||||
let mut insert: Option<(usize, Rule)> = None;
|
||||
|
|
@ -146,7 +146,7 @@ pub fn edit(ui: &mut egui::Ui, rs: &mut RuleSet, salt: &str, depth: usize) {
|
|||
Rule::Group { group } => {
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("−").on_hover_text("Remove this group").clicked() {
|
||||
if ui.small_button("−").on_hover_text(crate::i18n::tr("Remove this group")).clicked() {
|
||||
remove = Some(i);
|
||||
}
|
||||
ui.vertical(|ui| edit(ui, group, &rsalt, depth + 1));
|
||||
|
|
@ -182,7 +182,7 @@ pub fn edit(ui: &mut egui::Ui, rs: &mut RuleSet, salt: &str, depth: usize) {
|
|||
});
|
||||
value_editor(ui, field, op, value, &rsalt);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let add = ui.small_button("+").on_hover_text("Add a rule (⌥-click: a group)");
|
||||
let add = ui.small_button("+").on_hover_text(crate::i18n::tr("Add a rule (⌥-click: a group)"));
|
||||
register(ui.ctx(), format!("button:ruleAdd-{rsalt}"), add.rect);
|
||||
if add.clicked() {
|
||||
let group = ui.input(|i| i.modifiers.alt) && depth < 3;
|
||||
|
|
@ -191,7 +191,7 @@ pub fn edit(ui: &mut egui::Ui, rs: &mut RuleSet, salt: &str, depth: usize) {
|
|||
if group { Rule::Group { group: RuleSet { mode: Match::Any, rules: vec![new_rule()] } } } else { new_rule() },
|
||||
));
|
||||
}
|
||||
let del = ui.small_button("−").on_hover_text("Remove this rule");
|
||||
let del = ui.small_button("−").on_hover_text(crate::i18n::tr("Remove this rule"));
|
||||
register(ui.ctx(), format!("button:ruleRemove-{rsalt}"), del.rect);
|
||||
if del.clicked() {
|
||||
remove = Some(i);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ fn body(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
crate::widgets::register(ui.ctx(), "view:secondWindow", area);
|
||||
ui.allocate_rect(area, egui::Sense::hover());
|
||||
let Some(id) = app.session.active() else {
|
||||
ui.painter().text(area.center(), egui::Align2::CENTER_CENTER, "No photo selected", egui::FontId::proportional(14.0), Color32::GRAY);
|
||||
ui.painter().text(
|
||||
area.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
crate::i18n::tr("No photo selected"),
|
||||
egui::FontId::proportional(14.0),
|
||||
Color32::GRAY,
|
||||
);
|
||||
return;
|
||||
};
|
||||
// a render sized for this window
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, tab: &mut String) {
|
|||
|
||||
fn heading(ui: &mut egui::Ui, t: &Tokens, text: &str) {
|
||||
ui.add_space(4.0);
|
||||
ui.label(RichText::new(text).font(t.semibold(12.5)).color(t.text));
|
||||
ui.label(RichText::new(crate::i18n::tr(text)).font(t.semibold(12.5)).color(t.text));
|
||||
}
|
||||
|
||||
fn row<R>(ui: &mut egui::Ui, t: &Tokens, label: &str, add: impl FnOnce(&mut egui::Ui) -> R) -> R {
|
||||
ui.horizontal(|ui| {
|
||||
ui.allocate_ui_with_layout(egui::vec2(LABEL_W, 24.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.set_min_width(LABEL_W);
|
||||
ui.label(RichText::new(label).color(t.text_label));
|
||||
ui.label(RichText::new(crate::i18n::tr(label)).color(t.text_label));
|
||||
});
|
||||
add(ui)
|
||||
})
|
||||
|
|
@ -64,12 +64,12 @@ fn row<R>(ui: &mut egui::Ui, t: &Tokens, label: &str, add: impl FnOnce(&mut egui
|
|||
}
|
||||
|
||||
fn hint(ui: &mut egui::Ui, t: &Tokens, text: &str) {
|
||||
ui.label(RichText::new(text).size(11.0).color(t.text_dim));
|
||||
ui.label(RichText::new(crate::i18n::tr(text)).size(11.0).color(t.text_dim));
|
||||
}
|
||||
|
||||
/// A checkbox addressable as `check:{id}`; true when toggled.
|
||||
fn check(ui: &mut egui::Ui, id: &str, value: &mut bool, label: &str) -> bool {
|
||||
let r = ui.checkbox(value, label);
|
||||
let r = ui.checkbox(value, crate::i18n::tr(label));
|
||||
register(ui.ctx(), format!("check:{id}"), r.rect);
|
||||
r.changed()
|
||||
}
|
||||
|
|
@ -90,27 +90,31 @@ fn choices<V: PartialEq + Copy>(ui: &mut egui::Ui, id: &str, options: &[(V, &str
|
|||
// ------------------------------------------------------------------------------------- General
|
||||
|
||||
fn general_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
heading(ui, t, "Library");
|
||||
row(ui, t, crate::i18n::tr("Language"), |ui| {
|
||||
choices(ui, "settingsLanguage", &[(crate::i18n::Language::Ja, "日本語"), (crate::i18n::Language::En, "English")], &mut app.ui.language);
|
||||
crate::i18n::set_language(app.ui.language);
|
||||
});
|
||||
heading(ui, t, crate::i18n::tr("Library"));
|
||||
let location = match &app.session.library {
|
||||
Some(l) => l.dir.display().to_string(),
|
||||
None => "In memory — nothing is saved".to_string(),
|
||||
None => crate::i18n::tr("In memory — nothing is saved").to_string(),
|
||||
};
|
||||
row(ui, t, "Location", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Location"), |ui| {
|
||||
ui.label(RichText::new(location).color(t.text));
|
||||
});
|
||||
row(ui, t, "", |ui| {
|
||||
let can = app.services.pick_folder.is_some();
|
||||
let r = ui.add_enabled(can, egui::Button::new("Open Library…"));
|
||||
let r = ui.add_enabled(can, egui::Button::new(crate::i18n::tr("Open Library…")));
|
||||
register(ui.ctx(), "button:settingsOpenLibrary", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("app.openLibrary", json!({}));
|
||||
}
|
||||
if !can {
|
||||
hint(ui, t, "not available here");
|
||||
hint(ui, t, crate::i18n::tr("not available here"));
|
||||
}
|
||||
});
|
||||
heading(ui, t, "Startup");
|
||||
row(ui, t, "Open in", |ui| {
|
||||
heading(ui, t, crate::i18n::tr("Startup"));
|
||||
row(ui, t, crate::i18n::tr("Open in"), |ui| {
|
||||
choices(
|
||||
ui,
|
||||
"settingsStartup",
|
||||
|
|
@ -118,18 +122,21 @@ fn general_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
&mut app.ui.settings.startup_view,
|
||||
);
|
||||
});
|
||||
heading(ui, t, "Culling");
|
||||
heading(ui, t, crate::i18n::tr("Culling"));
|
||||
check(ui, "settings.autoAdvance", &mut app.ui.auto_advance, "Auto Advance: move to the next photo after rating or flagging");
|
||||
check(ui, "settings.confirmDelete", &mut app.ui.settings.confirm_delete, "Confirm before moving photos to Recently Deleted");
|
||||
heading(ui, t, "External Editor");
|
||||
row(ui, t, "Application", |ui| {
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut app.ui.settings.external_editor).hint_text("System default").desired_width(220.0));
|
||||
heading(ui, t, crate::i18n::tr("External Editor"));
|
||||
row(ui, t, crate::i18n::tr("Application"), |ui| {
|
||||
let r = ui
|
||||
.add(egui::TextEdit::singleline(&mut app.ui.settings.external_editor).hint_text(crate::i18n::tr("System default")).desired_width(220.0));
|
||||
register(ui.ctx(), "field:externalEditor", r.rect);
|
||||
});
|
||||
hint(
|
||||
ui,
|
||||
t,
|
||||
"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).",
|
||||
crate::i18n::tr(
|
||||
"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).",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +183,9 @@ fn cameras(app: &LightcraftApp) -> Vec<String> {
|
|||
|
||||
fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let d = app.session.import_defaults.clone();
|
||||
heading(ui, t, "Raw defaults");
|
||||
hint(ui, t, "Settings new raw photos start from. Changing them doesn't touch photos already in the library.");
|
||||
row(ui, t, "Raw photos", |ui| {
|
||||
heading(ui, t, crate::i18n::tr("Raw defaults"));
|
||||
hint(ui, t, crate::i18n::tr("Settings new raw photos start from. Changing them doesn't touch photos already in the library."));
|
||||
row(ui, t, crate::i18n::tr("Raw photos"), |ui| {
|
||||
if let Some(v) = preset_combo(app, ui, "settingsRawPreset", d.raw_preset.as_deref(), "LightCraft Default") {
|
||||
let _ = app.run("library.preferences", json!({"import": {"rawPreset": v}}));
|
||||
}
|
||||
|
|
@ -192,7 +199,7 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
if per {
|
||||
let cams = cameras(app);
|
||||
if cams.is_empty() {
|
||||
hint(ui, t, "No raw photos yet: cameras appear here once their photos are in the library.");
|
||||
hint(ui, t, crate::i18n::tr("No raw photos yet: cameras appear here once their photos are in the library."));
|
||||
}
|
||||
egui::ScrollArea::vertical().max_height(150.0).id_salt("settingsCameras").show(ui, |ui| {
|
||||
for (i, cam) in cams.iter().enumerate() {
|
||||
|
|
@ -212,10 +219,10 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
};
|
||||
let id = format!("settingsCamera-{i}");
|
||||
let r = egui::ComboBox::from_id_salt(&id).width(240.0).selected_text(label).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(current == Some(RAW_DEFAULT), "Same as raw default").clicked() {
|
||||
if ui.selectable_label(current == Some(RAW_DEFAULT), crate::i18n::tr("Same as raw default")).clicked() {
|
||||
pick = Some(json!({"camera": cam, "remove": true}));
|
||||
}
|
||||
if ui.selectable_label(current.is_none(), "LightCraft Default").clicked() {
|
||||
if ui.selectable_label(current.is_none(), crate::i18n::tr("LightCraft Default")).clicked() {
|
||||
pick = Some(json!({"camera": cam, "preset": null}));
|
||||
}
|
||||
for p in &app.session.presets {
|
||||
|
|
@ -232,14 +239,14 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
}
|
||||
});
|
||||
}
|
||||
heading(ui, t, "Other images (JPEG, PNG, TIFF, HEIC…)");
|
||||
row(ui, t, "Non-raw photos", |ui| {
|
||||
heading(ui, t, crate::i18n::tr("Other images (JPEG, PNG, TIFF, HEIC…)"));
|
||||
row(ui, t, crate::i18n::tr("Non-raw photos"), |ui| {
|
||||
if let Some(v) = preset_combo(app, ui, "settingsOtherPreset", d.other_preset.as_deref(), "None") {
|
||||
let _ = app.run("library.preferences", json!({"import": {"otherPreset": v}}));
|
||||
}
|
||||
});
|
||||
heading(ui, t, "Metadata");
|
||||
hint(ui, t, "Added to photos you import that don't already have it.");
|
||||
heading(ui, t, crate::i18n::tr("Metadata"));
|
||||
hint(ui, t, crate::i18n::tr("Added to photos you import that don't already have it."));
|
||||
for (key, label, hint_text, value) in
|
||||
[("copyright", "Copyright", "© 2026 Your Name", d.copyright.clone()), ("creator", "Creator", "Your Name", d.creator.clone())]
|
||||
{
|
||||
|
|
@ -259,14 +266,14 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
});
|
||||
}
|
||||
if !app.session.metadata_presets.is_empty() {
|
||||
row(ui, t, "Metadata preset", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Metadata preset"), |ui| {
|
||||
let cur = d.metadata_preset.clone();
|
||||
let mut pick = None;
|
||||
let r = egui::ComboBox::from_id_salt("settingsMetaPreset")
|
||||
.width(240.0)
|
||||
.selected_text(cur.clone().unwrap_or_else(|| "None".into()))
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_label(cur.is_none(), "None").clicked() {
|
||||
if ui.selectable_label(cur.is_none(), crate::i18n::tr("None")).clicked() {
|
||||
pick = Some(String::new());
|
||||
}
|
||||
for m in &app.session.metadata_presets {
|
||||
|
|
@ -281,12 +288,12 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
}
|
||||
});
|
||||
}
|
||||
heading(ui, t, "XMP sidecars");
|
||||
heading(ui, t, crate::i18n::tr("XMP sidecars"));
|
||||
let mut xmp = app.session.xmp;
|
||||
if check(ui, "settings.autoWriteXmp", &mut xmp.auto_write, "Automatically write changes into XMP sidecars") {
|
||||
let _ = app.run("library.xmpPreferences", json!({"autoWrite": xmp.auto_write}));
|
||||
}
|
||||
row(ui, t, "Sidecar names", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Sidecar names"), |ui| {
|
||||
use lightcraft_engine::sidecar::SidecarNaming as N;
|
||||
let mut n = xmp.naming;
|
||||
if choices(ui, "settingsXmpNaming", &[(N::Stem, "IMG_1.xmp"), (N::Full, "IMG_1.CR3.xmp")], &mut n) {
|
||||
|
|
@ -295,12 +302,12 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
}
|
||||
});
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
heading(ui, t, "Auto Import");
|
||||
hint(ui, t, "Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete.");
|
||||
row(ui, t, "Watched folder", |ui| {
|
||||
heading(ui, t, crate::i18n::tr("Auto Import"));
|
||||
hint(ui, t, crate::i18n::tr("Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete."));
|
||||
row(ui, t, crate::i18n::tr("Watched folder"), |ui| {
|
||||
ui.label(RichText::new(d.auto_folder.clone().unwrap_or_else(|| "Off".into())).color(t.text));
|
||||
let can = app.services.pick_folder.is_some();
|
||||
let r = ui.add_enabled(can, egui::Button::new("Choose…"));
|
||||
let r = ui.add_enabled(can, egui::Button::new(crate::i18n::tr("Choose…")));
|
||||
register(ui.ctx(), "button:settingsAutoFolder", r.rect);
|
||||
if r.clicked()
|
||||
&& let Some(f) = app.services.pick_folder.as_mut().and_then(|f| f())
|
||||
|
|
@ -308,21 +315,21 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
{
|
||||
app.toast(ui.ctx(), e);
|
||||
}
|
||||
if d.auto_folder.is_some() && ui.button("Turn Off").clicked() {
|
||||
if d.auto_folder.is_some() && ui.button(crate::i18n::tr("Turn Off")).clicked() {
|
||||
let _ = app.run("library.autoImport", json!({"folder": null}));
|
||||
}
|
||||
});
|
||||
if d.auto_folder.is_some() {
|
||||
row(ui, t, "", |ui| {
|
||||
let mut copy = d.auto_copy;
|
||||
if ui.checkbox(&mut copy, "Copy into the library (else use the files where they are)").changed() {
|
||||
if ui.checkbox(&mut copy, crate::i18n::tr("Copy into the library (else use the files where they are)")).changed() {
|
||||
let _ = app.run("library.autoImport", json!({"copy": copy}));
|
||||
}
|
||||
});
|
||||
row(ui, t, "Album", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Album"), |ui| {
|
||||
let id = egui::Id::new("auto-album");
|
||||
let mut name = ui.data(|m| m.get_temp::<String>(id)).unwrap_or_else(|| d.auto_album.clone().unwrap_or_default());
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text("None").desired_width(180.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text(crate::i18n::tr("None")).desired_width(180.0));
|
||||
if r.lost_focus() {
|
||||
let _ = app.run("library.autoImport", json!({"album": name.trim()}));
|
||||
}
|
||||
|
|
@ -331,7 +338,7 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
}
|
||||
}
|
||||
if app.session.library.is_none() {
|
||||
hint(ui, t, "In-memory session: these settings last until LightCraft quits.");
|
||||
hint(ui, t, crate::i18n::tr("In-memory session: these settings last until LightCraft quits."));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -339,31 +346,31 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
|
||||
fn performance_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
use lightcraft_engine::gpu;
|
||||
heading(ui, t, "Rendering");
|
||||
heading(ui, t, crate::i18n::tr("Rendering"));
|
||||
check(ui, "settings.gpu", &mut app.ui.settings.gpu, "Use the GPU for rendering");
|
||||
let status = if !gpu::available() {
|
||||
match gpu::unavailable_reason() {
|
||||
Some(why) => format!("Rendering on the CPU: {why}"),
|
||||
Some(why) => crate::i18n::tr_format!("Rendering on the CPU: {why}", why = why),
|
||||
None => "No usable GPU found: rendering on the CPU".to_string(),
|
||||
}
|
||||
} else {
|
||||
format!("GPU: {}", gpu::adapter_name().unwrap_or_else(|| "starting…".into()))
|
||||
};
|
||||
hint(ui, t, &status);
|
||||
row(ui, t, "Preview size", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Preview size"), |ui| {
|
||||
let opts: Vec<(u32, String)> = PREVIEW_EDGES.iter().map(|e| (*e, format!("{e} px"))).collect();
|
||||
let opts: Vec<(u32, &str)> = opts.iter().map(|(e, l)| (*e, l.as_str())).collect();
|
||||
choices(ui, "settingsPreview", &opts, &mut app.ui.settings.preview_edge);
|
||||
});
|
||||
hint(ui, t, "Largest long edge the Detail view renders at; larger is sharper on big displays but slower.");
|
||||
row(ui, t, "Memory for caches", |ui| {
|
||||
let auto = format!("Automatic ({} MB)", lightcraft_engine::memory::default_budget() >> 20);
|
||||
hint(ui, t, crate::i18n::tr("Largest long edge the Detail view renders at; larger is sharper on big displays but slower."));
|
||||
row(ui, t, crate::i18n::tr("Memory for caches"), |ui| {
|
||||
let auto = crate::i18n::tr_format!("Automatic ({} MB)", lightcraft_engine::memory::default_budget() >> 20);
|
||||
let opts = [(0u32, auto.as_str()), (512, "512 MB"), (1024, "1 GB"), (2048, "2 GB"), (4096, "4 GB")];
|
||||
choices(ui, "settingsMemory", &opts, &mut app.ui.settings.memory_mb);
|
||||
});
|
||||
heading(ui, t, "Thumbnail cache");
|
||||
heading(ui, t, crate::i18n::tr("Thumbnail cache"));
|
||||
let cur = (app.session.cache_bytes() >> 20) as u32;
|
||||
row(ui, t, "Size limit", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Size limit"), |ui| {
|
||||
let opts = [(512u32, "512 MB"), (1024, "1 GB"), (2048, "2 GB"), (4096, "4 GB"), (8192, "8 GB")];
|
||||
let mut v = if CACHE_SIZES.contains(&cur) { cur } else { 2048 };
|
||||
if choices(ui, "settingsCache", &opts, &mut v) {
|
||||
|
|
@ -371,16 +378,16 @@ fn performance_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
}
|
||||
});
|
||||
let used = app.session.media.rendered.disk().map(|d| d.size());
|
||||
row(ui, t, "In use", |ui| {
|
||||
row(ui, t, crate::i18n::tr("In use"), |ui| {
|
||||
ui.label(RichText::new(used.map(|b| format!("{:.1} MB", b as f64 / 1048576.0)).unwrap_or_else(|| "memory only".into())).color(t.text));
|
||||
let r = ui.button("Clear Cache");
|
||||
let r = ui.button(crate::i18n::tr("Clear Cache"));
|
||||
register(ui.ctx(), "button:settingsClearCache", r.rect);
|
||||
if r.clicked() {
|
||||
let _ = app.run("library.clearPreviews", json!({}));
|
||||
}
|
||||
});
|
||||
heading(ui, t, "Local folders");
|
||||
row(ui, t, "Forget unchanged photos", |ui| {
|
||||
heading(ui, t, crate::i18n::tr("Local folders"));
|
||||
row(ui, t, crate::i18n::tr("Forget unchanged photos"), |ui| {
|
||||
let opts = [(0u32, "Never"), (7, "After 7 days"), (30, "After 30 days"), (90, "After 90 days"), (365, "After a year")];
|
||||
let mut v = app.session.forget_local_days;
|
||||
if choices(ui, "settingsForgetLocal", &opts, &mut v) {
|
||||
|
|
@ -390,7 +397,9 @@ fn performance_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
hint(
|
||||
ui,
|
||||
t,
|
||||
"Photos seen in Local but never added or changed leave the catalog when their folder hasn't been browsed for this long (checked when the library opens). Files stay on disk; browsing the folder shows them again.",
|
||||
crate::i18n::tr(
|
||||
"Photos seen in Local but never added or changed leave the catalog when their folder hasn't been browsed for this long (checked when the library opens). Files stay on disk; browsing the folder shows them again.",
|
||||
),
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
smart_previews(app, ui, t);
|
||||
|
|
@ -414,29 +423,40 @@ fn smart_previews(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
if loc.is_null() {
|
||||
return;
|
||||
}
|
||||
heading(ui, t, "Smart previews");
|
||||
heading(ui, t, crate::i18n::tr("Smart previews"));
|
||||
let path = loc["path"].as_str().unwrap_or_default().to_string();
|
||||
let available = loc["available"].as_bool().unwrap_or(true);
|
||||
let custom = loc["custom"].as_bool().unwrap_or(false);
|
||||
row(ui, t, "Folder", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Folder"), |ui| {
|
||||
let text = format!("{path}{}", if custom { "" } else { " (library default)" });
|
||||
ui.add(egui::Label::new(RichText::new(text.clone()).color(if available { t.text } else { t.reject })).truncate()).on_hover_text(text);
|
||||
});
|
||||
let (count, bytes) = (loc["count"].as_u64().unwrap_or(0), loc["bytes"].as_u64().unwrap_or(0));
|
||||
if available {
|
||||
hint(ui, t, &format!("{count} smart preview{} · {:.1} MB", if count == 1 { "" } else { "s" }, bytes as f64 / 1048576.0));
|
||||
hint(
|
||||
ui,
|
||||
t,
|
||||
&crate::i18n::tr_format!(
|
||||
"{count} smart preview{} · {:.1} MB",
|
||||
if count == 1 { "" } else { "s" },
|
||||
bytes as f64 / 1048576.0,
|
||||
count = count
|
||||
),
|
||||
);
|
||||
} else {
|
||||
hint(
|
||||
ui,
|
||||
t,
|
||||
"This folder is not available (drive disconnected?). Smart previews are not built or used until it is back or you choose another folder.",
|
||||
crate::i18n::tr(
|
||||
"This folder is not available (drive disconnected?). Smart previews are not built or used until it is back or you choose another folder.",
|
||||
),
|
||||
);
|
||||
}
|
||||
// what happens to the previews already built when the folder changes
|
||||
let mode_id = egui::Id::new("smart-existing");
|
||||
let mut mode: u8 = ui.data(|d| d.get_temp(mode_id)).unwrap_or(0);
|
||||
if count > 0 {
|
||||
row(ui, t, "Existing previews", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Existing previews"), |ui| {
|
||||
if choices(ui, "settingsSmartExisting", &[(0u8, "Move them"), (1, "Leave them"), (2, "Delete them")], &mut mode) {
|
||||
ui.data_mut(|d| d.insert_temp(mode_id, mode));
|
||||
}
|
||||
|
|
@ -447,7 +467,7 @@ fn smart_previews(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
ui.horizontal(|ui| {
|
||||
ui.add_space(LABEL_W);
|
||||
if app.services.pick_folder.is_some() {
|
||||
let r = ui.button("Choose Folder…");
|
||||
let r = ui.button(crate::i18n::tr("Choose Folder…"));
|
||||
register(ui.ctx(), "button:settingsSmartChoose", r.rect);
|
||||
if r.clicked()
|
||||
&& let Some(dir) = app.services.pick_folder.as_mut().and_then(|f| f())
|
||||
|
|
@ -455,7 +475,7 @@ fn smart_previews(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
result = Some(app.run("library.smartPreviewsLocation", json!({"path": dir, "existing": existing})));
|
||||
}
|
||||
}
|
||||
let r = ui.add_enabled(custom, egui::Button::new("Use Library Folder"));
|
||||
let r = ui.add_enabled(custom, egui::Button::new(crate::i18n::tr("Use Library Folder")));
|
||||
register(ui.ctx(), "button:settingsSmartReset", r.rect);
|
||||
if r.clicked() {
|
||||
result = Some(app.run("library.smartPreviewsLocation", json!({"reset": true, "existing": existing})));
|
||||
|
|
@ -472,22 +492,22 @@ fn smart_previews(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
("discard", n) => format!("Smart previews folder changed; deleted {n}"),
|
||||
_ => "Smart previews folder changed".to_string(),
|
||||
};
|
||||
app.toast(ui.ctx(), if failed > 0 { format!("{msg} ({failed} failed)") } else { msg });
|
||||
app.toast(ui.ctx(), if failed > 0 { crate::i18n::tr_format!("{msg} ({failed} failed)", failed = failed, msg = msg) } else { msg });
|
||||
}
|
||||
Err(e) => app.toast(ui.ctx(), e),
|
||||
}
|
||||
}
|
||||
hint(ui, t, "Keep it on a drive with room: smart previews are about 1 MB per photo. The thumbnail cache stays in the library.");
|
||||
hint(ui, t, crate::i18n::tr("Keep it on a drive with room: smart previews are about 1 MB per photo. The thumbnail cache stays in the library."));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------- Interface
|
||||
|
||||
fn interface_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
heading(ui, t, "Filmstrip");
|
||||
heading(ui, t, crate::i18n::tr("Filmstrip"));
|
||||
check(ui, "settings.filmNames", &mut app.ui.settings.film_names, "Show file names");
|
||||
check(ui, "settings.filmBadges", &mut app.ui.settings.film_badges, "Show ratings, flags and edit badges");
|
||||
heading(ui, t, "Grid");
|
||||
row(ui, t, "Ratings & flags", |ui| {
|
||||
heading(ui, t, crate::i18n::tr("Grid"));
|
||||
row(ui, t, crate::i18n::tr("Ratings & flags"), |ui| {
|
||||
choices(
|
||||
ui,
|
||||
"settingsGridBadges",
|
||||
|
|
@ -496,9 +516,9 @@ fn interface_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||
);
|
||||
});
|
||||
check(ui, "settings.showFilenames", &mut app.ui.show_filenames, "Square Grid: show file names and formats");
|
||||
heading(ui, t, "Detail");
|
||||
heading(ui, t, crate::i18n::tr("Detail"));
|
||||
check(ui, "settings.navigator", &mut app.ui.navigator, "Show the Navigator while zoomed in");
|
||||
row(ui, t, "Info overlay", |ui| {
|
||||
row(ui, t, crate::i18n::tr("Info overlay"), |ui| {
|
||||
use crate::state::InfoOverlay as I;
|
||||
choices(ui, "settingsInfo", &[(I::Off, "Off"), (I::Basic, "File & date"), (I::Exposure, "Exposure")], &mut app.ui.info_overlay);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let mut sw = 640.0f32.min(full.width() - 460.0).max(200.0);
|
||||
if !app.native_menu {
|
||||
// leave room for the in-window menus left of the (centred) search field
|
||||
let menus_right = full.left() + 140.0 + crate::menubar::bar_width(ui, app.ui.language) + 24.0;
|
||||
let menus_right = full.left() + 140.0 + crate::menubar::bar_width(ui) + 24.0;
|
||||
sw = sw.min(2.0 * (full.center().x - menus_right)).max(200.0);
|
||||
}
|
||||
let search_left = full.center().x - sw / 2.0;
|
||||
|
|
@ -59,7 +59,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
ui.new_child(egui::UiBuilder::new().max_rect(sr.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center)));
|
||||
let empty = app.ui.search.is_empty();
|
||||
if empty && !focused {
|
||||
let g = child.painter().layout_no_wrap("Search Photos".into(), t.font(13.5), t.text_dim);
|
||||
let g = child.painter().layout_no_wrap(crate::i18n::tr("Search Photos").into(), t.font(13.5), t.text_dim);
|
||||
let w = g.size().x + 24.0;
|
||||
let x0 = sr.center().x - w / 2.0;
|
||||
paint(child.painter(), Rect::from_min_size(pos2(x0, sr.center().y - 8.0), vec2(16.0, 16.0)), Icon::Search, t.text_dim);
|
||||
|
|
@ -102,7 +102,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
ui.painter().text(c, Align2::CENTER_CENTER, active.to_string(), t.semibold(9.5), t.canvas);
|
||||
}
|
||||
let fresp = fresp.on_hover_text(if active > 0 {
|
||||
format!("Filter bar — {active} active filter{}", if active == 1 { "" } else { "s" })
|
||||
crate::i18n::tr_format!("Filter bar — {active} active filter{}", if active == 1 { "" } else { "s" }, active = active)
|
||||
} else {
|
||||
"Filter bar".into()
|
||||
});
|
||||
|
|
@ -113,10 +113,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let mut x = full.right() - 18.0;
|
||||
// saving is failing: the cloud icon turns into a warning until a save succeeds
|
||||
let unsaved = app.session.unsaved().map(|(n, e)| {
|
||||
format!(
|
||||
crate::i18n::tr_format!(
|
||||
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.",
|
||||
if n == 1 { "" } else { "s" },
|
||||
if n == 1 { "it" } else { "them" }
|
||||
if n == 1 { "it" } else { "them" },
|
||||
e = e,
|
||||
n = n
|
||||
)
|
||||
});
|
||||
let cloud_tip = unsaved.as_deref().unwrap_or("Local library — no cloud account needed");
|
||||
|
|
|
|||
|
|
@ -166,7 +166,8 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if let Some(rest) = f.strip_prefix("rate:") {
|
||||
let (n, adv) = rest.split_once(':').unwrap_or(("0", "0"));
|
||||
cull(app, "photo.rate", json!({"rating": n.parse::<u8>().unwrap_or(0)}), adv == "1");
|
||||
let label = if n == "0" { "Rating cleared".to_string() } else { format!("Rated {}", "★".repeat(n.parse().unwrap_or(0))) };
|
||||
let label =
|
||||
if n == "0" { "Rating cleared".to_string() } else { crate::i18n::tr_format!("Rated {}", "★".repeat(n.parse().unwrap_or(0))) };
|
||||
app.toast(ctx, label);
|
||||
} else if let Some(l) = f.strip_prefix("label:") {
|
||||
cull(app, "photo.label", json!({"label": l}), false);
|
||||
|
|
@ -217,7 +218,7 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if f == "tool.brush" && matches!(app.ui.view, crate::state::ViewMode::PhotoGrid | crate::state::ViewMode::SquareGrid) {
|
||||
if let Ok(r) = app.run("album.toggleTarget", json!({})) {
|
||||
let n = app.session.targets(&json!({})).len();
|
||||
let what = if n == 1 { "photo".to_string() } else { format!("{n} photos") };
|
||||
let what = if n == 1 { "photo".to_string() } else { crate::i18n::tr_format!("{n} photos", n = n) };
|
||||
let name = r["name"].as_str().unwrap_or("Quick Collection").to_string();
|
||||
app.toast(ctx, if r["added"] == true { format!("Added {what} to {name}") } else { format!("Removed {what} from {name}") });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ pub const MIN_PHOTO_WIDTH: f32 = 360.0;
|
|||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct UiState {
|
||||
#[serde(default)]
|
||||
#[serde(default = "crate::i18n::default_language")]
|
||||
pub language: crate::i18n::Language,
|
||||
/// The Build Previews run last announced (its identity, finished?).
|
||||
#[serde(skip)]
|
||||
|
|
@ -513,7 +513,7 @@ pub enum Dialog {
|
|||
impl Default for UiState {
|
||||
fn default() -> Self {
|
||||
UiState {
|
||||
language: crate::i18n::Language::default(),
|
||||
language: crate::i18n::default_language(),
|
||||
preview_build_seen: None,
|
||||
unsaved_seen: false,
|
||||
luminance_map_restore: None,
|
||||
|
|
|
|||
|
|
@ -110,17 +110,16 @@ pub fn install_fonts(ctx: &egui::Context) {
|
|||
let mut fonts = FontDefinitions::default();
|
||||
fonts.font_data.insert("Inter".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-Regular.ttf"))));
|
||||
fonts.font_data.insert("Inter-SemiBold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"))));
|
||||
fonts.font_data.insert("Japanese".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/BIZUDPGothic-Regular.ttf"))));
|
||||
fonts.font_data.insert("Japanese-Bold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/BIZUDPGothic-Bold.ttf"))));
|
||||
let fallback: Vec<String> = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
|
||||
let mut prop = vec!["Inter".to_string()];
|
||||
let mut prop = vec!["Inter".to_string(), "Japanese".to_string()];
|
||||
prop.extend(fallback.clone());
|
||||
fonts.families.insert(FontFamily::Proportional, prop);
|
||||
let mut semi = vec!["Inter-SemiBold".to_string()];
|
||||
let mut semi = vec!["Inter-SemiBold".to_string(), "Japanese-Bold".to_string()];
|
||||
semi.extend(fallback);
|
||||
fonts.families.insert(FontFamily::Name(FONT_SEMIBOLD.into()), semi);
|
||||
fonts.font_data.insert("japanese".into(), Arc::new(FontData::from_static(lightcraft_engine::export::WATERMARK_JAPANESE_FONT)));
|
||||
for stack in fonts.families.values_mut() {
|
||||
stack.push("japanese".into());
|
||||
}
|
||||
fonts.families.entry(FontFamily::Monospace).or_default().push("Japanese".into());
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,13 +54,13 @@ pub fn preview_only_notice(ui: &mut Ui, key: &str, reason: &str) {
|
|||
ui.horizontal(|ui| {
|
||||
let (ir, _) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::hover());
|
||||
paint(ui.painter(), ir, Icon::Info, t.caution);
|
||||
ui.label(egui::RichText::new("Preview only").font(t.semibold(12.5)).color(t.text));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Preview only")).font(t.semibold(12.5)).color(t.text));
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
ui.label(egui::RichText::new(preview_only_explanation(reason)).size(11.5).color(t.text_label));
|
||||
})
|
||||
.response;
|
||||
let r = r.on_hover_text(format!("Decoder: {reason}"));
|
||||
let r = r.on_hover_text(crate::i18n::tr_format!("Decoder: {reason}", reason = reason));
|
||||
r.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, true, "Preview only: editing the camera's embedded JPEG"));
|
||||
register(ui.ctx(), format!("notice:previewOnly:{key}"), r.rect);
|
||||
}
|
||||
|
|
@ -155,7 +155,7 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
|
|||
let id = ui.id().with(spec.id);
|
||||
let resp = ui.interact(track_rect.expand2(vec2(8.0, 2.0)), id, if enabled { Sense::click_and_drag() } else { Sense::hover() });
|
||||
// screen readers: a slider named after its control, with its value
|
||||
let label_text = label_override.unwrap_or(spec.label).to_string();
|
||||
let label_text = crate::i18n::tr(label_override.unwrap_or(spec.label)).to_string();
|
||||
resp.widget_info(|| egui::WidgetInfo::slider(enabled, value, label_text.clone()));
|
||||
let label_resp = ui.interact(label_rect, id.with("label"), Sense::click());
|
||||
register(ui.ctx(), format!("slider:{}", spec.id), track_rect);
|
||||
|
|
@ -223,7 +223,7 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
|
|||
let hovered = resp.hovered() || resp.dragged();
|
||||
let text_c = if enabled { t.text_label } else { t.text_disabled };
|
||||
let p = ui.painter();
|
||||
p.text(label_rect.left_center(), Align2::LEFT_CENTER, label_override.unwrap_or(spec.label), t.font(12.5), text_c);
|
||||
p.text(label_rect.left_center(), Align2::LEFT_CENTER, crate::i18n::tr(label_override.unwrap_or(spec.label)), t.font(12.5), text_c);
|
||||
let shown = if spec.id == "wb.temp" { format!("{v:.0}") } else { spec.format(v).replace("+0.00", "0").replace("-0.00", "0") };
|
||||
let shown = if shown == "+0" || shown == "-0" { "0".to_string() } else { shown };
|
||||
p.text(label_rect.right_center(), Align2::RIGHT_CENTER, shown, t.font(12.5), text_c);
|
||||
|
|
@ -256,6 +256,7 @@ pub fn nudged(spec: &ControlSpec, value: f64, steps: f64) -> f64 {
|
|||
|
||||
/// Collapsible section header ("› Light"). Returns the response (click toggles).
|
||||
pub fn section_header(ui: &mut Ui, id: &str, title: &str, open: bool, enabled: Option<bool>) -> (Response, Option<bool>) {
|
||||
let title = crate::i18n::tr(title);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let w = ui.available_width();
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 52.0), Sense::click());
|
||||
|
|
@ -296,6 +297,7 @@ pub fn divider(ui: &mut Ui) {
|
|||
|
||||
/// A flyout row inside a section (Curve, Color Mixer, Color Grading…): inset dark box with icon tile.
|
||||
pub fn flyout_row(ui: &mut Ui, id: &str, title: &str, icon: Icon, open: bool) -> Response {
|
||||
let title = crate::i18n::tr(title);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let w = ui.available_width();
|
||||
let (outer, _) = ui.allocate_exact_size(vec2(w, 40.0), Sense::hover());
|
||||
|
|
@ -320,6 +322,7 @@ pub fn flyout_row(ui: &mut Ui, id: &str, title: &str, icon: Icon, open: bool) ->
|
|||
|
||||
/// A small bordered text button (Auto, B&W, HDR…).
|
||||
pub fn text_button(ui: &mut Ui, id: &str, label: &str, active: bool) -> Response {
|
||||
let label = if id.starts_with("labelSet-") { label } else { crate::i18n::tr(label) };
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let font = t.semibold(11.5);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), font, t.text);
|
||||
|
|
@ -353,6 +356,7 @@ pub fn segmented(ui: &mut Ui, id: &str, items: &[(&str, &str)], active: Option<u
|
|||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = gap;
|
||||
for (j, (label, key)) in row.iter().enumerate() {
|
||||
let label = crate::i18n::tr(label);
|
||||
let i = row_i * per_row + j;
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(seg_w, 24.0), Sense::click());
|
||||
register(ui.ctx(), format!("button:{id}-{key}"), r);
|
||||
|
|
@ -365,7 +369,7 @@ pub fn segmented(ui: &mut Ui, id: &str, items: &[(&str, &str)], active: Option<u
|
|||
};
|
||||
let p = ui.painter();
|
||||
p.rect(r, CornerRadius::same(4), fill, Stroke::new(1.0, t.button_border), StrokeKind::Inside);
|
||||
p.text(r.center(), Align2::CENTER_CENTER, *label, t.semibold(11.5), t.text);
|
||||
p.text(r.center(), Align2::CENTER_CENTER, label, t.semibold(11.5), t.text);
|
||||
if resp.clicked() {
|
||||
clicked = Some(i);
|
||||
}
|
||||
|
|
@ -377,6 +381,7 @@ pub fn segmented(ui: &mut Ui, id: &str, items: &[(&str, &str)], active: Option<u
|
|||
|
||||
/// An icon-only button. `active` draws the selected background (tool strip).
|
||||
pub fn icon_button(ui: &mut Ui, id: &str, icon: Icon, size: egui::Vec2, active: bool, enabled: bool, tooltip: &str) -> Response {
|
||||
let tooltip = crate::i18n::tr(tooltip);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, resp) = ui.allocate_exact_size(size, if enabled { Sense::click() } else { Sense::hover() });
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, enabled, active, tooltip));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# LightCraft 日本語表示
|
||||
|
||||
LightCraft は日本語と英語(English)の表示に対応しています。
|
||||
言語設定は `ui.json` の `language` に保存します。
|
||||
|
||||
- 「編集 → 言語」または「設定 → 一般 → 言語」で日本語とEnglishを切り替えます。選択は次回起動にも引き継ぎます。
|
||||
- メニュー、写真編集、マスク、切り抜き、設定、読み込み、書き出し、主な処理状況を翻訳しています。
|
||||
- 通常表示と太字の双方に、SIL OFLのBIZ UDPGothicを組み込みました。
|
||||
- 翻訳は表示層で行います。操作コマンドのID、写真のファイル名、入力したメタデータを変更しません。
|
||||
- 翻訳されていない技術的なエラー、追加情報、リリースノートは英語で表示します。
|
||||
|
||||
## 翻訳の保守
|
||||
|
||||
静的な表示文言は `crates/ui-egui/locales/ja.json`、可変値を含む表示文言は
|
||||
`crates/ui-egui/locales/ja-formats.json` にあります。英文をキーにして翻訳を管理します。
|
||||
可変文言の両言語はビルド時にRustのフォーマット検査を受けます。
|
||||
英語の単複数語尾を日本語で省略する場合、対応する文字列引数は `{:.0}` で空にします。
|
||||
`LIGHTCRAFT_LANGUAGE=ja lightcraft-cli snapshot ...` で日本語の画面を描画できます。
|
||||
|
||||
表示・フォント・言語の切り替え・設定の保存・コマンドIDの保持は
|
||||
`cargo test -p lightcraft-ui-egui i18n::tests` で検証します。
|
||||
|
|
@ -505,7 +505,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
|||
| LR-BEHAV-EMPTY-STATES | Empty states | P1 | ✅ | `crates/ui-egui/src/panels/mod.rs` (`empty_message`) | |
|
||||
| LR-BEHAV-TOOLTIPS | Tooltips with shortcuts | P0 | ✅ | `crates/ui-egui/src/panels/bottombar.rs` | |
|
||||
| LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/grid.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control and value, buttons / icon buttons / dropdowns / section headers / sources their labels and state, grid thumbnails file name, rating, flag and label; the canvas tools (crop, masks) are pointer-only; not audited with a screen reader |
|
||||
| LR-BEHAV-LOCALIZE | Localisation | P2 | ⬜ | | |
|
||||
| LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization-ja.md` | Japanese/English UI with persisted language preference and Japanese regular/bold fonts; menus, edit, crop/masks, settings, import/export and primary progress messages. Technical errors/release notes and other locales remain untranslated. |
|
||||
| LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | |
|
||||
| LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights |
|
||||
| LR-BEHAV-AI-EA | Early-access badges | P2 | ⬜ | | |
|
||||
|
|
|
|||
Loading…
Reference in New Issue