mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #2502: LRC-LIB-IMPORT: month names in date templates (%B January, %
# Conflicts: # docs/whats-new.md
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
//! Template tokens (shared with import renaming and export file naming): `{name}` (current name
|
||||
//! without extension), `{num}` (the number at the end of the name: `IMG_0042` → `0042`), `{seq}` /
|
||||
//! `{seq:3}` (sequence number, zero-padded to 3 digits), `{date}` / `{date:%Y%m%d}` (capture date;
|
||||
//! `%Y %y %m %d %H %M %S`), `{folder}` (the original's folder name), `{camera}`, `{lens}`, `{iso}`,
|
||||
//! `%Y %y %m %B %b %d %H %M %S`), `{folder}` (the original's folder name), `{camera}`, `{lens}`, `{iso}`,
|
||||
//! `{rating}`, `{title}`, `{creator}`, `{ext}` (extension, without the dot). Unknown tokens stay as
|
||||
//! typed. Characters that aren't allowed in file names become `-`. The original extension is always
|
||||
//! kept.
|
||||
@@ -25,6 +25,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lightcraft_catalog::dates::MONTHS;
|
||||
use lightcraft_catalog::{Op, Photo, PhotoId, Source};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -57,9 +58,12 @@ fn sanitize(s: &str) -> String {
|
||||
t.trim().trim_matches('.').trim().to_string()
|
||||
}
|
||||
|
||||
/// `%Y%m%d`-style formatting of an ISO local time.
|
||||
/// `%Y%m%d`-style formatting of an ISO local time. `%B` / `%b` are the English month name
|
||||
/// (`January` / `Jan`), the same in every interface language so folder names stay put; a month
|
||||
/// that doesn't parse gives nothing.
|
||||
fn format_date(iso: &str, fmt: &str) -> String {
|
||||
let part = |a: usize, b: usize| iso.get(a..b).unwrap_or("00").to_string();
|
||||
let month = iso.get(5..7).and_then(|m| m.parse::<usize>().ok()).and_then(|m| m.checked_sub(1)).and_then(|i| MONTHS.get(i)).copied().unwrap_or("");
|
||||
let mut out = String::new();
|
||||
let mut it = fmt.chars();
|
||||
while let Some(c) = it.next() {
|
||||
@@ -71,6 +75,8 @@ fn format_date(iso: &str, fmt: &str) -> String {
|
||||
Some('Y') => out.push_str(&part(0, 4)),
|
||||
Some('y') => out.push_str(&part(2, 4)),
|
||||
Some('m') => out.push_str(&part(5, 7)),
|
||||
Some('B') => out.push_str(month),
|
||||
Some('b') => out.push_str(month.get(..3).unwrap_or(month)),
|
||||
Some('d') => out.push_str(&part(8, 10)),
|
||||
Some('H') => out.push_str(&part(11, 13)),
|
||||
Some('M') => out.push_str(&part(14, 16)),
|
||||
@@ -120,6 +126,8 @@ pub const DATE_DIRECTIVES: &[(&str, &str)] = &[
|
||||
("%Y", "year, 4 digits"),
|
||||
("%y", "year, 2 digits"),
|
||||
("%m", "month 01–12"),
|
||||
("%B", "month name (January)"),
|
||||
("%b", "short month name (Jan)"),
|
||||
("%d", "day 01–31"),
|
||||
("%H", "hour 00–23"),
|
||||
("%M", "minute"),
|
||||
@@ -1670,6 +1678,8 @@ pub(crate) mod tests {
|
||||
let mut p = sample_photo();
|
||||
assert_eq!(expand_folder("{date:%Y}/{date:%Y%m%d}", &p, 1), vec!["2026", "20260114"]);
|
||||
assert_eq!(expand_folder("{date:%Y}\\{date:%Y-%m}", &p, 1), vec!["2026", "2026-01"], "backslash separates too");
|
||||
assert_eq!(expand_folder("{date:%Y}/{date:%B}", &p, 1), vec!["2026", "January"]);
|
||||
assert_eq!(expand_folder("{date:%Y}/{date:%m %b}", &p, 1), vec!["2026", "01 Jan"]);
|
||||
// metadata can't add levels or climb out
|
||||
p.meta.camera = "../../etc/x".into();
|
||||
p.meta.title = "..".into();
|
||||
@@ -1711,6 +1721,12 @@ pub(crate) mod tests {
|
||||
let v = format_date("2026-01-14T05:58:48", d);
|
||||
assert!(!v.contains('%') || *d == "%%", "{d} is not a known directive");
|
||||
}
|
||||
assert_eq!(format_date("2026-09-30T14:05:00", "%B %b"), "September Sep");
|
||||
assert_eq!(format_date("2026-12-01T00:00:00", "%d %B %Y"), "01 December 2026");
|
||||
// a month that isn't 01–12 gives no name instead of panicking
|
||||
for bad in ["2026-00-01", "2026-13-01", "2026-x", "", "2026-é1-01"] {
|
||||
assert_eq!(format_date(bad, "%B|%b"), "|", "{bad:?}");
|
||||
}
|
||||
assert_eq!(unknown_tokens("{date}_{camra}-{seq:2}{x}{camra}"), vec!["{camra}".to_string(), "{x}".to_string()]);
|
||||
assert!(unknown_tokens("{name}{ext}{date:%Y}").is_empty());
|
||||
let json = token_help_json();
|
||||
|
||||
+1
-1
@@ -170,7 +170,7 @@ per-request `_meta` receive `resultType: complete` and list/read cache hints; re
|
||||
|---|---|
|
||||
| `list_commands {filter?}` | Every command: id, label, menu, shortcut, parameter doc, enabled now |
|
||||
| `run_command {command, params?}` | Run any command by id |
|
||||
| `import {paths, album?, mode?, destination?, organize?, rename?}` | Import files/folders (folders are scanned recursively); the first new photo becomes active. `mode: "copy"` copies into `destination` (default: the library's Originals/), `mode: "move"` moves there (each original and its XMP sidecars are removed from the source only after the copy is verified and catalogued; duplicates and failures keep their sources; the result's `moved` / `kept` list what moved and what stayed, and why; undo leaves the moved files at the destination), filed by `organize`: `date` (YYYY/YYYY-MM-DD), `month`, `flat` or a folder template such as `{date:%Y}/{date:%Y%m%d}` (→ `2026/20260114`; capture date, else the import date; always inside the destination), and named by the `rename` template (`run_command photo.renameTokens` lists the tags) |
|
||||
| `import {paths, album?, mode?, destination?, organize?, rename?}` | Import files/folders (folders are scanned recursively); the first new photo becomes active. `mode: "copy"` copies into `destination` (default: the library's Originals/), `mode: "move"` moves there (each original and its XMP sidecars are removed from the source only after the copy is verified and catalogued; duplicates and failures keep their sources; the result's `moved` / `kept` list what moved and what stayed, and why; undo leaves the moved files at the destination), filed by `organize`: `date` (YYYY/YYYY-MM-DD), `month`, `flat` or a folder template such as `{date:%Y}/{date:%Y%m%d}` (→ `2026/20260114`) or `{date:%Y}/{date:%B}` (→ `2026/January`; capture date, else the import date; always inside the destination), and named by the `rename` template (`run_command photo.renameTokens` lists the tags) |
|
||||
| `query_photos {filter?, sort?, offset?, limit?}` | Photos in the current view (or matching a catalog `Filter`) |
|
||||
| `select_photos {ids, active?, mode?}` | Set the selection / active photo. Every id (and `active`) must be in the library: an unknown id is a tool error (`no such photo 9999`) and the selection and active photo are left as they were |
|
||||
| `list_controls {section?}` | Every develop slider: id (`light.exposure`…), range, default, current value |
|
||||
|
||||
@@ -698,7 +698,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
||||
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LRC-LIB-IMPORT | Full import dialog | P1 | ✅ | `cmd:library.import` (`mode`, `destination`, `organize`, `rename`, `metadataPreset`, `preset`, `keywords`, `album`), `crates/ui-egui/src/import.rs` | review grid with duplicates skipped; add in place or copy (library Originals or any folder; by day / by month / one folder / custom folder template such as `{date:%Y}/{date:%Y%m%d}` → 2026/20260114, kept inside the destination; example destination shown; rename template with live example, Tags picker (insert at cursor) and unknown-tag warning, numbered across the import); develop preset, metadata preset, keywords, album on import |
|
||||
| LRC-LIB-IMPORT | Full import dialog | P1 | ✅ | `cmd:library.import` (`mode`, `destination`, `organize`, `rename`, `metadataPreset`, `preset`, `keywords`, `album`), `crates/ui-egui/src/import.rs` | review grid with duplicates skipped; add in place or copy (library Originals or any folder; by day / by month / one folder / custom folder template such as `{date:%Y}/{date:%Y%m%d}` → 2026/20260114 or `{date:%Y}/{date:%B}` → 2026/January, kept inside the destination; example destination shown; rename template with live example, Tags picker (insert at cursor) and unknown-tag warning, numbered across the import); develop preset, metadata preset, keywords, album on import |
|
||||
| LRC-LIB-AUTOIMPORT | Watched-folder import | P2 | ✅ | `cmd:library.autoImport` | see LR-IMP-AUTO |
|
||||
| LRC-LIB-TETHER | Tethered capture | P2 | ⬜ | | |
|
||||
| LRC-LIB-VIEWS | Grid / loupe / compare / survey / people | P1 | 🟡 | `cmd:view.photoGrid`, `cmd:view.squareGrid`, `cmd:view.detail`, `cmd:view.compare`, `cmd:view.survey`, `cmd:view.gridInfo` | grid, square grid, loupe, compare, survey, second window; square-grid captions: file name, exposure or capture date (View ▸ Grid Info; no people view; picking one in Photo Grid switches to Square Grid) |
|
||||
|
||||
@@ -115,6 +115,13 @@
|
||||
`photo.setCaptureTime` counts those photos as `unreadable`. Set date & time still repairs such a photo selected on
|
||||
its own; with other photos selected it says why it can't move them by the same amount. When the dialog can't make a
|
||||
change (a time that isn't a date, a shift past year 9999), it now says why and stays open, instead of closing.
|
||||
## 2026-10-21
|
||||
|
||||
### Month names in folder and file name templates
|
||||
- Date templates take the month by name: `%B` gives January, `%b` gives Jan. Import ▸ Folders ▸ Custom template…
|
||||
`{date:%Y}/{date:%B}` files photos into 2026/January, 2026/February…, the layout many existing libraries already
|
||||
use. The names are English in every interface language, so folders don't change when the language does. Rename
|
||||
Photos and export file names take them too.
|
||||
|
||||
## 2026-10-19
|
||||
|
||||
|
||||
Reference in New Issue
Block a user