day-cli 0.3.0

Declarative app development API using native UI toolkits
// Copyright © The Daybrite Project
// SPDX-License-Identifier: MPL-2.0

//! ArkUI (HarmonyOS) resource staging (§18.3).
//!
//! Both images and data go into `platform/harmony/entry/src/main/resources/rawfile/day/` (hvigor packages
//! rawfile uncompressed, and the OpenHarmony NDK can only reach `rawfile` — not `media` — from native
//! code). `day-arkui` sets an image node's src to `resource://RAWFILE/day/<name>.png` and its rawfile
//! opener mmaps `day/<name>` for random-access data.

use std::fs;

use super::{FontFile, ResourceSet, sanitize_ident};
use crate::meta::Project;

pub fn stage(project: &Project, set: &ResourceSet, fonts: &[FontFile]) -> Result<(), String> {
    let harmony = crate::ohos::harmony_dir(project);
    if !harmony.exists() {
        return Ok(());
    }
    let dir = harmony.join("entry/src/main/resources/rawfile/day");
    // Regenerate fresh so removed resources don't linger in the packaged rawfile tree.
    let _ = fs::remove_dir_all(&dir);
    // Vector glyph SVGs (docs/vectors.md): staged beside the raster under the same stem —
    // ArkUI's Image renders SVG natively and `NODE_IMAGE_FILL_COLOR` recolors it, so
    // day-arkui probes `day/<name>.svg` first and falls back to the png.
    let svgs: Vec<std::path::PathBuf> = super::vector_svg_dir(project)
        .read_dir()
        .map(|rd| {
            rd.flatten()
                .map(|e| e.path())
                .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("svg"))
                .collect()
        })
        .unwrap_or_default();
    if set.images.is_empty() && set.data.is_empty() && fonts.is_empty() && svgs.is_empty() {
        return Ok(());
    }
    fs::create_dir_all(&dir).map_err(|e| format!("mkdir {}: {e}", dir.display()))?;
    // Fonts (§18.4): rawfile `day/fonts/<ident>.<ext>` plus a `day/fonts.json` manifest
    // ([{family, file}]) that the platform/harmony scaffold's EntryAbility feeds to ArkTS
    // `font.registerFont` before the native UI loads — NODE_FONT_FAMILY then resolves the
    // family by name.
    if !fonts.is_empty() {
        let fdir = dir.join("fonts");
        fs::create_dir_all(&fdir).map_err(|e| format!("mkdir {}: {e}", fdir.display()))?;
        let mut manifest = Vec::new();
        for f in fonts {
            let name = f.staged_name();
            let dest = fdir.join(&name);
            fs::copy(&f.path, &dest).map_err(|e| format!("stage {}: {e}", dest.display()))?;
            manifest.push(serde_json::json!({ "family": f.family, "file": name }));
        }
        let json = serde_json::to_string_pretty(&manifest).expect("font manifest");
        fs::write(dir.join("fonts.json"), json).map_err(|e| format!("stage fonts.json: {e}"))?;
    }
    // Images: day-arkui references `resource://RAWFILE/day/<name>.png`, so normalize the file name to
    // `<name>.png` (ArkUI's Image decodes by content, not extension).
    for img in &set.images {
        let dest = dir.join(format!("{}.png", sanitize_ident(&img.name)));
        fs::copy(&img.path, &dest).map_err(|e| format!("stage {}: {e}", dest.display()))?;
    }
    for svg in &svgs {
        if let Some(name) = svg.file_name() {
            let dest = dir.join(name);
            fs::copy(svg, &dest).map_err(|e| format!("stage {}: {e}", dest.display()))?;
        }
    }
    // Data: the rawfile opener reads `day/<name>`; `name` is the `/`-relative tree path
    // (§18.5), so recreate its parents under the rawfile dir.
    for d in &set.data {
        let dest = dir.join(&d.name);
        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
        }
        fs::copy(&d.path, &dest).map_err(|e| format!("stage {}: {e}", dest.display()))?;
    }
    Ok(())
}