Add opt-out startup release checks and manual update status

This commit is contained in:
ZhuanTou33212
2026-10-14 16:45:20 +08:00
parent ed7df56a24
commit ac3cac0b95
43 changed files with 1000 additions and 29 deletions
Generated
+61 -13
View File
@@ -849,7 +849,7 @@ dependencies = [
"maybe-owned",
"rustix 1.1.5",
"rustix-linux-procfs",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
"winx",
]
@@ -1891,7 +1891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3028,7 +3028,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3690,7 +3690,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"png 0.18.1",
"thiserror 2.0.21",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4436,7 +4436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4646,6 +4646,7 @@ dependencies = [
"rfd",
"ron",
"serde_json",
"ureq",
"winit",
"winresource",
"winsafe",
@@ -4805,6 +4806,7 @@ dependencies = [
"photocraft-vector",
"rayon",
"roxmltree",
"semver",
"serde",
"serde_json",
"tempfile",
@@ -5457,7 +5459,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5976,7 +5978,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5996,7 +5998,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
@@ -6043,7 +6047,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6681,7 +6685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6908,10 +6912,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.5",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7311,7 +7315,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -7392,6 +7396,35 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
dependencies = [
"base64 0.23.1",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"ureq-proto",
"utf8-zero",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
dependencies = [
"base64 0.23.1",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -7431,6 +7464,12 @@ dependencies = [
"xmlwriter",
]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -7870,6 +7909,15 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -8071,7 +8119,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
+2
View File
@@ -40,6 +40,8 @@ arboard = { version = "3.6", features = ["wayland-data-control"] }
open = "5"
# Reads the saved window layout (eframe persistence) to drop values that would abort startup.
ron = "0.12"
# Background release checks; HTTPS with Rust TLS, no async runtime.
ureq = { version = "3.4", default-features = false, features = ["rustls", "platform-verifier"] }
[target.'cfg(target_os = "macos")'.dependencies]
# Safe CoreGraphics bindings, compatible with the existing macOS 11 deployment target.
+1
View File
@@ -50,6 +50,7 @@ mod services;
#[cfg(any(target_os = "macos", target_os = "linux", test))]
mod tablet;
mod ui_state;
mod updates;
mod window_frame;
use photocraft_engine::Session;
+1
View File
@@ -461,6 +461,7 @@ pub fn native(automation: Option<photocraft_automation::AuthorizedWorkspace>) ->
inbox: None,
open_url: Some(Box::new(|url: &str| open::that(url).map_err(|e| e.to_string()))),
beep: Some(platform_beep),
check_updates: Some(crate::updates::service()),
clipboard_set_image: Some({
let clip = clip.clone();
Box::new(move |w: u32, h: u32, px: &[u8]| {
+126
View File
@@ -0,0 +1,126 @@
//! Native HTTPS transport for release checks. Only public release metadata is requested.
use photocraft_engine::updates_cmds::{CheckFn, CheckResult, parse_release};
use std::time::Duration;
const LATEST: &str = "https://api.github.com/repos/storytold/photocraft/releases/latest";
const MAX_RESPONSE: u64 = 1024 * 1024;
pub fn service() -> CheckFn {
Box::new(|| {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("update check".into())
.spawn(move || {
let result = fetch(LATEST, Duration::from_secs(15));
if let Err(e) = &result {
log::warn!("Update check: {e}");
}
let _ = tx.send(result);
})
.map_err(|e| format!("Could not start update check: {e}"))?;
Ok(rx)
})
}
fn fetch(url: &str, timeout: Duration) -> CheckResult {
// Respect the OS trust store (including managed-network certificates), while still
// validating the certificate chain and server name.
let agent = ureq::Agent::config_builder()
.tls_config(ureq::tls::TlsConfig::builder().root_certs(ureq::tls::RootCerts::PlatformVerifier).build())
.timeout_global(Some(timeout))
.max_redirects(0)
.build()
.new_agent();
let mut response = match agent
.get(url)
.header("User-Agent", "PhotoCraft-update-check")
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2026-03-10")
.call()
{
Ok(response) => response,
// The repository may not have published a stable release yet.
Err(ureq::Error::StatusCode(404)) => return Ok(None),
Err(ureq::Error::StatusCode(403 | 429)) => return Err("GitHub is limiting update checks. Please try again later.".into()),
Err(e) => return Err(format!("Could not contact the release server: {e}")),
};
if response.status() != 200 {
return Err(format!("Unexpected release server status: {}", response.status()));
}
let bytes = response.body_mut().with_config().limit(MAX_RESPONSE).read_to_vec().map_err(|e| format!("Could not read release response: {e}"))?;
parse_release(&bytes, photocraft_engine::build_info::VERSION)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
use std::net::TcpListener;
fn server(status: &str, body: &str, extra: &str) -> (String, std::thread::JoinHandle<String>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}/latest", listener.local_addr().unwrap());
let response = format!("HTTP/1.1 {status}\r\nContent-Length: {}\r\n{extra}Connection: close\r\n\r\n{body}", body.len());
let worker = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut request = Vec::new();
let mut chunk = [0; 1024];
while !request.windows(4).any(|w| w == b"\r\n\r\n") {
let n = stream.read(&mut chunk).unwrap();
if n == 0 {
break;
}
request.extend_from_slice(&chunk[..n]);
}
let _ = stream.write_all(response.as_bytes());
String::from_utf8(request).unwrap()
});
(url, worker)
}
#[test]
fn fetches_release_metadata_with_generic_headers() {
let (url, worker) = server("200 OK", r#"{"tag_name":"v99.0.0","draft":false,"prerelease":false}"#, "");
let result = fetch(&url, Duration::from_secs(5)).unwrap().unwrap();
assert_eq!(result.version, "99.0.0");
let request = worker.join().unwrap().to_ascii_lowercase();
assert!(request.contains("user-agent: photocraft-update-check\r\n"));
assert!(request.contains("accept: application/vnd.github+json"));
assert!(!request.contains("authorization:") && !request.contains("cookie:"));
}
#[test]
fn handles_missing_release_rate_limits_and_bad_responses() {
for (status, body, extra, expected_none) in [
("404 Not Found", "", "", true),
("403 Forbidden", "", "", false),
("429 Too Many Requests", "", "", false),
("500 Internal Server Error", "", "", false),
("200 OK", "invalid JSON", "", false),
("302 Found", "", "Location: https://untrusted.example/\r\n", false),
] {
let (url, worker) = server(status, body, extra);
let result = fetch(&url, Duration::from_secs(5));
if expected_none {
assert_eq!(result, Ok(None));
} else {
assert!(result.is_err(), "{status}");
}
worker.join().unwrap();
}
}
#[test]
fn caps_response_size_and_times_out() {
let (url, worker) = server("200 OK", &" ".repeat(MAX_RESPONSE as usize + 1), "");
assert!(fetch(&url, Duration::from_secs(5)).is_err());
worker.join().unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}/latest", listener.local_addr().unwrap());
let start = std::time::Instant::now();
assert!(fetch(&url, Duration::from_millis(100)).is_err());
assert!(start.elapsed() < Duration::from_secs(2));
}
}
+2
View File
@@ -39,6 +39,8 @@ craft-print = { workspace = true }
# Photoshop `.kys` shortcut sets (`edit.keyboardShortcuts` `importKys`); already in the tree
# through photocraft-ui-egui and usvg.
roxmltree = "0.20"
# Release tags use the same semantic versions as Cargo.
semver = "1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tempfile = "3"
+1
View File
@@ -1306,6 +1306,7 @@ fn build() -> Vec<CommandSpec> {
v.extend(crate::layer_multi_cmds::specs());
v.extend(crate::layer_copy_cmds::specs());
v.extend(crate::prefs::specs());
v.extend(crate::updates_cmds::specs());
v.extend(crate::edit_menu_cmds::specs());
v.extend(crate::fill_key_cmds::specs());
v.extend(crate::sample_cmds::specs());
+3
View File
@@ -117,6 +117,7 @@ pub mod type_extra_cmds;
pub mod type_mask_cmds;
pub mod type_spell_cmds;
pub mod type_styles_cmds;
pub mod updates_cmds;
mod variables_cmds;
pub use variables_cmds::{export_data_sets_with, import_data_sets_text};
pub mod vector_cmds;
@@ -417,6 +418,8 @@ pub struct Session {
color_restrict: Option<usize>,
/// Edit › Preferences, keyboard shortcuts, menu and toolbar customisation (see `prefs`).
pub prefs: prefs::PrefsStore,
/// Release checks use an injected platform transport; no network I/O in the engine.
pub updates: updates_cmds::Updates,
/// Edit menu state: Fade source, custom shape library (see `edit_menu_cmds`).
pub edit_state: edit_menu_cmds::EditState,
/// The pattern library (Window › Patterns; see `pattern.*`).
+3
View File
@@ -221,6 +221,8 @@ pub struct General {
pub zoom_resizes_windows: bool,
pub use_legacy_free_transform: bool,
pub auto_show_home_screen: bool,
/// Desktop: check published stable releases once in the background at launch.
pub automatically_check_updates: bool,
}
impl Default for General {
@@ -237,6 +239,7 @@ impl Default for General {
zoom_resizes_windows: false,
use_legacy_free_transform: false,
auto_show_home_screen: true,
automatically_check_updates: true,
}
}
}
+221
View File
@@ -0,0 +1,221 @@
//! Release-check state and commands. The desktop injects a non-blocking transport; the engine
//! owns the result so CLI, MCP and the GUI query the same state without network dependencies.
use std::sync::mpsc::{Receiver, TryRecvError};
use semver::Version;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::commands::{CommandSpec, always};
use crate::{EngineError, Result, Session};
pub const RELEASES: &str = "https://github.com/storytold/photocraft/releases";
pub type CheckResult = std::result::Result<Option<Release>, String>;
/// Starts background work and returns immediately. No document or personal data is sent.
pub type CheckFn = Box<dyn FnMut() -> std::result::Result<Receiver<CheckResult>, String> + Send>;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Release {
pub version: String,
pub url: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum Status {
#[default]
Idle,
Checking,
UpToDate,
Available {
release: Release,
},
Failed {
message: String,
},
}
#[derive(Default)]
pub struct Updates {
pub check: Option<CheckFn>,
pub status: Status,
pending: Option<Receiver<CheckResult>>,
}
impl Updates {
/// Consume a completed check; a disconnected worker becomes an actionable error.
pub fn poll(&mut self) -> bool {
let Some(rx) = &self.pending else { return false };
let result = match rx.try_recv() {
Ok(r) => r,
Err(TryRecvError::Empty) => return false,
Err(TryRecvError::Disconnected) => Err("The update check stopped before returning a result".into()),
};
self.pending = None;
self.status = match result {
Ok(Some(release)) => Status::Available { release },
Ok(None) => Status::UpToDate,
Err(message) => Status::Failed { message },
};
true
}
}
/// Interpret GitHub's latest-release response. Never trust a remote URL: construct the release
/// page from a validated tag in our own repository. Build metadata doesn't affect precedence.
pub fn parse_release(bytes: &[u8], current: &str) -> CheckResult {
#[derive(Deserialize)]
struct GithubRelease {
tag_name: String,
draft: bool,
prerelease: bool,
}
let release: GithubRelease = serde_json::from_slice(bytes).map_err(|e| format!("Invalid release response: {e}"))?;
if release.draft || release.prerelease {
return Ok(None);
}
let tag = &release.tag_name;
if tag.len() > 128 || !tag.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'+')) {
return Err("Invalid release tag".into());
}
let version = Version::parse(tag.strip_prefix('v').unwrap_or(tag)).map_err(|e| format!("Invalid release version: {e}"))?;
let current = Version::parse(current).map_err(|e| format!("Invalid installed version: {e}"))?;
if !version.pre.is_empty() || !version.cmp_precedence(&current).is_gt() {
return Ok(None);
}
Ok(Some(Release { version: version.to_string(), url: format!("{RELEASES}/tag/{tag}") }))
}
fn empty_params(p: &Value) -> Result<()> {
if p.as_object().is_some_and(|p| p.is_empty()) { Ok(()) } else { Err(EngineError::Other("update commands take an empty object".into())) }
}
fn enabled(s: &Session) -> std::result::Result<(), String> {
s.updates.check.as_ref().map(|_| ()).ok_or_else(|| "Update checks are unavailable in this frontend".into())
}
fn check(s: &mut Session, p: &Value) -> Result<Value> {
empty_params(p)?;
// Repeated automation requests must not spawn an unbounded number of network workers.
if s.updates.pending.is_none() {
let start = s.updates.check.as_mut().ok_or_else(|| EngineError::Other("Update checks are unavailable in this frontend".into()))?;
match start() {
Ok(rx) => {
s.updates.pending = Some(rx);
s.updates.status = Status::Checking;
}
Err(message) => {
s.updates.status = Status::Failed { message: message.clone() };
return Err(EngineError::Other(message));
}
}
}
Ok(json!(s.updates.status))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec {
id: "help.checkForUpdates",
label: "Check for Updates…",
menu: &["Help"],
shortcut: None,
params: "{}",
enabled,
run: check,
journal: false,
},
CommandSpec {
id: "updates.status",
label: "Update Check Status",
menu: &[],
shortcut: None,
params: "{}",
enabled: always,
run: |s, p| {
empty_params(p)?;
s.updates.poll();
Ok(json!({"supported": s.updates.check.is_some(), "currentVersion": crate::build_info::VERSION, "result": s.updates.status}))
},
journal: false,
},
]
}
#[cfg(test)]
mod tests {
use super::*;
fn response(tag: &str) -> Vec<u8> {
serde_json::to_vec(&json!({"tag_name": tag, "draft": false, "prerelease": false, "html_url": "https://untrusted.example"})).unwrap()
}
#[test]
fn semantic_versions_and_trusted_links() {
let release = parse_release(&response("v0.10.0"), "0.9.0").unwrap().unwrap();
assert_eq!(release.version, "0.10.0");
assert_eq!(release.url, format!("{RELEASES}/tag/v0.10.0"));
for tag in ["v0.9.0", "v0.8.9", "v0.9.0+build.2", "v1.0.0-rc.1"] {
assert_eq!(parse_release(&response(tag), "0.9.0").unwrap(), None, "{tag}");
}
assert!(parse_release(&response("0.9.0"), "0.9.0-rc.1").unwrap().is_some());
}
#[test]
fn unpublished_and_hostile_responses() {
for flag in ["draft", "prerelease"] {
let mut v: Value = serde_json::from_slice(&response("v1.0.0")).unwrap();
v[flag] = json!(true);
assert_eq!(parse_release(&serde_json::to_vec(&v).unwrap(), "0.7.0").unwrap(), None);
}
for tag in ["", "latest", "v1.0.0/../evil", "v1.0.0?next=evil", "v1.0.0\n", "版本1.0.0", "999999999999999999999.0.0"] {
assert!(parse_release(&response(tag), "0.7.0").is_err(), "{tag}");
}
for bytes in [b"not JSON".as_slice(), b"{}", b"null", b"{\"tag_name\":42}"] {
assert!(parse_release(bytes, "0.7.0").is_err());
}
}
#[test]
fn commands_are_nonblocking_deduplicated_and_queryable_without_a_document() {
let mut s = Session::new();
let (tx, rx) = std::sync::mpsc::channel();
let mut rx = Some(rx);
s.updates.check = Some(Box::new(move || Ok(rx.take().expect("only one worker"))));
for _ in 0..3 {
assert_eq!(s.execute("help.checkForUpdates", json!({})).unwrap()["status"], "checking");
}
tx.send(Ok(Some(Release { version: "1.0.0".into(), url: format!("{RELEASES}/tag/v1.0.0") }))).unwrap();
let status = s.execute("updates.status", json!({})).unwrap();
assert_eq!(status["result"]["status"], "available");
assert_eq!(status["result"]["release"]["version"], "1.0.0");
assert!(s.journal.is_empty());
}
#[test]
fn graceful_failures_and_preference_compatibility() {
let mut s = Session::new();
assert!(s.execute("help.checkForUpdates", json!({})).is_err());
for p in [Value::Null, json!([]), json!({"url": "evil"}), json!("bad")] {
assert!(s.execute("updates.status", p.clone()).is_err());
assert!(check(&mut s, &p).is_err());
}
s.updates.check = Some(Box::new(|| Err("Could not start worker".into())));
assert!(s.execute("help.checkForUpdates", json!({})).is_err());
assert!(matches!(s.updates.status, Status::Failed { .. }));
s.updates.check = Some(Box::new(|| {
let (_tx, rx) = std::sync::mpsc::channel();
Ok(rx)
}));
s.execute("help.checkForUpdates", json!({})).unwrap();
assert_eq!(s.execute("updates.status", json!({})).unwrap()["result"]["status"], "failed");
s.load_prefs_json("{}").unwrap();
assert!(s.prefs().general.automatically_check_updates);
s.execute("prefs.set", json!({"path": "general.automaticallyCheckUpdates", "value": false})).unwrap();
let saved = s.prefs_to_json();
let mut restored = Session::new();
restored.load_prefs_json(&saved).unwrap();
assert!(!restored.prefs().general.automatically_check_updates);
}
}
+10
View File
@@ -10,6 +10,7 @@
//!
//! `--safe-gpu` draws the canvas on the CPU path, like the app's `--safe-gpu` launch.
//! `--wayland-notice` previews the native file drag-and-drop guidance shown in Wayland sessions.
//! `--update-notice` previews a newer release without contacting the network.
//! `--custom-titlebar` draws the Windows/Linux title bar (caption buttons in the top bar).
//!
//! `--monitor 1366x768 --window-top 31` simulates the display the window is on (in points) and
@@ -37,6 +38,7 @@ fn main() {
.unwrap_or((1440.0, 900.0));
let scale: f32 = arg(&args, "--scale").and_then(|s| s.parse().ok()).unwrap_or(2.0);
let wayland_notice = args.iter().any(|a| a == "--wayland-notice");
let update_notice = args.iter().any(|a| a == "--update-notice");
let script: Vec<(String, Value)> =
arg(&args, "--script").map(|s| serde_json::from_str::<Vec<(String, Value)>>(&s).expect("--script must be [[method, params], …]")).unwrap_or_default();
@@ -73,6 +75,14 @@ fn main() {
services.is_wayland = wayland_notice;
services.xwayland_command = wayland_notice.then(|| "WAYLAND_DISPLAY= photocraft".into());
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), services);
if update_notice {
app.session.updates.status = photocraft_engine::updates_cmds::Status::Available {
release: photocraft_engine::updates_cmds::Release {
version: "99.0.0".into(),
url: "https://github.com/storytold/photocraft/releases/tag/v99.0.0".into(),
},
};
}
app.background_jobs = background_jobs;
app.custom_titlebar = custom_titlebar;
// `--safe-gpu`: the CPU canvas, as the desktop app's `--safe-gpu` launch.
+8 -1
View File
@@ -131,7 +131,8 @@ pub enum Outcome {
/// field's value is validated before the first one is applied, so a typo, an unknown field, a
/// bad value or a bad nested key can't reply with success while nothing — or only half of it —
/// changed (#412).
pub const UI_SET_FIELDS: [&str; 35] = [
pub const UI_SET_FIELDS: [&str; 36] = [
"updatesOpen",
"tool",
"panels",
"dock",
@@ -539,6 +540,7 @@ fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest)
}
let panels = merged_object(&app.ui.panels, p.get("panels"), "panels")?;
let mask_target = bool_field(p, "maskTarget")?;
let updates_open = bool_field(p, "updatesOpen")?;
let vector_mask_target = bool_field(p, "vectorMaskTarget")?;
let selection_mode = uint_field(p, "selectionMode")?;
let dock_tabs = merged_object(&app.ui.dock_tabs, p.get("dockTabs"), "dockTabs")?;
@@ -693,6 +695,9 @@ fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest)
app.ui.vector_mask_target &= !m;
app.sync_mask_targets();
}
if let Some(open) = updates_open {
app.ui.updates_open = open;
}
if let Some(m) = vector_mask_target {
app.ui.vector_mask_target = m;
app.ui.mask_target &= !m;
@@ -1310,6 +1315,8 @@ pub fn inspect(app: &PhotocraftApp, ctx: &egui::Context) -> Value {
"status": app.ui.status,
"statusError": app.ui.status_error,
"notices": app.ui.notices,
"updatesOpen": app.ui.updates_open,
"updates": {"supported": app.session.updates.check.is_some(), "currentVersion": photocraft_engine::build_info::VERSION, "result": app.session.updates.status},
"gpuFallbackNotice": app.ui.gpu_fallback_notice,
"kysOffer": app.ui.kys_offer,
"frame": app.frame,
+13
View File
@@ -3752,3 +3752,16 @@ navigator Zoom Zvětšení
Sent to {printer} Odesláno do {printer}
set in Print Settings… nastavuje se v nastavení tisku…
ICO is limited to 256 × 256 pixels ICO je omezen na 256 × 256 pixelů
# PhotoCraft release checks.
Check for Updates… Zkontrolovat aktualizace…
PhotoCraft Updates Aktualizace PhotoCraft
Checking for updates… Kontrola aktualizací…
Open the release page to choose an installer for your system. Otevřete stránku vydání a vyberte instalační program pro svůj systém.
Open Download Page Otevřít stránku ke stažení
PhotoCraft is up to date. PhotoCraft je aktuální.
Could not check for updates. Aktualizace se nepodařilo zkontrolovat.
Updates have not been checked yet. Aktualizace ještě nebyly zkontrolovány.
Check Again Zkontrolovat znovu
Automatically Check for Updates Automaticky kontrolovat aktualizace
Installed version: {version} Nainstalovaná verze: {version}
PhotoCraft {version} is available. PhotoCraft {version} je k dispozici.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -3142,3 +3142,16 @@ curves Smooth Glätten
Sent to {printer} An {printer} gesendet
set in Print Settings… in den Druckeinstellungen festlegen…
ICO is limited to 256 × 256 pixels ICO ist auf 256 × 256 Pixel begrenzt
# PhotoCraft release checks.
Check for Updates… Nach Updates suchen…
PhotoCraft Updates PhotoCraft-Updates
Checking for updates… Suche nach Updates…
Open the release page to choose an installer for your system. Öffnen Sie die Versionsseite und wählen Sie ein Installationspaket für Ihr System.
Open Download Page Downloadseite öffnen
PhotoCraft is up to date. PhotoCraft ist auf dem neuesten Stand.
Could not check for updates. Updates konnten nicht geprüft werden.
Updates have not been checked yet. Es wurde noch nicht nach Updates gesucht.
Check Again Erneut prüfen
Automatically Check for Updates Automatisch nach Updates suchen
Installed version: {version} Installierte Version: {version}
PhotoCraft {version} is available. PhotoCraft {version} ist verfügbar.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -3842,3 +3842,16 @@ curves Smooth Εξομάλυνση
Sent to {printer} Στάλθηκε στον {printer}
set in Print Settings… ορίζεται στις ρυθμίσεις εκτύπωσης…
ICO is limited to 256 × 256 pixels Το ICO περιορίζεται σε 256 × 256 εικονοστοιχεία
# PhotoCraft release checks.
Check for Updates… Έλεγχος ενημερώσεων…
PhotoCraft Updates Ενημερώσεις PhotoCraft
Checking for updates… Έλεγχος για ενημερώσεις…
Open the release page to choose an installer for your system. Ανοίξτε τη σελίδα της έκδοσης για να επιλέξετε πρόγραμμα εγκατάστασης για το σύστημά σας.
Open Download Page Άνοιγμα σελίδας λήψης
PhotoCraft is up to date. Το PhotoCraft είναι ενημερωμένο.
Could not check for updates. Δεν ήταν δυνατός ο έλεγχος ενημερώσεων.
Updates have not been checked yet. Δεν έχει γίνει ακόμη έλεγχος ενημερώσεων.
Check Again Νέος έλεγχος
Automatically Check for Updates Αυτόματος έλεγχος ενημερώσεων
Installed version: {version} Εγκατεστημένη έκδοση: {version}
PhotoCraft {version} is available. Το PhotoCraft {version} είναι διαθέσιμο.
Can't render this file because it contains an unexpected character in line 1029 and column 18.
+13
View File
@@ -2829,3 +2829,16 @@ curves Smooth Suavizar
Sent to {printer} Enviado a {printer}
set in Print Settings… se establece en Ajustes de impresión…
ICO is limited to 256 × 256 pixels ICO está limitado a 256 × 256 píxeles
# PhotoCraft release checks.
Check for Updates… Buscar actualizaciones…
PhotoCraft Updates Actualizaciones de PhotoCraft
Checking for updates… Buscando actualizaciones…
Open the release page to choose an installer for your system. Abra la página de la versión para elegir un instalador para su sistema.
Open Download Page Abrir página de descarga
PhotoCraft is up to date. PhotoCraft está actualizado.
Could not check for updates. No se pudieron buscar actualizaciones.
Updates have not been checked yet. Aún no se han buscado actualizaciones.
Check Again Comprobar de nuevo
Automatically Check for Updates Buscar actualizaciones automáticamente
Installed version: {version} Versión instalada: {version}
PhotoCraft {version} is available. PhotoCraft {version} está disponible.
Can't render this file because it contains an unexpected character in line 4 and column 47.
+13
View File
@@ -3146,3 +3146,16 @@ curves Smooth Lisser
Sent to {printer} Envoyé à {printer}
set in Print Settings… défini dans les paramètres d’impression…
ICO is limited to 256 × 256 pixels Le format ICO est limité à 256 × 256 pixels
# PhotoCraft release checks.
Check for Updates… Rechercher des mises à jour…
PhotoCraft Updates Mises à jour de PhotoCraft
Checking for updates… Recherche de mises à jour…
Open the release page to choose an installer for your system. Ouvrez la page de la version pour choisir un programme d’installation pour votre système.
Open Download Page Ouvrir la page de téléchargement
PhotoCraft is up to date. PhotoCraft est à jour.
Could not check for updates. Impossible de rechercher des mises à jour.
Updates have not been checked yet. Les mises à jour n’ont pas encore été vérifiées.
Check Again Vérifier à nouveau
Automatically Check for Updates Rechercher automatiquement des mises à jour
Installed version: {version} Version installée : {version}
PhotoCraft {version} is available. PhotoCraft {version} est disponible.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12
View File
@@ -4065,3 +4065,15 @@ cameraRaw Yellow Sárga
Ink Characteristics Festék jellemzői
Solidity: Fedőképesség:
Redrawing Újrarajzolás
Check for Updates… Frissítések keresése…
PhotoCraft Updates PhotoCraft-frissítések
Checking for updates… Frissítések keresése…
Open the release page to choose an installer for your system. Nyissa meg a kiadás oldalát, és válassza ki a rendszerének megfelelő telepítőt.
Open Download Page Letöltési oldal megnyitása
PhotoCraft is up to date. A PhotoCraft naprakész.
Could not check for updates. Nem sikerült ellenőrizni a frissítéseket.
Updates have not been checked yet. A frissítések még nem lettek ellenőrizve.
Check Again Újraellenőrzés
Automatically Check for Updates Frissítések automatikus keresése
Installed version: {version} Telepített verzió: {version}
PhotoCraft {version} is available. Elérhető a PhotoCraft {version}.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -3133,3 +3133,16 @@ curves Smooth Haluskan
Sent to {printer} Dikirim ke {printer}
set in Print Settings… diatur di Pengaturan Cetak…
ICO is limited to 256 × 256 pixels ICO dibatasi hingga 256 × 256 piksel
# PhotoCraft release checks.
Check for Updates… Periksa Pembaruan…
PhotoCraft Updates Pembaruan PhotoCraft
Checking for updates… Memeriksa pembaruan…
Open the release page to choose an installer for your system. Buka halaman rilis untuk memilih penginstal bagi sistem Anda.
Open Download Page Buka Halaman Unduhan
PhotoCraft is up to date. PhotoCraft sudah versi terbaru.
Could not check for updates. Tidak dapat memeriksa pembaruan.
Updates have not been checked yet. Pembaruan belum diperiksa.
Check Again Periksa Lagi
Automatically Check for Updates Periksa Pembaruan Secara Otomatis
Installed version: {version} Versi terpasang: {version}
PhotoCraft {version} is available. PhotoCraft {version} tersedia.
Can't render this file because it contains an unexpected character in line 4 and column 47.
+13
View File
@@ -4107,3 +4107,16 @@ navigator Zoom Zoom
Sent to {printer} Inviato a {printer}
set in Print Settings… impostato nelle impostazioni di stampa…
ICO is limited to 256 × 256 pixels ICO è limitato a 256 × 256 pixel
# PhotoCraft release checks.
Check for Updates… Cerca aggiornamenti…
PhotoCraft Updates Aggiornamenti di PhotoCraft
Checking for updates… Ricerca aggiornamenti…
Open the release page to choose an installer for your system. Apri la pagina della versione per scegliere un programma di installazione per il tuo sistema.
Open Download Page Apri pagina di download
PhotoCraft is up to date. PhotoCraft è aggiornato.
Could not check for updates. Impossibile cercare aggiornamenti.
Updates have not been checked yet. Gli aggiornamenti non sono ancora stati verificati.
Check Again Controlla di nuovo
Automatically Check for Updates Cerca automaticamente aggiornamenti
Installed version: {version} Versione installata: {version}
PhotoCraft {version} is available. PhotoCraft {version} è disponibile.
Can't render this file because it contains an unexpected character in line 2647 and column 53.
+13
View File
@@ -3821,3 +3821,16 @@ curves Smooth スムーズ
Reset All Locked Settings ロックされた設定をすべて初期化
Copy Texture to Other Tools テクスチャを他のツールにコピー
ICO is limited to 256 × 256 pixels ICO は 256 × 256 ピクセルまでです
# PhotoCraft release checks.
Check for Updates… アップデートを確認…
PhotoCraft Updates PhotoCraft のアップデート
Checking for updates… アップデートを確認中…
Open the release page to choose an installer for your system. リリースページでお使いのシステム用のインストーラーを選択してください。
Open Download Page ダウンロードページを開く
PhotoCraft is up to date. PhotoCraft は最新です。
Could not check for updates. アップデートを確認できませんでした。
Updates have not been checked yet. まだアップデートを確認していません。
Check Again 再確認
Automatically Check for Updates アップデートを自動的に確認
Installed version: {version} インストール済みのバージョン: {version}
PhotoCraft {version} is available. PhotoCraft {version} が利用可能です。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -3319,3 +3319,16 @@ curves Smooth 매끄럽게
Sent to {printer} {printer}(으)로 전송됨
set in Print Settings… 인쇄 설정에서 지정…
ICO is limited to 256 × 256 pixels ICO는 256 × 256픽셀로 제한됩니다
# PhotoCraft release checks.
Check for Updates… 업데이트 확인…
PhotoCraft Updates PhotoCraft 업데이트
Checking for updates… 업데이트 확인 중…
Open the release page to choose an installer for your system. 릴리스 페이지에서 시스템에 맞는 설치 프로그램을 선택하세요.
Open Download Page 다운로드 페이지 열기
PhotoCraft is up to date. PhotoCraft가 최신 버전입니다.
Could not check for updates. 업데이트를 확인할 수 없습니다.
Updates have not been checked yet. 아직 업데이트를 확인하지 않았습니다.
Check Again 다시 확인
Automatically Check for Updates 자동으로 업데이트 확인
Installed version: {version} 설치된 버전: {version}
PhotoCraft {version} is available. PhotoCraft {version}을 사용할 수 있습니다.
Can't render this file because it contains an unexpected character in line 3160 and column 53.
+13
View File
@@ -3904,3 +3904,16 @@ curves Smooth Vloeiend maken
Sent to {printer} Verzonden naar {printer}
set in Print Settings… instellen in de afdrukinstellingen…
ICO is limited to 256 × 256 pixels ICO is beperkt tot 256 × 256 pixels
# PhotoCraft release checks.
Check for Updates… Controleren op updates…
PhotoCraft Updates PhotoCraft-updates
Checking for updates… Controleren op updates…
Open the release page to choose an installer for your system. Open de releasepagina om een installatieprogramma voor uw systeem te kiezen.
Open Download Page Downloadpagina openen
PhotoCraft is up to date. PhotoCraft is bijgewerkt.
Could not check for updates. Kan niet controleren op updates.
Updates have not been checked yet. Er is nog niet op updates gecontroleerd.
Check Again Opnieuw controleren
Automatically Check for Updates Automatisch op updates controleren
Installed version: {version} Geïnstalleerde versie: {version}
PhotoCraft {version} is available. PhotoCraft {version} is beschikbaar.
Can't render this file because it contains an unexpected character in line 8 and column 34.
+13
View File
@@ -3742,3 +3742,16 @@ curves Smooth Wygładź
Sent to {printer} Wysłano do {printer}
set in Print Settings… ustawiane w ustawieniach druku…
ICO is limited to 256 × 256 pixels ICO jest ograniczony do 256 × 256 pikseli
# PhotoCraft release checks.
Check for Updates… Sprawdź aktualizacje…
PhotoCraft Updates Aktualizacje PhotoCraft
Checking for updates… Sprawdzanie aktualizacji…
Open the release page to choose an installer for your system. Otwórz stronę wydania, aby wybrać instalator dla swojego systemu.
Open Download Page Otwórz stronę pobierania
PhotoCraft is up to date. PhotoCraft jest aktualny.
Could not check for updates. Nie udało się sprawdzić aktualizacji.
Updates have not been checked yet. Aktualizacje nie zostały jeszcze sprawdzone.
Check Again Sprawdź ponownie
Automatically Check for Updates Automatycznie sprawdzaj aktualizacje
Installed version: {version} Zainstalowana wersja: {version}
PhotoCraft {version} is available. PhotoCraft {version} jest dostępny.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -3934,3 +3934,16 @@ layerLabel No Color Sem cor
Sent to {printer} Enviado para {printer}
set in Print Settings… definido nas configurações de impressão…
ICO is limited to 256 × 256 pixels ICO é limitado a 256 × 256 pixels
# PhotoCraft release checks.
Check for Updates… Verificar atualizações…
PhotoCraft Updates Atualizações do PhotoCraft
Checking for updates… Verificando atualizações…
Open the release page to choose an installer for your system. Abra a página da versão para escolher um instalador para o seu sistema.
Open Download Page Abrir página de download
PhotoCraft is up to date. O PhotoCraft está atualizado.
Could not check for updates. Não foi possível verificar atualizações.
Updates have not been checked yet. As atualizações ainda não foram verificadas.
Check Again Verificar novamente
Automatically Check for Updates Verificar atualizações automaticamente
Installed version: {version} Versão instalada: {version}
PhotoCraft {version} is available. PhotoCraft {version} está disponível.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -3110,3 +3110,16 @@ curves Smooth Сгладить
Gray Value (Median) Уровень серого (медиана)
Integrated Density Интегральная плотность
Length Длина
# PhotoCraft release checks.
Check for Updates… Проверить обновления…
PhotoCraft Updates Обновления PhotoCraft
Checking for updates… Проверка обновлений…
Open the release page to choose an installer for your system. Откройте страницу выпуска и выберите установщик для своей системы.
Open Download Page Открыть страницу загрузки
PhotoCraft is up to date. Установлена последняя версия PhotoCraft.
Could not check for updates. Не удалось проверить обновления.
Updates have not been checked yet. Обновления ещё не проверялись.
Check Again Проверить снова
Automatically Check for Updates Автоматически проверять обновления
Installed version: {version} Установленная версия: {version}
PhotoCraft {version} is available. Доступна версия PhotoCraft {version}.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12
View File
@@ -3876,3 +3876,15 @@ cameraData Lens Lens
Ink Characteristics Mürekkep Özellikleri
Solidity: Doluluk:
Redrawing Yeniden çiziliyor
Check for Updates… Güncellemeleri Denetle…
PhotoCraft Updates PhotoCraft Güncellemeleri
Checking for updates… Güncellemeler denetleniyor…
Open the release page to choose an installer for your system. Sisteminize uygun yükleyiciyi seçmek için sürüm sayfasını açın.
Open Download Page İndirme Sayfasını Aç
PhotoCraft is up to date. PhotoCraft güncel.
Could not check for updates. Güncellemeler denetlenemedi.
Updates have not been checked yet. Güncellemeler henüz denetlenmedi.
Check Again Yeniden Denetle
Automatically Check for Updates Güncellemeleri Otomatik Denetle
Installed version: {version} Yüklü sürüm: {version}
PhotoCraft {version} is available. PhotoCraft {version} kullanılabilir.
Can't render this file because it contains an unexpected character in line 8 and column 34.
+13
View File
@@ -3843,3 +3843,16 @@ curves Smooth Згладити
Sent to {printer} Надіслано на {printer}
set in Print Settings… задається в параметрах друку…
ICO is limited to 256 × 256 pixels ICO обмежено розміром 256 × 256 пікселів
# PhotoCraft release checks.
Check for Updates… Перевірити оновлення…
PhotoCraft Updates Оновлення PhotoCraft
Checking for updates… Перевірка оновлень…
Open the release page to choose an installer for your system. Відкрийте сторінку випуску й виберіть інсталятор для своєї системи.
Open Download Page Відкрити сторінку завантаження
PhotoCraft is up to date. PhotoCraft оновлено до останньої версії.
Could not check for updates. Не вдалося перевірити оновлення.
Updates have not been checked yet. Оновлення ще не перевірялися.
Check Again Перевірити знову
Automatically Check for Updates Автоматично перевіряти оновлення
Installed version: {version} Встановлена версія: {version}
PhotoCraft {version} is available. Доступна версія PhotoCraft {version}.
Can't render this file because it contains an unexpected character in line 1387 and column 18.
+12
View File
@@ -3631,3 +3631,15 @@ curves Smooth Làm mượt
Sent to {printer} Đã gửi tới {printer}
set in Print Settings… đặt trong thiết lập in…
ICO is limited to 256 × 256 pixels ICO giới hạn ở 256 × 256 pixel
Check for Updates… Kiểm tra bản cập nhật…
PhotoCraft Updates Bản cập nhật PhotoCraft
Checking for updates… Đang kiểm tra bản cập nhật…
Open the release page to choose an installer for your system. Mở trang phát hành để chọn bộ cài đặt cho hệ thống của bạn.
Open Download Page Mở trang tải xuống
PhotoCraft is up to date. PhotoCraft đã được cập nhật.
Could not check for updates. Không thể kiểm tra bản cập nhật.
Updates have not been checked yet. Chưa kiểm tra bản cập nhật.
Check Again Kiểm tra lại
Automatically Check for Updates Tự động kiểm tra bản cập nhật
Installed version: {version} Phiên bản đã cài: {version}
PhotoCraft {version} is available. PhotoCraft {version} đã có sẵn.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -2754,3 +2754,16 @@ curves Smooth 平滑
Sent to {printer} 已发送到 {printer}
set in Print Settings… 在打印设置中设置…
ICO is limited to 256 × 256 pixels ICO 最大为 256 × 256 像素
# PhotoCraft release checks.
Check for Updates… 检查更新…
PhotoCraft Updates PhotoCraft 更新
Checking for updates… 正在检查更新…
Open the release page to choose an installer for your system. 打开发布页,选择适合您系统的安装包。
Open Download Page 打开下载页面
PhotoCraft is up to date. PhotoCraft 已是最新版本。
Could not check for updates. 无法检查更新。
Updates have not been checked yet. 尚未检查更新。
Check Again 重新检查
Automatically Check for Updates 自动检查更新
Installed version: {version} 已安装版本:{version}
PhotoCraft {version} is available. PhotoCraft {version} 已发布。
Can't render this file because it contains an unexpected character in line 2593 and column 53.
+13
View File
@@ -2739,3 +2739,16 @@ curves Smooth 平滑
Sent to {printer} 已傳送至 {printer}
set in Print Settings… 在列印設定中設定…
ICO is limited to 256 × 256 pixels ICO 最大為 256 × 256 像素
# PhotoCraft release checks.
Check for Updates… 檢查更新…
PhotoCraft Updates PhotoCraft 更新
Checking for updates… 正在檢查更新…
Open the release page to choose an installer for your system. 開啟發佈頁,選擇適合您系統的安裝程式。
Open Download Page 開啟下載頁面
PhotoCraft is up to date. PhotoCraft 已是最新版本。
Could not check for updates. 無法檢查更新。
Updates have not been checked yet. 尚未檢查更新。
Check Again 重新檢查
Automatically Check for Updates 自動檢查更新
Installed version: {version} 已安裝版本:{version}
PhotoCraft {version} is available. PhotoCraft {version} 已發佈。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+9 -1
View File
@@ -219,6 +219,7 @@ mod type_font_preview;
pub mod type_panels_ui;
pub mod type_tool;
mod type_transform;
pub mod updates_ui;
mod variables_ui;
pub mod vector_ui;
pub mod view_cmds;
@@ -430,6 +431,8 @@ pub struct Services {
/// Preferences ▸ General ▸ Beep When Done: the platform's default beep, played when a
/// background job finishes successfully. `None` in the web build (and in tests).
pub beep: Option<fn()>,
/// Starts a background check for published releases (desktop only).
pub check_updates: Option<photocraft_engine::updates_cmds::CheckFn>,
/// Files delivered asynchronously (web drag-and-drop): drained every frame.
pub inbox: Option<Inbox>,
/// OS clipboard images: copies go out, screenshots and images from other apps come in.
@@ -735,6 +738,7 @@ pub struct PhotocraftApp {
pub stroke_trace: stroke_trace::StrokeTrace,
/// Preferences, autosave and snapping runtime state (see `prefs_ui`, `snap_ui`).
pub(crate) prefs_rt: prefs_ui::Runtime,
pub(crate) updates_rt: updates_ui::Runtime,
/// Close, Revert or Exit parked behind the unsaved-changes prompt (see `discard_ui`).
pub(crate) discard: Option<discard_ui::Prompt>,
/// The Open or Save dialog in progress, and the action waiting on it (see `file_dialog`).
@@ -764,7 +768,8 @@ pub struct PhotocraftApp {
}
impl PhotocraftApp {
pub fn new(session: Session, services: Services) -> Self {
pub fn new(mut session: Session, mut services: Services) -> Self {
session.updates.check = services.check_updates.take();
let mut app = Self {
session,
ui: UiState::default(),
@@ -880,6 +885,7 @@ impl PhotocraftApp {
perf: Default::default(),
stroke_trace: stroke_trace::StrokeTrace::from_env(),
prefs_rt: Default::default(),
updates_rt: Default::default(),
discard: None,
file_dialog: None,
tiff_options: None,
@@ -1715,6 +1721,7 @@ impl eframe::App for PhotocraftApp {
self.collect_screenshots(ctx);
self.issue_screenshots(ctx);
prefs_ui::tick(self, ctx);
updates_ui::tick(self, ctx);
monitor_status::poll_frame(self, ctx, frame);
// Control requests and persisted preferences can change the language in this frame.
i18n::sync_context(ctx, &self.session.prefs().interface.language);
@@ -1875,6 +1882,7 @@ impl eframe::App for PhotocraftApp {
wide_angle_ui::show(self, &ctx);
canvas::extra_windows(self, &ctx);
notices::show(self, &ctx);
updates_ui::show(self, &ctx);
gpu_status::show_fallback(self, &ctx);
kys_import::show_offer(self, &ctx);
if chrome && self.ui.panels.menu_bar && self.custom_titlebar && !self.ui.dialogs.is_empty() {
+14 -1
View File
@@ -102,7 +102,20 @@ mod tests {
fn help_menu_lists_links_then_separator_then_system_info_and_about() {
let app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
let help: Vec<String> = crate::menus::menu_items(&app).into_iter().filter(|i| i.path == ["Help"]).map(|i| i.id).collect();
assert_eq!(help, ["help.discord", "help.website", "help.artcraftWebsite", "help.github", "help.reportIssue", "---", "help.systemInfo", "help.about"]);
assert_eq!(
help,
[
"help.discord",
"help.website",
"help.artcraftWebsite",
"help.github",
"help.reportIssue",
"help.checkForUpdates",
"---",
"help.systemInfo",
"help.about"
]
);
for (id, _) in COMMANDS {
assert!(crate::menus::is_live(id) && crate::menus::is_enabled(&app, id), "{id}");
}
+10 -1
View File
@@ -167,6 +167,9 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
app.ui.tool = tool;
return Ok(json!({"tool": tool}));
}
if id == "help.checkForUpdates" {
return crate::updates_ui::check(app, ctx, params);
}
if id == "ui.symmetryTransform" {
return crate::symmetry_ui::begin(app).map(|_| Value::Null);
}
@@ -1075,7 +1078,13 @@ pub fn menu_items(app: &PhotocraftApp) -> Vec<MenuItem> {
items.insert(after + 1 + k, it);
}
}
// Help: the link items, a separator, then System Info and About.
// Keep the update command with the links, ahead of System Info and About.
if let Some(at) = items.iter().position(|i| i.id == "help.checkForUpdates") {
let update = items.remove(at);
let before = items.iter().position(|i| i.id == "help.systemInfo" || i.id == "help.about").unwrap_or(items.len());
items.insert(before, update);
}
// Help: the link items and update check, a separator, then System Info and About.
if let Some(at) = items.iter().position(|i| i.id == "help.systemInfo" || i.id == "help.about") {
items.insert(
at,
+1
View File
@@ -1026,6 +1026,7 @@ fn humanize(key: &str) -> String {
"appearanceMode" => return "Appearance Mode".into(),
"darkTheme" => return "Dark Theme".into(),
"lightTheme" => return "Light Theme".into(),
"automaticallyCheckUpdates" => return "Automatically Check for Updates".into(),
_ => {}
}
// These controls appear only for WebP, so reuse the existing translated labels.
+4
View File
@@ -1248,6 +1248,9 @@ pub struct UiState {
/// Non-blocking notices (import/export warnings, files that couldn't open), newest last.
#[serde(default)]
pub notices: Vec<crate::notices::Notice>,
/// Non-modal release-check window; readable and controllable through `ui.inspect`/`ui.set`.
#[serde(default)]
pub updates_open: bool,
/// Pending GPU fallback warning, visible to automation.
#[serde(default)]
pub gpu_fallback_notice: Option<String>,
@@ -1342,6 +1345,7 @@ impl Default for UiState {
status: String::new(),
status_error: false,
notices: Vec::new(),
updates_open: false,
gpu_fallback_notice: None,
kys_offer: None,
slow_refresh_noticed: Vec::new(),
+233
View File
@@ -0,0 +1,233 @@
//! One background check at startup and a non-modal window for release information.
use photocraft_engine::updates_cmds::Status;
use serde_json::Value;
use crate::PhotocraftApp;
#[derive(Default)]
pub(crate) struct Runtime {
startup_checked: bool,
notified_version: Option<String>,
}
pub(crate) fn check(app: &mut PhotocraftApp, _ctx: &egui::Context, params: Value) -> Result<Value, String> {
let result = app.run("help.checkForUpdates", params)?;
app.ui.updates_open = true;
app.updates_rt.startup_checked = true;
Ok(result)
}
pub(crate) fn tick(app: &mut PhotocraftApp, ctx: &egui::Context) {
// `updates.status` also polls (automation may consume the result before this frame).
app.session.updates.poll();
if let Status::Available { release } = &app.session.updates.status
&& app.updates_rt.notified_version.as_ref() != Some(&release.version)
{
app.updates_rt.notified_version = Some(release.version.clone());
app.ui.updates_open = true;
}
if app.session.updates.check.is_none() {
return;
}
if !app.updates_rt.startup_checked {
app.updates_rt.startup_checked = true;
if app.session.prefs().general.automatically_check_updates && matches!(app.session.updates.status, Status::Idle) {
let _ = app.run("help.checkForUpdates", serde_json::json!({}));
}
}
if matches!(app.session.updates.status, Status::Checking) {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
}
pub(crate) fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
if !app.ui.updates_open {
return;
}
let mut open = true;
let status = app.session.updates.status.clone();
let mut retry = false;
let mut download = None;
egui::Window::new(tl!("PhotoCraft Updates"))
.id(egui::Id::new("photocraft_updates"))
.open(&mut open)
.collapsible(false)
.resizable(false)
.default_width(380.0)
.show(ctx, |ui| {
let t = crate::theme::Tokens::get(ctx);
ui.label(crate::i18n::fmt(crate::i18n::t("Installed version: {version}"), &[("version", photocraft_engine::build_info::VERSION)]));
ui.add_space(8.0);
match &status {
Status::Checking => {
ui.horizontal(|ui| {
ui.spinner();
ui.label(tl!("Checking for updates…"));
});
}
Status::Available { release } => {
ui.label(crate::i18n::fmt(crate::i18n::t("PhotoCraft {version} is available."), &[("version", &release.version)]));
ui.label(tl!("Open the release page to choose an installer for your system."));
if crate::widgets::primary_button(ui, tl!("Open Download Page"), 160.0).clicked() {
download = Some(release.url.clone());
}
}
Status::UpToDate => {
ui.label(tl!("PhotoCraft is up to date."));
}
Status::Failed { message } => {
ui.colored_label(t.warning, tl!("Could not check for updates."));
ui.label(message);
}
Status::Idle => {
ui.label(tl!("Updates have not been checked yet."));
}
}
if !matches!(status, Status::Checking) && ui.button(tl!("Check Again")).clicked() {
retry = true;
}
});
app.ui.updates_open = open;
if let Some(url) = download {
crate::links::open(app, ctx, &url);
}
if retry {
let _ = check(app, ctx, serde_json::json!({}));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Services, menus};
use photocraft_engine::{Session, updates_cmds::Release};
use std::sync::{Arc, Mutex, mpsc};
type Sender = Arc<Mutex<Option<mpsc::Sender<photocraft_engine::updates_cmds::CheckResult>>>>;
fn app() -> (PhotocraftApp, Arc<Mutex<u32>>, Sender) {
let calls = Arc::new(Mutex::new(0));
let count = calls.clone();
let sender = Arc::new(Mutex::new(None));
let tx = sender.clone();
let app = PhotocraftApp::new(
Session::new(),
Services {
check_updates: Some(Box::new(move || {
*count.lock().unwrap() += 1;
let (sender, receiver) = mpsc::channel();
*tx.lock().unwrap() = Some(sender);
Ok(receiver)
})),
..Default::default()
},
);
(app, calls, sender)
}
fn at(app: &mut PhotocraftApp, ctx: &egui::Context, time: f64) {
let mut output = ctx.run_ui(egui::RawInput { time: Some(time), ..Default::default() }, |ui| tick(app, ui.ctx()));
output.textures_delta.clear();
}
#[test]
fn automatic_check_runs_once_per_launch_even_after_days_or_preference_changes() {
let (mut app, calls, sender) = app();
let ctx = egui::Context::default();
at(&mut app, &ctx, 0.0);
at(&mut app, &ctx, 1.0);
at(&mut app, &ctx, 2.0);
assert_eq!(*calls.lock().unwrap(), 1);
sender.lock().unwrap().as_ref().unwrap().send(Ok(None)).unwrap();
at(&mut app, &ctx, 3.0);
assert!(!app.ui.updates_open, "automatic up-to-date checks stay quiet");
at(&mut app, &ctx, 86400.0);
app.session.edit_prefs(|p| p.general.automatically_check_updates = false);
at(&mut app, &ctx, 86401.0);
app.session.edit_prefs(|p| p.general.automatically_check_updates = true);
at(&mut app, &ctx, 172800.0);
assert_eq!(*calls.lock().unwrap(), 1, "no periodic checks or preference-triggered rechecks");
let (mut restarted, restarted_calls, _) = self::app();
at(&mut restarted, &ctx, 0.0);
assert_eq!(*restarted_calls.lock().unwrap(), 1, "the next launch checks again");
}
#[test]
fn startup_opt_out_and_failed_checks_do_not_retry_automatically() {
let (mut disabled, calls, _) = app();
let ctx = egui::Context::default();
disabled.session.edit_prefs(|p| p.general.automatically_check_updates = false);
at(&mut disabled, &ctx, 0.0);
disabled.session.edit_prefs(|p| p.general.automatically_check_updates = true);
at(&mut disabled, &ctx, 86400.0);
assert_eq!(*calls.lock().unwrap(), 0, "opt-out is evaluated at startup");
let (mut failed, calls, sender) = app();
at(&mut failed, &ctx, 0.0);
sender.lock().unwrap().as_ref().unwrap().send(Err("offline".into())).unwrap();
at(&mut failed, &ctx, 1.0);
at(&mut failed, &ctx, 172800.0);
assert_eq!(*calls.lock().unwrap(), 1);
assert!(!failed.ui.updates_open, "an automatic failure stays quiet");
}
#[test]
fn manual_check_works_when_automatic_checks_are_off_and_opens_status() {
let (mut app, calls, sender) = app();
let ctx = egui::Context::default();
app.session.edit_prefs(|p| p.general.automatically_check_updates = false);
assert!(menus::is_enabled(&app, "help.checkForUpdates"));
menus::invoke(&mut app, &ctx, "help.checkForUpdates", serde_json::json!({})).unwrap();
assert!(app.ui.updates_open);
assert_eq!(*calls.lock().unwrap(), 1);
sender.lock().unwrap().as_ref().unwrap().send(Err("offline".into())).unwrap();
at(&mut app, &ctx, 1.0);
assert!(matches!(app.session.updates.status, Status::Failed { .. }));
let mut web = PhotocraftApp::new(Session::new(), Services::default());
assert!(!menus::is_enabled(&web, "help.checkForUpdates"));
at(&mut web, &ctx, 2.0);
assert_eq!(web.session.updates.status, Status::Idle);
}
#[test]
fn available_release_notifies_once_even_when_automation_polls_first() {
let (mut app, _, sender) = app();
let ctx = egui::Context::default();
at(&mut app, &ctx, 0.0);
sender
.lock()
.unwrap()
.as_ref()
.unwrap()
.send(Ok(Some(Release { version: "99.0.0".into(), url: "https://github.com/storytold/photocraft/releases/tag/v99.0.0".into() })))
.unwrap();
app.run("updates.status", serde_json::json!({})).unwrap();
at(&mut app, &ctx, 1.0);
assert!(app.ui.updates_open);
app.ui.updates_open = false;
at(&mut app, &ctx, 2.0);
assert!(!app.ui.updates_open);
}
#[test]
fn update_window_is_drivable_and_bad_state_changes_are_atomic() {
let (mut app, _, _) = app();
let ctx = egui::Context::default();
let call = |app: &mut PhotocraftApp, method: &str, params: Value| {
let (req, _) = crate::control::ControlRequest::new(method, params);
match crate::control::handle(app, &ctx, &req) {
crate::control::Outcome::Done(value) => value,
_ => panic!("expected an immediate reply"),
}
};
assert_eq!(call(&mut app, "ui.set", serde_json::json!({"updatesOpen": true}))["ok"], true);
let inspected = call(&mut app, "ui.inspect", serde_json::json!({}));
assert_eq!(inspected["result"]["updatesOpen"], true);
assert_eq!(inspected["result"]["updates"]["supported"], true);
assert_eq!(call(&mut app, "ui.set", serde_json::json!({"updatesOpen": false, "maskTarget": "bad"}))["ok"], false);
assert!(app.ui.updates_open);
assert_eq!(call(&mut app, "ui.set", serde_json::json!({"updatesOpen": false}))["ok"], true);
assert!(!app.ui.updates_open);
}
}
+13
View File
@@ -267,6 +267,19 @@ toolbox colour as `backgroundColor: [r,g,b]` (0..1), so reusing it does not foll
changes. Up to 256 presets are kept. Invalid saved entries are skipped independently; other
presets and preferences still load. Automatic Recent configurations are separate follow-up work.
## Update checks
On desktop, `engine.execute` with `help.checkForUpdates` and `{}` starts a background
check; `updates.status` returns `{supported, currentVersion, result}`. The result's `status`
is `idle`, `checking`, `upToDate`, `available` (with `release: {version, url}`), or `failed`
(with `message`). No document is required and neither command adds history or journal entries.
Headless and web sessions without a release transport report `supported: false`.
Set `general.automaticallyCheckUpdates` with `prefs.set` to enable or disable the next
desktop startup's single check (enabled by default). Manual checks still work when automatic checks are off.
`ui.inspect` includes `updates` and `updatesOpen`; `ui.set {"updatesOpen": false}` closes
the non-modal update window.
## Snapping
With View › Snap on, tool gestures snap to the View › Snap To targets (guides and grid while they
+7 -3
View File
@@ -1,6 +1,6 @@
# Where PhotoCraft falls short of Photoshop
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (in-app startup/manual release checks implemented under G17; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
@@ -252,8 +252,11 @@ what an agent can't finish alone.
## Platform gaps
### G17. Platforms and distribution
- **Missing:** Windows code signing (material not obtained), in-app update check (DIST-220-1,
#2650, #2375), in-app bug report (DIST-220-3), Flathub (#173),
- **Implemented (2026-10-14):** in-app update check (DIST-220-1, #2650, #2375): native
one background check at launch, persisted opt-out, Help manual check and official
release-page link. Installer download/installation remains a user action.
- **Missing:** Windows code signing (material not obtained),
in-app bug report (DIST-220-3), Flathub (#173),
starting without a GPU adapter (#2519, #2412), Windows 7 (#2310, out of scope), Intel Macs with
OpenCore (#1859).
- **Estimate:** 30–60 h. **Human:** signing certificates.
@@ -262,6 +265,7 @@ what an agent can't finish alone.
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | Implemented stable release checks once per launch, opt-out and manual status/download link (DIST-220-1); estimates unchanged |
| 2026-10-13 | minor | G6: stale "missing tool" list corrected against `main` — Color Replacement (`paint.colorReplacement`), Red Eye (`paint.redEye`) and Add/Delete Anchor Point (P flyout, #4144) are live tools, so the toolbox is 67 of 68 and Art History Brush is the only one missing; no percentage or estimate changed |
| 2026-10-13 | minor | G4: Hue Jitter amount-track appearance (#3825, partial); no readiness percentage or estimate changed |
| 2026-10-13 | minor | G4: minimum inputs in Shape Dynamics and Transfer follow their Control (#3825, partial); no readiness percentage or estimate changed |
+16
View File
@@ -9,6 +9,22 @@ This is PhotoCraft's implementation of the shared
[release playbook](release-playbook.md). User-facing names say **PhotoCraft**. Files, binaries
and ids stay lowercase (`photocraft-<version>-<platform>-<arch>.<ext>`, `ai.storyteller.photocraft`).
## In-app update checks
The native desktop app checks the public GitHub latest-release endpoint in a background
thread once at launch, with no periodic checks or automatic retries. Preferences › General › Automatically
Check for Updates disables the startup check; Help › Check for Updates runs one manually.
Checks use a 15-second timeout and a 1 MiB response limit, need no GitHub token, and send
no document or personal data. Network failures are quiet during automatic checks and visible
in the manual check window. Concurrent requests share one check.
HTTPS validates certificates and host names against the operating system's trust store.
Tags must be semantic versions (`v0.8.0` or `0.8.0`); only published stable versions newer
than the installed workspace version trigger a notice. The notice opens the official release
page to choose an installer; installation remains a user action. Drafts and prereleases do
not trigger it. Web/headless frontends have no update transport. No release-workflow changes
are required: publishing a newer stable GitHub Release makes it discoverable.
## Cutting a release
1. **Bump the version** on `main`. The only place it lives is `[workspace.package] version`
+4 -4
View File
@@ -9,11 +9,11 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
| [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 17 not run, 5 not measurable yet (baseline 2026-10-05) |
| [File compatibility](#file-compatibility) | 7 | 13 | 7 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 145/256, round trip ≥ 258/256 |
| [Tools](#tools) | 29 | 13 | 1 | |
| [Workspace and UI](#workspace-and-ui) | 13 | 6 | 4 | settings that do nothing: 26 of 157 (target 0) |
| [Workspace and UI](#workspace-and-ui) | 13 | 6 | 4 | settings that do nothing: 26 of 158 (target 0) |
| [Type](#type) | 4 | 3 | 2 | |
| [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) |
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
| [Distribution](#distribution) | 5 | 4 | 2 | |
| [Distribution](#distribution) | 6 | 4 | 1 | |
| [Open bugs](#open-bugs) | 29 | 2 | 1 | |
## Performance
@@ -176,7 +176,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
## Workspace and UI
**Settings that do nothing: 26 of 157** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
**Settings that do nothing: 26 of 158** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `file_handling.save_in_background`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
@@ -262,7 +262,7 @@ Never-crash attribute on 28 of 28 crate roots.
| DIST-4 | Flatpak single-file bundles, x86_64 and aarch64 | done | | release.yml:204-216 (#163) |
| DIST-5 | Web build (wasm32, trunk) | done | | release.yml:312 |
| DIST-6 | FreeBSD 14 x86_64 tar.gz (/usr/local tree) built in a FreeBSD VM | partial | | release.yml:242-283 (freebsd job) + packaging/freebsd/package.sh; layout checked with --dry-run, not yet produced by a release run |
| DIST-220-1 | Help > Check for Updates plus an opt-out periodic release-feed check on all desktops | missing | [#220](https://github.com/storytold/photocraft/issues/220) | No update check in crates |
| DIST-220-1 | Help > Check for Updates plus an opt-out startup release-feed check on all desktops | done | [#220](https://github.com/storytold/photocraft/issues/220) | 2026-10-12: updates_cmds + native HTTPS service; one check per launch, persisted opt-out, non-modal manual status/download page; version, timeout, malformed response and startup tests |
| DIST-220-2 | Windows ARM64 (aarch64-pc-windows-msvc) in the release matrix, signed like x64 | done | [#220](https://github.com/storytold/photocraft/issues/220) | release.yml has { arch: arm64, target: aarch64-pc-windows-msvc } in the Windows matrix, and .github/workflows/windows-arm64.yml packages that MSI, installs it on a Windows 11 ARM64 runner, checks the installed binaries are ARM64 and runs the CLI natively before uninstalling. Signing takes the same path as x64: the secrets are optional and a missing one produces an unsigned artifact and a warning (see DIST-2) |
| DIST-220-3 | In-app bug report with reviewable diagnostics that opens a prefilled issue | missing | [#220](https://github.com/storytold/photocraft/issues/220) | No bug report dialog in crates |
| DIST-220-4 | Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew | partial | [#220](https://github.com/storytold/photocraft/issues/220) | `cargo xtask i18n-coverage` reports per-language coverage (#427); RTL languages mirror the shell (`direction.rs`), not yet panel interiors or dialogs |
+3 -2
View File
@@ -1,6 +1,6 @@
# PhotoCraft vs Adobe Photoshop: target-app parity
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (`.atn` import maps 112 events; Free Transform H/V skew fields; stale rows corrected against `main` and the scorecard, estimates unchanged; Photoshop Actions `.atn` load and save; 2026-10-10: Tilt-Shift measurements added; earlier the same day: major, full re-measure against Photoshop 2026 27.11.0; merged `parity-estimate.md` and the roadmap's honest assessment; stale missing-tool rows corrected: Color Replacement and Red Eye are live toolbar tools, Frame landed; percentages unchanged) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-14 · **Change:** minor (native startup/manual release checks; `.atn` import maps 112 events; Free Transform H/V skew fields; stale rows corrected against `main` and the scorecard, estimates unchanged; Photoshop Actions `.atn` load and save; 2026-10-10: Tilt-Shift measurements added; earlier the same day: major, full re-measure against Photoshop 2026 27.11.0; merged `parity-estimate.md` and the roadmap's honest assessment; stale missing-tool rows corrected: Color Replacement and Red Eye are live toolbar tools, Frame landed; percentages unchanged) · **Target:** Adobe Photoshop 2026
The authoritative answer to "how close is PhotoCraft to Photoshop, and how much work is left". The
one-page summary is [`ROADMAP.md`](../ROADMAP.md); the ranked work list is [`gaps.md`](gaps.md);
@@ -82,7 +82,7 @@ Weights reflect what a working Photoshop user depends on (photographers, retouch
| Stability | 10% | 45% | 150–250 | Never-crash lints on 28/28 crates, `panic_hunt` in the gate, atomic saves; but field reports of freezes (#2518 Type tool 60 s on macOS), GPU panics (#2020, #1215), no-GPU start failures (#2519, #2412) | [gaps.md](gaps.md) |
| Hardware | 5% | 50% | 80–150 | GPU compositor (wgpu), pen pressure on all desktops but tilt/rotation not on Windows, Wayland pen broken (#2622), no HDR/EDR output, monitor profile auto-detected on macOS only | [hardware-parity.md](hardware-parity.md) |
| Localization | 5% | 55% | 80–140 | 7 of the 12 key languages at 100% of measured UI keys; Hindi and Arabic absent, Vietnamese catalog added 2026-10-12 (#2636, coverage not re-measured); no RTL UI; engine messages English | [localization-parity.md](localization-parity.md) |
| Platforms | 5% | 80% | 30–60 | macOS (notarized), Windows (unsigned), Linux (AppImage/deb/rpm/Flatpak), web, FreeBSD partial; Windows ARM64 is packaged and install-checked by `.github/workflows/windows-arm64.yml` (DIST-220-2); no in-app updates | [scorecard.md](scorecard.md#distribution) |
| Platforms | 5% | 80% | 30–60 | macOS (notarized), Windows (unsigned), Linux (AppImage/deb/rpm/Flatpak), web, FreeBSD partial; Windows ARM64 is packaged and install-checked by `.github/workflows/windows-arm64.yml` (DIST-220-2); native release checks with opt-out (2026-10-14); no automatic installer | [scorecard.md](scorecard.md#distribution) |
| Ecosystem (plug-ins, scripting, presets, libraries) | 5% | 15% | 250–450 | No .8BF hosting (#2647), no UXP/ExtendScript; .atn load/save (2026-10-12, 112 events, the rest kept verbatim), .abr/.grd/.pat/.aco/.ase/.cube supported | [gaps.md](gaps.md) |
| AI features | 5% | 10% | 300–600 | No ML runtime at all (scorecard AUTO-219-7); classical substitutes only. Needs an owner decision and openly licensed models (#41) | [gaps.md](gaps.md) |
| **Weighted** | 100% | **~46%** | **2,340–4,100** | | |
@@ -565,6 +565,7 @@ See [method, raw measurements and reproduction](five-algorithm-performance.md).
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | Implemented native background/manual stable release checks with opt-out and download-page link (DIST-220-1); headline estimates unchanged |
| 2026-10-13 | minor | Stale "missing tool" rows corrected against main: Color Replacement (`paint.colorReplacement`), Red Eye (`paint.redEye`) and Add/Delete Anchor Point (the Pen flyout) are live toolbar tools and Frame landed, so Art History Brush is the only tool still missing; percentages unchanged |
| 2026-10-12 | minor | Free Transform options bar H/V skew fields; percentages unchanged |
| 2026-10-12 | minor | `.atn` import: 112 events mapped (TrueGrit Texture Supply sets play end to end); percentages unchanged |
+3 -3
View File
@@ -42,10 +42,10 @@ note = "release.yml:242-283 (freebsd job) + packaging/freebsd/package.sh; layout
[[item]]
id = "DIST-220-1"
target = "Help > Check for Updates plus an opt-out periodic release-feed check on all desktops"
status = "missing"
target = "Help > Check for Updates plus an opt-out startup release-feed check on all desktops"
status = "done"
issue = 220
note = "No update check in crates"
note = "2026-10-12: updates_cmds + native HTTPS service; one check per launch, persisted opt-out, non-modal manual status/download page; version, timeout, malformed response and startup tests"
[[item]]
id = "DIST-220-2"