strom-research 1.17.0 → 1.18.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.cs.md CHANGED
@@ -5,6 +5,24 @@
5
5
  Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou na
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (anglicky).
7
7
 
8
+ ## 1.18.0
9
+
10
+ *Připravuje se: bety do 1.18.0-beta.1. Oddíl bude úplný s vydáním.*
11
+
12
+ - **Fotky z telefonu nastojato.** Fotka otočená podle údaje EXIF se čte tak, jak se má dívat, nastojato; místa na ní
13
+ vyznačená (výřezy, poloviny dvoustrany, uložená část) jsou místa obrázku, jak se ukazuje. Fotka sama zůstává, jak
14
+ byla.
15
+ - **Fotky HEIC a dokumenty PDF jdou přečíst.** Fotky HEIC z telefonu a stránky PDF se pro čtení převádějí na JPEG —
16
+ na macOS, Windows i Linuxu — a originál zůstává, jak přišel. Fotka HEIC jde do aplikace Strom jako výřez JPEG.
17
+ - **Řečeno, když obrázek převést nejde.** Kde fotku nebo stránku PDF na počítači nic nepřevede, řekne to jedna věta
18
+ i s tím, co by pomohlo, v jazyce výzkumu.
19
+ - **Převedené obrázky v pořádku.** `strom tidy` převedené obrázky po 30 dnech nebo nad 500 MB uklidí; když jsou
20
+ potřeba, vzniknou znovu.
21
+ - **Souhlas v jednom jazyce.** Souhlas, který dává jen člověk (rozbalení výzkumu, odebrání stromu, odmítnuté okno),
22
+ zazní celý v jazyce výzkumu, nic z něj nezůstane anglicky.
23
+ - **Odmítnutá aktualizace.** Když se `strom update` odmítne, řekne, že se nic nenainstalovalo a která verze zůstává,
24
+ a skončí chybovým kódem; nepovedená instalace se řekne také v jazyce výzkumu.
25
+
8
26
  ## 1.17.0
9
27
 
10
28
  *19. 10. 2026 · patří k aplikaci Strom 3.13.1.*
package/CHANGELOG.de.md CHANGED
@@ -5,6 +5,27 @@
5
5
  Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Installationszeilen stehen bei
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (auf Englisch).
7
7
 
8
+ ## 1.18.0
9
+
10
+ *In Vorbereitung: die Betas bis 1.18.0-beta.1. Der Abschnitt ist mit dem Release vollständig.*
11
+
12
+ - **Handyfotos aufrecht.** Ein Foto, das nach seiner EXIF-Ausrichtung gedreht ist, wird gelesen, wie es gesehen
13
+ werden soll, aufrecht; die darauf markierten Stellen (Ausschnitte, die Hälften einer Doppelseite, ein gespeicherter
14
+ Teil) sind die des Bildes, wie es gezeigt wird. Das Foto selbst bleibt, wie es war.
15
+ - **HEIC-Fotos und PDF-Dokumente lesbar.** HEIC-Fotos vom Handy und die Seiten einer PDF werden zum Lesen in JPEG
16
+ umgewandelt — unter macOS, Windows und Linux —, das Original bleibt, wie es kam. Ein HEIC-Foto geht als
17
+ JPEG-Ausschnitt in die Strom-App.
18
+ - **Gesagt, wenn ein Bild nicht umgewandelt werden kann.** Wo nichts auf dem Computer ein Foto oder eine PDF-Seite
19
+ umwandelt, sagt es ein Satz, auch was helfen würde, in der Sprache der Forschung.
20
+ - **Umgewandelte Bilder in Ordnung gehalten.** `strom tidy` räumt umgewandelte Bilder nach 30 Tagen oder über 500 MB
21
+ weg; bei Bedarf entstehen sie neu.
22
+ - **Eine Zustimmung in einer Sprache.** Eine Zustimmung, die nur ein Mensch gibt (eine Forschung entpacken, einen
23
+ Stammbaum entfernen, ein abgelehntes Fenster), steht ganz in der Sprache der Forschung, nichts davon bleibt
24
+ Englisch.
25
+ - **Ein abgelehntes Update.** Wird `strom update` abgelehnt, sagt es, dass nichts installiert wurde und welche
26
+ Version bleibt, und endet mit einem Fehlercode; eine misslungene Installation wird auch in der Sprache der
27
+ Forschung gesagt.
28
+
8
29
  ## 1.17.0
9
30
 
10
31
  *19.10.2026 · gehört zur Strom-App 3.13.1.*
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ English · [Čeština](CHANGELOG.cs.md) · [Deutsch](CHANGELOG.de.md)
5
5
  The changes of each version in short. The full notes and the install lines are on
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases).
7
7
 
8
+ ## 1.18.0
9
+
10
+ *In preparation: the betas up to 1.18.0-beta.1. The section is complete with the release.*
11
+
12
+ - **Phone photos upright.** A photo turned by its EXIF orientation is read as it is meant to be seen, upright; the
13
+ places marked on it (excerpts, the halves of a spread, a cut-out part) are of the picture as it is shown. The
14
+ photo itself stays as it was.
15
+ - **HEIC photos and PDF documents can be read.** HEIC photos from a phone and the pages of a PDF are converted to
16
+ JPEG for reading — on macOS, Windows and Linux — and the original is kept as it came. A HEIC photo goes to the
17
+ Strom app as a JPEG excerpt.
18
+ - **Said when a picture cannot be converted.** Where nothing on the computer converts a photo or a PDF page, one
19
+ sentence says so and what would help, in the research's language.
20
+ - **Converted pictures kept in order.** `strom tidy` clears the converted pictures away after 30 days or above
21
+ 500 MB; they are made again when needed.
22
+ - **A consent in one language.** A consent only a person gives (unpacking a research, removing a tree, a window said
23
+ no to) is said whole in the research's language, nothing of it left in English.
24
+ - **An update said no to.** When `strom update` is declined, it says nothing was installed and which version stays,
25
+ and ends with an error code; a failed install is said in the research's language too.
26
+
8
27
  ## 1.17.0
9
28
 
10
29
  *2026-10-19 · goes with the Strom app 3.13.1.*
@@ -59,7 +59,6 @@
59
59
  "ui.dialog.title": "Strom výzkum",
60
60
  "ui.dialog.yes": "Povolit",
61
61
  "ui.dialog.no": "Nepovolit",
62
- "ui.dialog.refused": "Nepovoleno – nic se nezměnilo.",
63
62
  "ui.link.isolated": "Tohle je izolovaná instalace (STROM_ISOLATED): odkazy strom-research:// nezapisuje – zůstávají běžné instalaci stromu.",
64
63
  "ui.doc.isolated": "izolovaná instalace v {folder}",
