| |
| |
| |
| |
|
|
|
|
| (global => {
|
| 'use strict';
|
|
|
| |
| |
|
|
| const MODULE_DEFINITIONS = Object.freeze([
|
| Object.freeze({
|
| key: 'rotation_library',
|
| title: 'مكتبة المنشورات وتدوير المحتوى',
|
| icon: 'fa-arrows-spin',
|
| color: '#0d9488',
|
| desc: 'تصدير واستيراد كل منشوراتك المؤرشفة، الكلمات المفتاحية، الصور المرفقة، وحالة التدوير.'
|
| }),
|
| Object.freeze({
|
| key: 'rotation_groups',
|
| title: 'حزم ومجموعات المنشورات المخصصة',
|
| icon: 'fa-layer-group',
|
| color: '#8b5cf6',
|
| desc: 'تصدير واستيراد الحزم والمجموعات المخصصة بالمنشورات المحددة للتدوير.'
|
| }),
|
| Object.freeze({
|
| key: 'rotation_followups',
|
| title: 'رسائل المتابعة التلقائية للواتساب',
|
| icon: 'fa-brands fa-whatsapp',
|
| color: '#00a884',
|
| desc: 'تصدير واستيراد رسائل ومواعيد وتكرار المتابعة المرتبطة بالعملاء والمنشورات.'
|
| }),
|
| Object.freeze({
|
| key: 'campaign_history',
|
| title: 'أرشيف سجلات الحملات والنتائج',
|
| icon: 'fa-box-archive',
|
| color: '#3b82f6',
|
| desc: 'تصدير واستيراد السجل الكامل لنتائج النشر والبث وتفاصيل الوجهات.'
|
| }),
|
| Object.freeze({
|
| key: 'scheduled_campaigns',
|
| title: 'طابور الحملات المجدولة المستقبلية',
|
| icon: 'fa-calendar-days',
|
| color: '#f59e0b',
|
| desc: 'تصدير واستيراد طابور الحملات المجدولة ومواعيدها ومرفقاتها.'
|
| }),
|
| Object.freeze({
|
| key: 'facebook_registry',
|
| title: 'مجموعات فيسبوك، رادار السلامة، والتصنيفات',
|
| icon: 'fa-users-rectangle',
|
| color: '#1877f2',
|
| desc: 'تصدير واستيراد قائمة جروبات الفيسبوك، الشرائح، الحجر الصحي، والتصنيفات.'
|
| }),
|
| Object.freeze({
|
| key: 'whatsapp_queue',
|
| title: 'طابور مستلمي وأرقام واتساب',
|
| icon: 'fa-address-book',
|
| color: '#10b981',
|
| desc: 'تصدير واستيراد قائمة أرقام ومستلمي الواتساب في طابور الانتظار.'
|
| }),
|
| Object.freeze({
|
| key: 'protection_pacing',
|
| title: 'إعدادات التخفي وفترات تبريد الاتصال',
|
| icon: 'fa-shield-halved',
|
| color: '#6366f1',
|
| desc: 'تصدير واستيراد إعدادات دروع التخفي ومحاكاة السلوك وفترات الانتظار.'
|
| }),
|
| Object.freeze({
|
| key: 'contacts_manager',
|
| title: 'سجل جهات الاتصال والعملاء والوسوم',
|
| icon: 'fa-address-book',
|
| color: '#0284c7',
|
| desc: 'تصدير واستيراد كل بيانات العميل، مراحل المبيعات، الوسوم، والحقول المخصصة.'
|
| }),
|
| Object.freeze({
|
| key: 'calendar_agenda',
|
| title: 'تقويم المهام والأجندة الذكية والتذكيرات',
|
| icon: 'fa-calendar-check',
|
| color: '#1d4ed8',
|
| desc: 'تصدير واستيراد كل أحداث ومهام الأجندة والمواعيد المجدولة.'
|
| }),
|
| Object.freeze({
|
| key: 'fb_recent_published',
|
| title: 'سجل التعبئة السريعة لمنشورات فيسبوك',
|
| icon: 'fa-clock-rotate-left',
|
| color: '#059669',
|
| desc: 'تصدير واستيراد سجل التعبئة السريعة والمنشورات الحديثة.'
|
| }),
|
| Object.freeze({
|
| key: 'store_rates_cache',
|
| title: 'إعدادات المتجر وأسعار الصرف المؤرشفة',
|
| icon: 'fa-store',
|
| color: '#7c3aed',
|
| desc: 'تصدير واستيراد أسعار الصرف المؤرشفة محلياً وبيانات المتجر.'
|
| })
|
| ]);
|
|
|
| |
| |
|
|
| async function blobToBase64(blob) {
|
| if (global.FritreeStorage && typeof global.FritreeStorage.blobToB64 === 'function') {
|
| return await global.FritreeStorage.blobToB64(blob);
|
| }
|
| if (blob instanceof Uint8Array) {
|
| blob = new Blob([blob]);
|
| }
|
| const buffer = await blob.arrayBuffer();
|
| const bytes = new Uint8Array(buffer);
|
| let binary = '';
|
| for (let i = 0; i < bytes.byteLength; i++) {
|
| binary += String.fromCharCode(bytes[i]);
|
| }
|
| return `data:${blob.type || 'image/jpeg'};base64,${btoa(binary)}`;
|
| }
|
|
|
| |
| |
|
|
| function base64ToBlob(base64Data) {
|
| if (global.FritreeStorage && typeof global.FritreeStorage.b64ToBlob === 'function') {
|
| return global.FritreeStorage.b64ToBlob(base64Data);
|
| }
|
| const parts = base64Data.split(",");
|
| const mimeType = parts[0].split(":")[1].split(";")[0];
|
| const byteCharacters = atob(parts[1] || parts[0]);
|
| const byteArrays = [];
|
| for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
| const slice = byteCharacters.slice(offset, offset + 512);
|
| const byteNumbers = new Array(slice.length);
|
| for (let i = 0; i < slice.length; i++) {
|
| byteNumbers[i] = slice.charCodeAt(i);
|
| }
|
| const byteArray = new Uint8Array(byteNumbers);
|
| byteArrays.push(byteArray);
|
| }
|
| return new Blob(byteArrays, { type: mimeType });
|
| }
|
|
|
| |
| |
|
|
| function buildBackupPaneLayout() {
|
| const contentArea = document.getElementById('s7ihkirolg');
|
| if (!contentArea) return false;
|
|
|
| const host = document.getElementById('fritree-dynamic-panes-host') || contentArea;
|
| let pane = document.getElementById('oboufoudrv');
|
| if (pane) {
|
| if (pane.parentElement !== host) {
|
| host.appendChild(pane);
|
| }
|
| return true;
|
| }
|
|
|
| pane = document.createElement('section');
|
| pane.className = 'rjiai07g77';
|
| pane.id = 'oboufoudrv';
|
| pane.innerHTML = `
|
| <div class="d5tp7way8h zcjbe1otmg" style="margin: 0; padding: clamp(14px, 1.8vw, 24px); border-radius: var(--radius-xl); border: 1px solid var(--border); direction: rtl; text-align: right; background: var(--bg-card); box-sizing: border-box;">
|
|
|
| <!-- ترويسة مركز النسخ الاحتياطي -->
|
| <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 14px; margin-bottom: 18px; flex-wrap: wrap; gap: 10px;">
|
| <div>
|
| <h2 style="font-size: clamp(15px, 1.2vw, 18px); font-weight: 900; display: flex; align-items: center; gap: 8px; margin: 0; color: var(--text-main);">
|
| <i class="fa-solid fa-database" style="color: #059669;"></i>
|
| <span>مركز النسخ الاحتياطي وتصدير واستيراد بيئة العمل</span>
|
| </h2>
|
| <p style="font-size: 11px; color: var(--text-muted); margin-top: 4px; font-weight: 700;">
|
| <i class="fa-solid fa-shield-halved"></i>
|
| <span>تشفير متقدم AES-256-GCM مع دعم تقييد النسخ بكود الجهاز الموثق أو تصديرها كنسخ عامة.</span>
|
| </p>
|
| </div>
|
| </div>
|
|
|
| <!-- شبكة التصدير الشامل والاستيراد المباشر -->
|
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(280px, 35vw, 1fr), 1fr)); gap: 14px; margin-bottom: 20px;">
|
|
|
| <!-- 1. تصدير النسخة الشاملة -->
|
| <div style="border: 1px solid var(--border); border-radius: var(--radius-lg); padding: clamp(14px, 1.4vw, 18px); background: var(--bg-subtle); display: flex; flex-direction: column; gap: 10px; box-sizing: border-box;">
|
| <h3 style="font-size: 13.5px; font-weight: 900; margin: 0; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--border); padding-bottom: 8px; color: var(--text-main);">
|
| <i class="fa-solid fa-file-zipper" style="color: var(--primary);"></i> <span>تصدير حزمة بيئة العمل الشاملة (.zip)</span>
|
| </h3>
|
| <p style="font-size: 11px; color: var(--text-muted); line-height: 1.5; margin: 0; font-weight: 600;">
|
| يتم تشفير وحزم كافة الأقسام (المنشورات، جهات الاتصال، التقويم، الحملات، الإعدادات، والوسائط) داخل ملف ZIP مشفر.
|
| </p>
|
| <div class="d0uttwsjz2" style="margin: 0;">
|
| <label style="font-size: 11px; font-weight: 700;"><i class="fa-solid fa-key"></i> <span>كلمة مرور التشفير (اختياري):</span></label>
|
| <input type="password" id="backup-master-password" class="xy26ymifmf" placeholder="اتركها فارغة إذا كان الملف بدون كلمة سر..." style="padding: 7px 10px;">
|
| </div>
|
| <div class="d0uttwsjz2" style="margin: 0;">
|
| <label style="font-size: 11px; font-weight: 700;"><i class="fa-solid fa-lock"></i> <span>تقييد بكود جهاز محدد (128 حرف - اختياري):</span></label>
|
| <input type="text" id="backup-target-device-id" class="xy26ymifmf" placeholder="اتركه فارغاً لتصدير نسخة مفتوحة..." style="padding: 7px 10px; font-size: 10px; direction: ltr; text-align: left; font-family: var(--font-code);">
|
| </div>
|
| <button type="button" id="btn-export-master-backup" class="itdnt14mss" style="padding: 10px; font-size: 12px; font-weight: 800; margin-top: auto; border-radius: var(--radius-md);">
|
| <i class="fa-solid fa-download"></i> <span>تصدير وتنزيل الحزمة الشاملة الآن</span>
|
| </button>
|
| </div>
|
|
|
| <!-- 2. استيراد النسخة الاحتياطية -->
|
| <div style="border: 1px solid var(--border); border-radius: var(--radius-lg); padding: clamp(14px, 1.4vw, 18px); background: var(--bg-subtle); display: flex; flex-direction: column; gap: 10px; box-sizing: border-box;">
|
| <h3 style="font-size: 13.5px; font-weight: 900; margin: 0; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--border); padding-bottom: 8px; color: var(--text-main);">
|
| <i class="fa-solid fa-file-import" style="color: var(--wa-brand);"></i> <span>استيراد واستعادة ملف النسخة الاحتياطية</span>
|
| </h3>
|
| <div class="jbhynmzpf2" id="backup-import-dropzone" style="padding: 20px; text-align: center; cursor: pointer;">
|
| <i class="fa-solid fa-cloud-arrow-up fa-2x" style="color: var(--wa-brand); margin-bottom: 6px;"></i>
|
| <strong style="font-size: 12px; color: var(--text-main);"><i class="fa-solid fa-folder-open"></i> <span>اسحب وأفلت ملف الـ ZIP المشفر هنا</span></strong>
|
| <span style="font-size: 10.5px; color: var(--text-muted);">أو اضغط لاختيار الملف من جهازك</span>
|
| <input type="file" id="backup-import-file-input" accept=".zip" style="display: none;">
|
| </div>
|
| <div class="d0uttwsjz2" style="margin: 0;">
|
| <label style="font-size: 11px; font-weight: 700;"><i class="fa-solid fa-key"></i> <span>كلمة المرور لفك التشفير:</span></label>
|
| <input type="password" id="backup-import-password" class="xy26ymifmf" placeholder="اكتب كلمة المرور في حال كان الملف محمياً..." style="padding: 7px 10px;">
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <!-- شبكة تصدير الأقسام الفردية المستقلة -->
|
| <div style="border-top: 1px solid var(--border); padding-top: 16px;">
|
| <h3 style="font-size: 13.5px; font-weight: 900; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; color: var(--text-main);">
|
| <i class="fa-solid fa-cubes" style="color: #8b5cf6;"></i> <span>تصدير واستيراد الأقسام والوحدات المستقلة</span>
|
| </h3>
|
| <div id="backup-modules-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(clamp(250px, 24vw, 320px), 1fr)); gap: 12px; box-sizing: border-box;"></div>
|
| </div>
|
| </div>
|
| `;
|
|
|
| host.appendChild(pane);
|
| renderModulesGrid();
|
| bindBackupEvents();
|
| return true;
|
| }
|
|
|
| |
| |
|
|
| function renderModulesGrid() {
|
| const container = document.getElementById('backup-modules-grid');
|
| if (!container) return;
|
| container.innerHTML = '';
|
|
|
| MODULE_DEFINITIONS.forEach(mod => {
|
| const card = document.createElement('div');
|
| card.style.cssText = `
|
| border: 1px solid var(--border);
|
| border-radius: var(--radius-lg);
|
| padding: 14px;
|
| background: var(--bg-card);
|
| display: flex;
|
| flex-direction: column;
|
| justify-content: space-between;
|
| gap: 8px;
|
| box-shadow: var(--shadow-xs);
|
| box-sizing: border-box;
|
| `;
|
| card.innerHTML = `
|
| <div>
|
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
| <strong style="font-size: 12.5px; display: flex; align-items: center; gap: 6px; color: var(--text-main);">
|
| <i class="fa-solid ${mod.icon}" style="color: ${mod.color}; font-size: 14px;"></i>
|
| <span>${mod.title}</span>
|
| </strong>
|
| </div>
|
| <p style="font-size: 11px; color: var(--text-muted); line-height: 1.4; margin-bottom: 8px; font-weight: 600;">
|
| ${mod.desc}
|
| </p>
|
| <span id="module-count-${mod.key}" style="font-size: 10px; font-weight: 700; background: var(--bg-subtle); padding: 2px 8px; border-radius: var(--radius-pill); border: 1px solid var(--border); display: inline-block; color: var(--text-secondary); font-family: var(--font-code);">
|
| جاري الحساب...
|
| </span>
|
| </div>
|
| <button type="button" class="ipxi2jz4g0 btn-export-single-mod" data-mod-key="${mod.key}" style="padding: 6px 10px; font-size: 10.5px; font-weight: 800; margin-top: 6px; width: 100%; min-height: 30px;">
|
| <i class="fa-solid fa-file-export"></i> <span>تصدير هذا القسم فقط</span>
|
| </button>
|
| `;
|
|
|
| container.appendChild(card);
|
| });
|
|
|
| refreshModuleBadges();
|
| }
|
|
|
| |
| |
| |
| |
|
|
| async function collectModuleData(moduleKey) {
|
| const payload = {
|
| moduleKey: moduleKey,
|
| timestamp: new Date().toISOString(),
|
| version: "1.3.0-ModularFull",
|
| algo: "PBKDF2-SHA256-AES-GCM"
|
| };
|
| const mediaBlobsToExport = [];
|
|
|
| switch (moduleKey) {
|
| case 'rotation_library':
|
| payload.contentLibraryData = await global.FritreeStorage.get('local_content_library', []);
|
| payload.contentCategoriesData = await global.FritreeStorage.get('local_content_categories', []);
|
| payload.previousPostsData = await global.FritreeStorage.get('local_previous_posts', []);
|
| payload.selectedPrevPostIds = await global.FritreeStorage.get('local_selected_prev_posts', []);
|
| payload.isRotationActive = await global.FritreeStorage.get('local_is_rotation_active', 'false');
|
| payload.rotationMode = await global.FritreeStorage.get('local_rotation_mode', 'balanced');
|
|
|
| if (payload.previousPostsData && Array.isArray(payload.previousPostsData)) {
|
| for (const post of payload.previousPostsData) {
|
| if (post.mediaReferences && Array.isArray(post.mediaReferences)) {
|
| for (const ref of post.mediaReferences) {
|
| const fullBlob = await global.FritreeStorage.get(`media_blob_${ref.id}`);
|
| const thumbBlob = await global.FritreeStorage.get(`media_thumb_${ref.id}`);
|
| if (fullBlob) {
|
| mediaBlobsToExport.push({ id: ref.id, suffix: 'blob', blob: fullBlob, filename: ref.filename || `${ref.id}_blob.jpg` });
|
| }
|
| if (thumbBlob) {
|
| mediaBlobsToExport.push({ id: ref.id, suffix: 'thumb', blob: thumbBlob, filename: ref.filename || `${ref.id}_thumb.jpg` });
|
| }
|
| }
|
| }
|
| }
|
| }
|
| break;
|
|
|
| case 'rotation_groups':
|
| payload.postGroupsData = await global.FritreeStorage.get('local_post_groups', []);
|
| break;
|
|
|
| case 'rotation_followups':
|
| payload.followupPosts = (await global.FritreeStorage.get('local_previous_posts', [])).filter(p => p.enableWaFollowUp);
|
| break;
|
|
|
| case 'campaign_history':
|
| payload.campaignHistoryData = await global.FritreeStorage.get('campaignHistoryData', []);
|
| break;
|
|
|
| case 'scheduled_campaigns':
|
| payload.scheduledCampaignsData = await global.FritreeStorage.get('scheduledCampaignsData', []);
|
| break;
|
|
|
| case 'facebook_registry':
|
| payload.localGroups = await global.FritreeStorage.get('local_groups', []);
|
| payload.selectedGroupIds = await global.FritreeStorage.get('local_selected_groups', []);
|
| payload.savedTags = await global.FritreeStorage.get('local_saved_tags', {});
|
| payload.groupClassifications = await global.FritreeStorage.get('local_fb_group_classifications', {});
|
| payload.quarantinedGroups = await global.FritreeStorage.get('local_fb_quarantined_groups', []);
|
| payload.quarantineTimestamps = await global.FritreeStorage.get('local_fb_quarantine_timestamps', {});
|
| payload.customCooldowns = await global.FritreeStorage.get('local_fb_custom_group_cooldowns', {});
|
| payload.whitelistedGroups = await global.FritreeStorage.get('local_fb_whitelisted_groups', []);
|
| payload.blacklistedGroups = await global.FritreeStorage.get('local_fb_blacklisted_groups', []);
|
| break;
|
|
|
| case 'whatsapp_queue':
|
| payload.waRecipients = await global.FritreeStorage.get('local_wa_recipients_queue', []);
|
| break;
|
|
|
| case 'protection_pacing':
|
| payload.sleepIntervals = await global.FritreeStorage.get('local_sleep_intervals', {});
|
| payload.protectionSettings = await global.FritreeStorage.get('local_protection_settings', {});
|
| payload.shieldConfigMatrix = await global.FritreeStorage.get('local_shield_config_matrix', {});
|
| break;
|
|
|
| case 'contacts_manager':
|
| payload.contactsDatabase = await global.FritreeStorage.get('local_contacts_database', []);
|
| break;
|
|
|
| case 'calendar_agenda':
|
| payload.calendarEvents = await global.FritreeStorage.get('local_calendar_events_db', []);
|
| break;
|
|
|
| case 'fb_recent_published':
|
| payload.fbRecentPublishedPosts = await global.FritreeStorage.get('fbRecentPublishedPosts', []);
|
| if (payload.fbRecentPublishedPosts && Array.isArray(payload.fbRecentPublishedPosts)) {
|
| for (const item of payload.fbRecentPublishedPosts) {
|
| if (item.images && Array.isArray(item.images)) {
|
| for (const img of item.images) {
|
| const rawBlob = await global.FritreeStorage.get(`media_blob_recent_${item.id}_${img.id}`);
|
| if (rawBlob) {
|
| mediaBlobsToExport.push({ id: `recent_${item.id}_${img.id}`, suffix: 'blob', blob: rawBlob, filename: img.name || `${img.id}.jpg` });
|
| }
|
| }
|
| }
|
| }
|
| }
|
| break;
|
|
|
| case 'store_rates_cache':
|
| payload.cachedRates = await global.FritreeStorage.get('cached_internet_rates', {});
|
| payload.ratesLastUpdated = await global.FritreeStorage.get('rates_last_updated', '');
|
| break;
|
| }
|
|
|
| return { payload, mediaBlobsToExport };
|
| }
|
|
|
| |
| |
|
|
| async function restoreModuleData(payload, zipFiles = [], passphrase = '', currentId = '') {
|
| if (payload.contentCategoriesData) await global.FritreeStorage.set('local_content_categories', payload.contentCategoriesData);
|
| if (payload.contentLibraryData) await global.FritreeStorage.set('local_content_library', payload.contentLibraryData);
|
|
|
| if (payload.previousPostsData) {
|
| const existingPosts = await global.FritreeStorage.get('local_previous_posts', []);
|
| const mergedPosts = [...existingPosts];
|
| for (const importedPost of payload.previousPostsData) {
|
| if (!importedPost.id) {
|
| importedPost.id = 'rot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
|
| }
|
| const duplicateIndex = mergedPosts.findIndex(p => p.id === importedPost.id);
|
| if (duplicateIndex !== -1) {
|
| mergedPosts[duplicateIndex] = { ...mergedPosts[duplicateIndex], ...importedPost };
|
| } else {
|
| mergedPosts.push(importedPost);
|
| }
|
| }
|
| await global.FritreeStorage.set('local_previous_posts', mergedPosts);
|
| }
|
|
|
| if (payload.postGroupsData) await global.FritreeStorage.set('local_post_groups', payload.postGroupsData);
|
| if (payload.campaignHistoryData) await global.FritreeStorage.set('campaignHistoryData', payload.campaignHistoryData);
|
| if (payload.scheduledCampaignsData) await global.FritreeStorage.set('scheduledCampaignsData', payload.scheduledCampaignsData);
|
| if (payload.localGroups) await global.FritreeStorage.set('local_groups', payload.localGroups);
|
| if (payload.selectedGroupIds) await global.FritreeStorage.set('local_selected_groups', payload.selectedGroupIds);
|
| if (payload.savedTags) await global.FritreeStorage.set('local_saved_tags', payload.savedTags);
|
| if (payload.groupClassifications) await global.FritreeStorage.set('local_fb_group_classifications', payload.groupClassifications);
|
| if (payload.quarantinedGroups) await global.FritreeStorage.set('local_fb_quarantined_groups', payload.quarantinedGroups);
|
| if (payload.quarantineTimestamps) await global.FritreeStorage.set('local_fb_quarantine_timestamps', payload.quarantineTimestamps);
|
| if (payload.customCooldowns) await global.FritreeStorage.set('local_fb_custom_group_cooldowns', payload.customCooldowns);
|
| if (payload.whitelistedGroups) await global.FritreeStorage.set('local_fb_whitelisted_groups', payload.whitelistedGroups);
|
| if (payload.blacklistedGroups) await global.FritreeStorage.set('local_fb_blacklisted_groups', payload.blacklistedGroups);
|
| if (payload.waRecipients) await global.FritreeStorage.set('local_wa_recipients_queue', payload.waRecipients);
|
| if (payload.sleepIntervals) await global.FritreeStorage.set('local_sleep_intervals', payload.sleepIntervals);
|
| if (payload.protectionSettings) await global.FritreeStorage.set('local_protection_settings', payload.protectionSettings);
|
| if (payload.shieldConfigMatrix) await global.FritreeStorage.set('local_shield_config_matrix', payload.shieldConfigMatrix);
|
|
|
| if (payload.contactsDatabase && Array.isArray(payload.contactsDatabase)) {
|
| const existingContacts = await global.FritreeStorage.get('local_contacts_database', []);
|
| const mergedContacts = [...existingContacts];
|
| for (const impCnt of payload.contactsDatabase) {
|
| if (!impCnt.id) impCnt.id = 'cnt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
|
| const dupIdx = mergedContacts.findIndex(c => c.id === impCnt.id || (c.phone && impCnt.phone && c.phone === impCnt.phone));
|
| if (dupIdx !== -1) {
|
| mergedContacts[dupIdx] = { ...mergedContacts[dupIdx], ...impCnt };
|
| } else {
|
| mergedContacts.push(impCnt);
|
| }
|
| }
|
| await global.FritreeStorage.set('local_contacts_database', mergedContacts);
|
| }
|
|
|
| if (payload.calendarEvents && Array.isArray(payload.calendarEvents)) {
|
| const existingEvents = await global.FritreeStorage.get('local_calendar_events_db', []);
|
| const mergedEvents = [...existingEvents];
|
| for (const impEvt of payload.calendarEvents) {
|
| if (!impEvt.id) impEvt.id = 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
|
| const dupIdx = mergedEvents.findIndex(e => e.id === impEvt.id);
|
| if (dupIdx !== -1) {
|
| mergedEvents[dupIdx] = { ...mergedEvents[dupIdx], ...impEvt };
|
| } else {
|
| mergedEvents.push(impEvt);
|
| }
|
| }
|
| await global.FritreeStorage.set('local_calendar_events_db', mergedEvents);
|
| }
|
|
|
| if (payload.fbRecentPublishedPosts) await global.FritreeStorage.set('fbRecentPublishedPosts', payload.fbRecentPublishedPosts);
|
| if (payload.cachedRates) await global.FritreeStorage.set('cached_internet_rates', payload.cachedRates);
|
| if (payload.ratesLastUpdated) await global.FritreeStorage.set('rates_last_updated', payload.ratesLastUpdated);
|
|
|
|
|
| const imageFiles = zipFiles.filter(file => file.name.startsWith("images/") && file.name.endsWith(".enc"));
|
| for (const file of imageFiles) {
|
| const fileParts = file.name.slice("images/".length).replace(".enc", "").split("_");
|
| const suffix = fileParts.pop();
|
| const mediaId = fileParts.join("_");
|
| const encryptedImgText = new TextDecoder().decode(file.data);
|
| const decryptedImgB64 = await global.FritreeCrypto.decryptBackupString(encryptedImgText, passphrase, currentId);
|
| if (decryptedImgB64) {
|
| const imgBlob = base64ToBlob(decryptedImgB64);
|
| await global.FritreeStorage.set(`media_${suffix}_${mediaId}`, imgBlob);
|
| }
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function refreshModuleBadges() {
|
| const stats = {};
|
| for (const m of MODULE_DEFINITIONS) {
|
| let countText = "0 عناصر";
|
| switch (m.key) {
|
| case 'rotation_library':
|
| const posts = await global.FritreeStorage.get('local_previous_posts', []);
|
| countText = `${posts.length.toLocaleString('en-US')} منشورات بالمكتبة`;
|
| break;
|
| case 'rotation_groups':
|
| const groups = await global.FritreeStorage.get('local_post_groups', []);
|
| countText = `${groups.length.toLocaleString('en-US')} حزمة مخصصة`;
|
| break;
|
| case 'rotation_followups':
|
| const followups = (await global.FritreeStorage.get('local_previous_posts', [])).filter(p => p.enableWaFollowUp);
|
| countText = `${followups.length.toLocaleString('en-US')} رسالة متابعة`;
|
| break;
|
| case 'campaign_history':
|
| const history = await global.FritreeStorage.get('campaignHistoryData', []);
|
| countText = `${history.length.toLocaleString('en-US')} سجل حملات`;
|
| break;
|
| case 'scheduled_campaigns':
|
| const sched = await global.FritreeStorage.get('scheduledCampaignsData', []);
|
| countText = `${sched.length.toLocaleString('en-US')} حملات مجدولة`;
|
| break;
|
| case 'facebook_registry':
|
| const fbGroups = await global.FritreeStorage.get('local_groups', []);
|
| countText = `${fbGroups.length.toLocaleString('en-US')} مجموعة متصلة`;
|
| break;
|
| case 'whatsapp_queue':
|
| const waQ = await global.FritreeStorage.get('local_wa_recipients_queue', []);
|
| countText = `${waQ.length.toLocaleString('en-US')} مستلم بالطابور`;
|
| break;
|
| case 'protection_pacing':
|
| countText = "مصفوفة فترات الأمان والتبريد";
|
| break;
|
| case 'contacts_manager':
|
| const contacts = await global.FritreeStorage.get('local_contacts_database', []);
|
| countText = `${contacts.length.toLocaleString('en-US')} جهة اتصال بالسجل`;
|
| break;
|
| case 'calendar_agenda':
|
| const calEvents = await global.FritreeStorage.get('local_calendar_events_db', []);
|
| countText = `${calEvents.length.toLocaleString('en-US')} أحداث ومهام مجدولة`;
|
| break;
|
| case 'fb_recent_published':
|
| const recentFb = await global.FritreeStorage.get('fbRecentPublishedPosts', []);
|
| countText = `${recentFb.length.toLocaleString('en-US')} منشورات سريعة`;
|
| break;
|
| case 'store_rates_cache':
|
| countText = "أسعار الصرف وسجل الكاش المعتمد";
|
| break;
|
| }
|
| stats[m.key] = countText;
|
|
|
| const badge = document.getElementById(`module-count-${m.key}`);
|
| if (badge) {
|
| badge.textContent = countText;
|
| }
|
| }
|
| return stats;
|
| }
|
|
|
| |
| |
|
|
| async function exportSingleModule(moduleKey, passphrase = '', targetAccountId = '') {
|
| const { payload, mediaBlobsToExport } = await collectModuleData(moduleKey);
|
| const plainText = JSON.stringify(payload);
|
| const targetId = targetAccountId ? targetAccountId.trim() : null;
|
|
|
| const encryptedMetaBase64 = await global.FritreeCrypto.encryptBackupString(plainText, passphrase, targetId);
|
| if (!encryptedMetaBase64) {
|
| throw new Error("فشل تشفير بيانات القسم المحدد.");
|
| }
|
|
|
| const zipFiles = [];
|
| const textEncoder = new TextEncoder();
|
| zipFiles.push({
|
| name: "module_data.json.enc",
|
| data: textEncoder.encode(encryptedMetaBase64)
|
| });
|
|
|
| if (mediaBlobsToExport.length > 0) {
|
| for (const img of mediaBlobsToExport) {
|
| const imgB64 = await blobToBase64(img.blob);
|
| const encryptedImgB64 = await global.FritreeCrypto.encryptBackupString(imgB64, passphrase, targetId);
|
| if (encryptedImgB64) {
|
| zipFiles.push({
|
| name: `images/${img.id}_${img.suffix}.enc`,
|
| data: textEncoder.encode(encryptedImgB64)
|
| });
|
| }
|
| }
|
| }
|
|
|
| const zipBytes = global.FritreeZipArchive.createZip(zipFiles);
|
| const blob = new Blob([zipBytes], { type: 'application/zip' });
|
| const url = URL.createObjectURL(blob);
|
| const a = document.createElement('a');
|
| a.href = url;
|
|
|
| const now = new Date();
|
| const pad = num => String(num).padStart(2, '0');
|
| const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`;
|
| a.download = `قسم_مشفر_${moduleKey}_${timestamp}.zip`;
|
| a.click();
|
| URL.revokeObjectURL(url);
|
| }
|
|
|
| |
| |
|
|
| async function exportMasterFullBackup(selectedModuleKeys, passphrase = '', targetAccountId = '') {
|
| const masterPayload = {
|
| timestamp: new Date().toISOString(),
|
| version: "1.3.0-MasterFull",
|
| algo: "PBKDF2-SHA256-AES-GCM",
|
| modules: {}
|
| };
|
| const allMediaBlobs = [];
|
|
|
| for (const key of selectedModuleKeys) {
|
| const { payload, mediaBlobsToExport } = await collectModuleData(key);
|
| masterPayload.modules[key] = payload;
|
| if (mediaBlobsToExport.length > 0) {
|
| allMediaBlobs.push(...mediaBlobsToExport);
|
| }
|
| }
|
|
|
| const plainText = JSON.stringify(masterPayload);
|
| const targetId = targetAccountId ? targetAccountId.trim() : null;
|
| const encryptedMetadataBase64 = await global.FritreeCrypto.encryptBackupString(plainText, passphrase, targetId);
|
|
|
| if (!encryptedMetadataBase64) {
|
| throw new Error("فشل تشفير بيئة العمل الشاملة.");
|
| }
|
|
|
| const zipFiles = [];
|
| const textEncoder = new TextEncoder();
|
| zipFiles.push({
|
| name: "workspace_data.json.enc",
|
| data: textEncoder.encode(encryptedMetadataBase64)
|
| });
|
|
|
| for (const img of allMediaBlobs) {
|
| const imgB64 = await blobToBase64(img.blob);
|
| const encryptedImgB64 = await global.FritreeCrypto.encryptBackupString(imgB64, passphrase, targetId);
|
| if (encryptedImgB64) {
|
| zipFiles.push({
|
| name: `images/${img.id}_${img.suffix}.enc`,
|
| data: textEncoder.encode(encryptedImgB64)
|
| });
|
| }
|
| }
|
|
|
| const zipBytes = global.FritreeZipArchive.createZip(zipFiles);
|
| const blob = new Blob([zipBytes], { type: 'application/zip' });
|
| const url = URL.createObjectURL(blob);
|
| const a = document.createElement('a');
|
| a.href = url;
|
|
|
| const now = new Date();
|
| const pad = num => String(num).padStart(2, '0');
|
| const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`;
|
| const fileSuffix = targetId ? "مقيد_بمعرف" : "عام_مفتوح";
|
| a.download = `حزمة_بيئة_عمل_فريتري_شاملة_${fileSuffix}_${timestamp}.zip`;
|
| a.click();
|
| URL.revokeObjectURL(url);
|
| }
|
|
|
| |
| |
|
|
| async function importBackupZipFile(arrayBuffer, passphrase = '') {
|
| const zipFiles = global.FritreeZipArchive.readZip(arrayBuffer);
|
| const metaFile = zipFiles.find(f => f.name === "workspace_data.json.enc" || f.name === "module_data.json.enc");
|
|
|
| if (!metaFile) {
|
| throw new Error("فشل الاستيراد: صيغة الملف غير صالحة أو غير متوافقة.");
|
| }
|
|
|
| const encryptedText = new TextDecoder().decode(metaFile.data);
|
| const currentId = await global.FritreeCrypto.getOrGenerateAccountId();
|
| const decryptedText = await global.FritreeCrypto.decryptBackupString(encryptedText, passphrase, currentId);
|
|
|
| if (!decryptedText) {
|
| throw new Error("فشل فك التشفير: كلمة المرور غير صحيحة أو الملف مقفول لجهاز آخر.");
|
| }
|
|
|
| const data = JSON.parse(decryptedText);
|
| if (data.modules && typeof data.modules === 'object') {
|
| for (const [mKey, mPayload] of Object.entries(data.modules)) {
|
| await restoreModuleData(mPayload, zipFiles, passphrase, currentId);
|
| }
|
| } else {
|
| await restoreModuleData(data, zipFiles, passphrase, currentId);
|
| }
|
|
|
| await refreshModuleBadges();
|
| return true;
|
| }
|
|
|
| |
| |
|
|
| function bindBackupEvents() {
|
| const btnExportMaster = document.getElementById('btn-export-master-backup');
|
| if (btnExportMaster) {
|
| btnExportMaster.onclick = async () => {
|
| const pass = (document.getElementById('backup-master-password')?.value || '').trim();
|
| const targetId = (document.getElementById('backup-target-device-id')?.value || '').trim();
|
| const keys = MODULE_DEFINITIONS.map(m => m.key);
|
|
|
| btnExportMaster.disabled = true;
|
| btnExportMaster.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>جاري التشفير والحزم...</span>';
|
| try {
|
| await exportMasterFullBackup(keys, pass, targetId);
|
| alert('تم بنجاح تصدير وتنزيل حزمة بيئة العمل الشاملة المشفرة!');
|
| } catch (e) {
|
| alert('خطأ أثناء التصدير: ' + e.message);
|
| } finally {
|
| btnExportMaster.disabled = false;
|
| btnExportMaster.innerHTML = '<i class="fa-solid fa-download"></i> <span>تصدير وتنزيل الحزمة الشاملة الآن</span>';
|
| }
|
| };
|
| }
|
|
|
| const dropzone = document.getElementById('backup-import-dropzone');
|
| const fileInput = document.getElementById('backup-import-file-input');
|
|
|
| if (dropzone && fileInput) {
|
| dropzone.onclick = () => fileInput.click();
|
| fileInput.onchange = async (e) => {
|
| if (e.target.files.length > 0) {
|
| await handleIncomingZipFile(e.target.files[0]);
|
| }
|
| };
|
|
|
| dropzone.ondragover = (e) => {
|
| e.preventDefault();
|
| dropzone.style.borderColor = 'var(--primary)';
|
| dropzone.style.background = 'var(--primary-light)';
|
| };
|
| dropzone.ondragleave = () => {
|
| dropzone.style.borderColor = 'var(--border-strong)';
|
| dropzone.style.background = 'var(--bg-subtle)';
|
| };
|
| dropzone.ondrop = async (e) => {
|
| e.preventDefault();
|
| dropzone.style.borderColor = 'var(--border-strong)';
|
| dropzone.style.background = 'var(--bg-subtle)';
|
| if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
| await handleIncomingZipFile(e.dataTransfer.files[0]);
|
| }
|
| };
|
| }
|
|
|
| document.querySelectorAll('.btn-export-single-mod').forEach(btn => {
|
| btn.onclick = async () => {
|
| const modKey = btn.getAttribute('data-mod-key');
|
| if (!modKey) return;
|
| const pass = prompt("اكتب كلمة مرور لتشفير هذا القسم (اختياري، اضغط موافق بدون كتابة للتصدير العام):") || '';
|
| try {
|
| await exportSingleModule(modKey, pass);
|
| alert('تم بنجاح تصدير ملف القسم المشفر!');
|
| } catch (e) {
|
| alert('خطأ أثناء تصدير القسم: ' + e.message);
|
| }
|
| };
|
| });
|
| }
|
|
|
| async function handleIncomingZipFile(file) {
|
| if (!file || !file.name.endsWith('.zip')) {
|
| alert('يرجى اختيار ملف بصيغة ZIP صالحة!');
|
| return;
|
| }
|
|
|
| const pass = (document.getElementById('backup-import-password')?.value || '').trim();
|
| try {
|
| const buffer = await file.arrayBuffer();
|
| await importBackupZipFile(buffer, pass);
|
| alert('تم استيراد واستعادة بيئة العمل بنجاح تام!');
|
| if (global.syncWorkspaceUI) global.syncWorkspaceUI();
|
| } catch (e) {
|
| alert('فشل استيراد الملف: ' + e.message);
|
| }
|
| }
|
|
|
| |
| |
|
|
| global.FritreeBackupService = {
|
| MODULE_DEFINITIONS,
|
| buildPane: buildBackupPaneLayout,
|
| refreshBadges: refreshModuleBadges,
|
| exportSingleModule,
|
| exportMasterBackup: exportMasterFullBackup,
|
| importZipFile: importBackupZipFile,
|
| init: function() {
|
| buildBackupPaneLayout();
|
| return true;
|
| }
|
| };
|
|
|
| global.FritreeMigration = global.FritreeBackupService;
|
|
|
| if (typeof document !== 'undefined') {
|
| if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
| global.FritreeBackupService.init();
|
| } else {
|
| document.addEventListener('DOMContentLoaded', () => global.FritreeBackupService.init());
|
| }
|
| }
|
|
|
| })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this); |