store art from the live stores: storebound art pull (App Store screenshot sets and previews per locale and display type from imageAsset.templateUrl at the original size, the App Store icon from the newest build or the public artwork; per Play language icon, feature graphic, phone / 7" / 10" / TV / Wear screenshots as the =s0 originals checked against Play's sha256) into store/renders with pulled.json (sha256, store, URL host, store checksum); upsert (a re-run changes nothing, differing local files kept unless --force, the upload's file of a slot compared: Play .jpg, App Store .png, the App Store icon as a picture); onboard runs it; apple screenshots / previews and play images see unchanged pulled files as the store's own; check: render sources are notes with pulled art; local renders check: pulled files pass, store-empty sets not demanded; art live / thumb / replace; desktop Store art tab: live thumbnails per store, language and set (same / differs / not pulled), Pull from stores, store-only apps read-only, Replace with a file (image_pick, art.replace) in the lightbox and the art editor; boot check store art case; tests/art-pull.test.ts; console parity 76 rows (57 complete, 19 with the reason); store-screenshots skill (pull first for live apps), pitfall 59

This commit is contained in:
CRTkafa 2026-10-14 02:18:38 +08:00
parent 49a73c90e4
commit 00be838b76
40 changed files with 1669 additions and 58 deletions

View file

@ -49,6 +49,7 @@
"allow-project-import-fastlane",
"allow-export-save",
"allow-price-sheet-pick",
"allow-image-pick",
"allow-project-reveal",
"allow-project-locate",
"allow-state-reset",

View file

@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-image-pick"
description = "Enables the image_pick command without any pre-configured scope."
commands.allow = ["image_pick"]
[[permission]]
identifier = "deny-image-pick"
description = "Denies the image_pick command without any pre-configured scope."
commands.deny = ["image_pick"]

View file

@ -712,6 +712,25 @@ pub async fn price_sheet_pick(window: WebviewWindow, state: State<'_, AppState>,
Ok(Some(p.to_string_lossy().to_string()))
}
/// An image (PNG / JPEG) to replace one store image with, picked in a native dialog. None = cancelled. The path goes
/// back to the window only as the `from` argument of art.replace (a write: dry run, dialog, then the copy into the app
/// folder).
#[tauri::command]
pub async fn image_pick(window: WebviewWindow, state: State<'_, AppState>, project_id: String) -> Res<Option<String>> {
guard(&window)?;
let start = saved(&state)?.projects.iter().find(|p| p.id == project_id).map(|p| PathBuf::from(&p.path)).ok_or_else(|| Fail::host("unknown project"))?;
let w = window.clone();
let picked = tauri::async_runtime::spawn_blocking(move || w.app_handle().dialog().file().set_title("Choose an image (PNG or JPEG)").add_filter("Image", &["png", "jpg", "jpeg"]).set_directory(start).set_parent(&w).blocking_pick_file())
.await
.map_err(|e| Fail::host(e.to_string()))?;
let Some(picked) = picked else { return Ok(None) };
let p = picked.into_path().map_err(|e| Fail::host(e.to_string()))?;
if !p.extension().is_some_and(|e| ["png", "jpg", "jpeg"].iter().any(|x| e.eq_ignore_ascii_case(x))) {
return Err(Fail::host("pick a .png or .jpg file"));
}
Ok(Some(p.to_string_lossy().to_string()))
}
/// Shows a folder or file of an app folder in the file manager (`rel` empty: the app folder), or opens it with
/// `open` (a folder, or the gallery page storebound writes: <renders>/index.html in the default browser).
#[tauri::command]

View file

@ -41,6 +41,7 @@ pub const COMMANDS: &[&str] = &[
"project_import_fastlane",
"export_save",
"price_sheet_pick",
"image_pick",
"project_reveal",
"project_locate",
"state_reset",

View file

@ -251,6 +251,7 @@ pub fn run() {
commands::project_import_fastlane,
commands::export_save,
commands::price_sheet_pick,
commands::image_pick,
commands::project_reveal,
commands::project_locate,
commands::state_reset,

View file

@ -131,8 +131,8 @@ pub fn store_only_write(method: &str, kind: Option<Kind>) -> bool {
kind == Some(Kind::Write) && STORE_ONLY_WRITES.contains(&method)
}
/// Reads that are never kept in the offline cache (big: one rendered image as a data: URL).
pub const NO_CACHE_METHODS: &[&str] = &["project.render"];
/// Reads that are never kept in the offline cache (big: one rendered image or one live store thumbnail as a data: URL).
pub const NO_CACHE_METHODS: &[&str] = &["project.render", "art.thumb"];
/// File kinds the window may export through `export_save`, with their extension.
pub const EXPORT_KINDS: &[&str] = &["csv", "md", "json", "txt"];

View file

@ -38,8 +38,8 @@ fn app_folder_paths() {
#[test]
fn new_folder_methods_and_big_reads() {
assert_eq!(NEW_FOLDER_METHODS, &["project.init", "import.fastlane"]);
assert_eq!(NO_CACHE_METHODS, &["project.render"]);
for c in ["project_import_fastlane", "export_save", "price_sheet_pick", "project_reveal"] {
assert_eq!(NO_CACHE_METHODS, &["project.render", "art.thumb"]);
for c in ["project_import_fastlane", "export_save", "price_sheet_pick", "image_pick", "project_reveal"] {
assert!(COMMANDS.contains(&c), "{c}");
}
}