unfollinsta / index.html
R-Kentaren's picture
Update index.html
6d5500f verified
<!DOCTYPE html>
<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>