forked from hesabix/arc
550 lines
20 KiB
HTML
Executable file
550 lines
20 KiB
HTML
Executable file
<!DOCTYPE html>
|
|
<html lang="fa" dir="rtl">
|
|
<head>
|
|
<!--
|
|
If you are serving your web app in a path other than the root, change the
|
|
href value below to reflect the base path you are serving from.
|
|
|
|
The path provided below has to start and end with a slash "/" in order for
|
|
it to work correctly.
|
|
|
|
For more details:
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
|
|
This is a placeholder for base href that will be replaced by the value of
|
|
the `--base-href` argument provided to `flutter build`.
|
|
-->
|
|
<base href="$FLUTTER_BASE_HREF">
|
|
|
|
<meta charset="UTF-8">
|
|
<script>
|
|
/* بلافاصله lang/dir را از تنظیمات ذخیرهشده اعمال کن (پیشفرض: فارسی RTL) */
|
|
(function () {
|
|
function parseStored(raw) {
|
|
if (raw == null || raw === '') return null;
|
|
try { return JSON.parse(raw); } catch (e) { return raw; }
|
|
}
|
|
function langFromStorage() {
|
|
var keys = ['hesabix_locale', 'flutter.app_locale_code'];
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var raw = null;
|
|
try { raw = localStorage.getItem(keys[i]); } catch (e) {}
|
|
if (!raw) continue;
|
|
var v = parseStored(raw);
|
|
var s = String(v != null ? v : raw).trim();
|
|
var code = s.split('-')[0].toLowerCase().replace(/^["']|["']$/g, '');
|
|
if (code === 'en') return 'en';
|
|
if (code === 'fa') return 'fa';
|
|
}
|
|
return 'fa';
|
|
}
|
|
var loc = langFromStorage();
|
|
var html = document.documentElement;
|
|
html.setAttribute('lang', loc === 'en' ? 'en' : 'fa');
|
|
html.setAttribute('dir', loc === 'en' ? 'ltr' : 'rtl');
|
|
})();
|
|
</script>
|
|
<!-- Purge legacy Flutter service workers once per deploy (old SW + cache can mismatch CanvasKit). -->
|
|
<script>
|
|
(function () {
|
|
if (!('serviceWorker' in navigator)) return;
|
|
var purgeKey = 'hesabix_sw_purged_for';
|
|
fetch('version.json?t=' + Date.now(), { cache: 'no-store', credentials: 'same-origin' })
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (j) {
|
|
var build = j && typeof j.build === 'string' ? j.build : '';
|
|
if (build && localStorage.getItem(purgeKey) === build) return;
|
|
return navigator.serviceWorker.getRegistrations().then(function (regs) {
|
|
var hadSw = regs.length > 0;
|
|
var tasks = regs.map(function (reg) { return reg.unregister(); });
|
|
return Promise.all(tasks).then(function () {
|
|
if ('caches' in window) {
|
|
return caches.keys().then(function (keys) {
|
|
return Promise.all(keys.map(function (k) { return caches.delete(k); }));
|
|
});
|
|
}
|
|
}).then(function () {
|
|
if (build) {
|
|
try { localStorage.setItem(purgeKey, build); } catch (e) {}
|
|
}
|
|
if (hadSw) window.location.reload();
|
|
});
|
|
});
|
|
})
|
|
.catch(function () {});
|
|
})();
|
|
</script>
|
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
<meta name="description" content="Hesabix - سیستم مدیریت مالی">
|
|
<!-- viewport-fit=cover برای مدیریت بهتر فضای پایین صفحه روی موبایل (خصوصاً iOS) -->
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
|
|
<!-- iOS meta tags & icons -->
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="Hesabix">
|
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
|
|
|
<title>Hesabix</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<meta name="theme-color" content="#0F4C81" id="hesabix-theme-color">
|
|
<script>
|
|
/* اعمال فوری رنگ برند ذخیرهشده (قبل از لودر) برای theme-color و CSS vars */
|
|
(function () {
|
|
function parseStored(raw) {
|
|
if (raw == null || raw === '') return null;
|
|
try { return JSON.parse(raw); } catch (e) { return raw; }
|
|
}
|
|
function readBrand() {
|
|
try {
|
|
var raw = localStorage.getItem('hesabix_theme_brand');
|
|
var v = parseStored(raw);
|
|
var s = String(v != null ? v : (raw || '')).trim();
|
|
if (/^#[0-9A-Fa-f]{6}$/.test(s)) return s.toUpperCase();
|
|
} catch (e) {}
|
|
return null;
|
|
}
|
|
var brand = readBrand();
|
|
if (!brand) return;
|
|
var meta = document.getElementById('hesabix-theme-color');
|
|
if (meta) meta.setAttribute('content', brand);
|
|
var root = document.documentElement;
|
|
root.style.setProperty('--brand', brand);
|
|
try {
|
|
var r = parseInt(brand.slice(1, 3), 16);
|
|
var g = parseInt(brand.slice(3, 5), 16);
|
|
var b = parseInt(brand.slice(5, 7), 16);
|
|
root.style.setProperty('--brand-soft', 'rgba(' + r + ',' + g + ',' + b + ',0.14)');
|
|
root.style.setProperty('--loader-fill', brand);
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
|
|
<link rel="preload" href="fonts/YekanBakhFaNum-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="fonts/YekanBakhFaNum-SemiBold.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="hesabix_web_loader.css" as="style">
|
|
<link rel="preload" href="hesabix_web_loader.js" as="script">
|
|
<link rel="preconnect" href="https://hsxn.hesabix.ir" crossorigin>
|
|
<link rel="dns-prefetch" href="https://hsxn.hesabix.ir">
|
|
<link rel="preload" href="flutter_bootstrap.js" as="script">
|
|
<link rel="preload" href="flutter_web_preload.js" as="script">
|
|
<link rel="preload" href="images/logo-light.png" as="image" type="image/png">
|
|
|
|
<link rel="stylesheet" href="hesabix_web_loader.css">
|
|
|
|
<!-- Suppress Intl.v8BreakIterator deprecation warning -->
|
|
<script>
|
|
// Override console.warn to filter out Intl.v8BreakIterator deprecation warnings
|
|
const originalWarn = console.warn;
|
|
console.warn = function(...args) {
|
|
const message = args.join(' ');
|
|
if (message.includes('Intl.v8BreakIterator is deprecated') ||
|
|
message.includes('Please use Intl.Segmenter instead')) {
|
|
return; // Suppress this specific warning
|
|
}
|
|
originalWarn.apply(console, args);
|
|
};
|
|
</script>
|
|
|
|
<!-- pdf.js برای پیشنمایش چاپ (بستهٔ printing) در وب؛ بدون آن canRaster غیرفعال است -->
|
|
<script>
|
|
var dartPdfJsVersion = "3.2.146";
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<script src="hesabix_web_loader.js"></script>
|
|
|
|
<div id="flutter-loading-screen" role="status" aria-live="polite" aria-busy="true">
|
|
<div class="loader-center">
|
|
<div class="loader-logo-wrap">
|
|
<img
|
|
class="loader-logo"
|
|
src="images/logo-light.png"
|
|
width="44"
|
|
height="44"
|
|
alt="Hesabix"
|
|
decoding="async"
|
|
fetchpriority="high" />
|
|
</div>
|
|
<h1 class="loader-title" id="loader-title">حسابیکس</h1>
|
|
<p class="loader-tagline" id="loader-tagline">حسابداری ابری هوشمند برای کسبوکار شما</p>
|
|
|
|
<div class="loader-progress-block">
|
|
<div class="loader-progress-meta">
|
|
<span class="loader-step" id="loading-step"></span>
|
|
<span class="loader-percent" id="loading-percent"></span>
|
|
</div>
|
|
<div class="loader-progress-track is-indeterminate" id="loading-progress-wrap">
|
|
<div class="loader-progress-bar" id="loading-progress-bar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="loader-status" id="loading-status">در حال بارگذاری…</p>
|
|
|
|
<div class="loader-spinner" id="loader-spinner" hidden aria-hidden="true"></div>
|
|
|
|
<p class="loader-quote" id="loading-quote" aria-hidden="true">
|
|
<span id="loading-quote-text"></span>
|
|
</p>
|
|
|
|
<div id="loader-retry-wrap" hidden>
|
|
<button type="button" id="loader-retry-btn" onclick="window.location.reload()">تلاش مجدد</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// همگامسازی ارتفاع صفحه با visual viewport برای جلوگیری از فضای سفید زیرین هنگام نمایش کیبورد
|
|
(function() {
|
|
function updateAppHeight() {
|
|
try {
|
|
var height;
|
|
if (window.visualViewport && window.visualViewport.height) {
|
|
height = window.visualViewport.height;
|
|
} else {
|
|
height = window.innerHeight || document.documentElement.clientHeight;
|
|
}
|
|
document.documentElement.style.setProperty('--app-height', height + 'px');
|
|
} catch (e) {
|
|
// در صورت بروز خطا، تغییری اعمال نکنیم تا رفتار پیشفرض حفظ شود
|
|
console.warn('[Hesabix] Failed to update app height', e);
|
|
}
|
|
}
|
|
|
|
// اجرای اولیه
|
|
window.addEventListener('load', updateAppHeight);
|
|
// تغییر اندازه (چرخش، باز و بسته شدن کیبورد و غیره)
|
|
window.addEventListener('resize', updateAppHeight);
|
|
if (window.visualViewport) {
|
|
window.visualViewport.addEventListener('resize', updateAppHeight);
|
|
window.visualViewport.addEventListener('scroll', updateAppHeight);
|
|
}
|
|
})();
|
|
|
|
// Loader hide: توسط Flutter با __hesabixSignalAppReady() — نه flutter-first-frame
|
|
|
|
// پیکربندی Flutter — intercept loader قبل از اجرای flutter_bootstrap
|
|
if (!window._flutter) {
|
|
window._flutter = {};
|
|
}
|
|
|
|
// ذخیره تابع اصلی load برای override کردن بعداً
|
|
window._flutterConfig = {
|
|
canvasKitBaseUrl: "canvaskit/",
|
|
renderer: "canvaskit",
|
|
useLocalCanvasKit: true,
|
|
// فونتهای fallback موتور (و Roboto اضطراری) از همین مبدأ؛ نه fonts.gstatic.com
|
|
fontFallbackBaseUrl: (function () {
|
|
try {
|
|
return new URL("fonts/gstatic/s/", document.baseURI).href;
|
|
} catch (e) {
|
|
return "fonts/gstatic/s/";
|
|
}
|
|
})()
|
|
};
|
|
|
|
// Intercept loader.load() برای اضافه کردن configuration
|
|
Object.defineProperty(window._flutter, 'loader', {
|
|
set: function(loader) {
|
|
this._loaderInstance = loader;
|
|
|
|
// Override load method
|
|
if (loader && typeof loader.load === 'function') {
|
|
const originalLoad = loader.load;
|
|
loader.load = function(options) {
|
|
options = options || {};
|
|
options.config = options.config || {};
|
|
|
|
// اعمال configuration محلی
|
|
Object.assign(options.config, window._flutterConfig);
|
|
|
|
console.log('[Flutter] Local CanvasKit + local fontFallbackBaseUrl (no fonts.gstatic.com)');
|
|
return originalLoad.call(loader, options);
|
|
};
|
|
}
|
|
},
|
|
get: function() {
|
|
return this._loaderInstance;
|
|
},
|
|
configurable: true
|
|
});
|
|
</script>
|
|
|
|
<!-- بهروزرسانی: نسخهٔ سرور (version.json) + Service Worker (آفلاین حفظ میشود) -->
|
|
<script>
|
|
(function() {
|
|
var STORAGE_BUILD = 'hesabix_web_build';
|
|
var SESSION_PENDING = 'hesabix_pending_build';
|
|
var SESSION_NO_SW_RELOAD = 'hesabix_no_sw_build_reload';
|
|
var UPDATE_BANNER_ID = 'flutter-update-banner';
|
|
var hasReloadedForUpdate = false;
|
|
var VERSION_POLL_MS = 5 * 60 * 1000;
|
|
|
|
function storageGet(key) {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function storageSet(key, val) {
|
|
try {
|
|
localStorage.setItem(key, val);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function sessionGet(key) {
|
|
try {
|
|
return sessionStorage.getItem(key);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function sessionSet(key, val) {
|
|
try {
|
|
sessionStorage.setItem(key, val);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function sessionRemove(key) {
|
|
try {
|
|
sessionStorage.removeItem(key);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function commitPendingBuildBeforeReload() {
|
|
var pb = sessionGet(SESSION_PENDING);
|
|
if (pb) {
|
|
storageSet(STORAGE_BUILD, pb);
|
|
sessionRemove(SESSION_PENDING);
|
|
}
|
|
}
|
|
|
|
function createUpdateBanner() {
|
|
if (document.getElementById(UPDATE_BANNER_ID)) {
|
|
return;
|
|
}
|
|
|
|
var defaultFont = (window.getComputedStyle && window.getComputedStyle(document.body).fontFamily) || "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
|
|
|
var banner = document.createElement('div');
|
|
banner.id = UPDATE_BANNER_ID;
|
|
banner.style.position = 'fixed';
|
|
banner.style.bottom = '16px';
|
|
banner.style.left = '50%';
|
|
banner.style.transform = 'translateX(-50%)';
|
|
banner.style.background = '#0175C2';
|
|
banner.style.color = '#ffffff';
|
|
banner.style.padding = '12px 16px';
|
|
banner.style.borderRadius = '14px';
|
|
banner.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.25)';
|
|
banner.style.zIndex = '10000';
|
|
banner.style.display = 'flex';
|
|
banner.style.alignItems = 'center';
|
|
banner.style.gap = '12px';
|
|
banner.style.fontFamily = defaultFont;
|
|
banner.style.fontSize = '14px';
|
|
banner.style.direction = 'rtl';
|
|
banner.style.width = 'calc(100% - 24px)';
|
|
banner.style.maxWidth = '520px';
|
|
banner.style.flexWrap = 'wrap';
|
|
banner.style.justifyContent = 'space-between';
|
|
|
|
var text = document.createElement('span');
|
|
text.textContent = 'نسخه جدید حسابیکس در حال اعمال است، صفحه به زودی بهروزرسانی میشود.';
|
|
text.style.flex = '1';
|
|
text.style.fontFamily = 'inherit';
|
|
|
|
var closeBtn = document.createElement('button');
|
|
closeBtn.textContent = 'بستن';
|
|
closeBtn.style.background = 'transparent';
|
|
closeBtn.style.color = '#ffffff';
|
|
closeBtn.style.border = '1px solid rgba(255, 255, 255, 0.6)';
|
|
closeBtn.style.borderRadius = '10px';
|
|
closeBtn.style.padding = '6px 12px';
|
|
closeBtn.style.cursor = 'pointer';
|
|
closeBtn.style.fontWeight = '600';
|
|
closeBtn.style.fontSize = '12px';
|
|
closeBtn.style.opacity = '0.9';
|
|
closeBtn.style.fontFamily = 'inherit';
|
|
|
|
closeBtn.addEventListener('click', function() {
|
|
try {
|
|
document.body.removeChild(banner);
|
|
} catch (e) {}
|
|
});
|
|
|
|
banner.appendChild(text);
|
|
banner.appendChild(closeBtn);
|
|
document.body.appendChild(banner);
|
|
}
|
|
|
|
function fetchServerBuild() {
|
|
var url = 'version.json?t=' + Date.now();
|
|
return fetch(url, { cache: 'no-store', credentials: 'same-origin' })
|
|
.then(function(r) {
|
|
if (!r.ok) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(j) {
|
|
return j && typeof j.build === 'string' ? j.build : null;
|
|
})
|
|
.catch(function() {
|
|
return null;
|
|
});
|
|
}
|
|
|
|
function tryActivateWaitingWorker() {
|
|
return navigator.serviceWorker.getRegistration().then(function(reg) {
|
|
if (reg && reg.waiting) {
|
|
reg.waiting.postMessage('skipWaiting');
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/** اگر waiting باشد skipWaiting؛ در غیر این صورت فقط در مسیر قدیمی SW به reload برگردیم تا از حلقه جلوگیری شود */
|
|
function requestAppUpdate(allowReloadFallback) {
|
|
navigator.serviceWorker.getRegistration().then(function(reg) {
|
|
if (reg && reg.waiting) {
|
|
reg.waiting.postMessage('skipWaiting');
|
|
} else if (allowReloadFallback) {
|
|
window.location.reload();
|
|
}
|
|
}).catch(function() {
|
|
if (allowReloadFallback) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
}
|
|
|
|
function onServerNewerBuild(reg, serverBuild) {
|
|
sessionSet(SESSION_PENDING, serverBuild);
|
|
createUpdateBanner();
|
|
|
|
if (reg) {
|
|
reg.update()
|
|
.then(function() {
|
|
return tryActivateWaitingWorker();
|
|
})
|
|
.then(function(activated) {
|
|
if (activated) return;
|
|
return new Promise(function(resolve) {
|
|
setTimeout(resolve, 400);
|
|
}).then(function() {
|
|
return tryActivateWaitingWorker();
|
|
});
|
|
})
|
|
.then(function(activated) {
|
|
if (activated) return;
|
|
return new Promise(function(resolve) {
|
|
setTimeout(resolve, 1600);
|
|
}).then(function() {
|
|
return tryActivateWaitingWorker();
|
|
});
|
|
})
|
|
.catch(function() {});
|
|
} else {
|
|
if (!sessionGet(SESSION_NO_SW_RELOAD)) {
|
|
sessionSet(SESSION_NO_SW_RELOAD, '1');
|
|
storageSet(STORAGE_BUILD, serverBuild);
|
|
window.location.reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
function runVersionCheck(reg) {
|
|
fetchServerBuild().then(function(serverBuild) {
|
|
if (!serverBuild) return;
|
|
|
|
var stored = storageGet(STORAGE_BUILD);
|
|
if (stored === null || stored === '') {
|
|
storageSet(STORAGE_BUILD, serverBuild);
|
|
return;
|
|
}
|
|
if (stored === serverBuild) {
|
|
sessionRemove(SESSION_NO_SW_RELOAD);
|
|
return;
|
|
}
|
|
|
|
onServerNewerBuild(reg, serverBuild);
|
|
});
|
|
}
|
|
|
|
function startVersionPolling(reg) {
|
|
runVersionCheck(reg);
|
|
setInterval(function() {
|
|
if (document.visibilityState === 'visible') {
|
|
runVersionCheck(reg);
|
|
}
|
|
}, VERSION_POLL_MS);
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.visibilityState === 'visible') {
|
|
runVersionCheck(reg);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupServiceWorkerUpdateFlow() {
|
|
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
|
if (hasReloadedForUpdate) return;
|
|
hasReloadedForUpdate = true;
|
|
commitPendingBuildBeforeReload();
|
|
window.location.reload();
|
|
});
|
|
|
|
navigator.serviceWorker.ready
|
|
.then(function(reg) {
|
|
if (!reg) return;
|
|
|
|
startVersionPolling(reg);
|
|
|
|
function handleWaiting(worker) {
|
|
if (!worker) return;
|
|
if (navigator.serviceWorker.controller) {
|
|
createUpdateBanner();
|
|
requestAppUpdate(true);
|
|
}
|
|
}
|
|
|
|
if (reg.waiting) {
|
|
handleWaiting(reg.waiting);
|
|
}
|
|
|
|
reg.addEventListener('updatefound', function() {
|
|
var newWorker = reg.installing;
|
|
if (!newWorker) return;
|
|
|
|
newWorker.addEventListener('statechange', function() {
|
|
if (newWorker.state === 'installed') {
|
|
handleWaiting(reg.waiting || newWorker);
|
|
}
|
|
});
|
|
});
|
|
})
|
|
.catch(function() {});
|
|
}
|
|
|
|
window.addEventListener('load', function() {
|
|
if ('serviceWorker' in navigator) {
|
|
setupServiceWorkerUpdateFlow();
|
|
} else {
|
|
startVersionPolling(null);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<script src="hesabix_voice_capture.js"></script>
|
|
<script src="flutter_web_preload.js" defer></script>
|
|
<script src="flutter_bootstrap.js" defer></script>
|
|
</body>
|
|
</html>
|