mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 18:13:39 +00:00
Add opt-out startup release checks and manual update status
This commit is contained in:
Generated
+61
-13
@@ -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]]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]| {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.*`).
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¤t).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,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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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() {
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user