Support Studio release assets, skip ArtCraft on FreeBSD, and list as app #01

- Support ArtCraft Studio's published MSI and DMG asset formats in parse_asset_name while preserving existing asset naming
- Add real artcraft-release.json fixture and test asset selection across desktop platforms
- Add runs_on to CraftApp and filter out ArtCraft Studio on FreeBSD across backend and UI
- List ArtCraft Studio as app #01 in the README Craft App Suite table and xtask APPS array
This commit is contained in:
Dylan M. Taylor committed 2026-11-06 22:14:12 -05:00
1 parent 3ebc696f5d
commit 3f9d0afab0
9 files changed
+230 -41

No files matched your search

+21 -22
View File
@@ -15,7 +15,7 @@
<p align="center">
<b>One place to install, update and open every app in the Craft App Suite, rebuilt in pure Rust.</b><br>
PhotoCraft, LightCraft, FilmCraft, DesignCraft, VectorCraft, PdfCraft, EffectCraft, WordCraft,<br>
ArtCraft Studio, PhotoCraft, LightCraft, FilmCraft, DesignCraft, VectorCraft, PdfCraft, EffectCraft, WordCraft,<br>
DeckCraft, GridCraft, CADCraft, SoundCraft, FigCraft, Craft Anything, TextureCraft, MaterialCraft and the community app SolidCraft: the right build for your computer, kept up to date.
</p>
@@ -264,27 +264,26 @@ ArtCraft Launcher installs the **Craft App Suite**: free, open-source creative t
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
stand on its own.
| | App | What it's for | Code | Learn more |
|:-:|---|---|---|---|
| <img src="https://raw.githubusercontent.com/storytold/photocraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png" alt="" width="32" height="32"> | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/figcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.figcraft.png" alt="" width="32" height="32"> | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
| <img src="https://raw.githubusercontent.com/storytold/wordcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png" alt="" width="32" height="32"> | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
| <img src="https://raw.githubusercontent.com/storytold/soundcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" alt="" width="32" height="32"> | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
| <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
| <img src="https://raw.githubusercontent.com/storytold/cadcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.cadcraft.png" alt="" width="32" height="32"> | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
| <img src="https://raw.githubusercontent.com/storytold/texturecraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.texturecraft.png" alt="" width="32" height="32"> | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
| <img src="https://raw.githubusercontent.com/storytold/materialcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.materialcraft.png" alt="" width="32" height="32"> | **MaterialCraft** | Procedural material design | [GitHub](https://github.com/storytold/materialcraft) | [Project page](https://github.com/storytold/materialcraft) |
| <img src="https://raw.githubusercontent.com/storytold/craft-anything/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png" alt="" width="32" height="32"> | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
| <img src="https://raw.githubusercontent.com/tangency-labs/solidcraft/main/assets/icons/app/linux/exports/hicolor/64x64/apps/io.tangencylabs.solidcraft.png" alt="" width="32" height="32"> | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
| | App | What it's for | Code | Learn more |
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------|:------------------------------------------------------|:------------------------------------------------------|:-----------------------------------------------------------|
| <img src="https://raw.githubusercontent.com/storytold/artcraft/main/crates/desktop/artcraft/icons/64x64.png" alt="" width="32" height="32"> | **ArtCraft Studio** | AI image and video creation | [GitHub](https://github.com/storytold/artcraft) | [Website](https://getartcraft.com) |
| <img src="https://raw.githubusercontent.com/storytold/photocraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png" alt="" width="32" height="32"> | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/figcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.figcraft.png" alt="" width="32" height="32"> | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
| <img src="https://raw.githubusercontent.com/storytold/wordcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png" alt="" width="32" height="32"> | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
| <img src="https://raw.githubusercontent.com/storytold/soundcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" alt="" width="32" height="32"> | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
| <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
| <img src="https://raw.githubusercontent.com/storytold/cadcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.cadcraft.png" alt="" width="32" height="32"> | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
| <img src="https://raw.githubusercontent.com/storytold/texturecraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.texturecraft.png" alt="" width="32" height="32"> | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
| <img src="https://raw.githubusercontent.com/storytold/materialcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.materialcraft.png" alt="" width="32" height="32"> | **MaterialCraft** | Procedural material design | [GitHub](https://github.com/storytold/materialcraft) | [Project page](https://github.com/storytold/materialcraft) |
| <img src="https://raw.githubusercontent.com/storytold/craft-anything/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png" alt="" width="32" height="32"> | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
| <img src="https://raw.githubusercontent.com/tangency-labs/solidcraft/main/assets/icons/app/linux/exports/hicolor/64x64/apps/io.tangencylabs.solidcraft.png" alt="" width="32" height="32"> | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
<br>
+20
View File
@@ -75,6 +75,14 @@ impl CraftApp {
kind != crate::PackageKind::Dmg || self.mac_team.is_some()
}
/// Whether this app runs on the given operating system.
pub fn runs_on(&self, os: crate::Os) -> bool {
match os {
crate::Os::FreeBsd => self.id != "artcraft",
_ => true,
}
}
/// Bundle ids this app may be installed under, current first.
pub fn bundle_ids(&self) -> impl Iterator<Item = String> + '_ {
std::iter::once(self.bundle_id()).chain(self.former_bundles.iter().map(|b| b.to_string()))
@@ -398,4 +406,16 @@ mod tests {
assert_eq!(pdf.website(), "https://getartcraft.com/apps/pdfcraft");
assert_eq!(CraftApp::by_id("wordcraft").unwrap().website(), "https://getartcraft.com/apps/wordcraft");
}
#[test]
fn artcraft_studio_does_not_run_on_freebsd() {
let studio = CraftApp::by_id("artcraft").unwrap();
assert!(!studio.runs_on(crate::Os::FreeBsd));
assert!(studio.runs_on(crate::Os::Linux));
assert!(studio.runs_on(crate::Os::MacOs));
assert!(studio.runs_on(crate::Os::Windows));
let photo = CraftApp::by_id("photocraft").unwrap();
assert!(photo.runs_on(crate::Os::FreeBsd));
}
}
+68 -9
View File
@@ -205,8 +205,16 @@ impl AssetName {
}
}
/// Reads `<stem>-<version>-<os>-<arch>[-portable].<ext>`; `None` for checksums, zsync files, CLI
/// and web bundles, or anything else.
fn strip_stem_prefix<'a>(base: &'a str, stem: &str, sep: char) -> Option<&'a str> {
if base.len() > stem.len() && base[..stem.len()].eq_ignore_ascii_case(stem) && base.as_bytes()[stem.len()] == sep as u8 {
Some(&base[stem.len() + 1..])
} else {
None
}
}
/// Reads `<stem>-<version>-<os>-<arch>[-portable].<ext>` or Studio's `<Stem>_<version>_<arch>[_<locale>].<ext>`;
/// `None` for checksums, zsync files, CLI and web bundles, or anything else.
pub fn parse_asset_name(name: &str, stems: &[&str]) -> Option<AssetName> {
const KINDS: [PackageKind; 9] = [
PackageKind::PortableZip,
@@ -221,14 +229,35 @@ pub fn parse_asset_name(name: &str, stems: &[&str]) -> Option<AssetName> {
];
let (kind, base) = KINDS.iter().find_map(|k| name.strip_suffix(k.suffix()).map(|b| (*k, b)))?;
for stem in stems {
let Some(rest) = base.strip_prefix(stem).and_then(|r| r.strip_prefix('-')) else { continue };
// rest = <version>-<os>-<arch>; os and arch never contain '-', versions may.
let mut parts = rest.rsplitn(3, '-');
let (arch, os, version) = (parts.next()?, parts.next()?, parts.next()?);
if crate::version::parse(version).is_none() || !version.starts_with(|c: char| c.is_ascii_digit()) {
continue;
if let Some(rest) = strip_stem_prefix(base, stem, '-') {
// rest = <version>-<os>-<arch>; os and arch never contain '-', versions may.
let mut parts = rest.rsplitn(3, '-');
let (Some(arch), Some(os), Some(version)) = (parts.next(), parts.next(), parts.next()) else { continue };
if crate::version::parse(version).is_none() || !version.starts_with(|c: char| c.is_ascii_digit()) {
continue;
}
return Some(AssetName { stem: (*stem).to_string(), version: version.to_string(), token: format!("{os}-{arch}"), kind });
}
if let Some(rest) = strip_stem_prefix(base, stem, '_') {
// Studio / Tauri format: <version>_<arch>[_<locale>]
let mut parts = rest.split('_');
let (Some(version), Some(arch_token)) = (parts.next(), parts.next()) else { continue };
if crate::version::parse(version).is_none() || !version.starts_with(|c: char| c.is_ascii_digit()) {
continue;
}
let token = match (kind, arch_token) {
(PackageKind::Dmg, "universal") => Some("macos-universal".to_string()),
(PackageKind::Dmg, "x86_64" | "x64") => Some("macos-x86_64".to_string()),
(PackageKind::Dmg, "aarch64" | "arm64") => Some("macos-aarch64".to_string()),
(PackageKind::Msi | PackageKind::PortableZip, "x86_64" | "x64") => Some("windows-x64".to_string()),
(PackageKind::Msi | PackageKind::PortableZip, "x86") => Some("windows-x86".to_string()),
(PackageKind::Msi | PackageKind::PortableZip, "arm64" | "aarch64") => Some("windows-arm64".to_string()),
_ => None,
};
if let Some(token) = token {
return Some(AssetName { stem: (*stem).to_string(), version: version.to_string(), token, kind });
}
}
return Some(AssetName { stem: (*stem).to_string(), version: version.to_string(), token: format!("{os}-{arch}"), kind });
}
None
}
@@ -304,6 +333,27 @@ mod tests {
}
}
#[test]
fn artcraft_studio_selects_published_desktop_assets() {
let release =
crate::release::parse_github_release(include_str!("../tests/fixtures/artcraft-release.json"), "storytold/artcraft").unwrap();
assert_eq!(release.version, Some(crate::version::Version::new(0, 41, 1)));
for (os, arch, kind, expected) in [
(Os::Windows, Arch::X64, PackageKind::Msi, "ArtCraft_0.41.1_x64_en-US.msi"),
(Os::MacOs, Arch::Arm64, PackageKind::Dmg, "ArtCraft_0.41.1_universal.dmg"),
(Os::MacOs, Arch::X64, PackageKind::Dmg, "ArtCraft_0.41.1_universal.dmg"),
(Os::Linux, Arch::X64, PackageKind::AppImage, "artcraft-0.41.1-linux-x86_64.AppImage"),
(Os::Linux, Arch::X64, PackageKind::Flatpak, "artcraft-0.41.1-linux-x86_64.flatpak"),
(Os::Linux, Arch::X64, PackageKind::TarGz, "artcraft-0.41.1-linux-x86_64.tar.gz"),
(Os::Linux, Arch::Arm64, PackageKind::TarGz, "artcraft-0.41.1-linux-aarch64.tar.gz"),
] {
let platform = Platform { os, arch };
let (asset, selected) = pick_asset(&release, &["artcraft"], platform, &[kind]).unwrap();
assert_eq!(asset.name, expected);
assert_eq!(selected, kind);
}
}
#[test]
fn freebsd_picks_the_tarball_for_the_processor() {
// SoundCraft v0.10.0's GitHub metadata, trimmed to its FreeBSD tarballs.
@@ -382,6 +432,15 @@ mod tests {
assert!(parse_asset_name("figcraft-0.5.1-linux-x86_64.tar.gz", &["figcraft"]).is_some());
}
#[test]
fn reads_artcraft_studio_asset_names() {
let dmg = parse_asset_name("ArtCraft_0.41.1_universal.dmg", &["artcraft"]).unwrap();
assert_eq!((dmg.version.as_str(), dmg.token.as_str(), dmg.kind), ("0.41.1", "macos-universal", PackageKind::Dmg));
let msi = parse_asset_name("ArtCraft_0.41.1_x64_en-US.msi", &["artcraft"]).unwrap();
assert_eq!((msi.version.as_str(), msi.token.as_str(), msi.kind), ("0.41.1", "windows-x64", PackageKind::Msi));
}
#[test]
fn picks_the_native_build_first_and_falls_back_to_emulation() {
let r = release(PHOTO);
+76
View File
@@ -0,0 +1,76 @@
{
"tag_name": "artcraft-v0.41.1",
"name": "ArtCraft v0.41.1",
"html_url": "https://github.com/storytold/artcraft/releases/tag/artcraft-v0.41.1",
"published_at": "2026-11-07T02:49:42Z",
"prerelease": false,
"draft": false,
"assets": [
{
"name": "artcraft-0.41.1-linux-aarch64.AppImage",
"size": 177187336,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-aarch64.AppImage",
"digest": "sha256:31a4071758ce7989c6548e3976839aaf6010be414d0a1b5cc9a913537bbc88de"
},
{
"name": "artcraft-0.41.1-linux-aarch64.flatpak",
"size": 40561288,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-aarch64.flatpak",
"digest": "sha256:8f899c94f0c41fb1553c66fc6b4d3ac0763f4c034bfb06fe9f0d60426c153f7b"
},
{
"name": "artcraft-0.41.1-linux-aarch64.tar.gz",
"size": 49710171,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-aarch64.tar.gz",
"digest": "sha256:cb780e02e87234927d339c87dee03b224e1641320758be813d31d442d9a691ae"
},
{
"name": "artcraft-0.41.1-linux-riscv64.tar.gz",
"size": 48491446,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-riscv64.tar.gz",
"digest": "sha256:d32edc5ac3320f6789154fcd89552f0650d6d396c8cb576d1197d1e11bbc0745"
},
{
"name": "artcraft-0.41.1-linux-x86_64.AppImage",
"size": 182495736,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-x86_64.AppImage",
"digest": "sha256:bb32657b6e81e8e47da44ca4e764b1af14d77d8690b248a263a4acd1218b7d2d"
},
{
"name": "artcraft-0.41.1-linux-x86_64.flatpak",
"size": 41755576,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-x86_64.flatpak",
"digest": "sha256:7351e1bad48006b251ab943372fa15f1829d8b92ca944adb7f312b4e5c7e9912"
},
{
"name": "artcraft-0.41.1-linux-x86_64.tar.gz",
"size": 49365185,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-x86_64.tar.gz",
"digest": "sha256:149f9748ea33553177c021e1ade47a195fad25b881129d4178fe5bd922a94eb4"
},
{
"name": "ArtCraft_0.41.1_universal.dmg",
"size": 94884865,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/ArtCraft_0.41.1_universal.dmg",
"digest": "sha256:89009b8983d88919481ac936e2ada0409dff0e0fc73f487cce3e7324699866a6"
},
{
"name": "ArtCraft_0.41.1_x64_en-US.msi",
"size": 47058944,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/ArtCraft_0.41.1_x64_en-US.msi",
"digest": "sha256:450b15e53d8ccc9afde2d3644a547ab5f900167b837b861571ea4c833166417d"
},
{
"name": "ArtCraft_universal.app.tar.gz",
"size": 94791060,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/ArtCraft_universal.app.tar.gz",
"digest": "sha256:d1e4e9623ebf506fbc8d7476b69b2db957125761e7908624b3eb031ce044413e"
},
{
"name": "SHA256SUMS.txt",
"size": 4108,
"browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/SHA256SUMS.txt",
"digest": "sha256:5450801c2342f8ea05d9aa71cd31cb4b02462cebd9cab7abbe8884e17de6f075"
}
]
}
+17 -8
View File
@@ -246,7 +246,7 @@ impl NativeBackend {
let pre = options.prereleases;
let feed = if pre { None } else { load_feed(&self.paths) };
let now = crate::now();
for app in &SUITE {
for app in SUITE.iter().filter(|a| self.platform.is_none_or(|p| a.runs_on(p.os))) {
let answer = match feed.as_ref().map(|f| f.answer(app, now)) {
Some(FeedAnswer::Fresh(Some(r))) => Some((LatestRelease::Found(r), Origin::ArtCraftApi)),
Some(FeedAnswer::Fresh(None)) => Some((LatestRelease::None, Origin::ArtCraftApi)),
@@ -268,7 +268,7 @@ impl NativeBackend {
let paths = self.paths.clone();
let platform = self.platform;
std::thread::spawn(move || {
for app in &SUITE {
for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
report_partial(app, &shared, &paths, platform);
}
});
@@ -293,12 +293,12 @@ impl NativeBackend {
http::clean_downloads(&paths.downloads(), Duration::from_secs(30 * 86_400), Duration::from_secs(7 * 86_400));
let snapshot = crate::detect::Snapshot::take();
let manifest = shared.manifest.lock().unwrap_or_else(PoisonError::into_inner).clone();
for app in &SUITE {
for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
let found = crate::detect::detect(app, &manifest, &snapshot);
publish_detected(app, found, &shared, platform);
}
// Downloads paused (or cut off by quitting) in an earlier run.
for app in &SUITE {
for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
report_partial(app, &shared, &paths, platform);
}
drop(busy);
@@ -310,7 +310,7 @@ impl NativeBackend {
/// day old, or no answer at all), at most once an hour per app unless `force`.
fn check(&self, force: bool) {
let (Some(source), Some(http)) = (self.source.clone(), self.http.clone()) else {
for app in &SUITE {
for app in SUITE.iter().filter(|a| self.platform.is_none_or(|p| a.runs_on(p.os))) {
self.shared.emit(Event::Latest {
app: app.id.to_string(),
latest: LatestRelease::Failed("secure connections aren't available".into()),
@@ -339,17 +339,23 @@ impl NativeBackend {
shared.emit(Event::Notices { announcements: f.announcements.clone(), alerts: f.alerts.clone() });
}
let now = crate::now();
let queue = Mutex::new(SUITE.iter().chain(std::iter::once(&LAUNCHER)).collect::<Vec<&CraftApp>>());
let queue = Mutex::new(
SUITE
.iter()
.filter(|a| platform.is_none_or(|p| a.runs_on(p.os)))
.chain(std::iter::once(&LAUNCHER))
.collect::<Vec<&CraftApp>>(),
);
let sources = sweep(&queue, &mirror, feed.as_ref(), fallback, &source, &shared, &paths, (platform, &options), force, now);
source.save();
{
let answered = shared.answered.lock().unwrap_or_else(PoisonError::into_inner);
if SUITE.iter().all(|a| answered.contains(a.id)) {
if SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))).all(|a| answered.contains(a.id)) {
shared.loaded.store(true, Ordering::SeqCst);
}
}
// A newer release changes which file an install would use.
for app in &SUITE {
for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
report_partial(app, &shared, &paths, platform);
}
let source_used = source_used_of(&sources);
@@ -364,6 +370,9 @@ impl NativeBackend {
fn start_job(&self, app_id: &str, work: Work) {
let uninstall = matches!(work, Work::Uninstall);
let Some(app) = CraftApp::by_id(app_id) else { return };
if self.platform.is_some_and(|p| !app.runs_on(p.os)) {
return;
}
log::info!("{}: starting {} job", app.name, if uninstall { "uninstall" } else { "install/update" });
let stop = Arc::new(Stop::default());
let restarting = {
+2 -1
View File
@@ -260,7 +260,7 @@ impl LauncherUi {
Self {
backend,
settings,
entries: SUITE.iter().map(AppEntry::new).collect(),
entries: SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))).map(AppEntry::new).collect(),
caps,
platform,
checking: false,
@@ -926,6 +926,7 @@ impl LauncherUi {
.filter(|(_, e)| {
self.in_filter(e, self.settings.filter)
&& !(self.settings.hide_community_apps && e.app.kind == artcraft_launcher_core::catalog::AppKind::Community)
&& self.platform.is_none_or(|p| e.app.runs_on(p.os))
&& (needle.is_empty() || e.app.name.to_lowercase().contains(&needle) || e.app.tagline.to_lowercase().contains(&needle))
})
.collect();
+6 -1
View File
@@ -247,7 +247,12 @@ impl LauncherUi {
fn neighbour_apps(&self, id: &str) -> (Option<String>, Option<String>) {
let mut ids: Vec<&str> = self.visible_ids().into_iter().filter(|i| AppDetails::by_id(i).is_some()).collect();
if !ids.contains(&id) {
ids = artcraft_launcher_core::SUITE.iter().map(|a| a.id).filter(|i| AppDetails::by_id(i).is_some()).collect();
ids = artcraft_launcher_core::SUITE
.iter()
.filter(|a| self.platform.is_none_or(|p| a.runs_on(p.os)))
.map(|a| a.id)
.filter(|i| AppDetails::by_id(i).is_some())
.collect();
}
let Some(at) = ids.iter().position(|i| *i == id) else { return (None, None) };
if ids.len() < 2 {
+19
View File
@@ -788,6 +788,25 @@ fn a_freebsd_version_row_sizes_the_tarball() {
assert!(facts.ends_with(", 2026 · 140 MB"), "{facts:?}");
}
#[test]
fn freebsd_skips_artcraft_studio() {
let freebsd = Platform { os: Os::FreeBsd, arch: Arch::X64 };
let (h, _) = harness_full(
events(),
ViewMode::Grid,
egui::Theme::Dark,
true,
1.0,
artcraft_launcher_ui_egui::TitleBar::Native,
Rc::default(),
Some(freebsd),
None,
);
let ids = h.state().as_ref().unwrap().visible_ids_for_test();
assert!(!ids.contains(&"artcraft"), "ArtCraft Studio must be skipped on FreeBSD: {ids:?}");
assert!(ids.contains(&"photocraft"), "PhotoCraft remains visible on FreeBSD");
}
/// The launcher's own list in Settings sizes the launcher's download for this machine.
#[test]
fn the_launcher_version_list_sizes_the_launchers_download() {
+1
View File
@@ -19,6 +19,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
/// Every Craft App: (id = folder name, display name). Keep in step with `catalog::SUITE`.
const APPS: &[(&str, &str)] = &[
("artcraft", "ArtCraft Studio"),
("photocraft", "PhotoCraft"),
("vectorcraft", "VectorCraft"),
("filmcraft", "FilmCraft"),