65
64
  "ui.doc.second": "druhá instalace: příkaz {command}, ve složce {folder}, její odkazy {scheme}://",
@@ -760,9 +759,10 @@
760
759
  "ui.update.available": "Vyšla nová verze: {version} (tato je {current}) – nainstaluje ji strom update.",
761
760
  "ui.update.npm": "Vyšla nová verze: {version}. strom je tu nainstalovaný přes npm: npm install -g strom-research@{tag}",
762
761
  "ui.update.sure": "Aktualizovat strom z {current} na {version}? Výzkumy zůstanou, jak jsou.",
763
- "ui.update.later": "Teď ne – kdykoli strom update.",
762
+ "ui.update.later": "Neaktualizováno – nic se nenainstalovalo, strom zůstává ve verzi {current}. Kdykoli znovu: {command}",
764
763
  "ui.update.downloading": "Stahuji strom {version}…",
765
- "ui.update.failed": "Aktualizace se nepovedla: {detail}. strom zůstává, jak byl.",
764
+ "ui.error.update.failed": "aktualizace se nepovedla: {detail}. strom zůstává ve verzi {current}",
765
+ "ui.error.update.failed.hint": "znovu {command} – nebo spustit instalátor znovu",
766
766
  "ui.update.done": "✓ strom je aktualizovaný: {previous} → {version}.",
767
767
  "ui.update.whatsnew": "Co je nového: {url}",
768
768
  "ui.update.restart": "strom je aktualizovaný — tahle nabídka ale ještě běží ve staré verzi. Po novém spuštění stromu (nebo zástupce „{shortcut}“) poběží nová verze.",
@@ -1022,6 +1022,7 @@
1022
1022
  "ui.tidy.kind.views": "Náhledy skenů (udělají se znovu, až budou potřeba)",
1023
1023
  "ui.tidy.kind.excerpts": "Výřezy ze skenů (udělají se znovu, až budou potřeba)",
1024
1024
  "ui.tidy.kind.fetch": "Pracovní složky přerušených stahování",
1025
+ "ui.tidy.kind.converted": "Fotky a strany PDF převedené pro čtení (vytvoří se znovu, až budou potřeba)",
1025
1026
  "ui.tidy.kind.kept": "Starší poslání z aplikace Strom, už zapsaná (změny jsou v historii)",
1026
1027
  "ui.tidy.kind.inbox": "Dávná poslání z aplikace Strom, která ve schránce nechal starší strom",
1027
1028
  "ui.tidy.kind.pointers": "Zbytky skončených pracovních sezení",
@@ -1040,6 +1041,8 @@
1040
1041
  "ui.tidy.hint": "Vedle výzkumu si strom drží {strom}, z toho jde uvolnit {size}: strom tidy (v menu Nastavení → Údržba → Místo na disku)",
1041
1042
  "ui.error.consent": "tady je potřeba souhlas – {question}",
1042
1043
  "ui.error.consent.hint": "spustit ve vlastním terminálu: {set}",
1044
+ "ui.error.consent.refused": "nepovoleno – nic se nezměnilo",
1045
+ "ui.error.consent.refused.hint": "kdykoli znovu: {set}",
1043
1046
  "ui.status.disk": "Na disku: výzkum {research} · vedle něj si strom drží {strom}",
1044
1047
  "ui.status.disk.frees": " (strom tidy uvolní {size})",
1045
1048
  "ui.status.refused": "{host} odmítl automatický přístup stromu — snímky ručně; zrušit: strom allow host {host} --unblock",
@@ -1215,6 +1218,19 @@
1215
1218
  "ui.error.tree.empty": "soubor je prázdný, nejsou v něm žádné osoby",
1216
1219
  "ui.error.tree.empty.hint": "soubor vyexportovaný z aplikace Strom nebo z jiného programu pro rodokmeny",
1217
1220
  "ui.error.tree.unreadable": "tohle není soubor GEDCOM ani rodokmen z aplikace Strom",
1221
+ "ui.error.read.heic": "{file}: fotku HEIC nejde na tomto počítači převést pro čtení – originál zůstává beze změny",
1222
+ "ui.error.read.heic.windows.hint": "rozšíření HEIF a HEVC z Microsoft Storu, nebo fotku uložit jako JPEG",
1223
+ "ui.error.read.heic.mac.hint": "fotku uložit jako JPEG (Náhled: Soubor → Exportovat)",
1224
+ "ui.error.read.heic.linux.hint": "nainstalovat heif-convert (libheif) nebo ImageMagick, nebo fotku uložit jako JPEG",
1225
+ "ui.error.read.pdf-render": "{file}: toto PDF nejde na tomto počítači převést na obrázky pro čtení – originál zůstává beze změny",
1226
+ "ui.error.read.pdf-render.hint": "strany uložit jako JPEG",
1227
+ "ui.error.read.pdf-render.linux.hint": "nainstalovat pdftoppm (poppler-utils), nebo strany uložit jako JPEG",
1228
+ "ui.error.read.convert": "{file} se nepodařilo převést pro čtení – originál zůstává beze změny",
1229
+ "ui.error.read.convert.hint": "soubor uložit jako JPEG (umí to každý prohlížeč obrázků)",
1230
+ "ui.error.read.type": "{file} není obrázek, který jde přečíst (JPEG, PNG, HEIC nebo PDF) – originál zůstává beze změny",
1231
+ "ui.error.read.type.hint": "soubor uložit jako JPEG (umí to každý prohlížeč obrázků)",
1232
+ "ui.error.read.page": "{file} má {pages, plural, one {jednu stranu} few {# strany} other {# stran}}, strana {page} v něm není",
1233
+ "ui.error.read.page.hint": "strana od 1 do {pages}",
1218
1234
  "ui.error.adopt.none": "tento výzkum na žádný strom nečeká",
1219
1235
  "ui.error.tree.unknown": "žádný rodokmen „{name}“ tu není",
1220
1236
  "ui.error.tree.unknown.hint": "rodokmeny, které tu jsou: strom trees",
@@ -59,7 +59,6 @@
59
59
  "ui.dialog.title": "Strom Ahnenforschung",
60
60
  "ui.dialog.yes": "Erlauben",
61
61
  "ui.dialog.no": "Nicht erlauben",
62
- "ui.dialog.refused": "Nicht erlaubt – nichts wurde geändert.",
63
62
  "ui.link.isolated": "Dies ist eine isolierte Installation (STROM_ISOLATED): sie registriert keine strom-research://-Links – diese bleiben beim eigenen strom.",
64
63
  "ui.doc.isolated": "isolierte Installation in {folder}",
65
64
  "ui.doc.second": "zweite Installation: der Befehl {command}, in {folder}, ihre Links {scheme}://",
@@ -760,9 +759,10 @@
760
759
  "ui.update.available": "Eine neue Version ist da: {version} (diese ist {current}) – strom update installiert sie.",
