Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="theme-color" content="#6b21a8"> | |
| <title>Unfollinsta</title> | |
| <link rel="manifest" href="manifest.json"> | |
| <link rel="icon" type="image/png" href="icons/icon-192.png"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-color: #f9f4ff; | |
| --text-color: #1f1f1f; | |
| --card-bg: #ffffff; | |
| --primary-color: #6b21a8; | |
| --secondary-color: #d6bcfa; | |
| --text-inverse: #ffffff; | |
| --border-color: #e5e7eb; | |
| } | |
| body.dark-mode { | |
| --bg-color: #1c1c1c; | |
| --text-color: #e0e0e0; | |
| --card-bg: #2d2d2d; | |
| --primary-color: #a78bfa; | |
| --secondary-color: #4c1d95; | |
| --border-color: #4b4b4b; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| transition: background-color 0.3s, color 0.3s; | |
| } | |
| .topbar { | |
| width: 100%; | |
| max-width: 800px; | |
| padding: 1rem; | |
| background: var(--primary-color); | |
| color: var(--text-inverse); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .topbar-title { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| } | |
| .topbar-subtitle { | |
| font-size: 0.9rem; | |
| font-weight: 400; | |
| opacity: 0.9; | |
| } | |
| .settings-btn { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| padding: 0.5rem; | |
| } | |
| .settings-btn img { | |
| width: 24px; | |
| height: 24px; | |
| filter: invert(100%); | |
| } | |
| .container { | |
| max-width: 800px; | |
| width: 100%; | |
| padding: 1.5rem; | |
| background: var(--card-bg); | |
| border-radius: 12px; | |
| margin: 1rem; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
| } | |
| .description, .tutorial { | |
| font-size: 1rem; | |
| line-height: 1.5; | |
| margin-bottom: 1rem; | |
| } | |
| .tutorial .tooltip-trigger { | |
| color: var(--primary-color); | |
| text-decoration: underline; | |
| cursor: pointer; | |
| } | |
| input[type="file"], button { | |
| width: 100%; | |
| padding: 0.75rem; | |
| margin: 0.5rem 0; | |
| border-radius: 8px; | |
| font-size: 1rem; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| input[type="file"] { | |
| border: 1px solid var(--border-color); | |
| background: var(--card-bg); | |
| } | |
| button { | |
| background: var(--primary-color); | |
| color: var(--text-inverse); | |
| border: none; | |
| cursor: pointer; | |
| font-weight: 500; | |
| transition: background 0.2s; | |
| } | |
| button:hover { | |
| background: var(--secondary-color); | |
| } | |
| #result { | |
| margin-top: 1.5rem; | |
| } | |
| ol { | |
| padding-left: 1.5rem; | |
| } | |
| li { | |
| padding: 0.5rem 0; | |
| font-size: 0.95rem; | |
| } | |
| a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| font-weight: 500; | |
| } | |
| a:hover { | |
| text-decoration: underline; | |
| } | |
| .tooltip { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: var(--card-bg); | |
| color: var(--text-color); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| max-width: 90%; | |
| width: 400px; | |
| z-index: 2000; | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); | |
| display: none; | |
| } | |
| .tooltip.active { | |
| display: block; | |
| } | |
| .tooltip .close-btn { | |
| position: absolute; | |
| top: 0.5rem; | |
| right: 0.5rem; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| } | |
| #splashScreen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--bg-color); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 9999; | |
| transition: opacity 0.5s; | |
| } | |
| #splashScreen img { | |
| width: 80px; | |
| height: 80px; | |
| } | |
| #splashScreen h1 { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| } | |
| @media (max-width: 600px) { | |
| .topbar-title { | |
| font-size: 1.25rem; | |
| } | |
| .topbar-subtitle { | |
| font-size: 0.8rem; | |
| } | |
| .container { | |
| margin: 0.5rem; | |
| padding: 1rem; | |
| } | |
| .description, .tutorial { | |
| font-size: 0.9rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="splashScreen"> | |
| <img src="icons/unfollinsta192.png" alt="Unfollinsta Logo" /> | |
| <h1>Unfollinsta</h1> | |
| </div> | |
| <header class="topbar"> | |
| <div> | |
| <div class="topbar-title">Unfollinsta</div> | |
| <div class="topbar-subtitle">from terastudio</div> | |
| </div> | |
| <button class="settings-btn" aria-label="Settings"> | |
| <img src="icons/settings2.png" alt="Settings Icon"> | |
| </button> | |
| </header> | |
| <main class="container"> | |
| <p class="description" id="descriptionText"></p> | |
| <p class="tutorial" id="tutorialText"></p> | |
| <p id="uploadText"></p> | |
| <input type="file" id="zipFile" accept=".zip" aria-label="Upload ZIP file"> | |
| <button id="processBtn" onclick="processZip()">Process ZIP</button> | |
| <div id="result" role="region" aria-live="polite"></div> | |
| <p id="appVersion" style="text-align: center; font-size: 0.9rem; opacity: 0.7;"></p> | |
| <div id="tooltip" class="tooltip"> | |
| <span class="close-btn" onclick="hideTooltip()" aria-label="Close tooltip">✖</span> | |
| <p id="tooltipContent"></p> | |
| </div> | |
| </main> | |
| <script> | |
| const languageTexts = { | |
| id: { | |
| upload: 'Unggah file ZIP di bawah ini', | |
| process: 'Proses ZIP', | |
| total: 'Jumlah Unfollowers', | |
| allFollow: 'Semua orang yang kamu follow juga follow kamu balik.', | |
| fileError: 'File followers.html atau following.html tidak ditemukan.', | |
| description: 'Unfollinsta membantu mengetahui siapa yang tidak mengikuti balik akun Instagram kamu.', | |
| tutorial: `Dapatkan file ZIP dari Pusat Akun Instagram untuk diproses!<br> | |
| <span class="tooltip-trigger" onclick="showTooltip()">Lihat Tutorial</span>`, | |
| extracting: 'Mengekstrak ZIP...', | |
| tooltip: `Buka Pusat Akun > Informasi dan izin Anda > Unduh informasi Anda > Pilih akun Instagram > | |
| Pilih "Pengikut dan Mengikuti" > Unduh ke perangkat > Format HTML > Buat file > Unduh ZIP` | |
| }, | |
| en: { | |
| upload: 'Upload the ZIP file below', | |
| process: 'Process ZIP', | |
| total: 'Total Unfollowers', | |
| allFollow: 'Everyone you follow follows you back.', | |
| fileError: 'followers.html or following.html not found in ZIP.', | |
| description: 'Unfollinsta finds out who doesn’t follow back your Instagram account.', | |
| tutorial: `Get the ZIP file from Instagram Accounts Center to process!<br> | |
| <span class="tooltip-trigger" onclick="showTooltip()">See Tutorial</span>`, | |
| extracting: 'Extracting ZIP...', | |
| tooltip: `Go to Accounts Center > Your information and permissions > Download your information > | |
| Select Instagram account > Select "Followers and following" > Download to device > Format HTML > Create files > Download ZIP` | |
| } | |
| }; | |
| function extractUsernamesFromHTML(htmlContent) { | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(htmlContent, 'text/html'); | |
| return Array.from(doc.querySelectorAll('a')).map(a => a.textContent.trim()).filter(Boolean); | |
| } | |
| async function processZip() { | |
| const fileInput = document.getElementById('zipFile'); | |
| const file = fileInput.files[0]; | |
| if (!file) return; | |
| document.getElementById('result').innerHTML = languageTexts[lang].extracting; | |
| try { | |
| const zip = await JSZip.loadAsync(file); | |
| const followersFile = zip.file('connections/followers_and_following/followers_1.html'); | |
| const followingFile = zip.file('connections/followers_and_following/following.html'); | |
| if (!followersFile || !followingFile) { | |
| document.getElementById('result').innerHTML = `<p style="color: #dc2626;">${languageTexts[lang].fileError}</p>`; | |
| return; | |
| } | |
| const [followersHTML, followingHTML] = await Promise.all([ | |
| followersFile.async('string'), | |
| followingFile.async('string') | |
| ]); | |
| const followers = extractUsernamesFromHTML(followersHTML); | |
| const following = extractUsernamesFromHTML(followingHTML); | |
| const unfollowers = following.filter(user => !followers.includes(user)); | |
| displayResults(unfollowers); | |
| } catch (error) { | |
| document.getElementById('result').innerHTML = `<p style="color: #dc2626;">Error processing ZIP file.</p>`; | |
| } | |
| } | |
| function displayResults(unfollowers) { | |
| const resultDiv = document.getElementById('result'); | |
| resultDiv.innerHTML = `<h3>${languageTexts[lang].total}: ${unfollowers.length}</h3>`; | |
| if (unfollowers.length === 0) { | |
| resultDiv.innerHTML += `<p>${languageTexts[lang].allFollow}</p>`; | |
| return; | |
| } | |
| const ol = document.createElement('ol'); | |
| unfollowers.forEach(user => { | |
| const li = document.createElement('li'); | |
| const link = document.createElement('a'); | |
| link.href = `https://www.instagram.com/${user}/`; | |
| link.textContent = user; | |
| link.target = '_blank'; | |
| link.rel = 'noopener noreferrer'; | |
| li.appendChild(link); | |
| ol.appendChild(li); | |
| }); | |
| resultDiv.appendChild(ol); | |
| } | |
| function showTooltip() { | |
| document.getElementById('tooltipContent').innerHTML = languageTexts[lang].tooltip; | |
| document.getElementById('tooltip').classList.add('active'); | |
| } | |
| function hideTooltip() { | |
| document.getElementById('tooltip').classList.remove('active'); | |
| } | |
| const lang = localStorage.getItem('lang') || 'id'; | |
| document.getElementById('descriptionText').textContent = languageTexts[lang].description; | |
| document.getElementById('tutorialText').innerHTML = languageTexts[lang].tutorial; | |
| document.getElementById('uploadText').textContent = languageTexts[lang].upload; | |
| document.getElementById('processBtn').textContent = languageTexts[lang].process; | |
| window.addEventListener('load', () => { | |
| setTimeout(() => { | |
| const splash = document.getElementById('splashScreen'); | |
| splash.style.opacity = '0'; | |
| setTimeout(() => splash.remove(), 500); | |
| }, 1000); | |
| fetch('manifest.json') | |
| .then(response => response.json()) | |
| .then(data => { | |
| document.getElementById('appVersion').textContent = `v${data.version}`; | |
| }); | |
| }); | |
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker.register('service-worker.js') | |
| .then(() => console.log('Service Worker registered ✅')) | |
| .catch(err => console.error('Service Worker registration failed ❌', err)); | |
| } | |
| const savedTheme = localStorage.getItem('theme') || 'light'; | |
| if (savedTheme === 'dark') document.body.classList.add('dark-mode'); | |
| </script> | |
| </body> | |
| </html> |