mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-10 16:06:47 -05:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ebde1454 | ||
|
|
b4ae5275eb |
@@ -14,8 +14,6 @@ updates:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Maintain GitHub Actions workflows
|
||||
- package-ecosystem: "github-actions"
|
||||
@@ -30,5 +28,3 @@ updates:
|
||||
- "*"
|
||||
commit-message:
|
||||
prefix: "ci(deps)"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -13,13 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
- uses: storytold/craft-common-actions/actions/install-apt-packages@main
|
||||
with:
|
||||
packages: rpm jq binutils zsync
|
||||
@@ -40,13 +34,7 @@ jobs:
|
||||
TAURI_FRONTEND_PATH: ${{ github.workspace }}/frontend
|
||||
TAURI_APP_PATH: ${{ github.workspace }}/crates/desktop/artcraft
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- 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
|
||||
@@ -75,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@0a44ba7841725637a54e2801669c06500c541b61 # v7.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
@@ -92,7 +80,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
- run: ./node_modules/.bin/nx run artcraft:build
|
||||
working-directory: frontend
|
||||
- uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0.5.18
|
||||
- uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0
|
||||
with:
|
||||
projectPath: crates/desktop/artcraft
|
||||
args: --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --config ${{ github.workspace }}/packaging/linux/tauri.conf.json
|
||||
@@ -101,7 +89,7 @@ jobs:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: packaging/linux/package.sh "$ARCH" "$TARGET"
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-linux-${{ matrix.arch }}
|
||||
path: dist/release/*
|
||||
@@ -114,14 +102,8 @@ jobs:
|
||||
matrix:
|
||||
arch: [x86_64, aarch64]
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-linux-${{ matrix.arch }}
|
||||
path: dist/linux
|
||||
@@ -144,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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-arch-${{ matrix.arch }}
|
||||
path: dist/release/*.pkg.tar.zst
|
||||
@@ -163,14 +145,8 @@ jobs:
|
||||
- {arch: x86_64, runner: ubuntu-24.04}
|
||||
- {arch: aarch64, runner: ubuntu-24.04-arm}
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-linux-${{ matrix.arch }}
|
||||
path: dist/linux
|
||||
@@ -185,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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-flatpak-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -201,13 +175,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Read version
|
||||
id: version
|
||||
run: echo "value=$(jq -r .version crates/desktop/artcraft/tauri.conf.json)" >> "$GITHUB_OUTPUT"
|
||||
@@ -221,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 }}
|
||||
|
||||
@@ -10,13 +10,6 @@ on:
|
||||
# This workflow creates Universal macOS builds with signing certificate support.
|
||||
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||
|
||||
# One release build per branch at a time. A run in progress is never cancelled, because it may
|
||||
# already have created or uploaded to the draft release. If more pushes arrive while one runs,
|
||||
# GitHub keeps only the newest as pending, so the next build is of the latest commit.
|
||||
concurrency:
|
||||
group: macos-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish-artcraft:
|
||||
permissions:
|
||||
@@ -35,27 +28,17 @@ jobs:
|
||||
# args: ''
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
# Successful builds took 10-22 min (2026-04 to 2026-11).
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@0a44ba7841725637a54e2801669c06500c541b61 # v7.0.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.platform == 'macos-latest-xlarge' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
@@ -234,7 +217,7 @@ jobs:
|
||||
|
||||
|
||||
- name: build and publish
|
||||
uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0.5.18
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
|
||||
@@ -10,13 +10,6 @@ on:
|
||||
# This workflow creates Windows builds.
|
||||
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||
|
||||
# One release build per branch at a time. A run in progress is never cancelled, because it may
|
||||
# already have created or uploaded to the draft release. If more pushes arrive while one runs,
|
||||
# GitHub keeps only the newest as pending, so the next build is of the latest commit.
|
||||
concurrency:
|
||||
group: windows-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish-artcraft-windows:
|
||||
permissions:
|
||||
@@ -30,22 +23,11 @@ jobs:
|
||||
args: ''
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
# Successful builds took 17-36 min (2026-04 to 2026-11); one hung run used the full 6-hour default.
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: install wreq dependencies
|
||||
run: |
|
||||
@@ -57,7 +39,7 @@ jobs:
|
||||
shell: cmd
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@0a44ba7841725637a54e2801669c06500c541b61 # v7.0.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
@@ -110,7 +92,7 @@ jobs:
|
||||
$config | ConvertTo-Json -Depth 10 | Set-Content tauri.conf.json
|
||||
|
||||
- name: build and publish
|
||||
uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0.5.18
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
# NB(bt): This is needed by wreq. Hopefully it doesn't kill Tauri
|
||||
RUSTFLAGS: "-C target-feature=+crt-static"
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ chrono.workspace = true
|
||||
cookie.workspace = true
|
||||
log.workspace = true
|
||||
reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
url.workspace = true
|
||||
|
||||
# Serialization
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ pub async fn legacy_upload_media_file_from_file<P: AsRef<Path>>(
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let file_bytes = tokio::fs::read(args.path.as_ref()).await
|
||||
let file_bytes = std::fs::read(args.path.as_ref())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ pub async fn upload_image_media_file_from_file_with_idempotency_token<P: AsRef<P
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let file_bytes = tokio::fs::read(args.path.as_ref()).await
|
||||
let file_bytes = std::fs::read(args.path.as_ref())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ pub async fn upload_new_engine_asset_from_file<P: AsRef<Path>>(
|
||||
let client = storyteller_client_builder(api_host)
|
||||
.build()?;
|
||||
|
||||
let file_bytes = tokio::fs::read(path.as_ref()).await?;
|
||||
let file_bytes = std::fs::read(path.as_ref())?;
|
||||
let file_name = path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ pub async fn upload_video_media_file_from_file<P: AsRef<Path>>(
|
||||
let client = storyteller_client_builder(args.api_host)
|
||||
.build()?;
|
||||
|
||||
let file_bytes = tokio::fs::read(args.path.as_ref()).await?;
|
||||
let file_bytes = std::fs::read(args.path.as_ref())?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::{info, warn};
|
||||
@@ -66,7 +68,13 @@ pub async fn download_media_file<P: AsRef<Path>>(
|
||||
let bytes = download_bytes(cdn_url).await?;
|
||||
|
||||
// 4. Write to disk.
|
||||
tokio::fs::write(&output_path, &bytes).await
|
||||
let mut file = fs::File::create(&output_path)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.flush()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
info!("Downloaded {} bytes to {:?}", bytes.len(), output_path);
|
||||
|
||||
@@ -31,7 +31,7 @@ reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
wreq.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ pub(crate) async fn upload_to_kinovi_web(
|
||||
.unwrap_or_else(|| "jpg".to_string());
|
||||
|
||||
let file_bytes = match maybe_local_path {
|
||||
Some(local_path) => match tokio::fs::read(local_path).await {
|
||||
Some(local_path) => match std::fs::read(local_path) {
|
||||
Ok(bytes) => {
|
||||
info!("Reusing predownloaded file for kinovi upload: {}", source_url);
|
||||
bytes
|
||||
|
||||
@@ -93,7 +93,7 @@ impl MidjourneyCredentialManager {
|
||||
let _guard = self.mutation.lock().await;
|
||||
// Remove every format, so a later startup cannot migrate a logged-out account.
|
||||
for path in self.legacy_paths.iter().chain([&self.credential_path]) {
|
||||
match tokio::fs::remove_file(path).await {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => {},
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {},
|
||||
Err(err) => return Err(err.into()),
|
||||
|
||||
+8
-4
@@ -11,7 +11,7 @@ use errors::AnyhowResult;
|
||||
use log::{debug, error, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::path::Path;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use url::Url;
|
||||
use wreq::Client;
|
||||
|
||||
@@ -271,7 +271,9 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
|
||||
pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> AnyhowResult<()> {
|
||||
let dir = Path::new(dir);
|
||||
tokio::fs::create_dir_all(dir).await?;
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
for generation in generations {
|
||||
let response = wreq::get(generation.url.clone()).send().await?;
|
||||
let image_bytes = response.bytes().await?;
|
||||
@@ -280,13 +282,15 @@ pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> A
|
||||
let ext = url.path().split(".").last().unwrap_or("png");
|
||||
let path = Path::new(dir).join(format!("{}.{}", generation.id, ext));
|
||||
|
||||
tokio::fs::write(path, &image_bytes).await?;
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(&image_bytes)?;
|
||||
|
||||
let thumbnail_url = generation.encodings.thumbnail.path.clone();
|
||||
let thumbnail_response = wreq::get(thumbnail_url).send().await?;
|
||||
let thumbnail_bytes = thumbnail_response.bytes().await?;
|
||||
let thumbnail_path = Path::new(dir).join(format!("{}_thumbnail.{}", generation.id, ext));
|
||||
tokio::fs::write(thumbnail_path, &thumbnail_bytes).await?;
|
||||
let mut thumbnail_file = File::create(thumbnail_path)?;
|
||||
thumbnail_file.write_all(&thumbnail_bytes)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -119,7 +119,7 @@ pub(super) async fn upload_midjourney_batch(maybe_app_handle: Option<&AppHandle>
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = tokio::fs::remove_file(MidjourneyUploadCheckpoint::path(app_data_root, local_task)).await {
|
||||
if let Err(err) = std::fs::remove_file(MidjourneyUploadCheckpoint::path(app_data_root, local_task)) {
|
||||
log::warn!("Could not remove completed Midjourney upload checkpoint: {}", err);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use errors::AnyhowResult;
|
||||
use midjourney_client::utils::get_image_url::get_image_url;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use url::Url;
|
||||
use midjourney_client::utils::image_downloader_client::ImageDownloaderClient;
|
||||
@@ -26,7 +28,8 @@ pub async fn download_midjourney_image(
|
||||
let download_filename = format!("{}_{}.{}", job_id, index, ext);
|
||||
let download_path = tempdir.join(download_filename);
|
||||
|
||||
tokio::fs::write(&download_path, &image_bytes).await?;
|
||||
let mut file = File::create(&download_path)?;
|
||||
file.write_all(&image_bytes)?;
|
||||
|
||||
Ok(download_path)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> An
|
||||
return Ok(()); // Bearer token already exists.
|
||||
}
|
||||
|
||||
let cookie = tokio::fs::read_to_string(app_data_root.get_sora_cookie_file_path()).await?
|
||||
let cookie = std::fs::read_to_string(app_data_root.get_sora_cookie_file_path())?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> An
|
||||
|
||||
let bearer = generate_bearer_jwt_with_cookie(&cookie).await?;
|
||||
|
||||
tokio::fs::write(app_data_root.get_sora_bearer_token_file_path(), bearer).await?;
|
||||
std::fs::write(app_data_root.get_sora_bearer_token_file_path(), bearer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+30
-8
@@ -8,7 +8,8 @@ use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
|
||||
use openai_sora_client::creds::sora_sentinel_token::SoraSentinelToken;
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use tokio::fs::{read_to_string, write};
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
|
||||
pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentialSet> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
@@ -20,7 +21,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
|
||||
return Err(anyhow!("Cookie file does not exist: {:?}", cookie_file));
|
||||
}
|
||||
|
||||
let value = read_to_string(cookie_file).await?
|
||||
let value = read_to_string(cookie_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
@@ -31,14 +32,14 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
|
||||
let mut sentinel_token = None;
|
||||
|
||||
if bearer_file.exists() {
|
||||
let value = read_to_string(&bearer_file).await?
|
||||
let value = read_to_string(&bearer_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
bearer = Some(SoraJwtBearerToken::new(value)?);
|
||||
}
|
||||
|
||||
if legacy_sentinel_file.exists() {
|
||||
let value = read_to_string(&legacy_sentinel_file).await?
|
||||
let value = read_to_string(&legacy_sentinel_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
@@ -46,7 +47,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
|
||||
}
|
||||
|
||||
if sentinel_token_file.exists() {
|
||||
let value = read_to_string(&sentinel_token_file).await?
|
||||
let value = read_to_string(&sentinel_token_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
@@ -72,7 +73,14 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
|
||||
info!("Persisting bearer token value...");
|
||||
let value = bearer.token_str();
|
||||
|
||||
write(&bearer_file, value.as_bytes()).await?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&bearer_file)?;
|
||||
|
||||
file.write_all(value.as_bytes())?;
|
||||
file.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +89,14 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
|
||||
info!("Persisting sentinel value...");
|
||||
let value = sentinel.get_sentinel();
|
||||
|
||||
write(&legacy_sentinel_file, value.as_bytes()).await?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&legacy_sentinel_file)?;
|
||||
|
||||
file.write_all(value.as_bytes())?;
|
||||
file.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +105,14 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
|
||||
info!("Persisting sentinel token...");
|
||||
let value = sentinel_token.to_persistent_storage_json()?;
|
||||
|
||||
write(&sentinel_token_file, value.as_bytes()).await?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&sentinel_token_file)?;
|
||||
|
||||
file.write_all(value.as_bytes())?;
|
||||
file.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,14 @@ impl SoraCredentialManager {
|
||||
|
||||
self.holder.set_credentials(&creds)?;
|
||||
|
||||
tokio::fs::write(self.app_data_root.get_sora_legacy_sentinel_file_path(), sentinel.as_bytes()).await?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.app_data_root.get_sora_legacy_sentinel_file_path())?;
|
||||
|
||||
file.write_all(sentinel.as_bytes())?;
|
||||
file.flush()?;
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
@@ -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,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,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;
|
||||
|
||||
|
||||
@@ -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,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,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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user