Compare commits

..
62 changed files with 163 additions and 293 deletions
+12 -13
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: storytold/craft-common-actions/actions/install-apt-packages@main
with:
packages: rpm jq binutils zsync
@@ -34,7 +34,7 @@ jobs:
TAURI_FRONTEND_PATH: ${{ github.workspace }}/frontend
TAURI_APP_PATH: ${{ github.workspace }}/crates/desktop/artcraft
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: storytold/craft-common-actions/actions/install-apt-packages@main
with:
packages: libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev libclang-dev libasound2-dev libxdo-dev libdbus-1-dev build-essential clang file jq perl cmake nasm rpm zsync gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav
@@ -63,7 +63,7 @@ jobs:
PKG_CONFIG_LIBDIR_riscv64gc_unknown_linux_gnu=/usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
ENV
echo "CMAKE_TOOLCHAIN_FILE_riscv64gc_unknown_linux_gnu=$GITHUB_WORKSPACE/packaging/linux/riscv64.cmake" >> "$GITHUB_ENV"
- uses: actions/setup-node@v7
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
@@ -89,7 +89,7 @@ jobs:
ARCH: ${{ matrix.arch }}
TARGET: ${{ matrix.target }}
run: packaging/linux/package.sh "$ARCH" "$TARGET"
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: release-linux-${{ matrix.arch }}
path: dist/release/*
@@ -102,8 +102,8 @@ jobs:
matrix:
arch: [x86_64, aarch64]
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: release-linux-${{ matrix.arch }}
path: dist/linux
@@ -126,7 +126,7 @@ jobs:
for package in dist/release/*.pkg.tar.zst; do
tar --zstd -xOf "$package" .PKGINFO | grep -Fx 'depend = webkit2gtk-4.1'
done
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: release-arch-${{ matrix.arch }}
path: dist/release/*.pkg.tar.zst
@@ -145,8 +145,8 @@ jobs:
- {arch: x86_64, runner: ubuntu-24.04}
- {arch: aarch64, runner: ubuntu-24.04-arm}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: release-linux-${{ matrix.arch }}
path: dist/linux
@@ -161,10 +161,8 @@ jobs:
- name: Build Flatpak and publish OCI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: packaging/linux/flatpak.sh
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: release-flatpak-${{ matrix.arch }}
path: |
@@ -177,7 +175,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Read version
id: version
run: echo "value=$(jq -r .version crates/desktop/artcraft/tauri.conf.json)" >> "$GITHUB_OUTPUT"
@@ -191,5 +189,6 @@ jobs:
app-title: ArtCraft
tag-prefix: artcraft-v
retain: "true"
allow-published: "true"
github-token: ${{ secrets.GITHUB_TOKEN }}
signing-key: ${{ secrets.LINUX_SIGNING_KEY }}
+2 -2
View File
@@ -29,10 +29,10 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v7
uses: actions/setup-node@v4
with:
node-version: lts/*
@@ -24,7 +24,7 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -39,7 +39,7 @@ jobs:
shell: cmd
- name: setup node
uses: actions/setup-node@v7
uses: actions/setup-node@v4
with:
node-version: lts/*
+5
View File
@@ -21,4 +21,9 @@ jobs:
freebsd:
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main
with:
architectures: '["x86_64"]'
craft-fonts-ref: ""
cargo-build-extra-args: "--exclude artcraft --exclude artcraft_native --exclude tauri-plugin-http"
cargo-test-extra-args: "--exclude artcraft --exclude artcraft_native --exclude tauri-plugin-http"
Generated
-54
View File
@@ -553,28 +553,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
name = "ashpd"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
dependencies = [
"async-fs",
"async-net",
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.2",
"raw-window-handle",
"serde",
"serde_repr",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "assertor"
version = "0.0.3"
@@ -638,17 +616,6 @@ dependencies = [
"slab",
]
[[package]]
name = "async-fs"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
dependencies = [
"async-lock",
"blocking",
"futures-lite",
]
[[package]]
name = "async-io"
version = "2.6.0"
@@ -678,17 +645,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
@@ -6272,12 +6228,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "polycool"
version = "0.4.0"
@@ -6988,7 +6938,6 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"ashpd",
"block2 0.6.2",
"dispatch2",
"glib-sys",
@@ -7000,9 +6949,7 @@ dependencies = [
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"pollster",
"raw-window-handle",
"urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -12009,7 +11956,6 @@ dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow 1.0.0",
"zcheapstr",
"zvariant_derive",
-1
View File
@@ -2,7 +2,6 @@
<p align="center">
<a href="https://sonarcloud.io/project/overview?id=storytold_artcraft"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
<a href="https://coderabbit.ai"><img alt="Reviewed by CodeRabbit" src="https://img.shields.io/coderabbit/prs/github/storytold/artcraft?style=flat-square&label=Reviewed%20by%20CodeRabbit&logo=coderabbit"></a>
</p>
<p align="center"><strong>The IDE for artists.</strong></p>
+1 -1
View File
@@ -1,4 +1,4 @@
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
/// This should match the CI builds.
pub const ARTCRAFT_VERSION: &str = "0.42.0";
pub const ARTCRAFT_VERSION: &str = "0.41.1";
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.42.0",
"version": "0.41.1",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.42.0",
"version": "0.41.1",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
@@ -1,3 +1,4 @@
/// When downloading a remote file, it makes sense to try to match the same file extension (with reasonable safeguards).
#[deprecated(note = "Use `Extension` in `url_utils::extension::extension` instead")]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -12,7 +12,9 @@ pub fn extract_download_extension_from_url_str(url: &str) -> Option<DownloadExte
#[deprecated(note = "Use `extract_extension_from_url` in `url_utils::extension::extract_extension_from_url` instead")]
pub fn extract_download_extension_from_url(url: &Url) -> Option<DownloadExtension> {
let path = url.path();
let extension = std::path::Path::new(path).extension().and_then(|ext| ext.to_str())?;
let extension = std::path::Path::new(path)
.extension()
.and_then(|ext| ext.to_str())?;
DownloadExtension::try_from_str(extension)
}
@@ -24,14 +26,14 @@ mod tests {
#[test]
fn success_case() {
let url = "https://storyteller.ai/example.jpg";
let maybe_extension = extract_download_extension_from_url_str(url);
let maybe_extension= extract_download_extension_from_url_str(url);
assert_eq!(maybe_extension, Some(DownloadExtension::Jpg));
}
#[test]
fn difficult_url() {
let url = "https://videos.youtube.com/foo/bar/files/00000000-f19a-1275-a092-caa60b38f8d2%2Fraw?se=2025-11-21T22%3A26%3A53Z&sp=r&sv=2024-08-09&sr=b&skoid=8cfff87a-01fc-27c1-9091-32999dcd1389&sktid=aa8ccaf6-e6da-484e-ab11-9c7496c21c53&skt=2025-11-17T21%3A09%3A08Z&ske=2025-10-23T21%3A14%3A08Z&sks=b&skv=2024-08-04&sig=T2U6%2Brw9WJdJagBkCL4/GlFr72KhZGP3QPlobIO7JlU%3D&ac=oaisdsorprnorthcentralus";
let maybe_extension = extract_download_extension_from_url_str(url);
let maybe_extension= extract_download_extension_from_url_str(url);
assert!(maybe_extension.is_none());
}
}
@@ -1,4 +1,3 @@
#[expect(clippy::module_inception, reason = "Preserve the existing public module path")]
mod download_extension;
pub mod extract_download_extension_from_url;
+12 -29
View File
@@ -1,6 +1,5 @@
use std::borrow::Cow;
use std::fmt;
use std::hash::{Hash, Hasher};
/// A file extension that can be either a compile-time constant or a runtime-parsed value.
///
@@ -17,7 +16,7 @@ use std::hash::{Hash, Hasher};
/// assert_eq!(ext.without_period(), "mp4");
/// assert_eq!(ext.with_period(), ".mp4");
/// ```
#[derive(Clone, Eq)]
#[derive(Clone, Eq, Hash)]
pub struct Extension {
/// The extension without the leading period, e.g. "png", "mp4".
value: Cow<'static, str>,
@@ -35,14 +34,20 @@ impl Extension {
/// const MP4: Extension = Extension::from_static("mp4", ".mp4");
/// ```
pub const fn from_static(ext: &'static str, dotted: &'static str) -> Self {
Self { value: Cow::Borrowed(ext), dotted: Cow::Borrowed(dotted) }
Self {
value: Cow::Borrowed(ext),
dotted: Cow::Borrowed(dotted),
}
}
/// Creates an `Extension` from a `&str` at runtime.
/// The input must NOT include a leading period.
pub fn new(ext: &str) -> Self {
let dotted = format!(".{ext}");
Self { value: Cow::Owned(ext.to_string()), dotted: Cow::Owned(dotted) }
let dotted = format!(".{}", ext);
Self {
value: Cow::Owned(ext.to_string()),
dotted: Cow::Owned(dotted),
}
}
/// Returns the extension without the leading period, e.g. `"png"`.
@@ -62,12 +67,6 @@ impl PartialEq for Extension {
}
}
impl Hash for Extension {
fn hash<H: Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
impl fmt::Debug for Extension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Extension(\"{}\")", self.value)
@@ -136,28 +135,12 @@ mod tests {
#[test]
fn display() {
assert_eq!(format!("{PNG}"), "png");
assert_eq!(format!("{}", PNG), "png");
assert_eq!(format!("{}", Extension::new("xyz")), "xyz");
}
#[test]
fn debug() {
assert_eq!(format!("{PNG:?}"), "Extension(\"png\")");
}
#[test]
fn hash_matches_equality() {
let plain = Extension::from_static("png", ".png");
let inconsistent = Extension::from_static("png", ".not-a-suffix");
assert_eq!(plain, inconsistent);
assert_eq!(hash(&plain), hash(&inconsistent));
}
fn hash(extension: &Extension) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
extension.hash(&mut hasher);
hasher.finish()
assert_eq!(format!("{:?}", PNG), "Extension(\"png\")");
}
}
@@ -6,13 +6,24 @@ use url::Url;
// TODO: Lookups against these as lists will be slow
/// Known image extensions.
const KNOWN_IMAGE_EXTENSIONS: &[Extension] = &[Extension::from_static("png", ".png"), Extension::from_static("jpg", ".jpg"), Extension::from_static("jpeg", ".jpeg"), Extension::from_static("webp", ".webp")];
const KNOWN_IMAGE_EXTENSIONS: &[Extension] = &[
Extension::from_static("png", ".png"),
Extension::from_static("jpg", ".jpg"),
Extension::from_static("jpeg", ".jpeg"),
Extension::from_static("webp", ".webp"),
];
/// Known audio extensions.
const KNOWN_AUDIO_EXTENSIONS: &[Extension] = &[Extension::from_static("wav", ".wav"), Extension::from_static("mp3", ".mp3")];
const KNOWN_AUDIO_EXTENSIONS: &[Extension] = &[
Extension::from_static("wav", ".wav"),
Extension::from_static("mp3", ".mp3"),
];
/// Known video extensions.
const KNOWN_VIDEO_EXTENSIONS: &[Extension] = &[Extension::from_static("mp4", ".mp4"), Extension::from_static("webm", ".webm")];
const KNOWN_VIDEO_EXTENSIONS: &[Extension] = &[
Extension::from_static("mp4", ".mp4"),
Extension::from_static("webm", ".webm"),
];
/// Controls which extensions are accepted when extracting from a URL.
pub enum ExtractExtensions {
@@ -51,7 +62,9 @@ pub fn extract_extension_from_url_str(url: &str, accept: &ExtractExtensions) ->
/// Extracts a file extension from a parsed `Url`, filtered by `accept`.
pub fn extract_extension_from_url(url: &Url, accept: &ExtractExtensions) -> Option<Extension> {
let path = url.path();
let raw_ext = std::path::Path::new(path).extension().and_then(|ext| ext.to_str())?;
let raw_ext = std::path::Path::new(path)
.extension()
.and_then(|ext| ext.to_str())?;
let lower = raw_ext.to_lowercase();
let candidate = Extension::new(&lower);
@@ -59,40 +72,27 @@ pub fn extract_extension_from_url(url: &Url, accept: &ExtractExtensions) -> Opti
match accept {
ExtractExtensions::All => Some(candidate),
ExtractExtensions::Set(set) => {
if set.contains(&candidate) {
Some(candidate)
} else {
None
}
},
if set.contains(&candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownImage => {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) {
Some(candidate)
} else {
None
}
},
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownAudio => {
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) {
Some(candidate)
} else {
None
}
},
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownVideo => {
if slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) {
Some(candidate)
} else {
None
}
},
if slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownMedia => {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) || slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) || slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate)
|| slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate)
|| slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate)
{
Some(candidate)
} else {
None
}
},
}
}
}
@@ -190,14 +190,19 @@ mod tests {
#[test]
fn set_matches_custom_list() {
let accept = ExtractExtensions::from_vec(vec![Extension::from_static("csv", ".csv"), Extension::from_static("tsv", ".tsv")]);
let accept = ExtractExtensions::from_vec(vec![
Extension::from_static("csv", ".csv"),
Extension::from_static("tsv", ".tsv"),
]);
let ext = extract_extension_from_url_str("https://example.com/data.csv", &accept);
assert_eq!(ext.unwrap().without_period(), "csv");
}
#[test]
fn set_rejects_non_member() {
let accept = ExtractExtensions::from_vec(vec![Extension::from_static("csv", ".csv")]);
let accept = ExtractExtensions::from_vec(vec![
Extension::from_static("csv", ".csv"),
]);
let ext = extract_extension_from_url_str("https://example.com/data.png", &accept);
assert!(ext.is_none());
}
@@ -210,7 +215,10 @@ mod tests {
#[test]
fn query_params_ignored() {
let ext = extract_extension_from_url_str("https://example.com/file.mp4?token=abc&format=hd", &ExtractExtensions::KnownVideo);
let ext = extract_extension_from_url_str(
"https://example.com/file.mp4?token=abc&format=hd",
&ExtractExtensions::KnownVideo,
);
assert_eq!(ext.unwrap().without_period(), "mp4");
}
@@ -222,7 +230,8 @@ mod tests {
}
fn extract_extension_all(url: &str) -> Option<String> {
extract_extension_from_url_str(url, &ExtractExtensions::All).map(|ext| ext.without_period().to_string())
extract_extension_from_url_str(url, &ExtractExtensions::All)
.map(|ext| ext.without_period().to_string())
}
#[test]
+1 -2
View File
@@ -1,3 +1,2 @@
#[expect(clippy::module_inception, reason = "Preserve the existing public module path")]
pub mod extension;
pub mod extract_extension_from_url;
pub mod extract_extension_from_url;
+1 -2
View File
@@ -1,3 +1,2 @@
pub mod extension;
#[expect(deprecated, reason = "Keep the public compatibility module while downstream users migrate to Extension")]
pub mod download_extension;
pub mod download_extension;
@@ -948,7 +948,7 @@ export const GalleryModal = React.memo(
// couldn't see it — making the library button appear broken.
galleryModalDraggingUnder.value = false;
let cancelled = false;
void (async () => {
(async () => {
setUsernameError(false);
const name = await fetchCurrentUsername();
if (cancelled) return;
@@ -90,7 +90,7 @@ export function useMediaPromptTokens(
const key = [...new Set(cleaned)].sort().join(",");
useEffect(() => {
if (key.length > 0) void fetchMissing(key.split(","));
if (key.length > 0) fetchMissing(key.split(","));
}, [key]);
const result = new Map<string, string>();
@@ -63,7 +63,7 @@ export function usePrompts(
const key = [...new Set(cleaned)].sort().join(",");
useEffect(() => {
if (key.length > 0) void fetchMissing(key.split(","));
if (key.length > 0) fetchMissing(key.split(","));
}, [key]);
const result = new Map<string, Prompts>();
@@ -42,7 +42,7 @@ const NewsView = ({ onBack }: { onBack: () => void }) => {
console.warn("Failed to fetch news feed, using bundled content.", e);
}
};
void fetchNews();
fetchNews();
}, []);
const selectedPost = useMemo(
@@ -312,7 +312,7 @@ export function HelpMenuButton({
});
};
void run();
run();
return () => {
if (progressTimerRef.current) {
@@ -179,7 +179,7 @@ export function LightboxModal({
audioPlayerRef.current = null;
};
audioPlayerRef.current = audio;
void audio.play();
audio.play();
setPlayingAudioToken(token);
}
},
@@ -83,7 +83,7 @@ export function TagsSection({
useEffect(() => {
if (currentUsername !== undefined) return;
let cancelled = false;
void resolveSessionUsername().then((username) => {
resolveSessionUsername().then((username) => {
if (!cancelled) setResolvedUsername(username);
});
return () => {
@@ -133,7 +133,7 @@ export function TagsSection({
}
ownTagsLoadedRef.current = true;
let cancelled = false;
void (async () => {
(async () => {
const all: TagDetails[] = [];
let cursor: string | undefined = undefined;
for (let page = 0; page < 50; page++) {
@@ -1040,7 +1040,7 @@ const PageDraw = ({
await tryFit();
};
void autoFitCanvas();
autoFitCanvas();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [baseImageBitmap, baseImageInfo?.isBlankCanvas]);
@@ -1501,7 +1501,7 @@ export const PaintSurface = ({
// Load the checkboard image for transparency background
useEffect(() => {
void loadImageFromUrl(checkerboard).then(setCheckerImage);
loadImageFromUrl(checkerboard).then(setCheckerImage);
}, []);
// Separate function to render transformers on their own layer
@@ -1167,7 +1167,7 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
console.error("Failed to restore image:", error);
}
};
void loadImage();
loadImage();
}
return node;
@@ -387,7 +387,7 @@ export const Stage3DBody = ({
useEffect(() => {
const handler = onImageDrop(
(item: GalleryItem, position: { x: number; y: number }) => {
void (async () => {
(async () => {
if (!editor) {
console.warn("Cannot drop asset: editor engine not ready");
return;
@@ -14,7 +14,7 @@ export function toggleObjectVisibility(editor: Editor, uuid: string): void {
if (!obj) return;
const before = obj.visible;
editor.bus.emit(new OutlinerItemVisibilityToggledEvent(uuid));
void editor.sceneManager?.hideObject(uuid);
editor.sceneManager?.hideObject(uuid);
editor.history.record(
new VisibilityAction(editor, uuid, before, !before),
);
@@ -190,7 +190,7 @@ class Editor {
// Ammo.js WASM is a global side-effect script the physics path
// depends on. The singleton ensures it's appended to document.body
// exactly once, even across Editor re-construction.
void ensureAmmoLoaded();
ensureAmmoLoaded();
// Version and name.
this.version = 2.0;
@@ -667,9 +667,9 @@ class Editor {
};
if (!this.utils.isEmpty(cacheJson)) {
void this.save_manager.loadCache(cacheJson).then(applyIfLoaded);
this.save_manager.loadCache(cacheJson).then(applyIfLoaded);
} else if (!this.utils.isEmpty(sceneToken)) {
void this.save_manager.loadScene(sceneToken).then(applyIfLoaded);
this.save_manager.loadScene(sceneToken).then(applyIfLoaded);
} else {
this.adapter.onSceneTitleChange?.({
title: "Untitled New Scene",
@@ -1065,7 +1065,7 @@ class Editor {
}
this.lastFrameTime = frameTime;
void this.renderSingleFrame();
this.renderSingleFrame();
}
startRenderLoop() {
@@ -151,7 +151,7 @@ export class ModalTransformController {
}
}
this.target.position.copy(this.startPos).add(delta);
void this.editor.renderScene();
this.editor.renderScene();
}
private axisVector(axis: "x" | "y" | "z"): THREE.Vector3 {
@@ -190,7 +190,7 @@ export class ModalTransformController {
private cancel() {
if (this.target) this.target.position.copy(this.startPos);
void this.editor.renderScene();
this.editor.renderScene();
this.cleanup();
}
@@ -77,7 +77,7 @@ export class LoadingPlaceHolderManager {
if (result === undefined) {
return;
}
void result.destroyWithScene(this.scene);
result.destroyWithScene(this.scene);
this.messages.delete(id);
}
@@ -112,7 +112,7 @@ class Scene {
initialize() {
this.scene = new THREE.Scene();
this.placeholder_manager = new LoadingPlaceHolderManager(this.scene);
void this.placeholder_manager.initialize();
this.placeholder_manager.initialize();
this._createGrid();
this._create_base_lighting();
@@ -170,7 +170,7 @@ export function useCurrency(): UseCurrencyResult {
useEffect(() => {
let cancelled = false;
void fetchRates().then((r) => {
fetchRates().then((r) => {
if (!cancelled) {
setRates(r);
setRatesLoaded(true);
@@ -102,7 +102,7 @@ const AudioRefTile = ({
el.volume = 0.2;
audioRef.current = el;
el.onended = () => setIsPlaying(false);
void el.play();
el.play();
setIsPlaying(true);
}
},
@@ -347,7 +347,7 @@ export const PromptBox2D = ({
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
void handleGenerate();
handleGenerate();
}
};
@@ -543,7 +543,7 @@ export const PromptBox3D = ({
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
void handleEnqueue();
handleEnqueue();
}
};
@@ -352,7 +352,7 @@ export const PromptBoxAudio = ({
if (e.key !== "Enter") return;
if (enterToGenerate && !e.shiftKey) {
e.preventDefault();
void handleEnqueue();
handleEnqueue();
}
};
@@ -461,7 +461,7 @@ export const PromptBoxImage = ({
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
void handleEnqueue();
handleEnqueue();
}
};
@@ -1231,7 +1231,7 @@ export const PromptBoxVideo = ({
console.error("PromptBoxVideo - enqueue failed", failure.reason);
toast.error(commandErrorMessage(failure.reason, "Failed to start video generation. Please try again."));
}
if (acceptedIds.length) void onEnqueuePressed?.(prompt, acceptedIds);
if (acceptedIds.length) onEnqueuePressed?.(prompt, acceptedIds);
setIsEnqueueing(false);
};
@@ -1282,7 +1282,7 @@ export const PromptBoxVideo = ({
return;
}
void handleEnqueue();
handleEnqueue();
}
};
@@ -1392,7 +1392,7 @@ export const PromptBoxVideo = ({
)
return;
if (!prompt.trim()) return;
void handleEnqueue();
handleEnqueue();
}
}}
onFocus={() => setIsFocused(true)}
@@ -68,7 +68,7 @@ export const DeckCard = ({
el.volume = 0.2;
audioRef.current = el;
el.onended = () => setIsPlaying(false);
void el.play();
el.play();
setIsPlaying(true);
}
}, [isPlaying, item.url, stopAudio]);
@@ -516,8 +516,8 @@ export function useDeckMedia<
);
if (images.length > 0) processImageFiles(images, "start");
if (videos.length > 0 && setReferenceVideos) void processVideoFiles(videos);
if (audios.length > 0 && setReferenceAudios) void processAudioFiles(audios);
if (videos.length > 0 && setReferenceVideos) processVideoFiles(videos);
if (audios.length > 0 && setReferenceAudios) processAudioFiles(audios);
};
// maxImages of 0 means the page's model takes no image refs at all.
@@ -92,7 +92,7 @@ export function ProviderSetupModal({
icon={ArrowRightIcon}
iconFlip={true}
onClick={() => {
void buttonOnClick();
buttonOnClick();
}}
>
{modalButtonText}
@@ -27,7 +27,7 @@ export const AboutSettingsPane = (args: AboutSettingsPaneProps) => {
const prefs = await GetAppInfo();
setAppInfo(prefs.payload);
};
void fetchData();
fetchData();
}, []);
const handleVersionClick = () => {
@@ -40,7 +40,7 @@ export const ArtcraftAccountBlock = ({
setIsCheckingArtcraftSession(false);
}
};
void fetchSession();
fetchSession();
}, []);
const handleArtcraftButton = async () => {
@@ -24,11 +24,11 @@ export const GrokAccountBlock = () => {
};
useEffect(() => {
void fetchSession();
fetchSession();
}, []);
useRefreshAccountStateEvent(async (event: RefreshAccountStateEvent) => {
void fetchSession();
fetchSession();
});
const clearState = async() => {
@@ -24,11 +24,11 @@ export const MidjourneyAccountBlock = () => {
};
useEffect(() => {
void fetchSession();
fetchSession();
}, []);
useRefreshAccountStateEvent(async (event: RefreshAccountStateEvent) => {
void fetchSession();
fetchSession();
});
const clearState = async() => {
@@ -28,7 +28,7 @@ export const OpenAIApiKeyBlock = () => {
setIsCheckingOpenAIApiKey(false);
}
};
void fetchKey();
fetchKey();
}, []);
const syncOpenAIApiKey = async (key: string) => {
@@ -23,7 +23,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
const prefs = await GetAppPreferences();
setPreferences(prefs.preferences);
};
void fetchData();
fetchData();
}, []);
const playSounds = preferences?.play_sounds || false;
@@ -221,7 +221,7 @@ export const DownloadsSettingsPane = () => {
if (value === "custom") {
setFilenameMode("custom");
} else {
void saveFilenamePreference("artcraft_convention");
saveFilenamePreference("artcraft_convention");
}
}}
/>
@@ -139,7 +139,7 @@ export const ProviderPrioritySettingsPane = () => {
setItems(items);
};
void fetchData();
fetchData();
}, []);
const sensors = useSensors(
@@ -178,7 +178,7 @@ export const ProviderPrioritySettingsPane = () => {
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
// Send update to backend
void updateProviderPriorityOnBackend(newOrder);
updateProviderPriorityOnBackend(newOrder);
return newOrder;
});
@@ -270,7 +270,7 @@ export const UploadFiles3D = ({
// B's key.
useEffect(() => {
if (previewStatus.type === "OK" && canvasRef.current) {
void snapshotCanvasAsThumbnail({
snapshotCanvasAsThumbnail({
targetNode: canvasRef.current,
resultCallback: (blob) => {
const currentFile = fileEntries[previewIndex]?.file;
@@ -503,7 +503,7 @@ export const UploadFiles3D = ({
// can only ever be a clean single-file submission; the excludedCount
// check makes that invariant explicit rather than incidental.
if (excludedCount === 0 && files.length === 1 && fileEntries.length === 1) {
void upload3DObjects({
upload3DObjects({
title: files[0].name.slice(0, files[0].name.lastIndexOf(".")),
assetFile: files[0],
engineCategory,
@@ -521,7 +521,7 @@ export const UploadFiles3D = ({
setIsUploading(true);
setOverallProgress({ current: 0, total: files.length });
void upload3DObjectsBatch({
upload3DObjectsBatch({
files,
thumbnails: pendingThumbnails,
engineCategory,
@@ -581,7 +581,7 @@ export const UploadFiles3D = ({
setIsUploading(true);
setOverallProgress({ current: 0, total: failedFiles.length });
void upload3DObjectsBatch({
upload3DObjectsBatch({
files: failedFiles,
thumbnails: failedThumbnails,
engineCategory,
@@ -742,7 +742,7 @@ export const UploadFiles3D = ({
className="hidden items-center text-xs text-white/60 transition-colors hover:text-white group-hover:inline-flex"
onClick={(e) => {
e.stopPropagation();
void retrySingleFile(i);
retrySingleFile(i);
}}
title="Retry"
>
@@ -117,7 +117,7 @@ export const UploadFilesImage = ({
.filter((i) => i !== -1);
if (files.length === 1 && fileEntries.length === 1) {
void uploadImage({
uploadImage({
title: files[0].name.slice(0, files[0].name.lastIndexOf(".")),
assetFile: files[0],
progressCallback: onUploadProgress,
@@ -128,7 +128,7 @@ export const UploadFilesImage = ({
setIsUploading(true);
setOverallProgress({ current: 0, total: files.length });
void uploadImagesBatch({
uploadImagesBatch({
files,
onFileStatusChange: (batchIndex, status, errorMessage) =>
updateFileStatus(originalIndices[batchIndex], status, errorMessage),
@@ -159,7 +159,7 @@ export const UploadFilesImage = ({
setIsUploading(true);
setOverallProgress({ current: 0, total: failedFiles.length });
void uploadImagesBatch({
uploadImagesBatch({
files: failedFiles,
onFileStatusChange: (batchIndex, status, errorMessage) =>
updateFileStatus(failedIndices[batchIndex], status, errorMessage),
@@ -248,7 +248,7 @@ export const UploadFilesImage = ({
className="hidden group-hover:inline-flex items-center text-xs text-white/60 hover:text-white transition-colors"
onClick={(e) => {
e.stopPropagation();
void retrySingleFile(i);
retrySingleFile(i);
}}
title="Retry"
>
@@ -127,7 +127,7 @@ export const UploadFilesSplat = ({
const canvas = canvasRef.current;
const currentFile = fileEntries[previewIndex]?.file;
const rafId = requestAnimationFrame(() => {
void snapshotCanvasAsThumbnail({
snapshotCanvasAsThumbnail({
targetNode: canvas,
resultCallback: (blob) => {
if (blob && currentFile) {
@@ -222,7 +222,7 @@ export const UploadFilesSplat = ({
);
if (files.length === 1 && fileEntries.length === 1) {
void upload3DObjects({
upload3DObjects({
title: files[0].name.slice(0, files[0].name.lastIndexOf(".")),
assetFile: files[0],
engineCategory,
@@ -236,7 +236,7 @@ export const UploadFilesSplat = ({
setIsUploading(true);
setOverallProgress({ current: 0, total: files.length });
void upload3DObjectsBatch({
upload3DObjectsBatch({
files,
thumbnails: pendingThumbnails,
engineCategory,
@@ -277,7 +277,7 @@ export const UploadFilesSplat = ({
setIsUploading(true);
setOverallProgress({ current: 0, total: failedFiles.length });
void upload3DObjectsBatch({
upload3DObjectsBatch({
files: failedFiles,
thumbnails: failedThumbnails,
engineCategory,
@@ -375,7 +375,7 @@ export const UploadFilesSplat = ({
className="hidden group-hover:inline-flex items-center text-xs text-white/60 hover:text-white transition-colors"
onClick={(e) => {
e.stopPropagation();
void retrySingleFile(i);
retrySingleFile(i);
}}
title="Retry"
>
@@ -441,7 +441,7 @@ export function useEditorActions() {
useActionHandler(
"toggle-bookmark",
() => {
void editor.scenes.toggleBookmark({ time: editor.playback.getCurrentTime() });
editor.scenes.toggleBookmark({ time: editor.playback.getCurrentTime() });
},
undefined,
);
@@ -21,7 +21,7 @@ export function useFontAtlas({ open }: { open: boolean }) {
if (!open || atlas) return;
setStatus("loading");
void loadFontAtlas().then((data) => {
loadFontAtlas().then((data) => {
if (data) {
setAtlas(data);
setStatus("idle");
@@ -34,7 +34,7 @@ export function useFontAtlas({ open }: { open: boolean }) {
const retry = useCallback(() => {
clearFontAtlasCache();
setStatus("loading");
void loadFontAtlas().then((data) => {
loadFontAtlas().then((data) => {
if (data) {
setAtlas(data);
setStatus("idle");
@@ -20,7 +20,7 @@ export function useFullscreen({
const toggleFullscreen = useCallback(() => {
if (!containerRef.current) return;
if (document.fullscreenElement) {
void document.exitFullscreen();
document.exitFullscreen();
} else {
containerRef.current.requestFullscreen();
}
@@ -313,7 +313,7 @@ function BookmarkPopoverContent({
}, [bookmark.color]);
const handleRemove = () => {
void editor.scenes.removeBookmark({ time });
editor.scenes.removeBookmark({ time });
onPopoverClose();
};
@@ -334,7 +334,7 @@ function BookmarkPopoverContent({
updates.duration = duration;
}
if (Object.keys(updates).length === 0) return;
void editor.scenes.updateBookmark({ time, updates });
editor.scenes.updateBookmark({ time, updates });
};
const maxDuration = mediaTimeToSeconds({
time:
@@ -240,7 +240,7 @@ export function useBookmarkDrag({
const clampedTime =
dragState.currentTime > duration ? duration : dragState.currentTime;
void editor.scenes.moveBookmark({
editor.scenes.moveBookmark({
fromTime: dragState.bookmarkTime,
toTime: clampedTime,
});
+3 -4
View File
@@ -13,10 +13,9 @@ existing `artcraft-v<version>` draft, retaining Windows and macOS assets. The ve
Native AppImages also include zsync delta-update metadata.
Flatpak uses native Ubuntu 24.04 runners and GNOME 51 for GTK3/WebKitGTK 4.1. Its build checks
for missing ELF dependencies inside the sandbox, grants `--persist=Artcraft` so settings and task
data survive restarts, and provides a launcher wrapper keeping WebKitGTK's accelerated rendering
path enabled. RISC-V packages require Ubuntu 24.04-era libraries; x86_64 and ARM64 use Ubuntu 22.04
as their build baseline. The suite does not publish RISC-V AppImages, Arch packages or Flatpaks.
for missing ELF dependencies inside the sandbox. RISC-V packages require Ubuntu 24.04-era
libraries; x86_64 and ARM64 use Ubuntu 22.04 as their build baseline. The suite does not publish
RISC-V AppImages, Arch packages or Flatpaks.
`package.sh <architecture> <rust-target>` normalizes Tauri's DEB/RPM/AppImage filenames and
extracts the DEB's `/usr` tree into the suite's `bin/` and `share/` tarball layout. Shared Craft
+6 -43
View File
@@ -2,50 +2,13 @@
<component type="desktop-application">
<id>ai.artcraft.app</id>
<name>ArtCraft Studio</name>
<summary>Create images and video with AI</summary>
<summary>Create AI images and video</summary>
<metadata_license>MIT</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<developer id="ai.artcraft">
<name>ArtCraft</name>
</developer>
<url type="homepage">https://getartcraft.com</url>
<url type="bugtracker">https://github.com/storytold/artcraft/issues</url>
<url type="vcs-browser">https://github.com/storytold/artcraft</url>
<url type="contact">https://discord.gg/artcraft</url>
<description>
<p>ArtCraft Studio is a creative desktop workspace for generating AI images and video with connected AI providers.</p>
<ul>
<li>Text-to-image and image-to-image creation with state-of-the-art models</li>
<li>Video generation and camera motion controls</li>
<li>Direct integration with Fal, Grok, Midjourney, Sora, and WorldLabs</li>
<li>Local task persistence with an embedded SQLite queue</li>
</ul>
</description>
<description><p>ArtCraft Studio is a desktop workspace for generating AI images and video with connected AI providers.</p></description>
<launchable type="desktop-id">ai.artcraft.app.desktop</launchable>
<categories>
<category>Graphics</category>
<category>AudioVideo</category>
</categories>
<keywords>
<keyword>ai</keyword>
<keyword>image</keyword>
<keyword>video</keyword>
<keyword>generation</keyword>
<keyword>art</keyword>
</keywords>
<branding>
<color type="primary" scheme_preference="light">#2f7bf5</color>
<color type="primary" scheme_preference="dark">#1a5bc4</color>
</branding>
<requires>
<display_length compare="ge">760</display_length>
</requires>
<supports>
<control>keyboard</control>
<control>pointing</control>
</supports>
<content_rating type="oars-1.1" />
<provides>
<binary>artcraft</binary>
</provides>
<url type="homepage">https://getartcraft.com</url>
<developer id="ai.artcraft"><name>ArtCraft</name></developer>
<categories><category>Graphics</category><category>AudioVideo</category></categories>
<provides><binary>artcraft</binary></provides>
</component>
-16
View File
@@ -1,16 +0,0 @@
#!/bin/sh
set -eu
# ArtCraft Studio is a Tauri (WebKitGTK) desktop app.
#
# Keep WebKitGTK's accelerated path on, the opposite of the older recipe of
# disabling the DMABUF renderer. On org.gnome.Platform//51 the unaccelerated
# path draws this window wrong.
#
# ${VAR:-0} rather than plain assignment, so a user whose driver does need the
# unaccelerated path can take it back:
# flatpak override --user --env=WEBKIT_DISABLE_DMABUF_RENDERER=1 ai.artcraft.app
export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-0}"
export WEBKIT_DISABLE_COMPOSITING_MODE="${WEBKIT_DISABLE_COMPOSITING_MODE:-0}"
exec /app/bin/artcraft.real "$@"
+1 -11
View File
@@ -8,7 +8,6 @@ trap 'rm -rf "$work"' EXIT
mkdir -p "$work/stage" dist/release
tar -xzf "dist/linux/$base.tar.gz" --strip-components=1 -C "$work/stage"
cp packaging/linux/flatpak.yml "$work/manifest.yml"
cp packaging/linux/artcraft-wrapper "$work/artcraft-wrapper"
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
sign=()
keys=()
@@ -22,7 +21,7 @@ flatpak-builder --user --install-deps-from=flathub --disable-rofiles-fuse --forc
# Fail on missing WebKitGTK or other runtime libraries rather than shipping an unloadable binary.
# shellcheck disable=SC2016 # Expand these variables inside the Flatpak runtime shell.
flatpak build --runtime "$work/build" sh -c '
dependencies=$(ldd /app/bin/artcraft.real) || exit 1
dependencies=$(ldd /app/bin/artcraft) || exit 1
printf "%s\n" "$dependencies"
case "$dependencies" in *"not found"*) exit 1 ;; esac
'
@@ -38,13 +37,4 @@ if [[ -n "${SIGNING_FPR:-}" ]]; then
skopeo copy --format oci --dest-creds "$creds" "oci:dist/release/$base-oci" "docker://$registry:stable-$arch"
fi
skopeo inspect --raw --creds "$creds" "docker://$registry:$version-$arch" >"dist/release/$base.oci-manifest.json"
if [[ -n "${DOCKERHUB_USERNAME:-}" && -n "${DOCKERHUB_TOKEN:-}" ]]; then
dh_registry="docker.io/storytold/${GITHUB_REPOSITORY##*/}"
dh_creds="$DOCKERHUB_USERNAME:$DOCKERHUB_TOKEN"
skopeo copy --format oci --dest-creds "$dh_creds" "oci:dist/release/$base-oci" "docker://$dh_registry:$version-$arch"
if [[ "$version" != *-* && ( "$GITHUB_REF" == refs/heads/release || "$GITHUB_REF" == refs/heads/artcraft-release ) ]]; then
skopeo copy --format oci --dest-creds "$dh_creds" "oci:dist/release/$base-oci" "docker://$dh_registry:stable-$arch"
fi
fi
fi
-7
View File
@@ -13,9 +13,6 @@ finish-args:
- --filesystem=xdg-pictures
- --filesystem=xdg-videos
- --filesystem=xdg-download
# The app hardcodes its data to ~/Artcraft (settings, credentials, state).
# Persist it under ~/.var/app/ai.artcraft.app/Artcraft so data survives exits.
- --persist=Artcraft
modules:
- name: artcraft
buildsystem: simple
@@ -23,10 +20,6 @@ modules:
- cp -a . /app/
- install -Dm644 /app/share/icons/hicolor/128x128/apps/artcraft.png /app/share/icons/hicolor/128x128/apps/ai.artcraft.app.png
- sed -i 's/^Icon=.*/Icon=ai.artcraft.app/' /app/share/applications/ai.artcraft.app.desktop
- mv /app/bin/artcraft /app/bin/artcraft.real
- install -Dm755 artcraft-wrapper /app/bin/artcraft
sources:
- type: dir
path: stage
- type: file
path: artcraft-wrapper