Autofill Tiktok Multi-Product

Autofill Tiktok with Multi-Product Support + CSV Sync

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Autofill Tiktok Multi-Product 
// @version      1.2
// @description  Autofill Tiktok with Multi-Product Support + CSV Sync
// @match        https://www.tiktok.com/*
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @namespace https://greasyfork.org/users/1388863
// ==/UserScript==

(function () {
  'use strict';

  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const rand = (min, max) => Math.random() * (max - min) + min;
  const sleepRandom = (min = 1000, max = 1500) => sleep(rand(min, max));

  // ===== FUNGSI UTAMA: runFilling untuk 1 produk =====
  async function runFillingForOneProduct({
    searchText = 'Office',
    radioValue = 'Panduan Cepat Kuasai Microsoft Office',
    deskripsi = 'belajar office dengan mudah',
    productIndex = 1
  } = {}) {
    console.log(`🚀 runFilling Product ${productIndex}:`, {searchText, radioValue, deskripsi});

    try {
      function safeClick(el) {
        if (!el) return false;
        try {
          try { if (el.getAttribute && el.getAttribute('aria-disabled') === 'true') el.setAttribute('aria-disabled', 'false'); } catch(e){}
          try { if (el.disabled) el.disabled = false; } catch(e){}
          el.click();
          return true;
        } catch (err) {
          try {
            ['mouseover','mousedown','mouseup','click'].forEach(type => {
              el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }))
            });
            return true;
          } catch (err2) {
            console.warn('safeClick failed', err, err2);
            return false;
          }
        }
      }

      function clickButtonExact(text) {
        const btn = Array.from(document.querySelectorAll('button, [role="button"], .TUXButton'))
          .find(b => (b.textContent || '').trim().toLowerCase() === String(text).toLowerCase());
        if (!btn) return false;
        return safeClick(btn);
      }

      function findAddButton() {
        const footer = document.querySelector('.common-modal-footer');
        let btn = (footer && footer.querySelector('.TUXButton--primary')) || document.querySelector('button.TUXButton--primary');
        if (!btn) {
          btn = Array.from(document.querySelectorAll('button, [role="button"], .TUXButton'))
            .find(b => (b.textContent || '').trim().toLowerCase().includes('add'));
        }
        return btn || null;
      }

      console.log(`📋 Product ${productIndex}: Starting filling process...`);

      // 1) Add (first)
      try {
        const addBtn1 = findAddButton();
        if (addBtn1) {
          const ok = safeClick(addBtn1);
          console.log(`🖱️ Product ${productIndex}: Add (first) clicked?`, !!ok);
        } else {
          console.warn(`⚠️ Product ${productIndex}: Add (first) not found`);
        }
      } catch (e) { console.warn('Error clicking Add (first)', e); }
      await sleepRandom();

      // 2) Next (first)
      try {
        const ok = clickButtonExact('Next');
        console.log(`🖱️ Product ${productIndex}: Next (first) clicked?`, !!ok);
      } catch (e) { console.warn('Error clicking Next (first)', e); }
      await sleepRandom();

      // 3) Fill search input
      try {
        const input = document.querySelector('input.TUXTextInputCore-input[placeholder="Search products"]');
        if (!input) { console.warn(`❌ Product ${productIndex}: search input not found`); return false; }
        const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
        if (setter) setter.call(input, searchText); else input.value = searchText;
        input.dispatchEvent(new Event('input',{bubbles:true}));
        input.dispatchEvent(new Event('change',{bubbles:true}));
        console.log(`⌨️ Product ${productIndex}: search filled:`, searchText);
      } catch (e) { console.warn('Error filling search', e); return false; }
      await sleepRandom();

      // 4) Press Enter on search
      try {
        const input = document.querySelector('input.TUXTextInputCore-input[placeholder="Search products"]');
        if (input) {
          const enterOpts = { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13, which: 13 };
          ['keydown','keypress','keyup'].forEach(ev => input.dispatchEvent(new KeyboardEvent(ev, enterOpts)));
          console.log(`↩️ Product ${productIndex}: Enter dispatched on search input`);
        }
      } catch (e) { console.warn('Error dispatching Enter', e); }
      await sleepRandom();

      // 5) Select radio
      try {
        let radio = null;
        try {
          const safeVal = radioValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
          radio = document.querySelector(`input[type="radio"][value="${safeVal}"]`);
          if (!radio) {
            radio = document.querySelector(`input[type="radio"][name="${safeVal}"]`);
          }
          if (radio) {
            console.log(`✅ Product ${productIndex}: Radio found via direct selector!`);
          }
        } catch (errSel) {
          console.warn('Direct selector check failed:', errSel);
        }

        if (!radio) {
          console.log(`⚠️ Product ${productIndex}: Direct selector failed, trying robust search...`);

          function normalizeForCompare(s) {
            if (s == null) return '';
            return String(s).replace(/\u00A0/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
          }

          function findRadioByValueRobust(targetValue) {
            const normalizedTarget = normalizeForCompare(targetValue);
            const candidates = Array.from(document.querySelectorAll('input[type="radio"].TUXRadioStandalone-input, input[type="radio"]'));

            let found = candidates.find(el => normalizeForCompare(el.value) === normalizedTarget);
            if (found) return found;

            found = candidates.find(el => {
              try {
                const id = el.id;
                if (id) {
                  const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
                  if (label && normalizeForCompare(label.textContent) === normalizedTarget) return true;
                }
                const parentLabel = el.closest('label');
                if (parentLabel && normalizeForCompare(parentLabel.textContent) === normalizedTarget) return true;
              } catch (e) {}
              return false;
            });
            if (found) return found;

            return null;
          }

          radio = findRadioByValueRobust(radioValue);
        }

        if (!radio) {
          console.warn(`❌ Product ${productIndex}: radio not found (all methods):`, radioValue);
          return false;
        }

        radio.click();
        try {
          radio.checked = true;
        } catch (e) {}
        radio.dispatchEvent(new Event('input', { bubbles: true }));
        radio.dispatchEvent(new Event('change', { bubbles: true }));

        console.log(`✅ Product ${productIndex}: radio selected:`, radioValue);
      } catch (e) { console.warn('Error selecting radio', e); return false; }
      await sleepRandom();

      // 6) Next (second)
      try {
        console.log(`🔘 Product ${productIndex}: Finding Next (second) via code2 logic...`);
        const footer = document.querySelector('.common-modal-footer');
        let nextBtn = (footer && footer.querySelector('.TUXButton--primary')) || document.querySelector('button.TUXButton--primary');
        if (!nextBtn) {
          nextBtn = Array.from(document.querySelectorAll('button, [role="button"], .TUXButton'))
            .find(b => (b.textContent || '').trim().toLowerCase().includes('next'));
        }
        if (!nextBtn) { console.warn(`❌ Product ${productIndex}: Next (second) not found`); return false; }
        const ok = safeClick(nextBtn);
        console.log(`🖱️ Product ${productIndex}: Next (second) clicked?`, !!ok, {text: (nextBtn.textContent||'').trim(), classes: nextBtn.className});
      } catch (e) { console.warn('Error clicking Next (second)', e); return false; }
      await sleepRandom();

      // 7) Fill description
      try {
        let descInput = document.querySelector('input.TUXTextInputCore-input#\\:r2hi\\:')
          || Array.from(document.querySelectorAll('input.TUXTextInputCore-input')).find(el => (el.value || '').includes('Panduan Cepat Kuasai Microsoft'))
          || document.querySelector('.TUXTextInputCore input.TUXTextInputCore-input');
        if (!descInput) { console.warn(`❌ Product ${productIndex}: description input not found`); return false; }
        const setter2 = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
        const old = descInput.value;
        if (setter2) setter2.call(descInput, deskripsi); else descInput.value = deskripsi;
        descInput.dispatchEvent(new Event('input',{bubbles:true}));
        descInput.dispatchEvent(new Event('change',{bubbles:true}));
        console.log(`📝 Product ${productIndex}: description set:`, {from: old, to: deskripsi});
      } catch (e) { console.warn('Error filling description', e); return false; }
      await sleepRandom();

      // 8) Add (final)
      try {
        const addBtn2 = findAddButton();
        if (addBtn2) {
          const ok = safeClick(addBtn2);
          console.log(`🖱️ Product ${productIndex}: Add (final) clicked?`, !!ok, addBtn2 && {text: (addBtn2.textContent||'').trim(), classes: addBtn2.className});
        } else {
          console.warn(`⚠️ Product ${productIndex}: Add (final) not found`);
        }
      } catch (e) { console.warn('Error clicking Add (final)', e); return false; }
      await sleepRandom();

      console.log(`✅ Product ${productIndex}: Filling process complete!`);
      return true;

    } catch (err) {
      console.error(`❌ Product ${productIndex}: runFilling error:`, err);
      return false;
    }
  }

  // ===== FUNGSI WRAPPER: runAutomation untuk multi-product =====
  async function runAutomation({
    products = [],
    currentdate = "2025-11-12",
    targetDate = "2025-11-13",
    targetjam = "21",
    targetmenit = "00",
    autoUpload = false,
    uploadNow = false
  } = {}) {
    console.log('🎬 runAutomation started with', products.length, 'products');

    try {
      // Loop through all products
      for (let i = 0; i < products.length; i++) {
        const product = products[i];
        const productIndex = i + 1;

        console.log(`\n🔄 Processing Product ${productIndex}/${products.length}`);
        showTempNotice(`Processing Product ${productIndex}/${products.length}`, 0);

        // Run filling for this product
        const success = await runFillingForOneProduct({
          searchText: product.searchText,
          radioValue: product.radioValue,
          deskripsi: product.deskripsi,
          productIndex: productIndex
        });

        if (!success) {
          console.error(`❌ Product ${productIndex} failed, stopping automation`);
          showTempNotice(`❌ Product ${productIndex} failed`, 3000);
          return false;
        }

        // Delay 2 detik sebelum produk berikutnya (kecuali produk terakhir)
        if (i < products.length - 1) {
          console.log(`⏱️ Waiting 2 seconds before next product...`);
          await sleepRandom();
        }
      }

      console.log('\n✅ All products filled successfully!');

      // ===== SCHEDULING/POST NOW (dilakukan sekali setelah semua produk) =====
      if (uploadNow) {
        console.log('\n⚡ Upload Now mode - skipping scheduling...');
        await sleepRandom();
        const postNowRadio = document.querySelector('input[type="radio"][value="post_now"]');
        if (!postNowRadio) {
          console.warn('⚠️ Radio "post_now" tidak ditemukan!');
        } else {
          postNowRadio.click();
          console.log('✅ Radio "post_now" diklik.');
        }
        console.log('✔️ Upload Now mode complete.');
      } else {
        console.log('\n📅 Running scheduling process...');
        await runScheduling({currentdate, targetDate, targetjam, targetmenit});
        console.log('✔️ Scheduling complete.');
      }

      // ===== AUTO UPLOAD (jika enabled) =====
      if (autoUpload) {
        await triggerAutoUpload();
      }

      console.log('\n🎉 runAutomation COMPLETE!');
      showTempNotice('✅ Automation Complete!', 2000);
      return true;

    } catch (err) {
      console.error('❌ runAutomation error:', err);
      showTempNotice('❌ Automation Error', 3000);
      return false;
    }
  }

  // ===== FUNGSI SCHEDULING (dipanggil sekali setelah semua produk) =====
  async function runScheduling({currentdate, targetDate, targetjam, targetmenit}) {
    const randDelay = async (min = 200, max = 500) => sleep(rand(min, max));

    // Klik radio "schedule"
    await randDelay();
    const scheduleRadio = document.querySelector('input[type="radio"][value="schedule"]');
    if (!scheduleRadio) {
      console.warn('Radio "schedule" tidak ditemukan!');
    } else {
      scheduleRadio.click();
      console.log('✅ Radio "schedule" diklik.');
    }

    await randDelay();

    // Pilih tanggal
    const dateInputSelector = `input[readonly][value="${currentdate}"]`;
    let dateInput = document.querySelector(dateInputSelector);
    if (!dateInput) {
      dateInput = document.querySelector('input[readonly].TUXTextInputCore-input') || document.querySelector('input[readonly]');
    }
    if (!dateInput) {
      console.warn(`Input tanggal dengan value "${currentdate}" tidak ditemukan.`);
    } else {
      dateInput.click();
      console.log(`✅ Input tanggal (currentValue="${currentdate}") diklik.`);
    }

    await randDelay();

    // Parse dates
    const [tY, tM, tD] = targetDate.split('-').map(Number);
    const [cY, cM] = currentdate.split('-').map(Number);
    const diff = (tY - cY) * 12 + (tM - cM);

    await randDelay();

    // Navigate months
    const arrows = document.querySelectorAll('.jsx-1793871833.arrow');
    if (arrows && arrows.length >= 2) {
      const arrowToClick = diff > 0 ? arrows[1] : arrows[0];
      const steps = Math.abs(diff);
      for (let i = 0; i < steps; i++) {
        await randDelay();
        arrowToClick.click();
        console.log(`⤴︎ Navigasi bulan (${i + 1}/${steps})`);
      }
    }

    await randDelay();

    // Pilih hari
    const dayEls = Array.from(document.querySelectorAll('span.day.valid, td.day:not(.disabled), button.day, .datepicker-day, .react-datepicker__day'))
      .filter(el => el.textContent && el.textContent.trim() === String(tD));
    if (dayEls.length > 0) {
      const targetDay = dayEls.find(el => el.offsetParent !== null) || dayEls[0];
      await randDelay();
      targetDay.click();
      console.log(`✅ Tanggal ${targetDate} dipilih.`);
    } else {
      console.warn(`⚠️ Elemen hari "${tD}" tidak ditemukan.`);
    }

    await randDelay();

    // Pilih jam & menit
    function clickOptionByText(side, text) {
      const sel = `.tiktok-timepicker-option-text.tiktok-timepicker-${side}`;
      const els = Array.from(document.querySelectorAll(sel));
      const target = els.find(el => (el.textContent || '').trim() === text);
      if (!target) return null;
      try {
        target.click();
      } catch (e) {
        ['mousedown','mouseup','click'].forEach(type => {
          target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }))
        });
      }
      return target;
    }

    await randDelay();
    const jamEl = clickOptionByText('left', String(targetjam));
    if (jamEl) {
      console.log(`✅ Jam "${targetjam}" dipilih.`);
    } else {
      console.warn(`⚠️ Opsi jam "${targetjam}" tidak ditemukan.`);
    }

    await randDelay();
    const menitEl = clickOptionByText('right', String(targetmenit));
    if (menitEl) {
      console.log(`✅ Menit "${targetmenit}" dipilih.`);
    } else {
      console.warn(`⚠️ Opsi menit "${targetmenit}" tidak ditemukan.`);
    }
  }

  async function triggerAutoUpload() {
    try {
      const delayBeforeUpload = rand(500, 1000);
      console.log(`⏱️ Waiting ${Math.round(delayBeforeUpload)}ms before AutoUpload...`);
      await sleep(delayBeforeUpload);

      const postButton = document.querySelector('button[data-e2e="post_video_button"]');
      if (postButton) {
        postButton.click();
        console.log('🖱️ post_video_button clicked');
      } else {
        console.warn('❌ post_video_button not found');
      }

      const delay2 = rand(500, 1000);
      console.log(`⏱️ Waiting ${Math.round(delay2)}ms before clicking "Post now"...`);
      await sleep(delay2);

      const buttons = document.querySelectorAll('button.TUXButton');
      const targetButton = Array.from(buttons).find(btn => (btn.textContent || '').trim() === 'Post now');
      if (targetButton) {
        targetButton.click();
        console.log('✅ "Post now" clicked');
      } else {
        console.warn('❌ "Post now" not found');
      }
    } catch (errAuto) {
      console.error('Error during autoUpload', errAuto);
    }
  }

  // Expose to window
  window.runAutomation = runAutomation;

  /* ============================
     UI + localStorage + Multi-Product Management
     ============================ */

  const LS_KEY = 'runFillingSettings';
  const PRESETS_KEY = 'runFillingPresets';
  const CSV_URL_KEY = 'csvUrl';

  const defaultSettings = {
    totalProduct: 1,
    products: [
      {
        searchText: 'Office',
        radioValue: 'Panduan Cepat Kuasai Microsoft Office',
        deskripsi: 'belajar office dengan mudah'
      }
    ],
    currentdate: "2025-11-12",
    targetDate: "2025-11-13",
    targetjam: "21",
    targetmenit: "00",
    increaseDate: false,
    increaseHours: false,
    autoUpload: false,
    uploadNow: false,
    increaseHoursAmount: 2,
    increaseMinutesAmount: 0,
    counterStart: 1,
    currentCounter: 1,
    selectedPreset: '',
    csvUrl: ''
  };

  function saveRunSettings(obj) {
    localStorage.setItem(LS_KEY, JSON.stringify(obj));
  }

  function loadRunSettings() {
    try {
      const raw = localStorage.getItem(LS_KEY);
      if (!raw) {
        saveRunSettings(defaultSettings);
        return { ...defaultSettings };
      }
      return { ...defaultSettings, ...JSON.parse(raw) };
    } catch (e) {
      console.warn('loadRunSettings error', e);
      saveRunSettings(defaultSettings);
      return { ...defaultSettings };
    }
  }

  function escapeHtml(str) {
    return String(str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  }

  let noticeTimeout = null;
  function showTempNotice(text, ms = 1400) {
    let el = document.getElementById('rf_notice');
    if (!el) {
      el = document.createElement('div');
      el.id = 'rf_notice';
      Object.assign(el.style, {
        position: 'fixed',
        left: '20px',
        bottom: '60px',
        zIndex: 13000,
        padding: '10px 12px',
        background: '#007bff',
        color: '#fff',
        borderRadius: '4px',
        fontSize: '12px',
        opacity: '0',
        transition: 'opacity 0.3s ease',
        boxShadow: '0 4px 12px rgba(0, 123, 255, 0.3)',
        maxWidth: '220px',
        fontWeight: '500'
      });
      document.body.appendChild(el);
    }

    if (noticeTimeout) {
      clearTimeout(noticeTimeout);
      noticeTimeout = null;
    }

    el.textContent = text;
    el.style.opacity = '1';

    if (ms > 0) {
      noticeTimeout = setTimeout(() => {
        el.style.opacity = '0';
        noticeTimeout = null;
      }, ms);
    }
  }

  function parseYMD(s) {
    const p = String(s).split('-').map(Number);
    if (p.length !== 3 || p.some(isNaN)) return null;
    return new Date(p[0], p[1] - 1, p[2]);
  }

  function formatYMD(d) {
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  }

  let settingsPopup = null;

  function updateCounterDisplay() {
    const s = loadRunSettings();
    const counterEl = document.getElementById('rf_counter');
    if (counterEl) {
      counterEl.textContent = `#${s.currentCounter}`;
    }
  }

  function toggleRunSettingsPopup() {
    if (settingsPopup) {
      settingsPopup.remove();
      settingsPopup = null;
      return;
    }

    const s = loadRunSettings();
    settingsPopup = document.createElement('div');
    Object.assign(settingsPopup.style, {
      position: 'fixed',
      bottom: '80px',
      left: '20px',
      background: '#1a1a1a',
      color: '#fff',
      borderRadius: '6px',
      padding: '16px',
      width: '380px',
      maxHeight: '600px',
      overflowY: 'auto',
      zIndex: 11000,
      boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
      border: '1px solid #333'
    });

    settingsPopup.innerHTML = `
      <div style="display:flex;justify-content:space-between;margin-bottom:16px">
        <strong style="font-size:18px">Multi-Product Settings</strong>
        <button id="closeRunSettings" style="background:none;border:none;color:#fff;cursor:pointer;font-size:20px;padding:0">✖</button>
      </div>

      <!-- Total Product Input -->
      <div style="margin-bottom:16px">
        <label style="display:block;margin-bottom:8px">
          <span style="display:block;font-size:13px;color:#aaa;margin-bottom:4px">Total Product</span>
          <input type="number" id="rf_totalProduct" value="${s.totalProduct}" min="1" max="10"
            style="width:100%;background:#2d2d2d;color:#fff;border:1px solid #444;border-radius:4px;padding:8px;font-size:13px">
        </label>
        <button id="rf_updateProductCards" style="width:100%;background:#007bff;color:#fff;border:none;border-radius:4px;padding:8px;cursor:pointer;font-size:13px;font-weight:600;margin-top:4px">
          Update Product Cards
        </button>
      </div>

      <!-- Product Cards Container -->
      <div id="rf_productCardsContainer" style="margin-bottom:16px">
      </div>

      <!-- Scheduling Settings -->
      <div style="margin-bottom:16px;padding:12px;background:#2d2d2d;border-radius:4px">
        <strong style="display:block;margin-bottom:8px;font-size:14px">Scheduling</strong>

        <div style="display:flex;gap:8px;margin-bottom:8px">
          <label style="flex:1">
            <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Current Date</span>
            <input id="rf_currentdate" value="${s.currentdate}"
              style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px">
          </label>
          <label style="flex:1">
            <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Target Date</span>
            <input id="rf_targetDate" value="${s.targetDate}"
              style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px">
          </label>
        </div>

        <div style="display:flex;gap:8px;margin-bottom:8px">
          <label style="flex:1">
            <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Hour</span>
            <input id="rf_targetjam" value="${s.targetjam}"
              style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px">
          </label>
          <label style="flex:1">
            <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Minute</span>
            <input id="rf_targetmenit" value="${s.targetmenit}"
              style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px">
          </label>
        </div>

        <label style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
          <input type="checkbox" id="rf_uploadNow" ${s.uploadNow?'checked':''} style="width:16px;height:16px">
          <span style="font-size:12px">Upload Now (skip scheduling)</span>
        </label>

        <label style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
          <input type="checkbox" id="rf_autoUpload" ${s.autoUpload?'checked':''} style="width:16px;height:16px">
          <span style="font-size:12px">Auto Upload</span>
        </label>

        <label style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
          <input type="checkbox" id="rf_increaseDate" ${s.increaseDate?'checked':''} style="width:16px;height:16px">
          <span style="font-size:12px">Increase Date (+1 day)</span>
        </label>

        <label style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
          <input type="checkbox" id="rf_increaseHours" ${s.increaseHours?'checked':''} style="width:16px;height:16px">
          <span style="font-size:12px">Increase Time</span>
        </label>

        <div style="display:flex;gap:8px;margin-bottom:8px">
          <label style="flex:1">
            <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">+ Hours</span>
            <input type="number" id="rf_increaseHoursAmount" value="${s.increaseHoursAmount||2}" min="0" max="23"
              style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px">
          </label>
          <label style="flex:1">
            <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">+ Minutes</span>
            <input type="number" id="rf_increaseMinutesAmount" value="${s.increaseMinutesAmount||0}" min="0" max="59"
              style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px">
          </label>
        </div>

        <div style="font-size:10px;color:#888;margin-bottom:8px">
          Example: 1h 40m = Next time +1:40<br>
          ⚠️ Time > 21:xx resets to 08:00 next day
        </div>
      </div>

      <!-- Counter -->
      <div style="margin-bottom:16px">
        <label style="display:block">
          <span style="display:block;font-size:11px;color:#aaa;margin-bottom:4px">Counter</span>
          <input type="number" id="rf_counterStart" value="${s.currentCounter}" min="1"
            style="width:100%;background:#2d2d2d;color:#fff;border:1px solid #444;border-radius:4px;padding:8px;font-size:13px">
        </label>
      </div>

      <!-- Action Buttons -->
      <div style="display:flex;gap:8px">
        <button id="rf_saveSettings" style="flex:1;background:#28a745;color:#fff;border:none;border-radius:4px;padding:10px;cursor:pointer;font-size:13px;font-weight:600">
          Save Settings
        </button>
        <button id="rf_runAutomation" style="flex:1;background:#007bff;color:#fff;border:none;border-radius:4px;padding:10px;cursor:pointer;font-size:13px;font-weight:600" disabled>
          Run Automation
        </button>
      </div>
    `;

    document.body.appendChild(settingsPopup);

    // Render initial product cards
    renderProductCards();

    // Event Listeners
    document.getElementById('closeRunSettings').onclick = toggleRunSettingsPopup;

    document.getElementById('rf_updateProductCards').onclick = () => {
      const totalProduct = parseInt(document.getElementById('rf_totalProduct').value, 10);
      if (isNaN(totalProduct) || totalProduct < 1) {
        showTempNotice('Invalid Total Product', 2000);
        return;
      }
      const s = loadRunSettings();
      s.totalProduct = totalProduct;

      // Adjust products array
      while (s.products.length < totalProduct) {
        s.products.push({
          searchText: '',
          radioValue: '',
          deskripsi: ''
        });
      }
      while (s.products.length > totalProduct) {
        s.products.pop();
      }

      saveRunSettings(s);
      renderProductCards();
      showTempNotice(`Updated to ${totalProduct} product(s)`, 2000);
    };

    document.getElementById('rf_saveSettings').onclick = () => {
      toggleRunSettingsPopup();
      saveAllSettings();
      showTempNotice('Settings saved', 2000);
    };

    document.getElementById('rf_runAutomation').onclick = async () => {
      const btn = document.getElementById('rf_runAutomation');
      if (btn.disabled) return;

      const s = loadRunSettings();

      // Collect all product data
      const products = [];
      for (let i = 0; i < s.totalProduct; i++) {
        const searchText = document.getElementById(`rf_product_${i}_searchText`)?.value.trim() || '';
        const radioValue = document.getElementById(`rf_product_${i}_radioValue`)?.value.trim() || '';
        const deskripsi = document.getElementById(`rf_product_${i}_deskripsi`)?.value.trim() || '';
        products.push({ searchText, radioValue, deskripsi });
      }

      // Save before running
      s.products = products;
      saveRunSettings(s);

      // Close popup
      toggleRunSettingsPopup();

      // Run automation
      showTempNotice('Starting automation...', 0);

      try {
        const success = await window.runAutomation({
          products: products,
          currentdate: s.currentdate,
          targetDate: s.targetDate,
          targetjam: s.targetjam,
          targetmenit: s.targetmenit,
          autoUpload: s.autoUpload,
          uploadNow: s.uploadNow
        });

        if (success) {
          // Increase logic + counter increment
          let changed = false;
          const newS = { ...s };

          if (s.increaseDate) {
            const td = parseYMD(s.targetDate);
            if (td) {
              td.setDate(td.getDate() + 1);
              newS.targetDate = formatYMD(td);
              changed = true;
            }
          }

          if (s.increaseHours) {
            const currentHours = parseInt(s.targetjam, 10);
            const currentMinutes = parseInt(s.targetmenit, 10);
            const increaseHours = parseInt(s.increaseHoursAmount, 10) || 0;
            const increaseMinutes = parseInt(s.increaseMinutesAmount, 10) || 0;

            if (!isNaN(currentHours) && !isNaN(currentMinutes)) {
              let totalMinutes = (currentHours * 60) + currentMinutes + (increaseHours * 60) + increaseMinutes;

              // Handle day overflow
              if (totalMinutes >= 24 * 60) {
                totalMinutes = totalMinutes % (24 * 60);
                if (s.increaseDate) {
                  const td = parseYMD(s.targetDate);
                  if (td) {
                    td.setDate(td.getDate() + 1);
                    newS.targetDate = formatYMD(td);
                  }
                }
              }

              let newHours = Math.floor(totalMinutes / 60);
              const newMinutes = totalMinutes % 60;

              // Reset to 08:00 if exceeds 21:xx
              if (newHours > 21) {
                newHours = 8;
                // Increase date by 1 day when resetting to 08:00
                const td = parseYMD(newS.targetDate);
                if (td) {
                  td.setDate(td.getDate() + 1);
                  newS.targetDate = formatYMD(td);
                }
              }

              newS.targetjam = String(newHours).padStart(2, '0');
              newS.targetmenit = String(newMinutes).padStart(2, '0');
              changed = true;
            }
          }

          newS.currentCounter = s.currentCounter + 1;
          changed = true;

          if (changed) {
            saveRunSettings(newS);
            updateCounterDisplay();
            showTempNotice('✅ Success - Next: #' + newS.currentCounter, 2000);
          }
        }
      } catch (err) {
        console.error('❌ Error during automation:', err);
        showTempNotice('❌ Automation Error', 3000);
      }
    };

    // Initial validation check
    validateAllProducts();
  }

  function renderProductCards() {
    const s = loadRunSettings();
    const container = document.getElementById('rf_productCardsContainer');
    if (!container) return;

    container.innerHTML = '';

    for (let i = 0; i < s.totalProduct; i++) {
      const product = s.products[i] || { searchText: '', radioValue: '', deskripsi: '' };

      const card = document.createElement('div');
      card.style.cssText = 'background:#2d2d2d;border-radius:4px;padding:12px;margin-bottom:12px;border:1px solid #444';

      card.innerHTML = `
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
          <strong style="font-size:14px;color:#007bff">Product ${i + 1}</strong>
          ${s.totalProduct > 1 ? `<button class="rf_removeProduct" data-index="${i}" style="background:#dc3545;color:#fff;border:none;border-radius:3px;padding:4px 8px;cursor:pointer;font-size:11px">Remove</button>` : ''}
        </div>

        <label style="display:block;margin-bottom:8px">
          <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Search Text *</span>
          <input id="rf_product_${i}_searchText" value="${escapeHtml(product.searchText)}" placeholder="e.g. Office"
            style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px"
            class="rf_productInput">
        </label>

        <label style="display:block;margin-bottom:8px">
          <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Radio Value *</span>
          <input id="rf_product_${i}_radioValue" value="${escapeHtml(product.radioValue)}" placeholder="e.g. Panduan Cepat..."
            style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px"
            class="rf_productInput">
        </label>

        <label style="display:block">
          <span style="display:block;font-size:11px;color:#aaa;margin-bottom:2px">Description *</span>
          <input id="rf_product_${i}_deskripsi" value="${escapeHtml(product.deskripsi)}" placeholder="e.g. belajar office..."
            style="width:100%;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px"
            class="rf_productInput">
        </label>
      `;

      container.appendChild(card);
    }

    // Attach remove handlers
    container.querySelectorAll('.rf_removeProduct').forEach(btn => {
      btn.onclick = () => {
        const index = parseInt(btn.dataset.index, 10);
        removeProduct(index);
      };
    });

    // Attach input validation handlers
    container.querySelectorAll('.rf_productInput').forEach(input => {
      input.addEventListener('input', validateAllProducts);
    });
  }

  function removeProduct(index) {
    const s = loadRunSettings();
    if (s.totalProduct <= 1) {
      showTempNotice('Cannot remove last product', 2000);
      return;
    }

    s.products.splice(index, 1);
    s.totalProduct = s.products.length;

    document.getElementById('rf_totalProduct').value = s.totalProduct;

    saveRunSettings(s);
    renderProductCards();
    showTempNotice(`Product ${index + 1} removed`, 2000);
  }

  function validateAllProducts() {
    const s = loadRunSettings();
    const totalProductInput = document.getElementById('rf_totalProduct');
    const totalProduct = parseInt(totalProductInput?.value, 10) || 0;

    let allValid = true;

    // Check if total product matches card count
    if (s.products.length !== totalProduct) {
      allValid = false;
    }

    // Validate each product has all required fields
    for (let i = 0; i < s.totalProduct; i++) {
      const searchText = document.getElementById(`rf_product_${i}_searchText`)?.value.trim() || '';
      const radioValue = document.getElementById(`rf_product_${i}_radioValue`)?.value.trim() || '';
      const deskripsi = document.getElementById(`rf_product_${i}_deskripsi`)?.value.trim() || '';

      if (!searchText || !radioValue || !deskripsi) {
        allValid = false;
        break;
      }
    }

    // Enable/disable run button
    const runBtn = document.getElementById('rf_runAutomation');
    if (runBtn) {
      runBtn.disabled = !allValid;
      runBtn.style.opacity = allValid ? '1' : '0.5';
      runBtn.style.cursor = allValid ? 'pointer' : 'not-allowed';
    }

    return allValid;
  }

  function saveAllSettings() {
    const s = loadRunSettings();

    // Save scheduling settings
    s.currentdate = document.getElementById('rf_currentdate').value.trim();
    s.targetDate = document.getElementById('rf_targetDate').value.trim();
    s.targetjam = document.getElementById('rf_targetjam').value.trim();
    s.targetmenit = document.getElementById('rf_targetmenit').value.trim();
    s.uploadNow = document.getElementById('rf_uploadNow').checked;
    s.autoUpload = document.getElementById('rf_autoUpload').checked;
    s.increaseDate = document.getElementById('rf_increaseDate').checked;
    s.increaseHours = document.getElementById('rf_increaseHours').checked;
    s.increaseHoursAmount = parseInt(document.getElementById('rf_increaseHoursAmount')?.value || '2', 10) || 2;
    s.increaseMinutesAmount = parseInt(document.getElementById('rf_increaseMinutesAmount')?.value || '0', 10) || 0;
    s.currentCounter = parseInt(document.getElementById('rf_counterStart').value, 10) || 1;

    // Save product data
    const products = [];
    for (let i = 0; i < s.totalProduct; i++) {
      const searchText = document.getElementById(`rf_product_${i}_searchText`)?.value.trim() || '';
      const radioValue = document.getElementById(`rf_product_${i}_radioValue`)?.value.trim() || '';
      const deskripsi = document.getElementById(`rf_product_${i}_deskripsi`)?.value.trim() || '';
      products.push({ searchText, radioValue, deskripsi });
    }
    s.products = products;

    saveRunSettings(s);
    updateCounterDisplay();
  }

  function createRunUI() {
    if (document.getElementById('runFillingPanel')) return;

    const panel = document.createElement('div');
    panel.id = 'runFillingPanel';
    Object.assign(panel.style, {
      position: 'fixed',
      bottom: '20px',
      left: '20px',
      background: '#1a1a1a',
      color: '#fff',
      borderRadius: '4px',
      padding: '6px',
      display: 'flex',
      gap: '4px',
      alignItems: 'center',
      zIndex: 12000,
      boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
      border: '1px solid #333'
    });

    const s = loadRunSettings();
    const counterDiv = document.createElement('div');
    counterDiv.id = 'rf_counter';
    counterDiv.textContent = `#${s.currentCounter}`;
    Object.assign(counterDiv.style, {
      background: '#007bff',
      color: '#fff',
      padding: '2px 6px',
      borderRadius: '3px',
      fontWeight: 'bold',
      fontSize: '11px',
      minWidth: '35px',
      textAlign: 'center'
    });

    const startBtn = document.createElement('button');
    startBtn.id = 'runFillingStart';
    startBtn.textContent = 'Start';
    Object.assign(startBtn.style, {
      background: '#28a745',
      color: '#fff',
      border: 'none',
      padding: '4px 8px',
      borderRadius: '3px',
      cursor: 'pointer',
      fontWeight: '600',
      fontSize: '11px',
      minWidth: '45px'
    });

    const gearBtn = document.createElement('button');
    gearBtn.innerHTML = '⚙';
    Object.assign(gearBtn.style, {
      background: '#6c757d',
      color: '#fff',
      border: 'none',
      cursor: 'pointer',
      fontSize: '12px',
      width: '24px',
      height: '24px',
      borderRadius: '3px'
    });

    startBtn.onclick = async () => {
      const s = loadRunSettings();

      // Validate products
      let allValid = true;
      for (let i = 0; i < s.products.length; i++) {
        const p = s.products[i];
        if (!p.searchText || !p.radioValue || !p.deskripsi) {
          allValid = false;
          break;
        }
      }

      if (!allValid) {
        showTempNotice('⚠️ Please configure products first', 2000);
        toggleRunSettingsPopup();
        return;
      }

      if (typeof window.runAutomation !== 'function') {
        showTempNotice('runAutomation missing', 2000);
        return;
      }

      showTempNotice(`Running... #${s.currentCounter}`, 0);

      try {
        const success = await window.runAutomation({
          products: s.products,
          currentdate: s.currentdate,
          targetDate: s.targetDate,
          targetjam: s.targetjam,
          targetmenit: s.targetmenit,
          autoUpload: s.autoUpload,
          uploadNow: s.uploadNow
        });

        if (success) {
          let changed = false;
          const newS = { ...s };

          if (s.increaseDate) {
            const td = parseYMD(s.targetDate);
            if (td) {
              td.setDate(td.getDate() + 1);
              newS.targetDate = formatYMD(td);
              changed = true;
            }
          }

          if (s.increaseHours) {
            const currentHours = parseInt(s.targetjam, 10);
            const currentMinutes = parseInt(s.targetmenit, 10);
            const increaseHours = parseInt(s.increaseHoursAmount, 10) || 0;
            const increaseMinutes = parseInt(s.increaseMinutesAmount, 10) || 0;

            if (!isNaN(currentHours) && !isNaN(currentMinutes)) {
              let totalMinutes = (currentHours * 60) + currentMinutes + (increaseHours * 60) + increaseMinutes;

              // Handle day overflow
              if (totalMinutes >= 24 * 60) {
                totalMinutes = totalMinutes % (24 * 60);
                if (s.increaseDate) {
                  const td = parseYMD(s.targetDate);
                  if (td) {
                    td.setDate(td.getDate() + 1);
                    newS.targetDate = formatYMD(td);
                  }
                }
              }

              let newHours = Math.floor(totalMinutes / 60);
              const newMinutes = totalMinutes % 60;

              // Reset to 08:00 if exceeds 21:xx
              if (newHours > 21) {
                newHours = 8;
                // Increase date by 1 day when resetting to 08:00
                const td = parseYMD(newS.targetDate);
                if (td) {
                  td.setDate(td.getDate() + 1);
                  newS.targetDate = formatYMD(td);
                }
              }

              newS.targetjam = String(newHours).padStart(2, '0');
              newS.targetmenit = String(newMinutes).padStart(2, '0');
              changed = true;
            }
          }

          newS.currentCounter = s.currentCounter + 1;
          changed = true;

          if (changed) {
            saveRunSettings(newS);
            updateCounterDisplay();
            showTempNotice('✅ Success - Next: #' + newS.currentCounter, 2000);
          }
        }
      } catch (err) {
        console.error('❌ Error during automation:', err);
        showTempNotice('❌ Automation Error', 3000);
      }
    };

    gearBtn.onclick = toggleRunSettingsPopup;

    panel.append(counterDiv, startBtn, gearBtn);
    document.body.appendChild(panel);

    // Keyboard shortcuts
    document.addEventListener('keydown', e => {
      const active = document.activeElement;
      const typing = active && /INPUT|TEXTAREA/.test(active.tagName);
      if (!typing && e.code === 'Space') {
        e.preventDefault();
        toggleRunSettingsPopup();
      }
    });
  }

  try {
    createRunUI();
  } catch (e) {
    console.error(e);
  }

})();