arc/scripts/lib/hesabix_branding.sh

842 lines
31 KiB
Shell
Executable file

#!/usr/bin/env bash
# Build-time UI branding overlay for Hesabix Flutter web and Android.
#
# Pack layout (flat or nested — first match wins):
# BRANDING_DIR/
# names.json # { "fa": "…", "en": "…" } (optional)
# logo-light.png # or logos/ / images/
# logo-blue.png # optional
# Icon-192.png … # or icons/ (Android launcher uses Icon-512)
# favicon.ico / favicon.png # optional (web)
# ic_stat.png # optional white silhouette for Android status bar
#
# Env / .deploy_env:
# BRANDING_MODE=default|custom (empty → default)
# BRANDING_DIR=/opt/hesabix/branding
# APP_NAME_FA=… APP_NAME_EN=…
#
# When mode is default (or unset): no-op — Hesabix logos/names stay as in the repo.
# Android APK applies the same pack via build_android.sh (overlay + dart-define + restore).
# shellcheck shell=bash
HESABIX_BRANDING_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=hesabix_deploy_env.sh
if [[ -r "${HESABIX_BRANDING_LIB_DIR}/hesabix_deploy_env.sh" ]]; then
# shellcheck disable=SC1091
source "${HESABIX_BRANDING_LIB_DIR}/hesabix_deploy_env.sh"
fi
_HESABIX_BRANDING_BACKUP_DIR=""
_HESABIX_BRANDING_APPLIED=0
_HESABIX_BRANDING_EFFECTIVE_MODE="default"
HESABIX_BRANDING_NAME_FA=""
HESABIX_BRANDING_NAME_EN=""
hesabix_branding_default_dir() {
printf '%s' "${APP_ROOT:-/opt/hesabix}/branding"
}
hesabix_branding_normalize_mode() {
local m="${1:-}"
m="$(printf '%s' "$m" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
case "$m" in
""|default|hesabix|off|none|0) printf '%s' "default" ;;
custom|yes|on|1) printf '%s' "custom" ;;
*) printf '%s' "$m" ;;
esac
}
# Find a file in branding pack: nested or flat.
hesabix_branding_find_file() {
local root="$1"
local name="$2"
shift 2
local sub
for sub in "$@" "."; do
if [[ "$sub" == "." ]]; then
[[ -f "${root}/${name}" ]] && { printf '%s' "${root}/${name}"; return 0; }
else
[[ -f "${root}/${sub}/${name}" ]] && { printf '%s' "${root}/${sub}/${name}"; return 0; }
fi
done
return 1
}
# Prints two lines: FA then EN. Returns 0 on success.
hesabix_branding_read_names_json() {
local json_file="$1"
[[ -f "$json_file" ]] || return 1
python3 - "$json_file" <<'PY'
import json, sys
path = sys.argv[1]
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
print(f"warn: cannot parse names.json: {e}", file=sys.stderr)
sys.exit(1)
fa = str(data.get("fa") or data.get("fa_IR") or data.get("persian") or "").strip()
en = str(data.get("en") or data.get("en_US") or data.get("english") or "").strip()
print(fa)
print(en)
PY
}
# Resolve effective mode/dir/names into globals + exports.
# Optional overrides: BRANDING_MODE_OVERRIDE, BRANDING_DIR_OVERRIDE, APP_NAME_FA_OVERRIDE, APP_NAME_EN_OVERRIDE
hesabix_branding_resolve() {
local mode dir fa en json_path pair
mode="$(hesabix_branding_normalize_mode "${BRANDING_MODE_OVERRIDE:-${BRANDING_MODE:-}}")"
dir="${BRANDING_DIR_OVERRIDE:-${BRANDING_DIR:-}}"
[[ -n "$dir" ]] || dir="$(hesabix_branding_default_dir)"
fa="${APP_NAME_FA_OVERRIDE:-${APP_NAME_FA:-}}"
en="${APP_NAME_EN_OVERRIDE:-${APP_NAME_EN:-}}"
if [[ "$mode" != "custom" ]]; then
_HESABIX_BRANDING_EFFECTIVE_MODE="default"
HESABIX_BRANDING_NAME_FA=""
HESABIX_BRANDING_NAME_EN=""
export BRANDING_MODE="default"
return 0
fi
if [[ ! -d "$dir" ]]; then
echo "[branding] warn: BRANDING_MODE=custom but directory missing: $dir — using default" >&2
_HESABIX_BRANDING_EFFECTIVE_MODE="default"
HESABIX_BRANDING_NAME_FA=""
HESABIX_BRANDING_NAME_EN=""
export BRANDING_MODE="default"
return 0
fi
if [[ -z "$fa" || -z "$en" ]]; then
json_path=""
if json_path="$(hesabix_branding_find_file "$dir" "names.json" "." "meta" 2>/dev/null)"; then
local names_out="" json_fa="" json_en=""
if names_out="$(hesabix_branding_read_names_json "$json_path" 2>/dev/null)"; then
json_fa="$(printf '%s\n' "$names_out" | sed -n '1p')"
json_en="$(printf '%s\n' "$names_out" | sed -n '2p')"
[[ -z "$fa" && -n "$json_fa" ]] && fa="$json_fa"
[[ -z "$en" && -n "$json_en" ]] && en="$json_en"
fi
fi
fi
# Must have at least one asset or a custom name; otherwise fall back.
local has_asset=0
hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" "." >/dev/null 2>&1 && has_asset=1
hesabix_branding_find_file "$dir" "logo-blue.png" "logos" "images" "." >/dev/null 2>&1 && has_asset=1
hesabix_branding_find_file "$dir" "Icon-192.png" "icons" "." >/dev/null 2>&1 && has_asset=1
hesabix_branding_find_file "$dir" "Icon-512.png" "icons" "." >/dev/null 2>&1 && has_asset=1
hesabix_branding_find_file "$dir" "favicon.ico" "icons" "." >/dev/null 2>&1 && has_asset=1
hesabix_branding_find_file "$dir" "favicon.png" "icons" "." >/dev/null 2>&1 && has_asset=1
if [[ "$has_asset" -eq 0 && -z "$fa" && -z "$en" ]]; then
echo "[branding] warn: custom pack has no logos/icons/names — using default" >&2
_HESABIX_BRANDING_EFFECTIVE_MODE="default"
HESABIX_BRANDING_NAME_FA=""
HESABIX_BRANDING_NAME_EN=""
export BRANDING_MODE="default"
return 0
fi
_HESABIX_BRANDING_EFFECTIVE_MODE="custom"
HESABIX_BRANDING_NAME_FA="$fa"
HESABIX_BRANDING_NAME_EN="$en"
export BRANDING_MODE="custom"
export BRANDING_DIR="$dir"
export APP_NAME_FA="$fa"
export APP_NAME_EN="$en"
}
hesabix_branding_backup_file() {
local src="$1"
local backup_root="$2"
local app_dir="$3"
[[ -f "$src" ]] || return 0
local rel="${src#"$app_dir"/}"
local dest="${backup_root}/${rel}"
mkdir -p "$(dirname "$dest")"
cp -a "$src" "$dest"
}
hesabix_branding_install_file() {
local src="$1"
local dest="$2"
local backup_root="$3"
local app_dir="$4"
[[ -f "$src" ]] || return 1
mkdir -p "$(dirname "$dest")"
hesabix_branding_backup_file "$dest" "$backup_root" "$app_dir"
cp -f "$src" "$dest"
touch "$dest" 2>/dev/null || true
echo "[branding] overlay: ${dest#"$app_dir"/}"
}
hesabix_branding_patch_web_names() {
local app_dir="$1"
local fa="$2"
local en="$3"
[[ -n "$fa" || -n "$en" ]] || return 0
local fa_use="${fa:-$en}"
local en_use="${en:-$fa}"
python3 - "$app_dir" "$fa_use" "$en_use" <<'PY'
import pathlib
import re
import sys
app_dir = pathlib.Path(sys.argv[1])
fa = sys.argv[2]
en = sys.argv[3]
# --- index.html ---
index = app_dir / "web" / "index.html"
if index.is_file():
t = index.read_text(encoding="utf-8")
t = re.sub(
r'(<meta\s+name="apple-mobile-web-app-title"\s+content=")[^"]*(")',
rf'\1{en}\2',
t,
count=1,
)
t = re.sub(r"(<title>)[^<]*(</title>)", rf"\1{en}\2", t, count=1, flags=re.I)
t = re.sub(
r'(<meta\s+name="description"\s+content=")[^"]*(")',
rf'\1{en} - سیستم مدیریت مالی\2',
t,
count=1,
)
t = re.sub(r'(alt=")Hesabix(")', rf'\1{en}\2', t)
t = re.sub(
r'(id="loader-title">)حسابیکس(</h1>)',
rf"\1{fa}\2",
t,
count=1,
)
# لودر اولیه: پیش‌فرض تم روشن = لوگوی رنگی؛ نسخه سفید جدا preload می‌شود
if 'href="images/logo-blue.png" as="image"' not in t:
t = t.replace(
'<link rel="preload" href="images/logo-light.png" as="image" type="image/png">',
'<link rel="preload" href="images/logo-blue.png" as="image" type="image/png">\n'
' <link rel="preload" href="images/logo-light.png" as="image" type="image/png">',
1,
)
t = re.sub(
r'(class="loader-logo"\s*\n\s*src=")images/logo-light\.png(")',
r'\1images/logo-blue.png\2',
t,
count=1,
)
t = t.replace('src="images/logo-light.png"', 'src="images/logo-blue.png"', 1)
index.write_text(t, encoding="utf-8")
print("[branding] patched web/index.html names")
# --- manifest.json ---
manifest = app_dir / "web" / "manifest.json"
if manifest.is_file():
t = manifest.read_text(encoding="utf-8")
t = re.sub(r'("name"\s*:\s*")[^"]*(")', rf'\1{en}\2', t, count=1)
t = re.sub(r'("short_name"\s*:\s*")[^"]*(")', rf'\1{en}\2', t, count=1)
t = re.sub(
r'("description"\s*:\s*")[^"]*(")',
rf'\1{en} - سیستم مدیریت مالی\2',
t,
count=1,
)
manifest.write_text(t, encoding="utf-8")
print("[branding] patched web/manifest.json names")
# --- loader JS ---
loader = app_dir / "web" / "hesabix_web_loader.js"
if loader.is_file():
t = loader.read_text(encoding="utf-8")
t = re.sub(
r"(fa:\s*\{\s*title:\s*')[^']*(')",
rf"\1{fa}\2",
t,
count=1,
)
t = re.sub(
r"(en:\s*\{\s*title:\s*')[^']*(')",
rf"\1{en}\2",
t,
count=1,
)
t = re.sub(r"(logo\.alt\s*=\s*')Hesabix(')", rf"\1{en}\2", t, count=1)
# تم روشن: logo-blue (رنگی) / تم تیره: logo-light (سفید) — بدون mask حسابیکس
old_logo_block = """ if (logo) {
logo.src = 'images/logo-light.png';
logo.alt = '%s';
if (dark) {
logo.style.opacity = '1';
logo.style.filter = 'none';
if (wrap) {
wrap.classList.remove('loader-logo-wrap--tinted');
wrap.style.removeProperty('--loader-logo-tint');
}
} else {
// سیلوئت سفید مخفی؛ رنگ برند روی wrap با mask
logo.style.opacity = '0';
if (wrap) {
wrap.classList.add('loader-logo-wrap--tinted');
wrap.style.setProperty('--loader-logo-tint', readBrandFromStorage());
}
}
}""" % en
# alt may still be Hesabix if the replace above did not run on a custom already-patched file
old_logo_block_hesabix = old_logo_block.replace("logo.alt = '%s';" % en, "logo.alt = 'Hesabix';")
new_logo_block = """ if (logo) {
logo.src = dark ? 'images/logo-light.png' : 'images/logo-blue.png';
logo.alt = '%s';
logo.style.opacity = '1';
logo.style.filter = 'none';
if (wrap) {
wrap.classList.remove('loader-logo-wrap--tinted');
wrap.style.removeProperty('--loader-logo-tint');
}
}""" % en
if old_logo_block in t:
t = t.replace(old_logo_block, new_logo_block, 1)
elif old_logo_block_hesabix in t:
t = t.replace(old_logo_block_hesabix, new_logo_block, 1)
else:
# fallback: just switch the asset by theme
t = t.replace(
"logo.src = 'images/logo-light.png';",
"logo.src = dark ? 'images/logo-light.png' : 'images/logo-blue.png';",
1,
)
old_light = """} else {
// سیلوئت سفید مخفی؛ رنگ برند روی wrap با mask
logo.style.opacity = '0';
if (wrap) {
wrap.classList.add('loader-logo-wrap--tinted');
wrap.style.setProperty('--loader-logo-tint', readBrandFromStorage());
}
}"""
new_light = """} else {
logo.style.opacity = '1';
logo.style.filter = 'none';
if (wrap) {
wrap.classList.remove('loader-logo-wrap--tinted');
wrap.style.removeProperty('--loader-logo-tint');
}
}"""
if old_light in t:
t = t.replace(old_light, new_light, 1)
loader.write_text(t, encoding="utf-8")
print("[branding] patched web/hesabix_web_loader.js names")
PY
}
hesabix_branding_patch_android_names() {
local app_dir="$1"
local fa="$2"
local en="$3"
[[ -n "$fa" || -n "$en" ]] || return 0
local fa_use="${fa:-$en}"
local en_use="${en:-$fa}"
local values="$app_dir/android/app/src/main/res/values/strings.xml"
local values_fa="$app_dir/android/app/src/main/res/values-fa/strings.xml"
[[ -f "$values" || -f "$values_fa" ]] || return 0
python3 - "$values" "$values_fa" "$fa_use" "$en_use" <<'PY'
import pathlib
import re
import sys
import xml.sax.saxutils as xml_escape
def patch(path: pathlib.Path, name: str) -> None:
if not path.is_file() or not name:
return
raw = path.read_text(encoding="utf-8")
escaped = xml_escape.escape(name)
updated, n = re.subn(
r'(<string\s+name="app_name">)[^<]*(</string>)',
rf"\1{escaped}\2",
raw,
count=1,
)
if n:
path.write_text(updated, encoding="utf-8")
print(f"[branding] patched {path.name} app_name")
else:
print(f"[branding] warn: app_name string not found in {path}", file=sys.stderr)
values = pathlib.Path(sys.argv[1])
values_fa = pathlib.Path(sys.argv[2])
fa = sys.argv[3]
en = sys.argv[4]
patch(values, en)
patch(values_fa, fa)
PY
}
# Resize src onto dest in-place (dest must already exist). mode=fit|silhouette
hesabix_branding_overlay_resized() {
local src="$1"
local dest="$2"
local backup_root="$3"
local app_dir="$4"
local mode="${5:-fit}"
[[ -f "$src" && -f "$dest" ]] || return 0
hesabix_branding_backup_file "$dest" "$backup_root" "$app_dir"
local dest_mode=""
dest_mode="$(stat -c '%a' "$dest" 2>/dev/null || true)"
if python3 - "$src" "$dest" "$mode" <<'PY'
import sys
from pathlib import Path
try:
from PIL import Image
except ImportError:
print("[branding] warn: Pillow not installed; skip image overlay", file=sys.stderr)
sys.exit(1)
src_path = Path(sys.argv[1])
dest_path = Path(sys.argv[2])
mode = sys.argv[3]
src = Image.open(src_path).convert("RGBA")
with Image.open(dest_path) as current:
size = current.size
def to_white_silhouette(im: Image.Image) -> Image.Image:
im = im.convert("RGBA")
_r, _g, _b, a = im.split()
amin, amax = a.getextrema()
if amin >= 250:
gray = im.convert("L")
a = gray.point(lambda p: 0 if p > 245 else 255)
white = Image.new("L", im.size, 255)
return Image.merge("RGBA", (white, white, white, a))
if mode == "silhouette":
src = to_white_silhouette(src)
fitted = src.resize(size, Image.Resampling.LANCZOS)
tmp = dest_path.with_suffix(dest_path.suffix + ".tmp")
fitted.save(tmp, "PNG")
tmp.replace(dest_path)
PY
then
[[ -n "$dest_mode" ]] && chmod "$dest_mode" "$dest" 2>/dev/null || true
echo "[branding] overlay: ${dest#"$app_dir"/}"
else
echo "[branding] warn: failed to overlay ${dest#"$app_dir"/}" >&2
fi
}
hesabix_branding_overlay_android_named() {
local app_dir="$1"
local src="$2"
local basename="$3"
local backup_root="$4"
local mode="${5:-fit}"
local res_root="$app_dir/android/app/src/main/res"
[[ -f "$src" && -d "$res_root" ]] || return 0
local dest
while IFS= read -r -d '' dest; do
hesabix_branding_overlay_resized "$src" "$dest" "$backup_root" "$app_dir" "$mode"
done < <(find "$res_root" -type f -name "$basename" -print0 2>/dev/null)
}
hesabix_branding_apply_android_icons() {
local app_dir="$1"
local pack_dir="$2"
local backup_root="$3"
local res_root="$app_dir/android/app/src/main/res"
[[ -d "$res_root" ]] || return 0
local launcher="" logo_light="" logo_blue="" stat=""
launcher="$(hesabix_branding_find_file "$pack_dir" "Icon-512.png" "icons" "." 2>/dev/null || true)"
[[ -n "$launcher" ]] || launcher="$(hesabix_branding_find_file "$pack_dir" "Icon-192.png" "icons" "." 2>/dev/null || true)"
[[ -n "$launcher" ]] || launcher="$(hesabix_branding_find_file "$pack_dir" "logo-light.png" "logos" "images" "." 2>/dev/null || true)"
[[ -n "$launcher" ]] || launcher="$(hesabix_branding_find_file "$pack_dir" "logo-blue.png" "logos" "images" "." 2>/dev/null || true)"
logo_light="$(hesabix_branding_find_file "$pack_dir" "logo-light.png" "logos" "images" "." 2>/dev/null || true)"
logo_blue="$(hesabix_branding_find_file "$pack_dir" "logo-blue.png" "logos" "images" "." 2>/dev/null || true)"
stat="$(hesabix_branding_find_file "$pack_dir" "ic_stat.png" "icons" "logos" "images" "." 2>/dev/null || true)"
if [[ -n "$launcher" ]]; then
echo "[branding] android launcher icons from $(basename "$launcher")"
hesabix_branding_overlay_android_named "$app_dir" "$launcher" "ic_launcher.png" "$backup_root" "fit"
hesabix_branding_overlay_android_named "$app_dir" "$launcher" "ic_launcher_foreground.png" "$backup_root" "fit"
fi
local large_dark="${logo_blue:-$logo_light}"
local large_light="${logo_light:-$logo_blue}"
if [[ -n "$large_dark" ]]; then
hesabix_branding_overlay_android_named "$app_dir" "$large_dark" "ic_hesabix_logo.png" "$backup_root" "fit"
fi
if [[ -n "$large_light" ]]; then
hesabix_branding_overlay_android_named "$app_dir" "$large_light" "ic_hesabix_logo_light.png" "$backup_root" "fit"
fi
# Native splash (launch_background.xml) uses ic_hesabix_logo / ic_hesabix_logo_light.
local stat_src="${stat:-$logo_light}"
[[ -n "$stat_src" ]] || stat_src="$logo_blue"
if [[ -n "$stat_src" ]]; then
local stat_mode="silhouette"
[[ -n "$stat" ]] && stat_mode="fit"
echo "[branding] android status-bar icon from $(basename "$stat_src") ($stat_mode)"
hesabix_branding_overlay_android_named "$app_dir" "$stat_src" "ic_stat_hesabix.png" "$backup_root" "$stat_mode"
fi
}
# Apply overlays to Flutter project sources (must restore after build).
hesabix_branding_apply() {
local app_dir="$1"
hesabix_branding_resolve
if [[ "$_HESABIX_BRANDING_EFFECTIVE_MODE" != "custom" ]]; then
echo "[branding] mode=default — keeping Hesabix logos and names"
_HESABIX_BRANDING_APPLIED=0
return 0
fi
local dir="${BRANDING_DIR}"
local backup
backup="$(mktemp -d /tmp/hesabix-branding.XXXXXX)"
_HESABIX_BRANDING_BACKUP_DIR="$backup"
echo "[branding] mode=custom dir=$dir"
[[ -n "${HESABIX_BRANDING_NAME_FA}" ]] && echo "[branding] name FA: ${HESABIX_BRANDING_NAME_FA}"
[[ -n "${HESABIX_BRANDING_NAME_EN}" ]] && echo "[branding] name EN: ${HESABIX_BRANDING_NAME_EN}"
local src
# Logos → Flutter assets + web splash images
if src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo-light.png" "$backup" "$app_dir"
hesabix_branding_install_file "$src" "$app_dir/web/images/logo-light.png" "$backup" "$app_dir"
# Also used as web favicon.png fallback in some installs
if [[ -f "$app_dir/web/favicon.png" ]] && \
! hesabix_branding_find_file "$dir" "favicon.png" "icons" "." >/dev/null 2>&1; then
: # leave favicon.png unless pack provides one; don't force logo as favicon
fi
fi
if src="$(hesabix_branding_find_file "$dir" "logo-blue.png" "logos" "images" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo-blue.png" "$backup" "$app_dir"
hesabix_branding_install_file "$src" "$app_dir/web/images/logo-blue.png" "$backup" "$app_dir"
fi
if src="$(hesabix_branding_find_file "$dir" "logo-light.svg" "logos" "images" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo-light.svg" "$backup" "$app_dir"
if [[ -f "$app_dir/web/assets/images/logo-light.svg" ]]; then
hesabix_branding_install_file "$src" "$app_dir/web/assets/images/logo-light.svg" "$backup" "$app_dir"
fi
fi
if src="$(hesabix_branding_find_file "$dir" "logo32.png" "logos" "images" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo32.png" "$backup" "$app_dir"
elif src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
# لانچر موبایل از logo32 استفاده می‌کند
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo32.png" "$backup" "$app_dir"
fi
if src="$(hesabix_branding_find_file "$dir" "logo.png" "logos" "images" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo.png" "$backup" "$app_dir"
elif src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/assets/images/logo.png" "$backup" "$app_dir"
fi
# PWA icons
local icon_name pack_has_192=0 pack_has_512=0 pack_has_m192=0 pack_has_m512=0
for icon_name in Icon-192.png Icon-512.png Icon-maskable-192.png Icon-maskable-512.png; do
if src="$(hesabix_branding_find_file "$dir" "$icon_name" "icons" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/web/icons/$icon_name" "$backup" "$app_dir"
case "$icon_name" in
Icon-192.png) pack_has_192=1 ;;
Icon-512.png) pack_has_512=1 ;;
Icon-maskable-192.png) pack_has_m192=1 ;;
Icon-maskable-512.png) pack_has_m512=1 ;;
esac
fi
done
# Fill missing maskable from base icons when pack only shipped Icon-192/512
if [[ "$pack_has_192" -eq 1 && "$pack_has_m192" -eq 0 ]]; then
if src="$(hesabix_branding_find_file "$dir" "Icon-192.png" "icons" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/web/icons/Icon-maskable-192.png" "$backup" "$app_dir"
fi
fi
if [[ "$pack_has_512" -eq 1 && "$pack_has_m512" -eq 0 ]]; then
if src="$(hesabix_branding_find_file "$dir" "Icon-512.png" "icons" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/web/icons/Icon-maskable-512.png" "$backup" "$app_dir"
fi
fi
if src="$(hesabix_branding_find_file "$dir" "favicon.ico" "icons" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/web/favicon.ico" "$backup" "$app_dir"
fi
if src="$(hesabix_branding_find_file "$dir" "favicon.png" "icons" ".")"; then
hesabix_branding_install_file "$src" "$app_dir/web/favicon.png" "$backup" "$app_dir"
fi
# Backup then patch name-bearing web sources
for f in web/index.html web/manifest.json web/hesabix_web_loader.js; do
[[ -f "$app_dir/$f" ]] && hesabix_branding_backup_file "$app_dir/$f" "$backup" "$app_dir"
done
hesabix_branding_patch_web_names "$app_dir" "${HESABIX_BRANDING_NAME_FA}" "${HESABIX_BRANDING_NAME_EN}"
# Android launcher name + native icons (only existing drawables; restore copies them back)
for f in android/app/src/main/res/values/strings.xml android/app/src/main/res/values-fa/strings.xml; do
[[ -f "$app_dir/$f" ]] && hesabix_branding_backup_file "$app_dir/$f" "$backup" "$app_dir"
done
hesabix_branding_patch_android_names "$app_dir" "${HESABIX_BRANDING_NAME_FA}" "${HESABIX_BRANDING_NAME_EN}"
hesabix_branding_apply_android_icons "$app_dir" "$dir" "$backup"
_HESABIX_BRANDING_APPLIED=1
}
hesabix_branding_restore() {
if [[ "$_HESABIX_BRANDING_APPLIED" != "1" ]]; then
return 0
fi
local backup="${_HESABIX_BRANDING_BACKUP_DIR:-}"
local app_dir="${1:-}"
if [[ -z "$backup" || ! -d "$backup" ]]; then
_HESABIX_BRANDING_APPLIED=0
return 0
fi
if [[ -z "$app_dir" ]]; then
echo "[branding] warn: restore skipped (no app_dir)" >&2
rm -rf "$backup" 2>/dev/null || true
_HESABIX_BRANDING_APPLIED=0
_HESABIX_BRANDING_BACKUP_DIR=""
return 0
fi
echo "[branding] restoring original project assets from backup..."
local rel
while IFS= read -r -d '' rel; do
rel="${rel#./}"
mkdir -p "$(dirname "$app_dir/$rel")"
cp -a "$backup/$rel" "$app_dir/$rel"
done < <(cd "$backup" && find . -type f -print0)
rm -rf "$backup" 2>/dev/null || true
_HESABIX_BRANDING_BACKUP_DIR=""
_HESABIX_BRANDING_APPLIED=0
echo "[branding] restore complete"
}
# Copy branding files into the Flutter web output so cached assets cannot
# keep the default Hesabix logos after a custom build.
hesabix_branding_stamp_build_output() {
local build_dir="$1"
[[ -n "$build_dir" && -d "$build_dir" ]] || return 0
hesabix_branding_resolve
if [[ "$_HESABIX_BRANDING_EFFECTIVE_MODE" != "custom" ]]; then
return 0
fi
local dir="${BRANDING_DIR}"
[[ -d "$dir" ]] || return 0
echo "[branding] stamping custom logos/icons into $build_dir"
local src dest
_hesabix_branding_stamp_one() {
local from="$1"
local to="$2"
[[ -f "$from" ]] || return 0
mkdir -p "$(dirname "$to")"
cp -f "$from" "$to"
touch "$to" 2>/dev/null || true
echo "[branding] stamped: ${to#"$build_dir"/}"
}
if src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/images/logo-light.png"
_hesabix_branding_stamp_one "$src" "$build_dir/assets/assets/images/logo-light.png"
_hesabix_branding_stamp_one "$src" "$build_dir/assets/images/logo-light.png"
fi
if src="$(hesabix_branding_find_file "$dir" "logo-blue.png" "logos" "images" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/images/logo-blue.png"
_hesabix_branding_stamp_one "$src" "$build_dir/assets/assets/images/logo-blue.png"
_hesabix_branding_stamp_one "$src" "$build_dir/assets/images/logo-blue.png"
elif src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/images/logo-blue.png"
_hesabix_branding_stamp_one "$src" "$build_dir/assets/assets/images/logo-blue.png"
fi
if src="$(hesabix_branding_find_file "$dir" "logo.png" "logos" "images" ".")" \
|| src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/assets/assets/images/logo.png"
fi
if src="$(hesabix_branding_find_file "$dir" "logo32.png" "logos" "images" ".")" \
|| src="$(hesabix_branding_find_file "$dir" "logo-light.png" "logos" "images" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/assets/assets/images/logo32.png"
fi
local icon_name
for icon_name in Icon-192.png Icon-512.png Icon-maskable-192.png Icon-maskable-512.png; do
if src="$(hesabix_branding_find_file "$dir" "$icon_name" "icons" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/icons/$icon_name"
fi
done
if src="$(hesabix_branding_find_file "$dir" "favicon.ico" "icons" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/favicon.ico"
fi
if src="$(hesabix_branding_find_file "$dir" "favicon.png" "icons" ".")"; then
_hesabix_branding_stamp_one "$src" "$build_dir/favicon.png"
fi
}
# Append branding name dart-defines onto global array DART_DEFINE_ARGS.
hesabix_branding_append_dart_defines() {
# Accept optional array name for compatibility; always mutate DART_DEFINE_ARGS.
hesabix_branding_resolve
if [[ "$_HESABIX_BRANDING_EFFECTIVE_MODE" != "custom" ]]; then
return 0
fi
if [[ -n "${HESABIX_BRANDING_NAME_EN}" ]]; then
DART_DEFINE_ARGS+=(--dart-define "APP_NAME_EN=${HESABIX_BRANDING_NAME_EN}")
fi
if [[ -n "${HESABIX_BRANDING_NAME_FA}" ]]; then
DART_DEFINE_ARGS+=(--dart-define "APP_NAME_FA=${HESABIX_BRANDING_NAME_FA}")
fi
# لوگوی رنگی کاستوم را با ColorFilter تم حسابیکس بازنویسی نکن
DART_DEFINE_ARGS+=(--dart-define "BRAND_LOGO_TINT=0")
}
hesabix_branding_persist() {
local mode="${1:-default}"
local dir="${2:-}"
local fa="${3:-}"
local en="${4:-}"
mode="$(hesabix_branding_normalize_mode "$mode")"
local env_file="${HESABIX_DEPLOY_ENV:-${APP_ROOT:-/opt/hesabix}/.deploy_env}"
if [[ ! -f "$env_file" ]]; then
echo "Deploy env not found: $env_file" >&2
return 1
fi
if ! declare -F hesabix_patch_env_file >/dev/null 2>&1; then
echo "hesabix_patch_env_file unavailable" >&2
return 1
fi
if [[ "$mode" == "default" ]]; then
hesabix_patch_env_file "$env_file" \
"BRANDING_MODE=default" \
"BRANDING_DIR=" \
"APP_NAME_FA=" \
"APP_NAME_EN="
else
[[ -n "$dir" ]] || dir="$(hesabix_branding_default_dir)"
hesabix_patch_env_file "$env_file" \
"BRANDING_MODE=custom" \
"BRANDING_DIR=${dir}" \
"APP_NAME_FA=${fa}" \
"APP_NAME_EN=${en}"
fi
echo "[branding] saved to $env_file (mode=$mode)"
}
hesabix_branding_show() {
hesabix_load_deploy_env 2>/dev/null || true
hesabix_branding_resolve
echo "UI branding:"
echo " mode: ${_HESABIX_BRANDING_EFFECTIVE_MODE}"
echo " dir: ${BRANDING_DIR:-$(hesabix_branding_default_dir)}"
echo " name FA: ${HESABIX_BRANDING_NAME_FA:-«default حسابیکس»}"
echo " name EN: ${HESABIX_BRANDING_NAME_EN:-«default Hesabix»}"
if [[ "$_HESABIX_BRANDING_EFFECTIVE_MODE" == "custom" && -d "${BRANDING_DIR:-}" ]]; then
echo " pack files:"
local f
for f in logo-light.png logo-blue.png Icon-192.png Icon-512.png favicon.ico names.json ic_stat.png; do
if hesabix_branding_find_file "${BRANDING_DIR}" "$f" "logos" "images" "icons" "meta" "." >/dev/null 2>&1; then
echo " ✓ $f"
else
echo " · $f (missing)"
fi
done
fi
}
hesabix_branding_command() {
local action="${1:-show}"
shift || true
local dir="" fa="" en="" rebuild=0
case "$action" in
show|status)
hesabix_branding_show
return 0
;;
default|reset)
while [[ $# -gt 0 ]]; do
case "$1" in
--rebuild) rebuild=1; shift ;;
*) echo "Unknown arg: $1" >&2; return 1 ;;
esac
done
hesabix_branding_persist "default"
if [[ "$rebuild" -eq 1 ]]; then
# shellcheck source=hesabix_domains.sh
source "${HESABIX_BRANDING_LIB_DIR}/hesabix_domains.sh"
hesabix_rebuild_frontend
else
echo "Saved. Next web build / hesabix -update will use Hesabix defaults."
echo "Android APK uses the same .deploy_env on the next ./build_android.sh."
echo "Rebuild web now: sudo hesabix -branding apply"
fi
return 0
;;
set)
while [[ $# -gt 0 ]]; do
case "$1" in
--dir)
[[ $# -ge 2 ]] || { echo "--dir needs a path" >&2; return 1; }
dir="$2"; shift 2 ;;
--name-fa|--fa)
[[ $# -ge 2 ]] || { echo "--name-fa needs a value" >&2; return 1; }
fa="$2"; shift 2 ;;
--name-en|--en)
[[ $# -ge 2 ]] || { echo "--name-en needs a value" >&2; return 1; }
en="$2"; shift 2 ;;
--rebuild) rebuild=1; shift ;;
*) echo "Unknown arg: $1" >&2; return 1 ;;
esac
done
[[ -n "$dir" ]] || dir="$(hesabix_branding_default_dir)"
if [[ ! -d "$dir" ]]; then
echo "Branding directory does not exist: $dir" >&2
echo "Create it and add logo-light.png / icons / names.json first." >&2
return 1
fi
# Prefer names.json when flags omitted
if [[ -z "$fa" || -z "$en" ]]; then
BRANDING_MODE_OVERRIDE=custom
BRANDING_DIR_OVERRIDE="$dir"
APP_NAME_FA_OVERRIDE="$fa"
APP_NAME_EN_OVERRIDE="$en"
hesabix_branding_resolve
fa="${HESABIX_BRANDING_NAME_FA}"
en="${HESABIX_BRANDING_NAME_EN}"
unset BRANDING_MODE_OVERRIDE BRANDING_DIR_OVERRIDE APP_NAME_FA_OVERRIDE APP_NAME_EN_OVERRIDE
fi
hesabix_branding_persist "custom" "$dir" "$fa" "$en"
if [[ "$rebuild" -eq 1 ]]; then
source "${HESABIX_BRANDING_LIB_DIR}/hesabix_domains.sh"
hesabix_rebuild_frontend
else
echo "Saved. Apply web on next build: sudo hesabix -branding apply"
echo "Or: sudo hesabix -update"
echo "Android APK: ./build_android.sh (reads the same .deploy_env branding)"
fi
return 0
;;
apply|rebuild)
hesabix_load_deploy_env || return 1
# shellcheck source=hesabix_domains.sh
source "${HESABIX_BRANDING_LIB_DIR}/hesabix_domains.sh"
hesabix_rebuild_frontend
return $?
;;
*)
echo "Usage:" >&2
echo " hesabix -branding show" >&2
echo " hesabix -branding set --dir /opt/hesabix/branding [--name-fa …] [--name-en …] [--rebuild]" >&2
echo " hesabix -branding default [--rebuild]" >&2
echo " hesabix -branding apply # rebuild Flutter web only" >&2
echo " Android APK uses the same pack: ./build_android.sh" >&2
return 1
;;
esac
}