{"id":1160,"date":"2025-11-14T00:59:02","date_gmt":"2025-11-14T00:59:02","guid":{"rendered":"https:\/\/abatablaster.xyz\/?page_id=1160"},"modified":"2025-11-14T03:28:59","modified_gmt":"2025-11-14T03:28:59","slug":"whatsapp-file-manager","status":"publish","type":"page","link":"https:\/\/abatablaster.xyz\/index.php\/whatsapp-file-manager\/","title":{"rendered":"Whatsapp File Manager"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"ms\">\n\n<head>\n    <meta charset=\"utf-8\" \/>\n    <title>File Manager \u2014 WhatsApp Blaster<\/title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n    <style>\n        :root {\n            --bg: #e8f5e9;\n            \/* hijau lembut macam wallet *\/\n            --panel: #ffffff;\n            --muted: #607d8b;\n            --text: #1b5e20;\n            --brand: #43a047;\n            \/* hijau utama *\/\n            --danger: #e53935;\n            --line: #c8e6c9;\n            \/* border hijau lembut *\/\n        }\n\n        * {\n            box-sizing: border-box;\n        }\n\n        body {\n            margin: 0;\n            background: var(--bg);\n            color: var(--text);\n            font-family: 'Poppins', Arial, sans-serif;\n            font-size: 14px;\n            line-height: 1.5;\n        }\n\n        .container {\n            background: #fff;\n            max-width: 1180px;\n            \/* \u279c lebih lebar untuk desktop *\/\n            width: 96vw;\n            \/* hampir penuh skrin, ada ruang tepi sikit *\/\n            margin: 30px auto 32px;\n            padding: 28px 32px;\n            border-radius: 18px;\n            box-shadow: 0 8px 24px rgba(60, 80, 65, 0.08);\n        }\n\n        @media (max-width: 900px) {\n            .container {\n                max-width: 100%;\n                width: 100%;\n                margin: 16px auto 24px;\n                padding: 20px 14px;\n                border-radius: 0;\n                \/* kalau nak full bleed di mobile, boleh buang jika tak suka *\/\n            }\n        }\n\n        @media (max-width: 600px) {\n            body {\n                font-size: 13px;\n            }\n\n            .brand {\n                font-size: 16px;\n            }\n        }\n\n        header.card {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            background: transparent;\n            border: none;\n            padding: 0 0 6px 0;\n            margin-bottom: 10px;\n            gap: 10px;\n            flex-wrap: wrap;\n        }\n\n        .brand {\n            font-weight: 700;\n            font-size: 18px;\n            color: #388e3c;\n        }\n\n        .btn {\n            padding: 8px 18px;\n            background: linear-gradient(90deg, #43a047 60%, #81c784 100%);\n            color: #fff;\n            font-size: 13px;\n            border: none;\n            border-radius: 10px;\n            font-weight: 600;\n            cursor: pointer;\n            transition: background 0.2s;\n        }\n\n        .btn:hover {\n            background: linear-gradient(90deg, #388e3c 60%, #66bb6a 100%);\n        }\n\n        .btn.brand {\n            background: linear-gradient(90deg, #43a047 60%, #81c784 100%);\n        }\n\n        .btn.danger {\n            background: #e53935;\n            border: none;\n        }\n\n        input[type=\"file\"],\n        input[type=\"text\"],\n        textarea {\n            background: #ffffff;\n            border: 1px solid #c8e6c9;\n            border-radius: 8px;\n            padding: 6px 8px;\n            color: #1b5e20;\n            font-size: 13px;\n        }\n\n        textarea {\n            width: 100%;\n            min-height: 160px;\n        }\n\n        .tabs {\n            display: flex;\n            gap: 8px;\n            margin-bottom: 8px;\n        }\n\n        .tab {\n            padding: 8px 12px;\n            border-radius: 8px 8px 0 0;\n            border: 1px solid #c8e6c9;\n            border-bottom: none;\n            background: #f1f8e9;\n            cursor: pointer;\n            font-size: 13px;\n            opacity: 0.9;\n            color: #2e7d32;\n        }\n\n        .tab.active {\n            background: #ffffff;\n            opacity: 1;\n            border-color: #81c784;\n        }\n\n        .panel {\n            border: 1px solid #d9ead9;\n            border-radius: 14px;\n            background: #f7fbf7;\n            padding: 14px;\n            margin-bottom: 16px;\n        }\n\n        .grid {\n            display: grid;\n            grid-template-columns: repeat(4, minmax(0, 1fr));\n            gap: 10px;\n        }\n\n        @media(max-width:900px) {\n            .grid {\n                grid-template-columns: repeat(3, 1fr);\n            }\n        }\n\n        @media(max-width:700px) {\n            .grid {\n                grid-template-columns: repeat(2, 1fr);\n            }\n        }\n\n        @media(max-width:500px) {\n            .grid {\n                grid-template-columns: 1fr;\n            }\n        }\n\n        .card-media {\n            border: 1px solid var(--line);\n            border-radius: 12px;\n            overflow: hidden;\n            background: #ffffff;\n            display: flex;\n            flex-direction: column;\n        }\n\n        .card-media .preview {\n            background: #e8f5e9;\n            aspect-ratio: 16\/9;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .card-media img,\n        .card-media video {\n            width: 100%;\n            height: 100%;\n            object-fit: cover;\n        }\n\n        .card-media .meta {\n            padding: 8px;\n            display: flex;\n            flex-direction: column;\n            gap: 6px;\n            font-size: 12px;\n        }\n\n        .row {\n            display: flex;\n            align-items: center;\n            gap: 8px;\n            flex-wrap: wrap;\n        }\n\n        .row-between {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            gap: 8px;\n        }\n\n        .filename {\n            flex: 1;\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n        }\n\n        table {\n            width: 100%;\n            border-collapse: collapse;\n            font-size: 13px;\n        }\n\n        th,\n        td {\n            border-bottom: 1px solid var(--line);\n            padding: 6px 8px;\n        }\n\n        tr:hover {\n            background: #f1f8e9;\n        }\n\n        .split {\n            display: grid;\n            grid-template-columns: 1.1fr 1.2fr;\n            gap: 12px;\n        }\n\n        \/* \ud83d\udd27 penting untuk elak overflow dari table kiri *\/\n        .split>div {\n            min-width: 0;\n        }\n\n        @media(max-width:800px) {\n            .split {\n                grid-template-columns: 1fr;\n            }\n        }\n\n        .muted {\n            color: #607d8b;\n        }\n\n        .badge {\n            display: inline-block;\n            padding: 2px 8px;\n            border-radius: 999px;\n            border: 1px solid #c8e6c9;\n            font-size: 11px;\n            color: #2e7d32;\n            background: #e8f5e9;\n        }\n\n        .note {\n            font-size: 12px;\n            color: var(--muted);\n            margin-top: 4px;\n        }\n\n        @keyframes blink {\n            50% {\n                opacity: 0;\n            }\n        }\n\n        \/* \u2705 Wrapper untuk jadual supaya boleh scroll mendatar di mobile *\/\n        .table-wrap {\n            width: 100%;\n            overflow-x: auto;\n            -webkit-overflow-scrolling: touch;\n        }\n\n        .table-wrap table {\n            width: 100%;\n            border-collapse: collapse;\n        }\n\n        \/* Butang editor template *\/\n        .editor-actions {\n            justify-content: flex-start;\n            flex-wrap: wrap;\n            \/* \u2705 benarkan butang turun ke line baru *\/\n            gap: 8px;\n        }\n\n        .editor-actions .btn {\n            max-width: 100%;\n        }\n\n        \/* \u2705 Mobile \/ tablet tweak *\/\n        @media (max-width: 900px) {\n            .row-between {\n                flex-direction: column;\n                align-items: flex-start;\n            }\n\n            table {\n                font-size: 12px;\n            }\n\n            th,\n            td {\n                white-space: nowrap;\n            }\n\n            \/* butang dalam jadual lebih kecil sedikit *\/\n            td .btn,\n            th .btn {\n                padding: 5px 10px;\n                font-size: 12px;\n            }\n\n            \/* \u2705 Editor template: butang stack menegak *\/\n            .editor-actions {\n                flex-direction: column;\n                align-items: stretch;\n            }\n\n            .editor-actions .btn {\n                width: 100%;\n                text-align: center;\n            }\n        }\n\n        .toast-popup {\n            position: fixed;\n            bottom: 20px;\n            right: 20px;\n            background: #43a047;\n            color: white;\n            padding: 10px 18px;\n            border-radius: 8px;\n            font-size: 14px;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n            opacity: 0;\n            animation: fadeToast 3s forwards;\n            z-index: 9999;\n        }\n\n        @keyframes fadeToast {\n            0% {\n                opacity: 0;\n                transform: translateY(20px);\n            }\n\n            10% {\n                opacity: 1;\n                transform: translateY(0);\n            }\n\n            80% {\n                opacity: 1;\n            }\n\n            100% {\n                opacity: 0;\n                transform: translateY(20px);\n            }\n        }\n    <\/style>\n\n    <!-- Firebase (sesuaikan dengan versi yang anda guna di page lain) -->\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-app-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-auth-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-firestore-compat.js\"><\/script>\n    <script src=\"https:\/\/www.gstatic.com\/firebasejs\/9.22.2\/firebase-storage-compat.js\"><\/script>\n    <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/xlsx@0.18.5\/dist\/xlsx.full.min.js\"><\/script>\n\n<\/head>\n\n<body>\n\n    <!-- \u2705 STATUS HEADER (Hanya paparkan selepas login) -->\n    <div id=\"statusHeader\" style=\"display: none;\">\n        <div id=\"topUserStatus\"\n            style=\"background: #f1f8e9; border: 1px solid #c5e1a5; padding: 15px; border-radius: 10px; margin-bottom: 30px; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 30px; font-size: 14px;\">\n            <div>\n                <div>\ud83d\udce7 <strong>Email:<\/strong> <span id=\"userEmail\">\u2013<\/span><\/div>\n                <div>\ud83d\udcbc <strong>Pakej:<\/strong> <span id=\"infoPakej\">\u2013<\/span><\/div>\n                <div>\ud83d\udce4 <strong>Mesej Baki:<\/strong> <span id=\"infoMesej\">\u2013<\/span> (<span id=\"infoHari\">\u2013<\/span> hari)\n                <\/div>\n                <div>\ud83d\udcf1 <strong>WhatsApp Aktif:<\/strong> <span id=\"infoNombor\">\u2013<\/span><\/div>\n            <\/div>\n            <div style=\"margin-left: auto; align-self: center; text-align: right;\">\n                <a href=\"https:\/\/abatablaster.xyz\/index.php\/telegram-alert\/\"\n                    style=\"display:block; margin-bottom: 8px; color: #1565c0; font-weight: bold;\">\n                    \ud83d\udd14 Sambungkan Telegram\n                <\/a>\n                <button onclick=\"logout()\"\n                    style=\"background: #d32f2f; color: white; padding: 6px 15px; border-radius: 6px; border: none;\">\ud83d\udeaa\n                    Log\n                    Keluar<\/button>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <!-- \u26a0\ufe0f AMARAN LANGGANAN TAMAT -->\n    <div id=\"amaranLangganan\"\n        style=\"display: none; background: #ffebee; border: 1px solid #f44336; color: #b71c1c; padding: 12px; border-radius: 10px; margin-bottom: 20px; font-weight: bold; text-align: center; animation: blink 1s step-end infinite;\">\n        \u26a0\ufe0f Langganan anda telah tamat atau mesej telah habis. Sila upgrade untuk terus gunakan sistem ini.\n    <\/div>\n\n    <!-- \u2705 Shared Status MESTI DULU sebab initialize Firebase -->\n    <script src=\"https:\/\/abatablaster.xyz\/js\/shared-status.js\"><\/script>\n\n    <div class=\"container\">\n        <!-- HEADER -->\n\n        <div id=\"broadcastSection\" class=\"card\">\n            <div style=\"text-align:center;margin-bottom:10px;\">\n                <img decoding=\"async\" src=\"https:\/\/abatablaster.xyz\/wp-content\/uploads\/2025\/05\/photo_2024-05-16_15-47-59.jpg\" alt=\"Logo\"\n                    style=\"max-height:100px;border-radius:16px;\">\n            <\/div>\n\n            <header class=\"card\">\n                <div class=\"brand\">AbataBlaster \u2014 File Manager WhatsApp<\/div>\n            <\/header>\n\n            <!-- TABS -->\n            <div class=\"tabs\">\n                <div class=\"tab active\" data-tab=\"media\">Media<\/div>\n                <div class=\"tab\" data-tab=\"contacts\">Senarai Contact<\/div>\n                <div class=\"tab\" data-tab=\"templates\">Template Copywriting<\/div>\n            <\/div>\n\n            <!-- PANEL MEDIA -->\n            <section class=\"panel\" id=\"panel-media\">\n                <div class=\"row-between\" style=\"margin-bottom:8px\">\n                    <div class=\"row\">\n                        <span class=\"badge\">Media (gambar &#038; video)<\/span>\n                        <span class=\"note\">Simpan media untuk WhatsApp Blaster \/ Auto Reply<\/span>\n                    <\/div>\n                    <div class=\"row\">\n                        <label class=\"btn\">\n                            Upload Media\n                            <input type=\"file\" id=\"inputMedia\" accept=\"image\/*,video\/*\" style=\"display:none\" \/>\n                        <\/label>\n                    <\/div>\n                <\/div>\n                <div class=\"note\" id=\"mediaNote\"><\/div>\n                <div id=\"mediaGrid\" class=\"grid\"><\/div>\n            <\/section>\n\n            <!-- PANEL CONTACTS -->\n            <section class=\"panel\" id=\"panel-contacts\" style=\"display:none\">\n                <div class=\"row-between\" style=\"margin-bottom:8px\">\n                    <div class=\"row\">\n                        <span class=\"badge\">Senarai Contact (.csv \/ .txt \/ .xlsx)<\/span>\n                    <\/div>\n                    <div class=\"row\">\n                        <label class=\"btn\">\n                            Upload File\n                            <input type=\"file\" id=\"inputContacts\" accept=\".csv,.txt,.xlsx\" multiple\n                                style=\"display:none\" \/>\n                        <\/label>\n                    <\/div>\n                <\/div>\n                <div class=\"note\" id=\"contactsNote\"><\/div>\n                <div class=\"table-wrap\">\n                    <table>\n                        <thead>\n                            <tr>\n                                <th>Nama Fail<\/th>\n                                <th>Jenis<\/th>\n                                <th>Saiz<\/th>\n                                <th>Jumlah Contact<\/th>\n                                <th>Muat Turun<\/th>\n                                <th>Padam<\/th>\n                            <\/tr>\n                        <\/thead>\n                        <tbody id=\"contactsTbody\">\n                            <tr>\n                                <td colspan=\"6\" class=\"muted\">Memuatkan\u2026<\/td>\n                            <\/tr>\n                        <\/tbody>\n                    <\/table>\n                <\/div>\n            <\/section>\n\n            <!-- PANEL TEMPLATES -->\n            <section class=\"panel\" id=\"panel-templates\" style=\"display:none\">\n                <div class=\"split\">\n                    <div>\n                        <div class=\"row-between\" style=\"margin-bottom:8px\">\n                            <span class=\"badge\">Senarai Template<\/span>\n                            <button class=\"btn\" id=\"btnTemplatesRefresh\">Refresh<\/button>\n                        <\/div>\n                        <div class=\"table-wrap\">\n                            <table>\n                                <thead>\n                                    <tr>\n                                        <th>Nama<\/th>\n                                        <th>Kemaskini<\/th>\n                                        <th>Aksi<\/th>\n                                    <\/tr>\n                                <\/thead>\n                                <tbody id=\"templatesTbody\">\n                                    <tr>\n                                        <td colspan=\"3\" class=\"muted\">Memuatkan\u2026<\/td>\n                                    <\/tr>\n                                <\/tbody>\n                            <\/table>\n                        <\/div>\n                    <\/div>\n                    <div>\n                        <div class=\"row-between\" style=\"margin-bottom:8px\">\n                            <strong>Editor Template<\/strong>\n                            <div class=\"row\">\n                                <button class=\"btn\" id=\"btnTemplateBaru\">Baru<\/button>\n                                <button class=\"btn brand\" id=\"btnTemplateSimpan\">Simpan<\/button>\n                            <\/div>\n                        <\/div>\n                        <input type=\"text\" id=\"tplName\" placeholder=\"nama-template (cth: followup-welcome)\"\n                            style=\"width:100%;margin-bottom:6px\" \/>\n                        <textarea id=\"tplContent\" placeholder=\"Tulis copywriting di sini...\"><\/textarea>\n                        <div class=\"row editor-actions\" style=\"margin-top:8px\">\n                            <button class=\"btn\" id=\"btnTemplateCopy\">Copy ke Clipboard<\/button>\n                            <button class=\"btn danger\" id=\"btnTemplatePadam\">Padam Template Ini<\/button>\n                        <\/div>\n                    <\/div>\n                <\/div>\n            <\/section>\n        <\/div>\n\n        <script>\n            \/\/ ================== FIREBASE CONFIG ==================\n            const firebaseConfig = {\n                apiKey: \"AIzaSyBou8nlJ7uPZ4ioOJapzC8Dn3-K7Qs-yco\",\n                authDomain: \"whatsapp-ai-saas.firebaseapp.com\",\n                projectId: \"whatsapp-ai-saas\",\n                storageBucket: \"whatsapp-ai-saas.appspot.com\",\n                messagingSenderId: \"287462007544\",\n                appId: \"1:287462007544:web:913db884edf906a37cd10d\"\n            };\n\n            \/\/ Elak initialize dua kali kalau shared-status.js dah buat\n            if (!firebase.apps.length) {\n                firebase.initializeApp(firebaseConfig);\n            }\n\n            const auth = firebase.auth();\n            const db = firebase.firestore();\n            const storage = firebase.storage();\n\n            \/\/ Limit media dalam File Manager\n            const MAX_MEDIA_FILES = 10;                 \/\/ maksimum 10 media per user\n            const MAX_IMAGE_BYTES = 5 * 1024 * 1024;    \/\/ 5MB\n            const MAX_VIDEO_BYTES = 20 * 1024 * 1024;   \/\/ 20MB\n\n            let currentUser = null;\n            let currentTplId = null;\n\n            function fmtSize(n) {\n                if (!n && n !== 0) return \"-\";\n                if (n < 1024) return n + \" B\";\n                if (n < 1024 * 1024) return (n \/ 1024).toFixed(1) + \" KB\";\n                if (n < 1024 * 1024 * 1024) return (n \/ 1024 \/ 1024).toFixed(1) + \" MB\";\n                return (n \/ 1024 \/ 1024 \/ 1024).toFixed(1) + \" GB\";\n            }\n            function fmtDate(ts) {\n                if (!ts) return \"-\";\n                const d = ts.toDate ? ts.toDate() : new Date(ts);\n                return d.toLocaleString();\n            }\n            function toast(msg) {\n                const el = document.createElement(\"div\");\n                el.className = \"toast-popup\";\n                el.textContent = msg;\n                document.body.appendChild(el);\n                setTimeout(() => el.remove(), 3000);\n            }\n\n            \/\/ Kira jumlah contact dalam file (.csv \/ .txt \/ .xlsx)\n            \/\/ - Kira bilangan baris data (autodetect header jika ada \"phone\", \"no\", \"nombor\" dll)\n            async function kiraJumlahContact(file) {\n                return new Promise((resolve) => {\n                    const extMatch = file.name.match(\/\\.([a-z0-9]+)$\/i);\n                    const ext = extMatch ? extMatch[1].toLowerCase() : \"\";\n\n                    const reader = new FileReader();\n                    reader.onerror = () => {\n                        console.error(\"Gagal baca fail untuk kira contact.\", reader.error);\n                        resolve(null);\n                    };\n                    reader.onload = (e) => {\n                        try {\n                            let count = 0;\n\n                            \/\/ Helper: detect header row\n                            const adaHeader = (text) => {\n                                const t = String(text || \"\").toLowerCase();\n                                return (\n                                    t.includes(\"phone\") ||\n                                    t.includes(\"telefon\") ||\n                                    t.includes(\"nombor\") ||\n                                    t.includes(\"no \") ||\n                                    t.includes(\"no_\") ||\n                                    t.includes(\"no-\") ||\n                                    t.includes(\"hp\")\n                                );\n                            };\n\n                            if (ext === \"csv\" || ext === \"txt\") {\n                                const text = e.target.result;\n                                const lines = String(text)\n                                    .split(\/\\r?\\n\/)\n                                    .map(l => l.trim())\n                                    .filter(l => l);\n\n                                if (!lines.length) return resolve(0);\n\n                                let startIndex = 0;\n                                if (adaHeader(lines[0])) startIndex = 1;\n\n                                count = lines.slice(startIndex).length;\n                                return resolve(count);\n                            } else if (ext === \"xlsx\" || ext === \"xls\") {\n                                const data = new Uint8Array(e.target.result);\n                                const wb = XLSX.read(data, { type: \"array\" });\n                                const sheet = wb.Sheets[wb.SheetNames[0]];\n                                const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });\n\n                                const nonEmpty = rows.filter(\n                                    r => r && r.join(\"\").toString().trim() !== \"\"\n                                );\n                                if (!nonEmpty.length) return resolve(0);\n\n                                let startIndex = 0;\n                                if (adaHeader(nonEmpty[0].join(\" \"))) startIndex = 1;\n\n                                count = nonEmpty.slice(startIndex).length;\n                                return resolve(count);\n                            } else {\n                                \/\/ format lain \u2013 tak pasti, biar null\n                                return resolve(null);\n                            }\n                        } catch (err) {\n                            console.error(\"Kira contact gagal:\", err);\n                            resolve(null);\n                        }\n                    };\n\n                    if (ext === \"xlsx\" || ext === \"xls\") reader.readAsArrayBuffer(file);\n                    else reader.readAsText(file);\n                });\n            }\n\n            \/\/ Helper: compress image di browser\n            function compressImage(file, maxWidth = 1280, quality = 0.7) {\n                return new Promise((resolve, reject) => {\n                    const img = new Image();\n                    img.onload = () => {\n                        const scale = Math.min(1, maxWidth \/ img.width);\n                        const canvas = document.createElement('canvas');\n                        canvas.width = Math.round(img.width * scale);\n                        canvas.height = Math.round(img.height * scale);\n\n                        const ctx = canvas.getContext('2d');\n                        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n\n                        canvas.toBlob(\n                            (blob) => {\n                                if (!blob) {\n                                    reject(new Error(\"Gagal compress gambar.\"));\n                                    return;\n                                }\n                                \/\/ Simpan sebagai JPEG baru\n                                const compressedFile = new File(\n                                    [blob],\n                                    (file.name.replace(\/\\.(png|jpg|jpeg|webp)$\/i, '') || 'image') + '-compressed.jpg',\n                                    { type: 'image\/jpeg' }\n                                );\n                                resolve(compressedFile);\n                            },\n                            'image\/jpeg',\n                            quality\n                        );\n                    };\n                    img.onerror = () => reject(new Error(\"Tidak dapat baca gambar.\"));\n                    img.src = URL.createObjectURL(file);\n                });\n            }\n\n            \/\/ ================== AUTH ==================\n            auth.onAuthStateChanged(user => {\n                const statusEl = document.getElementById(\"statusHeader\");\n                const amaranEl = document.getElementById(\"amaranLangganan\");\n\n                if (!user) {\n                    \/\/ Sembunyikan header & amaran jika tak login\n                    if (statusEl) statusEl.style.display = \"none\";\n                    if (amaranEl) amaranEl.style.display = \"none\";\n\n                    \/\/ Terus redirect ke login\n                    setTimeout(() => window.location.href = \"login.html\", 800);\n                    return;\n                }\n\n                currentUser = user;\n\n                \/\/ \u2705 Paksa tunjuk shared status header bila user sudah login\n                if (statusEl) statusEl.style.display = \"block\";\n\n                \/\/ load semua tab\n                loadMedia();\n                loadContacts();\n                loadTemplates();\n            });\n\n            \/\/ ================== TAB SWITCH ==================\n            const tabs = Array.from(document.querySelectorAll(\".tab\"));\n            const panels = {\n                media: document.getElementById(\"panel-media\"),\n                contacts: document.getElementById(\"panel-contacts\"),\n                templates: document.getElementById(\"panel-templates\"),\n            };\n\n            tabs.forEach(t => {\n                t.addEventListener(\"click\", () => {\n                    tabs.forEach(x => x.classList.remove(\"active\"));\n                    t.classList.add(\"active\");\n                    const key = t.dataset.tab;\n                    Object.keys(panels).forEach(k => {\n                        panels[k].style.display = (k === key) ? \"block\" : \"none\";\n                    });\n                    if (key === \"media\") loadMedia();\n                    if (key === \"contacts\") loadContacts();\n                    if (key === \"templates\") loadTemplates();\n                });\n            });\n\n            \/\/ ================== MEDIA ==================\n            const inputMedia = document.getElementById(\"inputMedia\");\n            const mediaGrid = document.getElementById(\"mediaGrid\");\n            const mediaNote = document.getElementById(\"mediaNote\");\n\n            inputMedia.addEventListener(\"change\", async (e) => {\n                if (!currentUser) return;\n                const file = e.target.files[0];\n                if (!file) return;\n\n                const isVideo = file.type.startsWith(\"video\");\n                let uploadFile = file; \/\/ default: guna file asal\n\n                try {\n                    \/\/ \ud83c\udfaf Kalau VIDEO: kekal limit 20MB (tak compress)\n                    if (isVideo) {\n                        if (file.size > MAX_VIDEO_BYTES) {\n                            const maxMb = (MAX_VIDEO_BYTES \/ 1024 \/ 1024).toFixed(0);\n\n                            \/\/ popup\n                            const msg =\n                                `Video terlalu besar. Maksimum ${maxMb}MB sahaja.\\n\\n` +\n                                `Sila compress video dahulu sebelum upload.\\n` +\n                                `Klik \"OK\" untuk buka laman Compress Video.`;\n                            if (confirm(msg)) {\n                                window.open(\"https:\/\/www.onlineconverter.com\/compress-video\", \"_blank\");\n                            }\n\n                            \/\/ nota + button di bawah tajuk Media\n                            mediaNote.dataset.persistent = \"1\";\n                            mediaNote.innerHTML =\n                                `Video terlalu besar (>${maxMb}MB). ` +\n                                `Sila compress dahulu kemudian cuba upload semula. ` +\n                                `<button type=\"button\" class=\"btn\" ` +\n                                `onclick=\"window.open('https:\/\/www.onlineconverter.com\/compress-video','_blank')\">` +\n                                `Compress Video<\/button>`;\n\n                            e.target.value = \"\";\n                            return;\n                        }\n                    } else {\n                        \/\/ \ud83c\udfaf Kalau GAMBAR: cuba auto-compress jika > MAX_IMAGE_BYTES\n                        if (file.size > MAX_IMAGE_BYTES) {\n                            mediaNote.textContent = \"Gambar besar, sedang compress...\";\n                            uploadFile = await compressImage(file, 1280, 0.7); \/\/ width max 1280px\n                        }\n                    }\n\n                    mediaNote.textContent = \"Sedang upload \" + uploadFile.name + \"...\";\n                    const uid = currentUser.uid;\n\n                    \/\/ \ud83d\udd22 Semak jumlah media sedia ada\n                    const existing = await db.collection(\"wa_files_media\")\n                        .where(\"uid\", \"==\", uid)\n                        .get();\n\n                    if (existing.size >= MAX_MEDIA_FILES) {\n                        mediaNote.textContent = \"\";\n                        alert(`Anda sudah simpan ${MAX_MEDIA_FILES} media. Sila padam salah satu dahulu sebelum upload baru.`);\n                        e.target.value = \"\";\n                        return;\n                    }\n\n                    const path = `wa-files\/${uid}\/media\/${Date.now()}-${uploadFile.name}`;\n                    const ref = storage.ref().child(path);\n                    await ref.put(uploadFile);\n                    const url = await ref.getDownloadURL();\n\n                    await db.collection(\"wa_files_media\").add({\n                        uid,\n                        filename: uploadFile.name,\n                        storagePath: path,\n                        downloadURL: url,\n                        type: isVideo ? \"video\" : \"image\",\n                        size: uploadFile.size,\n                        createdAt: firebase.firestore.FieldValue.serverTimestamp()\n                    });\n\n                    mediaNote.textContent = \"Upload siap.\";\n                    delete mediaNote.dataset.persistent;   \/\/ reset supaya nota lepas ni auto-clear\n                    loadMedia();\n                } catch (err) {\n                    console.error(err);\n                    mediaNote.textContent = \"Ralat upload: \" + err.message;\n                } finally {\n                    if (!mediaNote.dataset.persistent) {\n                        setTimeout(() => mediaNote.textContent = \"\", 2500);\n                    }\n                    e.target.value = \"\";\n                }\n            });\n\n            async function loadMedia() {\n                if (!currentUser) return;\n                mediaGrid.innerHTML = \"<div class='muted'>Memuatkan media...<\/div>\";\n                try {\n                    const q = await db.collection(\"wa_files_media\")\n                        .where(\"uid\", \"==\", currentUser.uid)\n                        .orderBy(\"createdAt\", \"desc\")\n                        .get();\n                    if (q.empty) {\n                        mediaGrid.innerHTML = \"<div class='muted'>Tiada media disimpan.<\/div>\";\n                        return;\n                    }\n                    const cards = [];\n                    q.forEach(doc => {\n                        const d = doc.data();\n                        const url = d.downloadURL;\n                        const isVideo = d.type === \"video\" || (d.filename || \"\").match(\/\\.(mp4|mov|m4v|webm)$\/i);\n                        const mediaTag = isVideo\n                            ? `<video src=\"${url}\" controls preload=\"metadata\"><\/video>`\n                            : `<img decoding=\"async\" src=\"${url}\" alt=\"\">`;\n                        cards.push(`\n          <div class=\"card-media\">\n            <div class=\"preview\">${mediaTag}<\/div>\n            <div class=\"meta\">\n              <div class=\"row-between\">\n                <div class=\"filename\" title=\"${d.filename || \"\"}\">${d.filename || \"-\"}<\/div>\n                <button class=\"btn danger\" onclick=\"deleteMedia('${doc.id}')\">Padam<\/button>\n              <\/div>\n              <div class=\"row\">\n                <a class=\"btn\" href=\"${url}\" download=\"${d.filename || 'media'}\">Download<\/a>\n                <span class=\"muted\">${fmtSize(d.size)} \u2022 ${fmtDate(d.createdAt)}<\/span>\n              <\/div>\n            <\/div>\n          <\/div>\n        `);\n                    });\n                    mediaGrid.innerHTML = cards.join(\"\");\n                } catch (err) {\n                    console.error(err);\n                    mediaGrid.innerHTML = \"<div class='muted'>Ralat memuat media.<\/div>\";\n                }\n            }\n\n            async function deleteMedia(id) {\n                if (!currentUser) return;\n                if (!confirm(\"Padam media ini?\")) return;\n                try {\n                    const docRef = db.collection(\"wa_files_media\").doc(id);\n                    const snap = await docRef.get();\n                    if (snap.exists) {\n                        const d = snap.data();\n                        if (d.storagePath) {\n                            await storage.ref().child(d.storagePath).delete().catch(() => { });\n                        }\n                        await docRef.delete();\n                    }\n                    loadMedia();\n                } catch (err) {\n                    alert(\"Gagal padam: \" + err.message);\n                }\n            }\n            window.deleteMedia = deleteMedia; \/\/ supaya boleh dipanggil dari HTML\n\n            \/\/ ================== CONTACT FILES ==================\n            const inputContacts = document.getElementById(\"inputContacts\");\n            const contactsTbody = document.getElementById(\"contactsTbody\");\n            const contactsNote = document.getElementById(\"contactsNote\");\n\n            inputContacts.addEventListener(\"change\", async (e) => {\n                if (!currentUser) return;\n                const files = Array.from(e.target.files || []);\n                if (!files.length) return;\n                const uid = currentUser.uid;\n                for (const file of files) {\n                    contactsNote.textContent = \"Upload \" + file.name + \"...\";\n                    const path = `wa-files\/${uid}\/contacts\/${Date.now()}-${file.name}`;\n\n                    try {\n                        \/\/ \ud83d\udd22 Kira jumlah contact sebelum upload\n                        const contactCount = await kiraJumlahContact(file);\n\n                        const ref = storage.ref().child(path);\n                        await ref.put(file);\n                        const url = await ref.getDownloadURL();\n\n                        await db.collection(\"wa_files_contacts\").add({\n                            uid,\n                            filename: file.name,\n                            storagePath: path,\n                            downloadURL: url,\n                            size: file.size,\n                            contactCount: contactCount,   \/\/ \u2b05\ufe0f simpan di Firestore\n                            createdAt: firebase.firestore.FieldValue.serverTimestamp()\n                        });\n                    } catch (err) {\n                        console.error(err);\n                        contactsNote.textContent = \"Ralat upload: \" + err.message;\n                    }\n                }\n                contactsNote.textContent = \"Upload siap.\";\n                setTimeout(() => contactsNote.textContent = \"\", 2500);\n                e.target.value = \"\";\n                loadContacts();\n            });\n\n            async function loadContacts() {\n                if (!currentUser) return;\n                contactsTbody.innerHTML = `<tr><td colspan=\"6\" class=\"muted\">Memuatkan...<\/td><\/tr>`;\n                try {\n                    const q = await db.collection(\"wa_files_contacts\")\n                        .where(\"uid\", \"==\", currentUser.uid)\n                        .orderBy(\"createdAt\", \"desc\")\n                        .get();\n                    if (q.empty) {\n                        contactsTbody.innerHTML = `<tr><td colspan=\"6\" class=\"muted\">Tiada fail contact.<\/td><\/tr>`;\n                        return;\n                    }\n                    const rows = [];\n                    q.forEach(doc => {\n                        const d = doc.data();\n                        const url = d.downloadURL;\n                        const name = d.filename || \"-\";\n                        const extMatch = name.match(\/\\.([a-z0-9]+)$\/i);\n                        const ext = extMatch ? extMatch[1].toLowerCase() : \"-\";\n                        const count = (typeof d.contactCount === \"number\") ? d.contactCount : \"-\";\n\n                        rows.push(`\n                          <tr>\n                            <td>${name}<\/td>\n                            <td>${ext}<\/td>\n                            <td>${fmtSize(d.size)}<\/td>\n                            <td>${count}<\/td>\n                            <td><a class=\"btn\" href=\"${url}\" download=\"${name}\">Download<\/a><\/td>\n                            <td><button class=\"btn danger\" onclick=\"deleteContactFile('${doc.id}')\">Padam<\/button><\/td>\n                          <\/tr>\n                        `);\n                    });\n                    contactsTbody.innerHTML = rows.join(\"\");\n\n                } catch (err) {\n                    console.error(err);\n                    contactsTbody.innerHTML = `<tr><td colspan=\"6\" class=\"muted\">Ralat memuat data.<\/td><\/tr>`;\n                }\n            }\n\n            async function deleteContactFile(id) {\n                if (!currentUser) return;\n                if (!confirm(\"Padam fail contact ini?\")) return;\n                try {\n                    const ref = db.collection(\"wa_files_contacts\").doc(id);\n                    const snap = await ref.get();\n                    if (snap.exists) {\n                        const d = snap.data();\n                        if (d.storagePath) {\n                            await storage.ref().child(d.storagePath).delete().catch(() => { });\n                        }\n                        await ref.delete();\n                    }\n                    loadContacts();\n                } catch (err) {\n                    alert(\"Gagal padam: \" + err.message);\n                }\n            }\n            window.deleteContactFile = deleteContactFile;\n\n            \/\/ ================== TEMPLATES ==================\n            const templatesTbody = document.getElementById(\"templatesTbody\");\n            const tplName = document.getElementById(\"tplName\");\n            const tplContent = document.getElementById(\"tplContent\");\n\n            async function loadTemplates() {\n                if (!currentUser) return;\n                templatesTbody.innerHTML = `<tr><td colspan=\"3\" class=\"muted\">Memuatkan...<\/td><\/tr>`;\n                try {\n                    const q = await db.collection(\"wa_templates_copywriting\")\n                        .where(\"uid\", \"==\", currentUser.uid)\n                        .orderBy(\"updatedAt\", \"desc\")\n                        .get();\n                    if (q.empty) {\n                        templatesTbody.innerHTML = `<tr><td colspan=\"3\" class=\"muted\">Tiada template.<\/td><\/tr>`;\n                        return;\n                    }\n                    const rows = [];\n                    q.forEach(doc => {\n                        const d = doc.data();\n                        rows.push(`\n          <tr>\n            <td>${d.name || \"-\"}<\/td>\n            <td>${fmtDate(d.updatedAt)}<\/td>\n            <td>\n              <button class=\"btn\" onclick=\"loadTemplate('${doc.id}')\">Buka<\/button>\n            <\/td>\n          <\/tr>\n        `);\n                    });\n                    templatesTbody.innerHTML = rows.join(\"\");\n                } catch (err) {\n                    console.error(err);\n                    templatesTbody.innerHTML = `<tr><td colspan=\"3\" class=\"muted\">Ralat memuat template.<\/td><\/tr>`;\n                }\n            }\n\n            async function loadTemplate(id) {\n                if (!currentUser) return;\n                try {\n                    const snap = await db.collection(\"wa_templates_copywriting\").doc(id).get();\n                    if (!snap.exists) return;\n                    const d = snap.data();\n                    currentTplId = id;\n                    tplName.value = d.name || \"\";\n                    tplContent.value = d.content || \"\";\n                } catch (err) {\n                    alert(\"Gagal buka template: \" + err.message);\n                }\n            }\n            window.loadTemplate = loadTemplate;\n\n            document.getElementById(\"btnTemplatesRefresh\").addEventListener(\"click\", loadTemplates);\n            document.getElementById(\"btnTemplateBaru\").addEventListener(\"click\", () => {\n                currentTplId = null;\n                tplName.value = \"\";\n                tplContent.value = \"\";\n            });\n\n            document.getElementById(\"btnTemplateSimpan\").addEventListener(\"click\", async () => {\n                if (!currentUser) return;\n                const nameRaw = tplName.value.trim();\n                const content = tplContent.value || \"\";\n                if (!nameRaw) { alert(\"Sila isi nama template.\"); return; }\n                const name = nameRaw.replace(\/\\s+\/g, \"-\").substring(0, 60);\n\n                const payload = {\n                    uid: currentUser.uid,\n                    name,\n                    content,\n                    updatedAt: firebase.firestore.FieldValue.serverTimestamp()\n                };\n\n                try {\n                    if (currentTplId) {\n                        await db.collection(\"wa_templates_copywriting\").doc(currentTplId).set(payload, { merge: true });\n                    } else {\n                        const ref = await db.collection(\"wa_templates_copywriting\").add(payload);\n                        currentTplId = ref.id;\n                    }\n                    toast(\"Template disimpan.\");\n                    loadTemplates();\n                } catch (err) {\n                    alert(\"Gagal simpan template: \" + err.message);\n                }\n            });\n\n            document.getElementById(\"btnTemplatePadam\").addEventListener(\"click\", async () => {\n                if (!currentUser || !currentTplId) return;\n                if (!confirm(\"Padam template ini?\")) return;\n                try {\n                    await db.collection(\"wa_templates_copywriting\").doc(currentTplId).delete();\n                    currentTplId = null;\n                    tplName.value = \"\";\n                    tplContent.value = \"\";\n                    loadTemplates();\n                } catch (err) {\n                    alert(\"Gagal padam template: \" + err.message);\n                }\n            });\n\n            document.getElementById(\"btnTemplateCopy\").addEventListener(\"click\", async () => {\n                try {\n                    await navigator.clipboard.writeText(tplContent.value || \"\");\n                    toast(\"Disalin ke clipboard.\");\n                } catch (err) {\n                    alert(\"Tidak boleh copy: \" + err.message);\n                }\n            });\n        <\/script>\n<\/body>\n\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>File Manager \u2014 WhatsApp Blaster \ud83d\udce7 Email: \u2013 \ud83d\udcbc Pakej: \u2013 \ud83d\udce4 Mesej Baki: \u2013 (\u2013 hari) \ud83d\udcf1 WhatsApp Aktif: \u2013 \ud83d\udd14 Sambungkan Telegram \ud83d\udeaa Log Keluar \u26a0\ufe0f Langganan anda telah tamat atau mesej telah habis. Sila upgrade untuk terus gunakan sistem ini. AbataBlaster \u2014 File Manager WhatsApp Media Senarai Contact Template Copywriting Media (gambar [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-1160","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/1160","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/comments?post=1160"}],"version-history":[{"count":19,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/1160\/revisions"}],"predecessor-version":[{"id":1179,"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/pages\/1160\/revisions\/1179"}],"wp:attachment":[{"href":"https:\/\/abatablaster.xyz\/index.php\/wp-json\/wp\/v2\/media?parent=1160"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}