761
760
  "ui.update.npm": "Eine neue Version ist da: {version}. strom ist hier mit npm installiert: npm install -g strom-research@{tag}",
762
761
  "ui.update.sure": "strom von {current} auf {version} aktualisieren? Die Forschungen bleiben, wie sie sind.",
763
- "ui.update.later": "Nicht jetzt – jederzeit strom update.",
762
+ "ui.update.later": "Nicht aktualisiert – nichts wurde installiert, strom bleibt bei {current}. Jederzeit erneut: {command}",
764
763
  "ui.update.downloading": "strom {version} wird heruntergeladen…",
765
- "ui.update.failed": "Die Aktualisierung hat nicht geklappt: {detail}. strom bleibt, wie er war.",
764
+ "ui.error.update.failed": "die Aktualisierung hat nicht geklappt: {detail}. strom bleibt bei {current}",
765
+ "ui.error.update.failed.hint": "erneut {command} – oder das Installationsprogramm erneut ausführen",
766
766
  "ui.update.done": "✓ strom ist aktualisiert: {previous} → {version}.",
767
767
  "ui.update.whatsnew": "Was ist neu: {url}",
768
768
  "ui.update.restart": "strom ist aktualisiert — dieses Menü läuft aber noch in der alten Version. Nach einem Neustart von strom (oder der Verknüpfung „{shortcut}“) läuft die neue Version.",
@@ -1022,6 +1022,7 @@
1022
1022
  "ui.tidy.kind.views": "Ansichten von Scans (werden bei Bedarf neu erstellt)",
1023
1023
  "ui.tidy.kind.excerpts": "Ausschnitte aus Scans (werden bei Bedarf neu erstellt)",
1024
1024
  "ui.tidy.kind.fetch": "Arbeitsordner abgebrochener Downloads",
1025
+ "ui.tidy.kind.converted": "Fotos und PDF-Seiten, zum Lesen umgewandelt (bei Bedarf neu erstellt)",
1025
1026
  "ui.tidy.kind.kept": "Ältere Sendungen der Strom-App, schon geschrieben (die Änderungen sind in der Geschichte)",
1026
1027
  "ui.tidy.kind.inbox": "Längst vergangene Sendungen der Strom-App, die ein älterer strom im Eingang ließ",
1027
1028
  "ui.tidy.kind.pointers": "Reste beendeter Arbeitssitzungen",
@@ -1040,6 +1041,8 @@
1040
1041
  "ui.tidy.hint": "Neben der Forschung hält strom {strom}, davon lassen sich {size} freigeben: strom tidy (im Menü Einstellungen → Wartung → Speicherplatz)",
1041
1042
  "ui.error.consent": "dafür braucht es eine Zustimmung – {question}",
1042
1043
  "ui.error.consent.hint": "im eigenen Terminal ausführen: {set}",
1044
+ "ui.error.consent.refused": "nicht erlaubt – nichts wurde geändert",
1045
+ "ui.error.consent.refused.hint": "jederzeit erneut: {set}",
1043
1046
  "ui.status.disk": "Auf der Festplatte: die Forschung {research} · daneben hält strom {strom}",
1044
1047
  "ui.status.disk.frees": " (strom tidy gibt {size} frei)",
1045
1048
  "ui.status.refused": "{host} hat den automatischen Zugriff von strom abgelehnt — Bilder von Hand; aufheben: strom allow host {host} --unblock",
@@ -1215,6 +1218,19 @@
1215
1218
  "ui.error.tree.empty": "die Datei ist leer, es sind keine Personen darin",
1216
1219
  "ui.error.tree.empty.hint": "eine Datei, aus der Strom-App oder einem anderen Stammbaumprogramm exportiert",
1217
1220
  "ui.error.tree.unreadable": "das ist weder eine GEDCOM-Datei noch ein Stammbaum der Strom-App",
1221
+ "ui.error.read.heic": "{file}: ein HEIC-Foto lässt sich auf diesem Computer nicht zum Lesen umwandeln – das Original bleibt unverändert",
1222
+ "ui.error.read.heic.windows.hint": "die Erweiterungen HEIF und HEVC aus dem Microsoft Store, oder das Foto als JPEG speichern",
1223
+ "ui.error.read.heic.mac.hint": "das Foto als JPEG speichern (Vorschau: Ablage → Exportieren)",
1224
+ "ui.error.read.heic.linux.hint": "heif-convert (libheif) oder ImageMagick installieren, oder das Foto als JPEG speichern",
1225
+ "ui.error.read.pdf-render": "{file}: dieses PDF lässt sich auf diesem Computer nicht in Bilder zum Lesen umwandeln – das Original bleibt unverändert",
1226
+ "ui.error.read.pdf-render.hint": "die Seiten als JPEG speichern",
1227
+ "ui.error.read.pdf-render.linux.hint": "pdftoppm (poppler-utils) installieren, oder die Seiten als JPEG speichern",
1228
+ "ui.error.read.convert": "{file} ließ sich nicht zum Lesen umwandeln – das Original bleibt unverändert",
1229
+ "ui.error.read.convert.hint": "die Datei als JPEG speichern (jeder Bildbetrachter kann das)",
1230
+ "ui.error.read.type": "{file} ist kein Bild, das sich lesen lässt (JPEG, PNG, HEIC oder PDF) – das Original bleibt unverändert",
1231
+ "ui.error.read.type.hint": "die Datei als JPEG speichern (jeder Bildbetrachter kann das)",
1232
+ "ui.error.read.page": "{file} hat {pages, plural, one {eine Seite} other {# Seiten}}, keine Seite {page}",
1233
+ "ui.error.read.page.hint": "eine Seite von 1 bis {pages}",
1218
1234
  "ui.error.adopt.none": "diese Forschung wartet auf keinen Stammbaum",
1219
1235
  "ui.error.tree.unknown": "keinen Stammbaum „{name}“ gibt es hier",
1220
1236
  "ui.error.tree.unknown.hint": "die Stammbäume hier: strom trees",
@@ -209,8 +209,10 @@ export class Context {
209
209
  const answer = this.io.dialog ? this.io.dialog(text) : systemDialog(text, this.env);
210
210
  if (answer === true)
211
211
  return "window";
212
+ // said no: an agent reads the English and what not to do; a person (the app, a script) their language — nothing
213
+ // changed and how to ask again (printError: ui.error.consent.refused), never the agent's English under it
212
214
  if (answer === false)
213
- throw new StromError(ui(lang, "ui.dialog.refused"), { hint: `the user said no to: ${question} — do not ask again unless they want to` });
215
+ throw new StromError(ui("en", "ui.error.consent.refused"), { hint: `the user said no to: ${question} — do not ask again unless they want to`, code: "consent.refused", params: { set } });
214
216
  }
