A catalog row, download source or install path for an app outside storytold/, or a rule change
that would allow one, is for a maintainer to decide. Agents flag it and stop.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The pictures repo was renamed from artcraft-launcher-assets to match craft-launcher. GitHub
redirects the old name for now, but the launcher should not depend on that.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a hand-written details page for each Craft App
core::details holds, for every app in the suite, a headline, a few
paragraphs, a list of features, where it runs and the list of its
pictures. The copy is the launcher's own: written from each app's page on
getartcraft.com and, for the five newer apps, from its README. It is
compiled in, so it works offline and in the web build, and it leaves out
figures that go stale (effect counts, timings).
The pictures are not in this repository. A page names them (a key and a
caption each) and AppDetails::screenshot_url turns a key into the address
of a 1200 px JPEG in storytold/artcraft-launcher-assets, pinned to a tag
(SCREENSHOTS_REV) so a saved copy never goes stale. It only answers for
keys the page lists, so no address is ever built from anyone else's text.
MAX_SCREENSHOT_BYTES caps what is accepted, and looks_like_jpeg refuses a
login page or error page that arrives as a 200.
PdfCraft lists none yet: its screenshots still show its old name
(PrintCraft). ATTRIBUTION.md points to the pictures repository, which
carries each picture's own provenance.
Tests check that every app has exactly one page, that each is filled in,
and that addresses come only from a page's own pictures.
* Open an app's details in a drawer from its card
Clicking a card (or an app's name in the table) slides a drawer in from
the right over the dimmed grid. It shows the app's headline, version and
main button, links to its website, GitHub and releases, a gallery of
screenshots, its features, the facts and notes of its latest release, and
the same trademark notice as the grid. Esc, the close button or a click
on the grid closes it. The page's text is the hand-written copy in
core::details; the version, date, download size and notes come from the
release the launcher already fetches.
The pictures are fetched when a page is opened, not bundled. The UI sends
Command::FetchScreenshots and gets one Event::Screenshot per picture, the
lead one first. The desktop backend downloads them through the launcher's
Http (its User-Agent, a 2 MB cap, redirects followed) and keeps them under
<cache>/screenshots/<revision>/<app>/<key>.jpg, so later opens, and any
time offline, read from disk. Only a JPEG is kept: a login page or error
page that arrives as a 200 is refused. A missing picture doesn't hold the
others back, but once the network has failed the rest fail at once instead
of each waiting for its own timeout. The web build does the same with
fetch and leaves the caching to the browser.
Until a picture is here, a frame of the same size says "Loading pictures…"
or, when it failed, that it couldn't be loaded and that reopening the page
tries again; nothing moves when the picture arrives. The lead picture
opens full-window, the arrows over the pictures and the arrow keys flip
through them, wrapping around, and a scroll bar stays visible (egui's
floating one only shows under the pointer).
The drawer's labels are tl! strings, with 13 rows in each of ru, zh-CN,
de, fr and pt-BR; details.rs joins the sources the catalog coverage test
reads. Every size taken from the window is kept non-negative: the new
tests resize it from 40 x 30 to 4000 x 2400 with the drawer open.
actions() becomes actions_in(), which can leave the links out of the
... menu where the page already shows them as buttons. The image crate's
jpeg feature is enabled to decode the pictures (zune-jpeg and zune-core
join Cargo.lock). The dim and the enlarged picture's backdrop are colours
in theme.rs.
* Bring the details drawer up to date with main
- Website is always a button now (#198); drop the "Like {name}" line and the trademark notice,
which main removed with the product comparisons (93caf01).
- Bring back widgets::eyebrow for the drawer's section headings (#187 dropped its last caller).
- The drawer's ⋯ menu leaves out only the links, so Hide/Unhide (#185) stays.
- No slide when the OS asks for reduced motion (#201).
- Translate the screen readers' "{name} details" label, and add the drawer's strings to the
Spanish, Czech, Polish, Hungarian and Japanese catalogs.
- AGENTS.md: name the pictures' download origin.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* macOS: build the DMG with hdiutil create so the app passes codesign --strict
hdiutil makehybrid -hfs put a com.apple.FinderInfo on every file in the
image, so the signed app inside the DMG (and any ditto copy of it) failed
codesign --verify --deep --strict. That is why verify.sh had dropped
--strict. Build the image with hdiutil create -srcfolder (retried, since
create can hit "Resource busy" on CI), clear build-time xattrs before
signing, and make verify.sh check the app in the finished DMG and a ditto
copy of it with --strict again.
Ported from storytold/pdfcraft#823 and storytold/lightcraft#959.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* macOS DMG: branded window background and icon layout
The launcher's DMG opened as a plain Finder window, unlike every other
Craft app. Add packaging/macos/dmg/ the same way WordCraft and PhotoCraft
did: a background (the ArtCraft mark whole on its blue, title, Ink and
Paper), a .DS_Store with the window size and icon positions, and the
generate.py that writes both reproducibly. The volume is now named
"ArtCraft Launcher" without the version, which the background alias needs;
the DMG file name keeps the version.
Fixes#20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Linux: draw on the desktop's GPU, not always the discrete one
egui asks wgpu for the high-performance adapter. On a hybrid laptop in
PRIME on-demand mode (Intel + NVIDIA, X11, GNOME) that is the NVIDIA GPU
while the display server runs on the Intel one, so every frame is copied
between the GPUs. During a resize the window then shows stale or garbled
frames and can stop drawing until it is resized again.
On Linux and the BSDs, ask for no power preference instead: wgpu then
takes the first adapter Vulkan lists, and Mesa's device-select layer lists
the display server's GPU first (or the one DRI_PRIME picks). Measured on
an RTX 4060 + Intel RPL-S laptop: in on-demand mode the list starts with
the Intel GPU, in `prime-select nvidia` mode with the NVIDIA one, so the
launcher draws on whichever GPU drives the screens. WGPU_POWER_PREF still
overrides it. Windows and macOS keep their current choice.
Refs #69
* Linux GPU: keep wgpu's adapter order, but a software rasterizer last
Without Mesa's device-select layer the Vulkan order follows the ICD files, and lavapipe can come
before the real driver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a Polish interface
- pl.tsv: the full catalog (287 rows), written clean-room from the English
labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.
* Add Hungarian localization and fix responsive settings layout
* i18n: add Czech (cs) interface language
Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: add Japanese (ja) interface language
ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.
Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.
A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.
The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).
* i18n: make the CJK font choice language-neutral
The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.
* Hide Craft apps the user is not interested in.
* Read the Linux desktop appearance portal so system theme matching works on Wayland.
* UI: follow the OS's animation setting
With Windows' "Animation effects" switched off, the launcher still glided each
notch of the mouse wheel over several frames and faded its dropdowns in. egui
does both by default, and nothing asked the OS.
The shell now says what the OS wants (Host::reduce_motion) and the UI asks it
about once a second while it draws, so a change shows without a restart. With
fewer animations asked for:
- a mouse wheel scrolls at once. egui has no switch for its wheel smoothing but
applies a trackpad's small steps as they come, so each wheel event is
rewritten as a run of those in the shell's raw_input_hook;
- dropdowns, tooltips and dialogs appear without a fade, and scrolling to a
place jumps there (style.animation_time, style.scroll_animation);
- the rule along the top of a hovered card appears instead of wiping in.
Progress bars and spinners keep moving: they say that work is going on.
Where the setting comes from:
- Windows: SPI_GETCLIENTAREAANIMATION, read as bit 33 of UserPreferencesMask in
the registry, as SystemParametersInfoW needs unsafe;
- macOS: NSWorkspace's accessibilityDisplayShouldReduceMotion;
- Linux and the BSDs: the XDG settings portal over D-Bus (its reduced-motion,
GNOME's enable-animations, KDE's AnimationDurationFactor), followed on a
thread so the UI never waits for D-Bus;
- web: prefers-reduced-motion.
Animations stay on wherever the setting can't be read. winreg and zbus become
direct dependencies of the desktop app; both were already in Cargo.lock.
The Windows path was run on a machine with the setting off. The macOS and Linux
paths were type-checked for those targets but never run, and the web one only
compiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI tests: hovering a card follows the OS's animation setting
With animations off, moving the pointer onto a card leaves the window still
within 3 frames: its rule and its icon's rise show at once. With them on, the
0.3 s wipe takes 19 frames at 60 frames a second. Each test fails when the
card's timing is made to ignore the setting.
The test window no longer has a download in progress, so it is still unless
something animates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"
plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* system theme: back off to every 5 minutes when there's no appearance portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge #189 and main into #201: one raw_input_hook, and test anchors on taglines
Main no longer shows the "Like …" product lines, so the tests find cards by their taglines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(hu): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(ja): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: hide/unhide strings for the Spanish, Czech, Polish, Hungarian and Japanese catalogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* hungarian test: count only the settings body's links, not the cards' Website links behind the modal
#198 gave every card a Website link, and Hungarian labels both Webhely.
* hungarian test: card links are 14px text, settings icons 24px; filter on that
---------
Co-authored-by: DzikStar <82441193+DzikStar@users.noreply.github.com>
Co-authored-by: ferivoq <ferikegameplay@gmail.com>
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: leonaci <leonacism@gmail.com>
Co-authored-by: satchmo91 <eherrmann1991@gmail.com>
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Read the Linux desktop appearance portal so system theme matching works on Wayland.
* system theme: back off to every 5 minutes when there's no appearance portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a Polish interface
- pl.tsv: the full catalog (287 rows), written clean-room from the English
labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.
* Add Hungarian localization and fix responsive settings layout
* i18n: add Czech (cs) interface language
Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: add Japanese (ja) interface language
ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.
Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.
A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.
The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).
* i18n: make the CJK font choice language-neutral
The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.
* Hide Craft apps the user is not interested in.
* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"
plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(hu): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(ja): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: hide/unhide strings for the Spanish, Czech, Polish, Hungarian and Japanese catalogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* hungarian test: count only the settings body's links, not the cards' Website links behind the modal
#198 gave every card a Website link, and Hungarian labels both Webhely.
* hungarian test: card links are 14px text, settings icons 24px; filter on that
---------
Co-authored-by: DzikStar <82441193+DzikStar@users.noreply.github.com>
Co-authored-by: ferivoq <ferikegameplay@gmail.com>
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: leonaci <leonacism@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add a Polish interface
- pl.tsv: the full catalog (287 rows), written clean-room from the English
labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.
* Add Hungarian localization and fix responsive settings layout
* i18n: add Czech (cs) interface language
Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: add Japanese (ja) interface language
ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.
Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.
A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.
The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).
* i18n: make the CJK font choice language-neutral
The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.
* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"
plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(hu): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(ja): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* hungarian test: count only the settings body's links, not the cards' Website links behind the modal
#198 gave every card a Website link, and Hungarian labels both Webhely.
* hungarian test: card links are 14px text, settings icons 24px; filter on that
---------
Co-authored-by: DzikStar <82441193+DzikStar@users.noreply.github.com>
Co-authored-by: ferivoq <ferikegameplay@gmail.com>
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add a Polish interface
- pl.tsv: the full catalog (287 rows), written clean-room from the English
labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.
* Add Hungarian localization and fix responsive settings layout
* i18n: add Czech (cs) interface language
Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"
plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(hu): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* hungarian test: count only the settings body's links, not the cards' Website links behind the modal
#198 gave every card a Website link, and Hungarian labels both Webhely.
* hungarian test: card links are 14px text, settings icons 24px; filter on that
---------
Co-authored-by: DzikStar <82441193+DzikStar@users.noreply.github.com>
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add a Polish interface
- pl.tsv: the full catalog (287 rows), written clean-room from the English
labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.
* i18n: add Czech (cs) interface language
Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"
plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* i18n: add Czech (cs) interface language
Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate "Installed · latest {latest}" (added by #195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#195 added the string and #196 marked Spanish complete; they landed together, so the
completeness test failed on main.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Improve indeterminate progress bar animation
Refactor indeterminate progress bar rendering for smoother animation.
* progress: rustfmt, and skip the wrapped segment while it's off the track
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Match MSI entries and find app executables through pure functions
Windows detection decided which uninstall entry belongs to an app, and
which paths might hold its exe, inline in the registry-reading code
under #[cfg(windows)]. Neither decision needs the registry or the disk,
so move both into module-level functions, is_installer_entry and
exe_candidates, that tests can call on every platform. The windows
module keeps the registry reads and the exists() checks.
No behaviour change: an entry still has to carry the app's display name
and the Learning Machines LLC publisher, exactly, and the candidates are
the same paths in the same order. The new tests pin that down for every
app but PdfCraft, and for PdfCraft's own entry name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Detect PdfCraft installs made by its PrintCraft-named MSI
PdfCraft's Windows installer shipped as PrintCraft up to 0.2.1
(printcraft-0.2.1-windows-x64.msi). That MSI registers its uninstall
entry with DisplayName "PrintCraft" and installs printcraft.exe into
Program Files\PrintCraft, but Windows detection only accepted an entry
named after the app's current name and looked only for pdfcraft.exe, in
App Paths, the install folder and Program Files\PdfCraft. On a machine
still on that install the launcher showed PdfCraft as not installed,
with no version, and uninstall_msi found nothing to remove.
The catalog already knows the old asset stem (former_ids); add
former_names for the display names an app's installers registered,
since the capitalisation can't be derived from the stem, and names()
to go with stems(). Entry matching now accepts any of the app's names,
still exactly and still only from Learning Machines LLC, and the exe is
looked up under every stem in App Paths and under every name in Program
Files, current first. Apps without former names see the same match and
the same candidates as before.
The impact is limited to early adopters: PdfCraft 0.4.0 and later
register as PdfCraft, and their MSI keeps the same UpgradeCode, so
installing from the launcher already upgraded a PrintCraft install in
place. What this fixes is the wrong status and the missing Uninstall
until then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Fedora Atomic variants (Bazzite, Silverblue, etc.) block local dnf install;
prefer Flatpak and AppImage so one-click installs succeed without pkexec dnf.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wgpu's default DX12 compiler (Auto) loads whichever dxcompiler.dll the DLL
search path finds. The launcher ships none, and without a matching dxil.dll
the unsigned-DXIL warning fails wgpu's indirect-validation pipeline, the
device is lost and the launcher exits before the first frame. Pin the
compiler to FXC, which is part of Windows; WGPU_DX12_COMPILER still
overrides it. GL stays in the Windows backends: it is the deliberate
fallback for machines whose D3D12 adapters can't present to the window.
Fixes#126
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add experimental flake.nix for NixOS/Linux development
This adds an experimental flake-based development environment and package entry for ArtCraft Launcher.
What this adds:
- `flake.nix` at the repository root
- Rust toolchain via `rust-overlay`
- Required native Linux libraries for the Rust workspace
- A dev shell via `nix develop`
- A package/app target via `nix run` / `nix build`
Why:
ArtCraft Launcher is a Rust workspace and currently has no Nix flake support. This makes local development and installation much easier for NixOS/Linux users.
Notes:
- This is intentionally minimal and experimental.
- It targets the development environment and local package use.
- Additional refinements may be needed for specific Wayland/X11 setups.
* Fix flake metadata to match ArtCraft Launcher and remove GTK dependency
* add flake.lock
* Update flake.nix: replace deprecated xorg packages with their new names
es.tsv translates the whole interface, the tray menu and tooltip, each
app's tagline and the backend's install/update messages, in neutral
international Spanish. Settings > Language offers Español; es, es-ES,
es-MX, es-419 and the like select it under Auto.
AI-assisted, native review welcome.
Models: Opus 5.5, Terra 5.6, DeepSeek V4 Flash
Co-authored-by: Diego Ospina <diegozpy@users.noreply.github.com>
A launcher started from the Dock, the Start menu or at login has no
terminal, and the Windows build has no console, so its log records and
panic reports were lost.
env_logger now writes each record to standard error and to
logs/artcraft-launcher.log in the launcher's data folder (portable mode
included). Each start keeps the previous two runs' logs; the file is
capped at 16 MiB. The file is opened once this is the only launcher
running, so a second start doesn't rotate the first one's log away.
Panics are reported through the logger. The default filter is now info
for the launcher's crates and warn for the rest; RUST_LOG still wins.
Ported from storytold/pdfcraft#239.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tray-icon's default id is "<pid>-<n>" and the title is empty, so
StatusNotifierItem hosts showed numbers like "61965-1". On Linux and the
BSDs, give the icon the id "artcraft-launcher" and the title
"ArtCraft Launcher". Windows and macOS are unchanged.
Fixes#136
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
eframe sets the app icon at runtime, and on macOS that replaces the
Dock icon with a raw bitmap (the 256 px PNG we passed, or egui's logo
when none is given), which skips the system's icon treatment. Pass the
empty icon on macOS so eframe leaves the bundle's .icns alone.
Fixes#32.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Those binaries print a former id (`printcraft 0.2.1`) or the display
name (`SoundCraft 0.5.0`). Detection required an exact machine-id
prefix, so the version was missing and the card said "installed
installed · latest …". Parse --version against stems and the product
name, and stop using the word "installed" as a version placeholder.
Two download-trust gaps from #90.
A repo rename in the release feed only had to look like owner/name, so
a compromised feed could send an app's downloads, or the launcher's own
self-update, to another account's repository with matching links and
checksums, and every check would vouch for them. A rename now has to
stay under `catalog::GITHUB_OWNER` (storytold) at both ends, or it is
dropped and the feed's entry stops matching, so GitHub decides as before.
A release with neither a per-file digest nor a SHA256SUMS.txt line was
still installed, checked by size only, with a note after the fact; a
same-length altered file would have passed. `install::download` now
refuses it before fetching anything, with an error that says why. With
a checksum always in hand, the size-only path goes: `http::download`
takes the SHA-256 as `&str`, and `Verified` and `keep_download` are
removed.
Tests: a rename within storytold is followed, one out of it is dropped
in either direction, and a release without a checksum fetches nothing
and never reaches the installer.
Fixes#90
Co-authored-by: YangXZ2008 <141027290+YangXZ2008@users.noreply.github.com>
Every dialog shared one egui area id, "dialog". egui lays an area out in
the size it had the frame before, and the uninstall prompt's button row
was a right-to-left layout placed straight in the column, which fills
whatever height is left. So after Settings or the release notes had been
open, the prompt came up as tall as they were, with its buttons floating
in the middle, and never shrank back.
- Each dialog gets its own area id (`Dialog::key`), so a small prompt
starts from its own size, and so does each feed alert.
- The uninstall prompt's and the feed alert's button rows sit in
`ui.horizontal` like the other dialogs' rows, so they take only a
row's height.
- A UI test opens Settings, closes it, opens the uninstall prompt and
checks the buttons follow the text (they sat 243 px below it before).
Fixes#88
Co-authored-by: YangXZ2008 <141027290+YangXZ2008@users.noreply.github.com>
Since the release workflow moved to craft-common-actions (#129), the caller
passed only the Apple and WINDOWS_CERTIFICATE secrets, not the AZURE_* ones that
Trusted Signing uses, so the 0.3.1 MSIs and portable exes came out unsigned.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
craft-common-actions calls `package.sh --formats deb,rpm,tar`, but package.sh
split formats on spaces only, so the riscv64 job built the binary, matched no
format, wrote nothing to dist/release and failed the 0.3.0 release at upload.
package.sh now accepts commas or spaces and rejects unknown format names.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bumps the workspace version to 0.3.0 for the new features since 0.2.1, and
applies cargo fmt to the two files from #149 that failed CI's format check.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ci: refactor workflows to use reusable artcraft-common-actions
* ci: point reusable workflows to storytold/craft-common-actions
* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly
* ci: use @main (unpinned) for craft-common-actions with explanation comment
* macOS: open disk images reliably, and say why when they won't open
Installing or updating an app on macOS could fail with only "couldn't open
the disk image". hdiutil's own reason was thrown away, and every install of
an app mounted its image at the same folder, which an attach that was
interrupted or timed out could still be holding.
- Each install mounts its image at a new, empty folder.
- The attach skips hdiutil's checksum pass (-noverify): the download's
SHA-256 is already checked against the release, and on a large image that
pass alone could run past the time limit, now 5 minutes.
- When the image still won't open, or the app won't copy, the message says
why in hdiutil's or ditto's words ("couldn't open the disk image:
Resource temporarily unavailable").
- hdiutil, ditto and codesign are run by full path: a launcher started from
Finder or at login gets a minimal PATH.
process::run is capture's sibling that returns the helper's error message;
capture is now built on it. The launcher's own macOS self-update installs
through the same code, so it gets all of this too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release 0.2.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
It asked for a wgpu device up front, which panics where there is none: the
FreeBSD VM job has failed on it since it landed. Like the other tests, let
the default renderer create a device only when a snapshot is rendered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diskimages-helper turns an attach away ('Resource temporarily unavailable')
while it is busy with another image. The DMG tests added in #51 run in
parallel and made this a flaky failure (about 1 run in 5 locally); the
launcher hits the same thing when several apps update at once. Attaches now
take a process-wide lock (which the tests' hdiutil create shares) and back
off and retry (1, 2, 4, 8 s) when something outside the launcher holds it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The footer laid out the check status and the rate-limit notice first at
their full width, then put the links, social icons and platform label in a
right-to-left layout in whatever space was left. When the left side was
wider than that space (720 and 960 px, and even 1180 px while rate limited)
the two drew on top of each other.
The right side now comes first and keeps its size; the status and notice
share what is left. The notice gets priority: it switches to a short form
("GitHub limit hit; back in N min", full text on hover) when the full one
won't fit, and the check status is cut short with "…" (new
widgets::hud_elided). Below about 300 px of room for the status the
platform label is dropped.
A UI test renders the rate-limited footer at 720, 960 and 1180 px and checks
the notice and status don't intersect any link, icon or the platform label.
Fixes#16
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Install and update Craft apps from a local mirror
Add an opt-in developer mode for installing the Craft apps from builds on
this machine instead of GitHub:
- ARTCRAFT_LAUNCHER_DOWNLOAD_BASE (loopback http only) points the launcher
at a local mirror. While set, every check reads the mirror's
release_info.json, trusts download links under the mirror, never treats
entries as stale, and never falls back to GitHub or ArtCraft's API.
- cargo xtask local sync pulls every sibling app checkout (git pull
--ff-only --recurse-submodules), packages each app whose version isn't
mirrored yet with its own packaging script, and files the results under
<mirror>/<app>/<version>/ with SHA256SUMS.txt and notes from git log.
- cargo xtask local serve serves the mirror on 127.0.0.1 (with range
requests, so paused downloads resume) and writes the feed fresh from the
mirror's contents on every request.
- cargo xtask local status compares each checkout with what's mirrored.
* Sync the local mirror on an interval while serving it
cargo xtask local serve --sync-every <30m|1h|1d…> runs the same sync as
cargo xtask local sync on a background thread, one after another, so the
mirror follows origin without a second process. Downloads keep being
served during a build, and a version only joins the feed once its build
is complete. Pulls are --no-stat to keep the server's log readable.
* Build every new commit as a dev version with --every-commit
cargo xtask local sync --every-commit (and serve --sync-every … --every-commit)
builds each new commit rather than only new versions. A commit of a 0.4.0
checkout is packaged as 0.4.1-dev.<commit time> through the app's
<APP>_VERSION override, so it orders after 0.4.0, after earlier commits,
and before the real 0.4.1. The full version lands in the bundle's
<App>Version key, which the launcher reads, so it offers each build as an
update and sees it as current once installed.
* Keep the local mirror out of release builds; parse loopback URLs strictly
ARTCRAFT_LAUNCHER_DOWNLOAD_BASE makes the launcher trust the mirror's own
checksums and skip GitHub, so a shipped launcher must not read it: it now
needs the local-mirror cargo feature (cargo run --features local-mirror),
which release builds never enable. The loopback check parsed the URL by
prefix, so http://127.0.0.1:1@evil.example/ passed; it now requires the host
to be exactly 127.0.0.1, localhost or [::1] with a numeric port. The same
check guards ARTCRAFT_LAUNCHER_FEED_URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: khutchings <7854672+khutchings@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Issue #29's window wasn't 0.1.2: its tagline and header are 0.1.0's, and
it offered Download where a portable 0.1.2 offers Update and restart. A
0.1.0 launcher sat in the tray; starting 0.1.2 asked it to show its
window and exited, and that window still offered 0.1.1, found while 0.1.1
was the latest release.
A second start now says hello with its version and the running launcher
answers with its own. A newer start makes the running one quit (unless
it's downloading or installing; then it shows its window) and takes over
the port. The same version or older shows the running window as before.
Launchers from before the handshake hang up without an answer and can't
be asked to quit, so the new start runs anyway. A bare "show" from an
old launcher is still understood.
Tests: the handshake over loopback, including a running launcher that
predates it.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix Show in Explorer opening Documents for paths with spaces
reveal() built `/select,<path>` as one argument and passed it with
Command::arg, which quotes any argument containing a space. The Craft
apps' MSIs install under C:\Program Files, so Explorer received
"/select,C:\Program Files\...\app.exe", didn't recognise the switch and
opened its default folder, Documents.
The argument is now `/select,"<path>"`, with only the path quoted and
forward slashes turned into backslashes, passed with raw_arg so Rust
leaves it alone. The argument building is a plain function with unit
tests that run on every platform.
Fixes#39
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse a path with a quote in it; give the Explorer tests their own module
A " would end the quoted /select path early and pass the rest to Explorer as
switches. The module name clashed with the pgrep tests that landed in #81.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(wgpu): prefer D3D12 (incl. WARP) over GL on Windows (#2087)
On Windows, rank any Direct3D 12 adapter (WARP included) above GL when
WGPU_BACKEND is unset. wgpu ranks WARP last, so GPUs without a D3D12
driver (Intel HD 4000, Radeon TeraScale) got the GL adapter, whose legacy
driver fails Surface::configure; wgpu panics on that before any app code
runs. Hardware D3D12 adapters keep wgpu's order. Reporter verified
WGPU_BACKEND=d3d12 (WARP) works on the affected machine.
Refs storytold/artcraft#2087
* Drop a reference to an issue number from another tracker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add Brazilian Portuguese (pt-BR) UI localization
* Rebase onto German and French: pt-BR last in the registry, new update strings
Document that pt-PT falls back to Brazilian Portuguese; smoother wording for
'Verifying checksum…'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: AlastorIRL <100581152+AlastorIRL@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a French interface
fr.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages, and Settings > Language
offers Français. fr, fr-FR, fr_CA.UTF-8 and the like select it. French counts
0 and 1 as singular, so it gets its own plural rule. tests/french.rs checks the
grid, the table, dates and a backend message.
* Rebase onto German: French last in the registry, new update strings, lowercase months
French writes month abbreviations in lower case (12 août 2026). Tests use a
never-supported tag (xx-XX) next to fr-FR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>