215
217
  // a person (a script, no terminal) reads it in their language; an agent the English it goes by
216
218
  // a run nobody watches: nobody to say yes before it ends — a task for it, for the user to see, and on another way
package/dist/cli/ui.js CHANGED
@@ -15,7 +15,6 @@ export const UI = {
15
15
  "ui.dialog.title": "Strom research",
16
16
  "ui.dialog.yes": "Allow",
17
17
  "ui.dialog.no": "Don't allow",
18
- "ui.dialog.refused": "Not allowed — nothing was changed.",
19
18
  "ui.link.isolated": "This is an isolated installation (STROM_ISOLATED): it registers no strom-research:// links — they stay the regular strom's.",
20
19
  "ui.doc.isolated": "isolated installation in {folder}",
21
20
  "ui.doc.second": "a second installation: the command {command}, in {folder}, its links {scheme}://",
@@ -687,9 +686,10 @@ export const UI = {
687
686
  "ui.update.available": "A new version is out: {version} (this is {current}) — strom update installs it.",
688
687
  "ui.update.npm": "A new version is out: {version}. strom is installed with npm here: npm install -g strom-research@{tag}",
689
688
  "ui.update.sure": "Update strom from {current} to {version}? The research stays as it is.",
690
- "ui.update.later": "Not now — strom update any time.",
689
+ "ui.update.later": "Not updated — nothing was installed, strom stays at {current}. Any time again: {command}",
691
690
  "ui.update.downloading": "Downloading strom {version}…",
692
- "ui.update.failed": "The update did not work: {detail}. strom stays as it was.",
691
+ "ui.error.update.failed": "the update did not work: {detail}. strom stays at {current}",
692
+ "ui.error.update.failed.hint": "{command} again — or run the installer again",
693
693
  "ui.update.done": "✓ strom is updated: {previous} → {version}.",
694
694
  "ui.update.whatsnew": "What's new: {url}",
695
695
  "ui.update.restart": "strom is updated — this menu still runs the old version. Start strom again (or the shortcut \"{shortcut}\"), and the new version is at work.",
@@ -926,6 +926,7 @@ export const UI = {
926
926
  "ui.tidy.kind.views": "Views of scans (made again when needed)",
927
927
  "ui.tidy.kind.excerpts": "Excerpts of scans (made again when needed)",
928
928
  "ui.tidy.kind.fetch": "Work folders of downloads cut short",
929
+ "ui.tidy.kind.converted": "Photos and PDF pages converted for reading (made again when needed)",
929
930
  "ui.tidy.kind.kept": "Sends of the Strom app written long ago (their changes are in the history)",
930
931
  "ui.tidy.kind.inbox": "Sends of the Strom app long ago, left in the inbox by an older strom",
931
932
  "ui.tidy.kind.pointers": "Leftovers of work sessions that ended",
@@ -944,6 +945,8 @@ export const UI = {
944
945
  "ui.tidy.hint": "Beside the research strom keeps {strom}, {size} of it can go: strom tidy (the menu: Settings → Maintenance → Disk space)",
945
946
  "ui.error.consent": "this needs a yes — {question}",
946
947
  "ui.error.consent.hint": "run in a terminal of its own: {set}",
948
+ "ui.error.consent.refused": "not allowed — nothing was changed",
949
+ "ui.error.consent.refused.hint": "any time again: {set}",
947
950
  "ui.status.disk": "On disk: the research {research} · strom's own beside it {strom}",
948
951
  "ui.status.disk.frees": " (strom tidy frees {size})",
949
952
  "ui.status.refused": "{host} refused strom's automated access — images by hand; to lift it: strom allow host {host} --unblock",
@@ -1128,6 +1131,19 @@ export const UI = {
1128
1131
  "ui.error.tree.empty": "the file is empty: no people in it",
1129
1132
  "ui.error.tree.empty.hint": "a file exported from the Strom app or another family-tree program",
1130
1133
  "ui.error.tree.unreadable": "this is neither a GEDCOM file nor a family tree of the Strom app",
1134
+ "ui.error.read.heic": "{file}: a HEIC photo cannot be converted for reading on this computer — the original stays as it is",
1135
+ "ui.error.read.heic.windows.hint": "the HEIF and HEVC extensions from the Microsoft Store, or the photo saved as JPEG",
1136
+ "ui.error.read.heic.mac.hint": "the photo saved as JPEG (Preview: File → Export)",
1137
+ "ui.error.read.heic.linux.hint": "heif-convert (libheif) or ImageMagick installed, or the photo saved as JPEG",
1138
+ "ui.error.read.pdf-render": "{file}: this PDF cannot be turned into pictures for reading on this computer — the original stays as it is",
1139
+ "ui.error.read.pdf-render.hint": "the pages saved as JPEG",
1140
+ "ui.error.read.pdf-render.linux.hint": "pdftoppm (poppler-utils) installed, or the pages saved as JPEG",
1141
+ "ui.error.read.convert": "{file} could not be converted for reading — the original stays as it is",
1142
+ "ui.error.read.convert.hint": "the file saved as JPEG (any image viewer can)",
1143
+ "ui.error.read.type": "{file} is no picture that can be read (JPEG, PNG, HEIC or PDF) — the original stays as it is",
1144
+ "ui.error.read.type.hint": "the file saved as JPEG (any image viewer can)",
1145
+ "ui.error.read.page": "{file} has {pages, plural, one {one page} other {# pages}}, no page {page}",
1146
+ "ui.error.read.page.hint": "a page from 1 to {pages}",
1131
1147
  "ui.error.adopt.none": "this research waits for no tree",
1132
1148
  "ui.error.tree.unknown": "no tree \"{name}\"",
1133
1149
  "ui.error.tree.unknown.hint": "strom trees",
@@ -10,6 +10,8 @@ import { counted, lines, truncate } from "../cli/format.js";
10
10
  import { UsageError } from "../core/errors.js";
11
11
  import { normId, requireRecord, update } from "../core/records.js";
12
12
  import { makeView } from "../core/views.js";
13
+ import { shownToFile } from "../image/image.js";
14
+ import { exifOrientation } from "../image/index.js";
13
15
  import { clipText, MAX_CLIPS, regionText } from "../core/media.js";
14
16
  import { withMargin } from "../core/excerpt.js";
15
17
  import { batches, READER_MINUTES_HELP } from "../core/reader.js";
@@ -161,10 +163,12 @@ register({
161
163
  // what the readers cost goes to the session that started them (apart from the agent's own)
162
164
  addReaders(tree, ctx.env, ctx.refs, { usd: r0.cost(), runs: r0.runs(), partial: r0.partial() });
163
165
  // 3. What passed becomes the source's clips.
166
+ // the readers saw each image upright; a clip is kept as the file lies (the app turns it as it turns the image)
164
167
  const clipsOf = new Map();
168
+ const asFile = (l) => shownToFile(l.region, exifOrientation(path.join(shared, all.get(l.media).file)));
165
169
  for (const l of hits)
166
170
  if (passed.has(`${l.source} ${l.media}`))
167
- clipsOf.set(l.source, [...(clipsOf.get(l.source) ?? []), { media: l.media, region: l.region }].slice(0, MAX_CLIPS));
171
+ clipsOf.set(l.source, [...(clipsOf.get(l.source) ?? []), { media: l.media, region: asFile(l) }].slice(0, MAX_CLIPS));
168
172
  for (const [id, clips] of clipsOf)
169
173
  update(tree, id, "source", (s) => ({ ...s, clips }), { op: "source.edit", summary: `${id} clips ${clips.map(clipText).join(" ")} (found by a reader, checked)` });
170
174
  // The reports are working notes of the research: committed with it.
@@ -11,9 +11,10 @@ import { notFound, UsageError } from "../core/errors.js";
11
11
  import { makeNote } from "../core/actions.js";
12
12
  import { clipText, collectFiles, coveringPart, fileSha256, findImage, imageNumbers, imageOfRef, imageRange, IMAGE_FILE_EXT, inboxFolders, inputPath, isWhole, mimeOf, onlyParts, otherCopies, parseImageList, regionText, sharperPart, storeShared, wholeSizeOf } from "../core/media.js";
13
13
  import { create, normId, requireRecord, update } from "../core/records.js";
14
- import { imageSizeOfFile } from "../image/index.js";
14
+ import { exifOrientation, imageSizeOfFile } from "../image/index.js";
15
+ import { fileToShown, shownToFile, turnsAcross } from "../image/image.js";
15
16
  import { imageOf, pageOf } from "../core/calibration.js";
16
- import { cropOf, describeView, ENLARGED_HINT, halves, makeView, OVERVIEW_HINT, parseCrop, partRegion, readInHalves, REDUCED_HINT, SHARPER_SCAN, viewLine, viewRegion, viewSize } from "../core/views.js";
17
+ import { cropOf, describeView, ENLARGED_HINT, halves, makeView, mediaOrient, OVERVIEW_HINT, parseCrop, partRegion, readInHalves, REDUCED_HINT, SHARPER_SCAN, viewLine, viewRegion, viewSize } from "../core/views.js";
17
18
  import { IMAGE_MAX, IMAGE_MAX_LARGE, OVERVIEW_MAX } from "../agents/images.js";
18
19
  import { viewSizesFor } from "../core/viewsizes.js";
19
20
  import { PROFILES } from "../agents/profiles.js";
@@ -141,13 +142,18 @@ function resolveTarget(tree, shared, ref, opts) {
141
142
  throw new UsageError(`image ${image} of ${b.id} is not registered`, { hint: `strom media list --recordset ${b.id} · strom media add <files> --recordset ${b.id}` });
142
143
  return { key: m.id, file: path.join(shared, m.file), media: m };
143
144
  }
144
- const id = normId(ref);
145
+ // a page of a document the user gave: I0002:3
146
+ const paged = /^([Ii]\d+):(\d+)$/.exec(ref.trim());
147
+ const id = normId(paged ? paged[1] : ref);
145
148
  if (id.startsWith("I")) {
146
149
  const i = requireRecord(tree, id, "input");
147
150
  const file = inputPath(tree, i);
148
151
  if (!file)
149
152
  throw new UsageError(`${i.id} has no file`);
150
- return { key: i.id, file };
153
+ const page = paged ? Number(paged[2]) : undefined;
154
+ if (page !== undefined && page < 1)
155
+ throw new UsageError(`no page ${page}`, { hint: `${i.id}:1 is its first page` });
156
+ return { key: i.id, file, ...(page !== undefined ? { page } : {}) };
151
157
  }
152
158
  const m = requireRecord(tree, id, "media");
153
159
  return { key: m.id, file: path.join(shared, m.file), media: m };
@@ -243,8 +249,10 @@ function partSpecs(t, p, max, inHalves = false) {
243
249
  const size = ofParts ?? (t.media?.width && t.media.height ? { width: t.media.width, height: t.media.height } : imageSizeOfFile(t.file));
244
250
  if (!size)
245
251
  return [{ half: p.half, crop: p.crop, label: p.label }]; // no image there: the view says why
246
- const W = size.width;
247
- const H = size.height;
252
+ // the boxes are of the picture as the view shows it: a photo turned a quarter by its EXIF has its sides swapped
253
+ const across = !ofParts && turnsAcross(exifOrientation(t.file));
254
+ const W = across ? size.height : size.width;
255
+ const H = across ? size.width : size.height;
248
256
  let boxes;
249
257
  if (p.both) {
250
258
  // the halves rule: the two pages of a spread only where they are sharper than one view of it — a book read worse
@@ -489,11 +497,11 @@ function resolveTargets(tree, shared, all, args, opts, parts, maxViews = VIEW_MA
489
497
  }
490
498
  else {
491
499
  const r = resolveTarget(tree, shared, w.ref, {});
492
- t = { ...r, ref: r.media && !r.media.part && r.media.recordset && r.media.image !== undefined ? `${r.media.recordset}:${r.media.image}` : r.key };
500
+ t = { ...r, ref: r.media && !r.media.part && r.media.recordset && r.media.image !== undefined ? `${r.media.recordset}:${r.media.image}` : r.page ? `${r.key}:${r.page}` : r.key };
493
501
  }
494
- if (seen.has(t.key))
502
+ if (seen.has(`${t.key}:${t.page ?? 1}`))
495
503
  continue;
496
- seen.add(t.key);
504
+ seen.add(`${t.key}:${t.page ?? 1}`);
497
505
  targets.push(t);
498
506
  }
499
507
  if (!targets.length && !several) {
@@ -513,24 +521,29 @@ function oneView(ctx, tree, shared, all, target, asked, meta = {}, noSharper = f
513
521
  let v;
514
522
  let sharper;
515
523
  if (t.media?.width && t.media.height && !t.media.part) {
516
- const px = viewRegion(spec, t.media.width, t.media.height);
517
- const s = sharperPart(all, t.media, { x: px.x / t.media.width, y: px.y / t.media.height, w: px.w / t.media.width, h: px.h / t.media.height });
524
+ // the place asked is of the picture shown (upright by its EXIF); parts are kept as the file lies
525
+ const o = mediaOrient(shared, t.media);
526
+ const [W, H] = turnsAcross(o) ? [t.media.height, t.media.width] : [t.media.width, t.media.height];
527
+ const px = viewRegion(spec, W, H);
528
+ const s = sharperPart(all, t.media, shownToFile({ x: px.x / W, y: px.y / H, w: px.w / W, h: px.h / H }, o));
518
529
  if (s) {
519
530
  sharper = s;
520
531
  shown = s.part;
521
532
  const whole = !s.part.part && !spec.crop && !spec.half;
522
- v = makeView(tree, path.join(shared, s.part.file), s.part.id, { ...spec, half: undefined, crop: whole ? undefined : regionText(s.crop) }, meta);
533
+ v = makeView(tree, path.join(shared, s.part.file), s.part.id, { ...spec, half: undefined, crop: whole ? undefined : regionText(fileToShown(s.crop, mediaOrient(shared, s.part))) }, meta);
523
534
  }
524
535
  }
525
- v ??= makeView(tree, t.file, t.key, spec, meta);
536
+ v ??= makeView(tree, t.file, t.key, t.page ? { ...spec, page: t.page } : spec, meta);
526
537
  const page = t.media ? pageLabel(tree, t.media) : "";
527
538
  // still too little detail: a connector that fetched the image may fetch this part of it sharper
528
539
  const base = shown?.part ?? { x: 0, y: 0, w: 1, h: 1 };
540
+ // what the view shows, as the picture lies in its file (a photo shown turned by its EXIF): where clips are kept
541
+ const inFile = shownToFile({ x: v.region.x / v.original.width, y: v.region.y / v.original.height, w: v.region.w / v.original.width, h: v.region.h / v.original.height }, v.orient);
529
542
  const inWhole = {
530
- x: base.x + (v.region.x / v.original.width) * base.w,
531
- y: base.y + (v.region.y / v.original.height) * base.h,
532
- w: (v.region.w / v.original.width) * base.w,
533
- h: (v.region.h / v.original.height) * base.h,
543
+ x: base.x + inFile.x * base.w,
544
+ y: base.y + inFile.y * base.h,
545
+ w: inFile.w * base.w,
546
+ h: inFile.h * base.h,
534
547
  };
535
548
  const whole = t.media?.recordset !== undefined && t.media.image !== undefined ? findImage(all, t.media.recordset, t.media.image) : undefined;
536
549
  const from = whole?.fetched ? listConnectors(shared).find((c) => c.name === whole.fetched.connector) : undefined;
@@ -538,11 +551,12 @@ function oneView(ctx, tree, shared, all, target, asked, meta = {}, noSharper = f
538
551
  const fetchPart = canPart && !noSharper && v.scale > 1.25 && inWhole.w * inWhole.h < 0.9
539
552
  ? `strom fetch ${from.name} --recordset ${whole.recordset} --images ${whole.image} --crop ${regionText(inWhole)}`
540
553
  : undefined;
541
- // The same part as a clip of the source read in it: of the whole image where it is registered.
554
+ // The same part as a clip of the source read in it: of the whole image where it is registered — as --clip takes it,
555
+ // of the picture as its view shows it (source add keeps it as the file lies)
542
556
  const clip = !t.media || (!spec.crop && !spec.half)
543
557
  ? undefined
544
558
  : whole && !whole.part
545
- ? clipText({ media: whole.id, region: inWhole })
559
+ ? clipText({ media: whole.id, region: fileToShown(inWhole, mediaOrient(shared, whole)) })
546
560
  : clipText({ media: t.key, region: { x: v.region.x / v.original.width, y: v.region.y / v.original.height, w: v.region.w / v.original.width, h: v.region.h / v.original.height } });
547
561
  return { v, page, ...(t !== target ? { part: t.media } : {}), ...(sharper ? { sharper } : {}), ...(fetchPart ? { fetchPart } : {}), ...(clip ? { clip } : {}) };
548
562
  }
@@ -697,7 +711,8 @@ register({
697
711
  if (it.image === undefined)
698
712
  throw new UsageError("which image is it a part of? --image <n>", { hint: "strom media add <file> --recordset B… --image <n> --half left" });
699
713
  const whole = findImage(tree.list("media").filter((m) => !m.part), recordset, it.image);
700
- it.part = partRegion(opts, whole);
714
+ // of the whole image as its view shows it (upright by its EXIF)
715
+ it.part = partRegion(opts, whole, mediaOrient(shared, whole));
701
716
  }
702
717
  if (recordset) {
703
718
  // an image of a book without its number cannot be cited — nor found again
@@ -845,7 +860,7 @@ register({
845
860
  "--half right; B0001 --image 2 --crop … B0001 --image 7 --crop …; B0001 2 --crop … 7 --crop …); parts after the\n" +
846
861
  "last image are of every image. At most\n" +
847
862
  `${VIEW_MAX_IMAGES} images and ${VIEW_MAX_VIEWS} views in one call: every view stays in the context that opens it.`,
848
- args: [{ name: "image", description: "M0012, B0001:57 (record set:image), a range B0001:57-60, B0001 with --page, or an input I0002; several = several images", required: true, variadic: true }],
863
+ args: [{ name: "image", description: "M0012, B0001:57 (record set:image), a range B0001:57-60, B0001 with --page, an input I0002, or a page of an input document I0002:3 (a PDF; HEIC and PDF are converted for reading, the original kept); several = several images", required: true, variadic: true }],
849
864
  options: [
850
865
  { name: "crop", type: "string", multiple: true, value: "<x,y,w,h>", description: "part of the image: fractions (0.1,0.35,0.4,0.2) or pixels; repeated = several parts; after one image of several: of that image alone (--half, --split, --grid too)" },
851
866
  { name: "half", type: "string", multiple: true, value: "<side>", description: "left or right page of a double page (top, bottom); both = the two pages, overlapping at the gutter" },
@@ -870,6 +885,7 @@ register({
870
885
  "strom media view B0001:2 --crop 0.05,0.10,0.45,0.30 --crop 0.05,0.40,0.45,0.30",
871
886
  "strom media view B0001:2 --crop 0.05,0.10,0.45,0.30 B0001:3 --crop 0.50,0.60,0.45,0.20",
872
887
  "strom media view B0001 --page 112 113 --split 2x2",
888
+ "strom media view I0005:2 --half top",
873
889
  ],
874
890
  run(ctx, { args, opts, seq }) {
875
891
  // the whole image is what a view is without a part of it (strom read --whole: each image in one view)
@@ -81,8 +81,9 @@ register({
81
81
  async run(ctx, { args }) {
82
82
  const lang = ctx.uiLang();
83
83
  const src = ctx.resolvePath(args[0]);
84
+ // the English an agent (and a program) goes by; a person (a script, a launcher) reads it in their language
84
85
  if (!ctx.interactive)
85
- throw new NeedsConsentError([{ key: "unpack", kind: "consent", question: ui(lang, "ui.unpack.person"), set: `strom unpack "${src}"` }]);
86
+ throw new NeedsConsentError([{ key: "unpack", kind: "consent", question: ui("en", "ui.unpack.person"), set: `strom unpack "${src}"` }], ui(lang, "ui.unpack.person"));
86
87
  // someone new to strom (a launcher of the package installed it just now): the setup wizard first
87
88
  if (!ctx.settings.home() && !isAgent(ctx.env)) {
88
89
  await setupWizard(ctx);
@@ -17,7 +17,8 @@ import { IMAGE_MAX, IMAGE_MAX_LARGE } from "../agents/images.js";
17
17
  import { viewSizesFor } from "../core/viewsizes.js";
18
18
  import { currentSession } from "../core/session.js";
19
19
  import { BATCH, BATCH_MAX, batches, NEXT_BLOCK, parseReport, READER_MINUTES_HELP, readerPrompt, readerSettings, viewCount, withoutNextBlock } from "../core/reader.js";
20
- import { imageSize } from "../image/index.js";
20
+ import { exifOrientation, shownSize } from "../image/index.js";
21
+ import { turnsAcross } from "../image/image.js";
21
22
  import { RUNNERS } from "../runners/index.js";
22
23
  import { which, withoutAgentMarks } from "../core/which.js";
23
24
  import { permissionPath } from "../agents/files.js";
@@ -306,13 +307,17 @@ register({
306
307
  const file = path.join(ctx.settings.shared().value, m.file);
307
308
  const page = m.page ?? (b && m.image !== undefined ? pageOf(b, m.image) : undefined);
308
309
  const contrast = Boolean(opts.contrast);
309
- if (typeof opts.crop === "string" && m.width && m.height) {
310
+ // the image as a reader sees it: upright (a photo turned by its EXIF has its sides swapped)
311
+ const across = fs.existsSync(file) && turnsAcross(exifOrientation(file));
312
+ const W = across ? m.height : m.width;
313
+ const H = across ? m.width : m.height;
314
+ if (typeof opts.crop === "string" && W && H) {
310
315
  // a crop is read at full resolution — in overlapping parts when it is bigger than one look
311
- const all = tiles(parseCrop(partCrops.get(m.id) ?? opts.crop, m.width, m.height), max);
316
+ const all = tiles(parseCrop(partCrops.get(m.id) ?? opts.crop, W, H), max);
312
317
  const parts = all.map((t) => ({ label: t.label, view: view(file, m, { crop: t.crop, max, contrast }, all.length > 1 ? "split" : "crop").file }));
313
318
  return { id: m.id, image: m.image, page, view: parts[0].view, ...(parts.length > 1 ? { parts } : {}) };
314
319
  }
315
- const size = !opts.whole && fs.existsSync(file) ? (m.width && m.height ? { width: m.width, height: m.height } : imageSize(new Uint8Array(fs.readFileSync(file)))) : undefined;
320
+ const size = !opts.whole && fs.existsSync(file) ? (W && H ? { width: W, height: H } : shownSize(new Uint8Array(fs.readFileSync(file)))) : undefined;
316
321
  if (size && half) {
317
322
  // one page of a spread, with the strip past the gutter (a scan is seldom centred on it)
318
323
  const v = view(file, m, { crop: cropOf(halves(size.width, size.height)[half]), max, contrast }, "half");
@@ -45,7 +45,7 @@ import { agentSettingsLine, chooseEffort } from "../cli/model-choice.js";
45
45
  import { calibrationKey, calibrationLabel, calibrationOffer, forgetCalibration, researchKey, sizesText, viewModel, viewSizesFor } from "../core/viewsizes.js";
46
46
  import { bookTuned, bookViewSizes, forgetTuning, keyTuningOf, readingOf, selfTune, treeTuning } from "../core/tune.js";
47
47
  import { loadAliases } from "../core/modelkey.js";
48
- import { NeedsInputError, StromError, UsageError } from "../core/errors.js";
48
+ import { EXIT, NeedsInputError, StromError, UsageError } from "../core/errors.js";
49
49
  import { worsenings } from "../core/tunereset.js";
50
50
  import { resetReading } from "../cli/tunereset.js";
51
51
  import { loadRollup, manyWeb } from "../core/readstats.js";
@@ -646,13 +646,16 @@ register({
646
646
  : newer
647
647
  ? t("ui.update.sure", { version: newest, current: VERSION })
648
648
  : t("ui.update.node.sure", { from: inst.node ?? "?", to: node.version });
649
+ const again = to ? `strom update --channel ${to}` : "strom update";
649
650
  if (isAgent(ctx.env) || !ctx.yes) {
651
+ // said no: said so, nothing installed and the version it stays at, and not ok (exit 1) — never a quiet 0 that
652
+ // reads as done (found: strom update --channel beta ended 0 at the old version, nobody knew why)
650
653
  if (!isAgent(ctx.env) && ctx.interactive) {
651
654
  if (!(await ctx.confirm(question, true)))
652
- return { text: t("ui.update.later"), data: { current: VERSION, latest: newest, updated: false, ...extra } };
655
+ return { text: t("ui.update.later", { current: VERSION, command: again }), data: { current: VERSION, latest: newest, updated: false, refused: true, ...extra }, exitCode: EXIT.error };
653
656
  }
654
657
  else
655
- ctx.requireHuman(`update strom to ${newest}${to ? ` (--channel ${to})` : ""}`, to ? `strom update --channel ${to}` : "strom update", "update", question);
658
+ ctx.requireHuman(`update strom to ${newest}${to ? ` (--channel ${to})` : ""}`, again, "update", question);
656
659
  }
657
660
  // Another channel, or an older version: every research and the settings backed up before anything is installed —
658
661
  // none made, nothing installed (Milan, 2026-10-07: "přechod beta ↔ produkce nikdy nepřijde o data")
@@ -666,7 +669,9 @@ register({
666
669
  done = await installUpdate(ctx.env, inst.root, process.platform, { release: latest, keep: ctx.researchFolders(), ...(switching ? { channel: to } : {}) });
667
670
  }
668
671
  catch (e) {
669
- throw new StromError(t("ui.update.failed", { detail: e.message }), { hint: "strom update — or run the installer again" });
672
+ // the person's language for a person (ui.error.update.failed and its hint), the English for an agent and a program
673
+ const params = { detail: e.message, current: VERSION, command: again };
674
+ throw new StromError(ui("en", "ui.error.update.failed", params), { hint: ui("en", "ui.error.update.failed.hint", params), code: "update.failed", params });
670
675
  }
671
676
  // The agents learn what the new version tells them.
672
677
  const { command, args } = stromLauncher();
@@ -11,7 +11,8 @@ import { makeNote, makeNotes, parseDate } from "../core/actions.js";
11
11
  import { calibrationLine, fitCalibration } from "../core/calibration.js";
12
12
  import { foldText } from "../core/text.js";
13
13
  import { clipNote, clipText, imageOfRef, MAX_CLIPS, transcriptNote } from "../core/media.js";
14
- import { partRegion } from "../core/views.js";
14
+ import { mediaOrient, partRegion } from "../core/views.js";
15
+ import { fileToShown } from "../image/image.js";
15
16
  import { formatName } from "../core/people.js";
16
17
  import { yearsOption, yearsOverlap } from "../core/years.js";
17
18
  import { levelWords } from "../core/archiveasks.js";
@@ -49,8 +50,9 @@ function mediaRefs(tree, refs) {
49
50
  /**
50
51
  * Where the entry is on an image, as its view was cut: "M0012@0.05,0.4,0.45,0.18", "B0001:57@left:0.1,0.4,0.8,0.1"
51
52
  * (a half, then a crop within it), pixels of the registered image, or "M0012" alone (the entry is the whole image).
53
+ * Of the picture as the view showed it (a photo upright, turned by its EXIF); kept as the picture lies in its file.
52
54
  */
53
- function clipRefs(tree, specs) {
55
+ function clipRefs(tree, specs, shared) {
54
56
  return specs.map((spec) => {
55
57
  const m = /^([^@]+?)(?:@(.+))?$/.exec(spec.trim()) ?? [];
56
58
  let [ref, where] = [m[1] ?? "", m[2]];
@@ -62,7 +64,8 @@ function clipRefs(tree, specs) {
62
64
  if (!where)
63
65
  return { media, region: { x: 0, y: 0, w: 1, h: 1 } };
64
66
  const w = /^(?:(left|right|top|bottom)(?::|$))?(.*)$/.exec(where.trim());
65
- const region = partRegion({ half: w[1], crop: w[2] || undefined }, tree.get(media));
67
+ const img = tree.get(media);
68
+ const region = partRegion({ half: w[1], crop: w[2] || undefined }, img, mediaOrient(shared, img));
66
69
  return { media, region };
67
70
  });
68
71
  }
@@ -180,7 +183,7 @@ register({
180
183
  requireRecord(tree, opts.repo, "repository");
181
184
  if (opts.input)
182
185
  requireRecord(tree, opts.input, "input");
183
- const clips = clipRefs(tree, listOpt(opts.clip));
186
+ const clips = clipRefs(tree, listOpt(opts.clip), ctx.settings.shared()?.value);
184
187
  if (clips.length > MAX_CLIPS)
185
188
  throw new UsageError(`at most ${MAX_CLIPS} clips: an entry, and its continuation over a page break`);
186
189
  const media = [...new Set([...mediaRefs(tree, csvOpt(opts.media)), ...clips.map((c) => c.media)])];
@@ -243,6 +246,7 @@ register({
243
246
  args: [{ name: "source", description: "source ID (S0001)", required: true }],
244
247
  run(ctx, { args }) {
245
248
  const tree = ctx.tree();
249
+ const shared = ctx.settings.shared()?.value;
246
250
  const s = requireRecord(tree, args[0], "source");
247
251
  const citing = [];
248
252
  const at = (c) => (c.locator ? ` (${c.locator})` : "");
@@ -265,7 +269,9 @@ register({
265
269
  if (c.source === s.id)
266
270
  citing.push(`${f.id} ${e.id} ${e.kind}${at(c)}`);
267
271
  }
268
- const text = lines(`${s.id} ${s.title}`, `kind ${s.kind} · ${s.form} · information ${s.information}${s.language ? ` · language ${s.language}` : ""}${s.date ? ` · dated ${s.date}` : ""}`, s.recordset || s.repository ? `in ${[s.recordset, s.repository].filter(Boolean).join(" · ")}${s.locator ? ` · ${s.locator}` : ""}` : s.locator ? `at ${s.locator}` : undefined, s.url ? `url ${s.url}` : undefined, s.media?.length ? `images ${s.media.map((id) => { const m = tree.get(id); return m ? `${id}${m.recordset ? ` (${m.recordset}:${m.image ?? "?"})` : ""}` : id; }).join(" · ")} — strom media view <M…>` : undefined, s.clips?.length ? `clips ${s.clips.map((c, i) => `${i + 1}. ${clipText(c)}`).join(" · ")} — where the entry is on its image` : undefined, s.transcript ? `\ntranscript\n${s.transcript}` : undefined, s.translation ? `\ntranslation\n${s.translation}` : undefined, citing.length ? `\ncited by\n${citing.map((c) => ` ${c}`).join("\n")}` : "\ncited by nothing yet", ...s.notes.map((n) => `note ${n.text}`));
272
+ const text = lines(`${s.id} ${s.title}`, `kind ${s.kind} · ${s.form} · information ${s.information}${s.language ? ` · language ${s.language}` : ""}${s.date ? ` · dated ${s.date}` : ""}`, s.recordset || s.repository ? `in ${[s.recordset, s.repository].filter(Boolean).join(" · ")}${s.locator ? ` · ${s.locator}` : ""}` : s.locator ? `at ${s.locator}` : undefined, s.url ? `url ${s.url}` : undefined, s.media?.length ? `images ${s.media.map((id) => { const m = tree.get(id); return m ? `${id}${m.recordset ? ` (${m.recordset}:${m.image ?? "?"})` : ""}` : id; }).join(" · ")} — strom media view <M…>` : undefined,
273
+ // as --clip takes them: of the picture as its view shows it
274
+ s.clips?.length ? `clips ${s.clips.map((c, i) => `${i + 1}. ${clipText({ media: c.media, region: fileToShown(c.region, mediaOrient(shared, tree.get(c.media))) })}`).join(" · ")} — where the entry is on its image` : undefined, s.transcript ? `\ntranscript\n${s.transcript}` : undefined, s.translation ? `\ntranslation\n${s.translation}` : undefined, citing.length ? `\ncited by\n${citing.map((c) => ` ${c}`).join("\n")}` : "\ncited by nothing yet", ...s.notes.map((n) => `note ${n.text}`));
269
275
  return { text, data: { source: s, citedBy: citing } };
270
276
  },
271
277
  });
@@ -725,7 +731,7 @@ register({
725
731
  requireRecord(tree, opts.recordset, "recordset");
726
732
  if (opts.repo)
727
733
  requireRecord(tree, opts.repo, "repository");
728
- const clips = clipRefs(tree, listOpt(opts.clip));
734
+ const clips = clipRefs(tree, listOpt(opts.clip), ctx.settings.shared()?.value);
729
735
  const drop = str(opts["clip-remove"]);
730
736
  const media = [...new Set([...mediaRefs(tree, csvOpt(opts.media)), ...clips.map((c) => c.media)])];
731
737
  const s = editFields(tree, args[0], "source", {