// ==UserScript==
// @name DeGoogler Browser Assistant
// @namespace https://github.com/SysAdminDoc
// @version 0.0.5
// @updateURL https://raw.githubusercontent.com/SysAdminDoc/DeGoogler/main/DeGoogler-BrowserAssistant.user.js
// @downloadURL https://raw.githubusercontent.com/SysAdminDoc/DeGoogler/main/DeGoogler-BrowserAssistant.user.js
// @description Automates Google Takeout selection, exports YouTube subscriptions, audits connected apps/OAuth services, tracks migration of every account tied to your Google login, and assists with Gmail forwarding setup during the degoogling process.
// @author SysAdminDoc
// @match https://takeout.google.com/*
// @match https://myaccount.google.com/*
// @match https://mail.google.com/*
// @match https://www.youtube.com/*
// @match https://sysadmindoc.github.io/DeGoogler/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_download
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ── TrustedTypes policy (required by Google CSP) ──
const TTP = (typeof trustedTypes !== 'undefined' && trustedTypes.createPolicy)
? trustedTypes.createPolicy('degoogler', { createHTML: s => s })
: { createHTML: s => s };
// ── Anti-FOUC ──
const antiFouc = document.createElement('style');
antiFouc.id = 'degoogler-antifouc';
antiFouc.textContent = '.degoogler-panel{opacity:0;transition:opacity .3s ease}';
document.documentElement.appendChild(antiFouc);
// ── Config ──
const CFG = {
accentColor: '#3b82f6',
bgDark: '#0f1117',
bgPanel: '#161822',
bgCard: '#1c1e2e',
bgHover: '#252840',
border: '#2a2d42',
text: '#e2e4f0',
textMuted: '#6b7094',
green: '#22c55e',
orange: '#f59e0b',
red: '#ef4444',
fontStack: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
};
// ── CSS ──
const STYLES = `
/* DeGoogler Panel Base */
.dg-panel {
position: fixed;
top: 12px;
right: 12px;
width: 380px;
max-height: calc(100vh - 24px);
background: ${CFG.bgPanel};
border: 1px solid ${CFG.border};
border-radius: 14px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(59,130,246,0.08);
z-index: 999999;
font-family: ${CFG.fontStack};
color: ${CFG.text};
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
pointer-events: auto;
}
.dg-panel.dg-collapsed {
width: 52px;
height: 52px;
border-radius: 14px;
cursor: pointer;
overflow: hidden;
}
.dg-panel.dg-collapsed .dg-body,
.dg-panel.dg-collapsed .dg-header-text { display: none; }
.dg-panel.dg-collapsed .dg-header { padding: 12px; justify-content: center; border: none; }
.dg-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid ${CFG.border};
background: ${CFG.bgDark};
cursor: move;
user-select: none;
}
.dg-logo {
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, ${CFG.accentColor}, #7c3aed);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 800;
color: #fff;
flex-shrink: 0;
}
.dg-header-text {
flex: 1;
min-width: 0;
}
.dg-header-text h3 {
margin: 0;
font-size: 13px;
font-weight: 700;
color: ${CFG.text};
}
.dg-header-text p {
margin: 2px 0 0;
font-size: 10px;
color: ${CFG.textMuted};
}
.dg-close {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: ${CFG.textMuted};
cursor: pointer;
border-radius: 6px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.dg-close:hover { background: ${CFG.bgHover}; color: ${CFG.text}; }
.dg-body {
overflow-y: auto;
max-height: calc(100vh - 120px);
padding: 12px;
}
.dg-body::-webkit-scrollbar { width: 6px; }
.dg-body::-webkit-scrollbar-track { background: transparent; }
.dg-body::-webkit-scrollbar-thumb { background: ${CFG.border}; border-radius: 3px; }
.dg-section {
background: ${CFG.bgCard};
border: 1px solid ${CFG.border};
border-radius: 10px;
padding: 14px;
margin-bottom: 10px;
}
.dg-section h4 {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
color: ${CFG.text};
display: flex;
align-items: center;
gap: 6px;
}
.dg-section p {
margin: 0;
font-size: 11px;
color: ${CFG.textMuted};
line-height: 1.5;
}
.dg-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
}
.dg-btn-primary {
background: ${CFG.accentColor};
color: #fff;
}
.dg-btn-primary:hover { background: #2563eb; transform: translateY(-1px); }
.dg-btn-secondary {
background: ${CFG.bgHover};
color: ${CFG.text};
border: 1px solid ${CFG.border};
}
.dg-btn-secondary:hover { background: ${CFG.border}; }
.dg-btn-success {
background: ${CFG.green};
color: #fff;
}
.dg-btn-row {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.dg-checklist {
list-style: none;
padding: 0;
margin: 8px 0 0;
}
.dg-checklist li {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
font-size: 11px;
color: ${CFG.text};
border-bottom: 1px solid rgba(42,45,66,0.5);
}
.dg-checklist li:last-child { border-bottom: none; }
.dg-check {
width: 16px;
height: 16px;
border-radius: 4px;
border: 2px solid ${CFG.border};
flex-shrink: 0;
margin-top: 1px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
transition: all 0.15s;
}
.dg-check.checked {
background: ${CFG.green};
border-color: ${CFG.green};
color: #fff;
}
.dg-badge {
display: inline-flex;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
}
.dg-badge-green { background: rgba(34,197,94,0.12); color: ${CFG.green}; }
.dg-badge-orange { background: rgba(245,158,11,0.12); color: ${CFG.orange}; }
.dg-badge-blue { background: rgba(59,130,246,0.12); color: ${CFG.accentColor}; }
.dg-progress {
height: 4px;
background: ${CFG.bgDark};
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.dg-progress-fill {
height: 100%;
background: linear-gradient(90deg, ${CFG.accentColor}, ${CFG.green});
border-radius: 2px;
transition: width 0.4s cubic-bezier(0.4,0,0.2,1);
}
.dg-toast {
position: fixed;
bottom: 20px;
right: 20px;
background: ${CFG.bgPanel};
color: ${CFG.text};
border: 1px solid ${CFG.border};
padding: 12px 18px;
border-radius: 10px;
font-size: 12px;
font-family: ${CFG.fontStack};
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 9999999;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.dg-toast.visible { opacity: 1; transform: none; }
/* Connected Services Auditor */
.dg-app-card {
background: ${CFG.bgDark};
border: 1px solid ${CFG.border};
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 6px;
transition: all 0.15s;
}
.dg-app-card:hover { border-color: ${CFG.accentColor}33; }
.dg-app-row {
display: flex;
align-items: center;
gap: 8px;
}
.dg-app-name {
flex: 1;
font-size: 12px;
font-weight: 600;
color: ${CFG.text};
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dg-app-type {
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 600;
flex-shrink: 0;
}
.dg-type-oauth { background: rgba(139,92,246,0.15); color: #a78bfa; }
.dg-type-email { background: rgba(59,130,246,0.15); color: #60a5fa; }
.dg-type-billing { background: rgba(245,158,11,0.15); color: ${CFG.orange}; }
.dg-app-steps {
display: flex;
gap: 4px;
margin-top: 8px;
}
.dg-step-pill {
font-size: 9px;
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
border: 1px solid ${CFG.border};
background: transparent;
color: ${CFG.textMuted};
font-family: inherit;
transition: all 0.15s;
white-space: nowrap;
}
.dg-step-pill:hover { border-color: ${CFG.accentColor}66; color: ${CFG.text}; }
.dg-step-pill.done {
background: ${CFG.green}22;
border-color: ${CFG.green}44;
color: ${CFG.green};
}
.dg-step-pill.active {
background: ${CFG.accentColor}22;
border-color: ${CFG.accentColor}55;
color: ${CFG.accentColor};
}
.dg-priority-label {
font-size: 9px;
padding: 1px 6px;
border-radius: 3px;
font-weight: 700;
flex-shrink: 0;
}
.dg-pri-critical { background: ${CFG.red}22; color: ${CFG.red}; }
.dg-pri-important { background: ${CFG.orange}22; color: ${CFG.orange}; }
.dg-pri-optional { background: ${CFG.border}; color: ${CFG.textMuted}; }
.dg-input-row {
display: flex;
gap: 6px;
margin-top: 8px;
}
.dg-input {
flex: 1;
padding: 7px 10px;
border: 1px solid ${CFG.border};
border-radius: 6px;
background: ${CFG.bgDark};
color: ${CFG.text};
font-size: 12px;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.dg-input:focus { border-color: ${CFG.accentColor}; }
.dg-input::placeholder { color: ${CFG.textMuted}; }
.dg-select {
padding: 7px 8px;
border: 1px solid ${CFG.border};
border-radius: 6px;
background: ${CFG.bgDark};
color: ${CFG.text};
font-size: 11px;
font-family: inherit;
outline: none;
cursor: pointer;
}
.dg-tabs {
display: flex;
gap: 4px;
margin-bottom: 10px;
}
.dg-tab {
padding: 5px 10px;
border: 1px solid ${CFG.border};
border-radius: 6px;
background: transparent;
color: ${CFG.textMuted};
font-size: 11px;
font-family: inherit;
cursor: pointer;
transition: all 0.15s;
}
.dg-tab:hover { color: ${CFG.text}; }
.dg-tab.active {
background: ${CFG.accentColor}22;
border-color: ${CFG.accentColor}55;
color: ${CFG.accentColor};
}
.dg-counter {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
border-radius: 9px;
font-size: 10px;
font-weight: 700;
padding: 0 5px;
}
.dg-app-delete {
width: 20px;
height: 20px;
border: none;
background: transparent;
color: ${CFG.textMuted};
cursor: pointer;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s;
}
.dg-app-delete:hover { color: ${CFG.red}; background: ${CFG.red}22; }
.dg-stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 10px;
}
.dg-stat-box {
background: ${CFG.bgDark};
border: 1px solid ${CFG.border};
border-radius: 8px;
padding: 10px;
text-align: center;
}
.dg-stat-num {
font-size: 20px;
font-weight: 800;
color: ${CFG.text};
}
.dg-stat-label {
font-size: 9px;
color: ${CFG.textMuted};
margin-top: 2px;
}
`;
// ── Utility Functions ──
function showToast(msg, duration = 3000) {
const toast = document.createElement('div');
toast.className = 'dg-toast';
toast.textContent = msg;
document.body.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('visible'));
setTimeout(() => {
toast.classList.remove('visible');
setTimeout(() => toast.remove(), 300);
}, duration);
}
function waitForElement(selector, timeout = 15000) {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const obs = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) { obs.disconnect(); resolve(el); }
});
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(() => { obs.disconnect(); reject(new Error('Timeout')); }, timeout);
});
}
function createPanel(title, subtitle, bodyHTML) {
const panel = document.createElement('div');
panel.className = 'dg-panel';
panel.innerHTML = TTP.createHTML(`
${bodyHTML}
`);
// Collapse toggle
const closeBtn = panel.querySelector('.dg-close');
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
panel.classList.toggle('dg-collapsed');
});
panel.addEventListener('click', (e) => {
if (panel.classList.contains('dg-collapsed')) {
panel.classList.remove('dg-collapsed');
}
});
return panel;
}
// ═══════════════════════════════════════════════
// MODULE: Google Takeout Helper
// ═══════════════════════════════════════════════
function initTakeoutHelper() {
const bodyHTML = `
Quick Actions
Automate the Takeout selection process. Click below to select all services for a complete backup.
Recommended Settings
- Export as .zip (most compatible)
- Set max file size to 10GB+
- Send download link via email
- Export once (not scheduled)
Export Priority
Most critical data to export first:
- High Gmail (emails, attachments)
- High Google Drive (all files)
- High Google Photos
- High Contacts
- Med Calendar
- Med Chrome (bookmarks, passwords)
- Low YouTube (subscriptions, history)
- Low Keep (notes)
- Low Maps (saved places, timeline)
After Export
Once your Takeout is ready, use the DeGoogler Toolkit (PowerShell) to extract, organize, and convert your data for import into your new services.
`;
const panel = createPanel('Takeout Helper', 'DeGoogler Browser Assistant', bodyHTML);
document.body.appendChild(panel);
// Select All Services
document.getElementById('dg-select-all').addEventListener('click', async () => {
showToast('Selecting all services...');
// Google Takeout uses toggle switches - find and enable them
const toggles = document.querySelectorAll('[role="checkbox"][aria-checked="false"], input[type="checkbox"]:not(:checked)');
let count = 0;
for (const toggle of toggles) {
toggle.click();
count++;
await new Promise(r => setTimeout(r, 80));
}
// Also try material design switches
const mdSwitches = document.querySelectorAll('.mdc-switch:not(.mdc-switch--checked), [data-is-checked="false"]');
for (const sw of mdSwitches) {
sw.click();
count++;
await new Promise(r => setTimeout(r, 80));
}
showToast(count > 0 ? `Enabled ${count} services` : 'All services already selected (or page structure changed)');
});
// Deselect All
document.getElementById('dg-deselect-all').addEventListener('click', async () => {
showToast('Deselecting all services...');
const toggles = document.querySelectorAll('[role="checkbox"][aria-checked="true"], input[type="checkbox"]:checked');
let count = 0;
for (const toggle of toggles) {
toggle.click();
count++;
await new Promise(r => setTimeout(r, 80));
}
const mdSwitches = document.querySelectorAll('.mdc-switch--checked, [data-is-checked="true"]');
for (const sw of mdSwitches) {
sw.click();
count++;
await new Promise(r => setTimeout(r, 80));
}
showToast(`Deselected ${count} services`);
});
// Persist checklist state
panel.querySelectorAll('.dg-check[data-key]').forEach(check => {
const key = check.dataset.key;
if (GM_getValue(key, false)) {
check.classList.add('checked');
check.textContent = '\u2713';
}
check.addEventListener('click', () => {
check.classList.toggle('checked');
const isChecked = check.classList.contains('checked');
check.textContent = isChecked ? '\u2713' : '';
GM_setValue(key, isChecked);
});
});
}
// ═══════════════════════════════════════════════
// MODULE: YouTube Subscription Exporter
// ═══════════════════════════════════════════════
function initYouTubeExporter() {
const bodyHTML = `
YouTube Data Export
Export your subscriptions as OPML for import into NewPipe, FreeTube, or Invidious.
Import Targets
- OPML NewPipe: Settings > Content > Import from file
- OPML FreeTube: Settings > Data Settings > Import Subscriptions
- OPML Invidious: Import/Export Data > Import OPML
`;
const panel = createPanel('YouTube Exporter', 'DeGoogler Browser Assistant', bodyHTML);
document.body.appendChild(panel);
panel.classList.add('dg-collapsed');
document.getElementById('dg-yt-export-subs').addEventListener('click', async () => {
const statusEl = document.getElementById('dg-yt-status');
const progressBar = document.getElementById('dg-yt-progress');
const progressFill = document.getElementById('dg-yt-progress-fill');
progressBar.style.display = 'block';
statusEl.textContent = 'Fetching subscriptions...';
statusEl.style.color = CFG.orange;
try {
// Method 1: Try YouTube's subscription manager page data
const response = await fetch('https://www.youtube.com/feed/channels', { credentials: 'include' });
const html = await response.text();
// Extract ytInitialData from page source
const match = html.match(/var\s+ytInitialData\s*=\s*({.+?});\s*<\/script>/);
let channels = [];
if (match) {
try {
const data = JSON.parse(match[1]);
// Navigate the YouTube data structure to find channel info
const items = findNestedChannels(data);
channels = items;
} catch (e) {
statusEl.textContent = 'Parsing page data... trying alternate method';
}
}
// Method 2: Try scraping subscription links from page
if (channels.length === 0) {
statusEl.textContent = 'Using alternate extraction method...';
const guideResponse = await fetch('https://www.youtube.com/feed/subscriptions', { credentials: 'include' });
const guideHtml = await guideResponse.text();
const guideMatch = guideHtml.match(/var\s+ytInitialData\s*=\s*({.+?});\s*<\/script>/);
if (guideMatch) {
try {
const guideData = JSON.parse(guideMatch[1]);
channels = findNestedChannels(guideData);
} catch (e) {}
}
}
// Method 3: Scrape from guide sidebar
if (channels.length === 0) {
statusEl.textContent = 'Scraping sidebar subscriptions...';
const sidebarLinks = document.querySelectorAll('a[href*="/channel/"], a[href*="/@"]');
const seen = new Set();
sidebarLinks.forEach(link => {
const href = link.href;
const title = link.textContent.trim() || link.title || '';
if (title && !seen.has(href) && (href.includes('/channel/') || href.includes('/@'))) {
seen.add(href);
const channelId = href.match(/\/channel\/(UC[\w-]+)/)?.[1] || '';
channels.push({ title: title, channelId: channelId, url: href });
}
});
}
progressFill.style.width = '80%';
if (channels.length === 0) {
statusEl.textContent = 'No subscriptions found. Try Google Takeout instead (YouTube data > subscriptions.csv).';
statusEl.style.color = CFG.red;
progressBar.style.display = 'none';
return;
}
// Generate OPML
let opml = '\n';
opml += '\n';
opml += ' \n';
opml += ' YouTube Subscriptions - DeGoogler Export\n';
opml += ` ${new Date().toISOString()}\n`;
opml += ' \n';
opml += ' \n';
opml += ' \n';
channels.forEach(ch => {
const safeName = ch.title.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const feedUrl = ch.channelId
? `https://www.youtube.com/feeds/videos.xml?channel_id=${ch.channelId}`
: ch.url;
const htmlUrl = ch.channelId
? `https://www.youtube.com/channel/${ch.channelId}`
: ch.url;
opml += ` \n`;
});
opml += ' \n';
opml += ' \n';
opml += '';
// Download
const blob = new Blob([opml], { type: 'text/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `youtube-subscriptions-${new Date().toISOString().split('T')[0]}.opml`;
a.click();
URL.revokeObjectURL(url);
progressFill.style.width = '100%';
statusEl.textContent = `Exported ${channels.length} subscriptions as OPML`;
statusEl.style.color = CFG.green;
showToast(`Exported ${channels.length} YouTube subscriptions`);
} catch (err) {
statusEl.textContent = 'Export failed. Use Google Takeout for YouTube data instead.';
statusEl.style.color = CFG.red;
progressBar.style.display = 'none';
console.error('[DeGoogler] YouTube export error:', err);
}
});
function findNestedChannels(obj, results = []) {
if (!obj || typeof obj !== 'object') return results;
if (obj.subscriberCountText && obj.title && obj.channelId) {
results.push({ title: obj.title.simpleText || obj.title, channelId: obj.channelId });
}
if (obj.channelRenderer) {
const cr = obj.channelRenderer;
results.push({
title: cr.title?.simpleText || cr.title?.runs?.[0]?.text || '',
channelId: cr.channelId || ''
});
}
if (obj.gridChannelRenderer) {
const gcr = obj.gridChannelRenderer;
results.push({
title: gcr.title?.simpleText || gcr.title?.runs?.[0]?.text || '',
channelId: gcr.channelId || ''
});
}
if (obj.guideEntryRenderer) {
const ger = obj.guideEntryRenderer;
const navUrl = ger.navigationEndpoint?.browseEndpoint?.browseId || '';
if (navUrl.startsWith('UC')) {
results.push({
title: ger.formattedTitle?.simpleText || '',
channelId: navUrl
});
}
}
for (const key of Object.keys(obj)) {
if (Array.isArray(obj[key])) {
obj[key].forEach(item => findNestedChannels(item, results));
} else if (typeof obj[key] === 'object') {
findNestedChannels(obj[key], results);
}
}
return results;
}
}
// ═══════════════════════════════════════════════
// MODULE: Google Account Audit
// ═══════════════════════════════════════════════
function initAccountAudit() {
const bodyHTML = `
Account Audit Checklist
Review and clean up your Google account before migrating. Check off each item as you complete it.
0 of 8 complete
Quick Links
Only delete your account after confirming ALL data has been migrated successfully.
`;
const panel = createPanel('Account Audit', 'DeGoogler Browser Assistant', bodyHTML);
document.body.appendChild(panel);
function updateAuditProgress() {
const checks = panel.querySelectorAll('.dg-check[data-key^="audit-"]');
let done = 0;
checks.forEach(c => { if (c.classList.contains('checked')) done++; });
const pct = Math.round((done / checks.length) * 100);
const progressFill = document.getElementById('dg-audit-progress');
const statusEl = document.getElementById('dg-audit-status');
if (progressFill) progressFill.style.width = pct + '%';
if (statusEl) statusEl.textContent = `${done} of ${checks.length} complete`;
}
// Persist checklist state
panel.querySelectorAll('.dg-check[data-key]').forEach(check => {
const key = check.dataset.key;
if (GM_getValue(key, false)) {
check.classList.add('checked');
check.textContent = '\u2713';
}
check.addEventListener('click', () => {
check.classList.toggle('checked');
const isChecked = check.classList.contains('checked');
check.textContent = isChecked ? '\u2713' : '';
GM_setValue(key, isChecked);
updateAuditProgress();
});
});
updateAuditProgress();
}
// ═══════════════════════════════════════════════
// MODULE: Gmail Migration Helper
// ═══════════════════════════════════════════════
function initGmailHelper() {
const bodyHTML = `
Gmail Migration Steps
-
Set up forwarding to new email
Settings > Forwarding and POP/IMAP
-
Create auto-reply with new address
Settings > Vacation responder
-
Export filter list for reference
Settings > Filters > Export
-
-
Note your label/folder structure
Replicate in your new email service
-
Update email on critical accounts
Banking, insurance, government, medical
-
Update email on social accounts
Social media, subscriptions, newsletters
-
Email contacts your new address
Send a brief "I've moved" email
Forwarding Quick Setup
Click below to jump directly to Gmail's forwarding settings:
`;
const panel = createPanel('Gmail Migration', 'DeGoogler Browser Assistant', bodyHTML);
document.body.appendChild(panel);
panel.classList.add('dg-collapsed');
// Persist checklist
panel.querySelectorAll('.dg-check[data-key]').forEach(check => {
const key = check.dataset.key;
if (GM_getValue(key, false)) {
check.classList.add('checked');
check.textContent = '\u2713';
}
check.addEventListener('click', () => {
check.classList.toggle('checked');
const isChecked = check.classList.contains('checked');
check.textContent = isChecked ? '\u2713' : '';
GM_setValue(key, isChecked);
});
});
}
// ═══════════════════════════════════════════════
// MODULE: Connected Services Auditor
// ═══════════════════════════════════════════════
function initConnectedServicesAuditor() {
// ── Persistent service store ──
const STORE_KEY = 'dg-connected-services';
function loadServices() {
try { return JSON.parse(GM_getValue(STORE_KEY, '[]')); }
catch { return []; }
}
function saveServices(svcs) { GM_setValue(STORE_KEY, JSON.stringify(svcs)); }
function genId() { return Date.now().toString(36) + Math.random().toString(36).slice(2,6); }
// ── Known service domain map ──
const DOMAIN_MAP = {
'amazon': 'amazon.com', 'apple': 'appleid.apple.com', 'adobe': 'account.adobe.com',
'airbnb': 'airbnb.com', 'asana': 'asana.com', 'atlassian': 'id.atlassian.com',
'bitbucket': 'bitbucket.org', 'canva': 'canva.com', 'capcut': 'capcut.com',
'cesium ion': 'ion.cesium.com', 'chatgpt': 'chatgpt.com', 'claude': 'claude.ai',
'cloudflare': 'dash.cloudflare.com', 'coinbase': 'coinbase.com',
'cursor': 'cursor.com', 'deepseek': 'chat.deepseek.com',
'deviantart': 'deviantart.com', 'digg': 'digg.com', 'discogs': 'discogs.com',
'discord': 'discord.com', 'distrokid': 'distrokid.com', 'dropbox': 'dropbox.com',
'ebay': 'ebay.com', 'elevenlabs': 'elevenlabs.io', 'em client': 'emclient.com',
'epic games': 'epicgames.com', 'etsy': 'etsy.com', 'evernote': 'evernote.com',
'facebook': 'facebook.com', 'figma': 'figma.com', 'fiverr': 'fiverr.com',
'github': 'github.com', 'gitlab': 'gitlab.com', 'grammarly': 'grammarly.com',
'heygen': 'heygen.com', 'hulu': 'hulu.com', 'ideogram': 'ideogram.ai',
'imdb': 'imdb.com', 'indeed': 'indeed.com', 'instagram': 'instagram.com',
'jira': 'atlassian.net', 'klingai': 'klingai.com', 'linkedin': 'linkedin.com',
'maptiler': 'maptiler.com', 'marketplace': 'facebook.com/marketplace',
'medium': 'medium.com', 'messenger': 'messenger.com', 'microsoft': 'account.microsoft.com',
'microsoft copilot': 'copilot.microsoft.com', 'musixmatch': 'musixmatch.com',
'netflix': 'netflix.com', 'notion': 'notion.so', 'nocodexport': 'nocodexport.com',
'openai': 'openai.com', 'openrouter': 'openrouter.ai', 'paypal': 'paypal.com',
'perplexity': 'perplexity.ai', 'perplexity ask': 'perplexity.ai',
'pexels': 'pexels.com', 'pinterest': 'pinterest.com', 'pixabay': 'pixabay.com',
'rclone': 'rclone.org', 'recall': 'recall.ai', 'reddit': 'reddit.com',
'reddit enhancement suite': 'reddit.com', 'robinhood': 'robinhood.com',
'runway': 'runway.com', 'shopify': 'shopify.com', 'slack': 'slack.com',
'snapchat': 'snapchat.com', 'snippets ai': 'snippets.ai',
'sorrywatermark': 'sorrywatermark.com', 'spotify': 'spotify.com',
'steam': 'store.steampowered.com', 'stripe': 'stripe.com', 'stylebot': 'stylebot.dev',
'stylus extension': 'add0n.com/stylus.html', 'suno': 'suno.com',
'swift backup': 'swiftbackup.com', 'tampermonkey': 'tampermonkey.net',
'ticktick': 'ticktick.com', 'tiktok': 'tiktok.com', 'tinyurl': 'tinyurl.com',
'trello': 'trello.com', 'tumblr': 'tumblr.com', 'twitch': 'twitch.tv',
'twitter': 'twitter.com', 'uber': 'uber.com', 'venmo': 'venmo.com',
'violentmonkey': 'violentmonkey.github.io', 'vmake': 'vmake.ai',
'x': 'x.com', 'yahoo': 'login.yahoo.com', 'yelp': 'yelp.com', 'zoom': 'zoom.us',
};
// Common password reset URL patterns
const RESET_PATTERNS = [
'/forgot-password', '/account/forgot', '/reset-password',
'/password/reset', '/auth/forgot', '/forgot',
];
function getDomain(name) {
const lower = name.toLowerCase().trim();
// Direct match
if (DOMAIN_MAP[lower]) return DOMAIN_MAP[lower];
// Partial match
for (const [key, domain] of Object.entries(DOMAIN_MAP)) {
if (lower.includes(key) || key.includes(lower)) return domain;
}
// Domain-looking names (e.g. "chat.deepseek.com")
if (/^[\w-]+\.[\w-]+\.\w+$/.test(lower)) return lower;
// Fallback: construct from name
const slug = lower.replace(/[^a-z0-9]/g, '');
return slug ? slug + '.com' : null;
}
function getResetUrl(domain) {
if (!domain) return null;
const base = domain.startsWith('http') ? domain : 'https://' + domain;
return base + '/forgot-password';
}
function getLoginUrl(domain) {
if (!domain) return null;
return domain.startsWith('http') ? domain : 'https://' + domain;
}
// Migration steps per auth type
const STEPS_OAUTH = [
{ key: 'password', label: 'Set Password' },
{ key: 'email', label: 'Update Email' },
{ key: 'disconnect', label: 'Revoke Google' },
{ key: 'verified', label: 'Verified' }
];
const STEPS_EMAIL = [
{ key: 'email', label: 'Update Email' },
{ key: 'verified', label: 'Verified' }
];
const STEPS_ACCESS = [
{ key: 'review', label: 'Review Access' },
{ key: 'disconnect', label: 'Revoke' },
{ key: 'verified', label: 'Verified' }
];
function getSteps(type) {
return type === 'oauth' ? STEPS_OAUTH : type === 'access' ? STEPS_ACCESS : STEPS_EMAIL;
}
// Priority auto-classification
const CRITICAL_KW = ['bank','chase','wells fargo','capital one','citi','fidelity','schwab','vanguard','paypal','venmo','zelle','irs','ssa','gov','healthcare','insurance','geico','state farm','medicare','medicaid','anthem','kaiser','tax','turbotax','intuit','credit','loan','mortgage','coinbase','robinhood'];
const IMPORTANT_KW = ['amazon','microsoft','apple','github','gitlab','slack','zoom','dropbox','linkedin','indeed','notion','figma','adobe','canva','trello','asana','jira','heroku','aws','azure','netlify','vercel','cloudflare','domain','godaddy','namecheap','shopify','stripe','square','spotify','netflix','hulu','disney','openai','chatgpt','claude','cursor','elevenlabs'];
function autoClassify(name) {
const l = name.toLowerCase();
if (CRITICAL_KW.some(k => l.includes(k))) return 'critical';
if (IMPORTANT_KW.some(k => l.includes(k))) return 'important';
return 'optional';
}
const bodyHTML = `
Scan Google Connections
Reads "Sign in with Google" and "Account access" lists from this page. Click a tab below then Scan to capture that category.
Add Service Manually
For accounts not connected via Google OAuth (email-only registrations).
Connected Services
No services tracked yet. Click a Scan button above to auto-detect connected apps.
Common Forgotten Services
Click to add any you use:
`;
const panel = createPanel('Connected Services', 'DeGoogler Browser Assistant', bodyHTML);
document.body.appendChild(panel);
const listEl = document.getElementById('dg-svc-list');
const emptyEl = document.getElementById('dg-svc-empty');
const statusEl = document.getElementById('dg-svc-scan-status');
let activeFilter = 'all';
// ── DOM Scraper ──
// Scrapes Google's connections page using the actual DOM selectors from MHTML analysis
async function scrapeApps(type) {
const services = loadServices();
let found = 0;
const authType = type === 'signin' ? 'oauth' : 'access';
// If we need to switch tabs, click the appropriate chip
if (type === 'signin') {
// Click "Sign in with Google" tab chip
const signinChip = document.querySelector('[data-input-chip-label*="Sign in with Google"]');
const signinChipAlt = document.querySelector('[data-relationship-type="4"]');
const chip = signinChip || signinChipAlt;
if (chip) {
chip.click();
await new Promise(r => setTimeout(r, 800));
}
} else if (type === 'access') {
// Click "Access to" tab chip
const accessChip = document.querySelector('[data-input-chip-label*="Access to"]');
const accessChipAlt = document.querySelector('[data-relationship-type="3"]');
const chip = accessChip || accessChipAlt;
if (chip) {
chip.click();
await new Promise(r => setTimeout(r, 800));
}
}
// Wait for apps to render
await new Promise(r => setTimeout(r, 500));
// Primary selector: a[data-provider-index] with .mMsbvc name
const appLinks = document.querySelectorAll('a[data-provider-index]');
appLinks.forEach(link => {
const nameEl = link.querySelector('.mMsbvc');
if (!nameEl) return;
const name = nameEl.textContent.trim();
if (!name || name.length < 2) return;
// Skip if already tracked
if (services.some(s => s.name.toLowerCase() === name.toLowerCase())) return;
const detailUrl = link.href || '';
const iconEl = link.querySelector('img.bLJ69, img[role="presentation"]');
const iconUrl = iconEl ? iconEl.src : '';
const domain = getDomain(name);
services.push({
id: genId(),
name: name,
type: authType,
priority: autoClassify(name),
steps: {},
domain: domain,
detailUrl: detailUrl,
iconUrl: iconUrl,
addedAt: Date.now(),
source: 'scan'
});
found++;
});
// Fallback: if primary selector found nothing, try broader selectors
if (found === 0) {
// Try c-wiz based card structures
const cards = document.querySelectorAll('[data-provider-index]');
cards.forEach(card => {
const parent = card.closest('a') || card;
const nameEl = parent.querySelector('.mMsbvc') || parent.querySelector('div[class*="mMsbvc"]');
if (!nameEl) return;
const name = nameEl.textContent.trim();
if (!name || name.length < 2 || services.some(s => s.name.toLowerCase() === name.toLowerCase())) return;
const domain = getDomain(name);
services.push({
id: genId(),
name: name,
type: authType,
priority: autoClassify(name),
steps: {},
domain: domain,
detailUrl: parent.href || '',
iconUrl: '',
addedAt: Date.now(),
source: 'scan-fallback'
});
found++;
});
}
saveServices(services);
return found;
}
// ── Scan buttons ──
document.getElementById('dg-svc-scan-signin').addEventListener('click', async () => {
statusEl.textContent = 'Clicking "Sign in with Google" tab...';
statusEl.style.color = CFG.orange;
const found = await scrapeApps('signin');
statusEl.textContent = found > 0
? `Found ${found} new Sign-in apps`
: 'No new Sign-in apps found. Make sure the "Sign in with Google" tab is active.';
statusEl.style.color = found > 0 ? CFG.green : CFG.textMuted;
renderList();
});
document.getElementById('dg-svc-scan-access').addEventListener('click', async () => {
statusEl.textContent = 'Clicking "Access to" tab...';
statusEl.style.color = CFG.orange;
const found = await scrapeApps('access');
statusEl.textContent = found > 0
? `Found ${found} new Account Access apps`
: 'No new Access apps found. Make sure the "Access to" tab is active.';
statusEl.style.color = found > 0 ? CFG.green : CFG.textMuted;
renderList();
});
document.getElementById('dg-svc-scan-all').addEventListener('click', async () => {
statusEl.textContent = 'Scanning Sign-in apps...';
statusEl.style.color = CFG.orange;
const found1 = await scrapeApps('signin');
statusEl.textContent = `Found ${found1} Sign-in apps. Now scanning Access apps...`;
await new Promise(r => setTimeout(r, 500));
const found2 = await scrapeApps('access');
const total = found1 + found2;
statusEl.textContent = total > 0
? `Scan complete: ${found1} Sign-in + ${found2} Access = ${total} new apps`
: 'No new apps found. They may already be in your list.';
statusEl.style.color = total > 0 ? CFG.green : CFG.textMuted;
renderList();
});
// ── Quick-add common services ──
const commonServices = [
{ name: 'Amazon', type: 'email', pri: 'important' },
{ name: 'PayPal', type: 'email', pri: 'critical' },
{ name: 'Netflix', type: 'email', pri: 'important' },
{ name: 'Spotify', type: 'email', pri: 'important' },
{ name: 'Apple ID', type: 'email', pri: 'critical' },
{ name: 'Microsoft', type: 'email', pri: 'important' },
{ name: 'GitHub', type: 'oauth', pri: 'important' },
{ name: 'LinkedIn', type: 'email', pri: 'important' },
{ name: 'Facebook', type: 'email', pri: 'optional' },
{ name: 'Reddit', type: 'email', pri: 'optional' },
{ name: 'Discord', type: 'email', pri: 'optional' },
{ name: 'Twitch', type: 'oauth', pri: 'optional' },
{ name: 'Bank (Primary)', type: 'email', pri: 'critical' },
{ name: 'Credit Card', type: 'email', pri: 'critical' },
{ name: 'Health Insurance', type: 'email', pri: 'critical' },
{ name: 'IRS / Tax Service', type: 'email', pri: 'critical' },
{ name: 'Employer/HR Portal', type: 'email', pri: 'critical' },
{ name: 'Domain Registrar', type: 'email', pri: 'important' },
{ name: 'Cloud Hosting', type: 'email', pri: 'important' },
{ name: 'Adobe', type: 'email', pri: 'important' },
{ name: 'Steam', type: 'email', pri: 'optional' },
{ name: 'Uber/Lyft', type: 'email', pri: 'optional' },
{ name: 'DoorDash/UberEats', type: 'email', pri: 'optional' },
{ name: 'Airbnb', type: 'email', pri: 'optional' },
];
const quickAddEl = document.getElementById('dg-svc-quickadd');
function refreshQuickAdd() {
const existing = loadServices();
quickAddEl.querySelectorAll('button').forEach(btn => {
const tracked = existing.some(s => s.name.toLowerCase() === btn.textContent.toLowerCase());
btn.style.opacity = tracked ? '0.3' : '1';
btn.style.pointerEvents = tracked ? 'none' : 'auto';
});
}
commonServices.forEach(svc => {
const btn = document.createElement('button');
btn.className = 'dg-btn dg-btn-secondary';
btn.style.cssText = 'padding:4px 10px;font-size:10px;margin:0';
btn.textContent = svc.name;
btn.addEventListener('click', () => {
const services = loadServices();
if (services.some(s => s.name.toLowerCase() === svc.name.toLowerCase())) {
showToast(svc.name + ' already tracked'); return;
}
const domain = getDomain(svc.name);
services.push({
id: genId(), name: svc.name, type: svc.type, priority: svc.pri,
steps: {}, domain: domain, detailUrl: '', iconUrl: '',
addedAt: Date.now(), source: 'manual'
});
saveServices(services);
renderList();
refreshQuickAdd();
showToast('Added ' + svc.name);
});
quickAddEl.appendChild(btn);
});
// ── Render service list ──
function renderList() {
const services = loadServices();
listEl.innerHTML = TTP.createHTML('');
let filtered = services;
if (activeFilter === 'done') {
filtered = services.filter(s => getSteps(s.type).every(st => s.steps[st.key]));
} else if (activeFilter !== 'all') {
filtered = services.filter(s => s.priority === activeFilter);
}
const priOrder = { critical: 0, important: 1, optional: 2 };
filtered.sort((a, b) => {
const aDone = getSteps(a.type).every(st => a.steps[st.key]);
const bDone = getSteps(b.type).every(st => b.steps[st.key]);
if (aDone !== bDone) return aDone ? 1 : -1;
return (priOrder[a.priority] || 2) - (priOrder[b.priority] || 2);
});
emptyEl.style.display = filtered.length === 0 ? 'block' : 'none';
filtered.forEach(svc => {
const steps = getSteps(svc.type);
const allDone = steps.every(st => svc.steps[st.key]);
const domain = svc.domain || getDomain(svc.name);
const loginUrl = getLoginUrl(domain);
const card = document.createElement('div');
card.className = 'dg-app-card';
if (allDone) card.style.opacity = '0.5';
const typeClass = svc.type === 'oauth' ? 'dg-type-oauth' : svc.type === 'access' ? 'dg-type-billing' : 'dg-type-email';
const typeLabel = svc.type === 'oauth' ? 'Sign-In' : svc.type === 'access' ? 'Access' : 'Email';
const priClass = svc.priority === 'critical' ? 'dg-pri-critical' : svc.priority === 'important' ? 'dg-pri-important' : 'dg-pri-optional';
const priLabel = svc.priority.charAt(0).toUpperCase() + svc.priority.slice(1);
let cardHTML = '';
cardHTML += '' + priLabel + '';
cardHTML += '' + svc.name + '';
cardHTML += '' + typeLabel + '';
cardHTML += '';
cardHTML += '
';
// Domain + action links row
if (domain || svc.detailUrl) {
cardHTML += '';
if (domain) {
cardHTML += '
' + domain + '';
}
if (loginUrl && svc.type === 'oauth') {
cardHTML += '
Login';
}
if (svc.detailUrl) {
cardHTML += '
Google Details';
}
cardHTML += '
';
}
// Migration step pills
cardHTML += '';
steps.forEach((st, idx) => {
const done = svc.steps[st.key];
const isNext = !done && (idx === 0 || svc.steps[steps[idx-1].key]);
const cls = done ? 'done' : isNext ? 'active' : '';
cardHTML += '';
});
cardHTML += '
';
card.innerHTML = TTP.createHTML(cardHTML);
listEl.appendChild(card);
});
// Wire step clicks
listEl.querySelectorAll('.dg-step-pill').forEach(pill => {
pill.addEventListener('click', () => {
const svcs = loadServices();
const svc = svcs.find(s => s.id === pill.dataset.id);
if (!svc) return;
svc.steps[pill.dataset.step] = !svc.steps[pill.dataset.step];
saveServices(svcs);
renderList();
});
});
// Wire delete clicks
listEl.querySelectorAll('.dg-app-delete').forEach(btn => {
btn.addEventListener('click', () => {
let svcs = loadServices();
svcs = svcs.filter(s => s.id !== btn.dataset.id);
saveServices(svcs);
renderList();
refreshQuickAdd();
});
});
updateStats();
refreshQuickAdd();
}
function updateStats() {
const services = loadServices();
let total = services.length, migrated = 0, inProgress = 0, pending = 0;
services.forEach(svc => {
const steps = getSteps(svc.type);
const done = steps.filter(st => svc.steps[st.key]).length;
if (done === steps.length) migrated++;
else if (done > 0) inProgress++;
else pending++;
});
const el = (id) => document.getElementById(id);
el('dg-stat-total').textContent = total;
el('dg-stat-migrated').textContent = migrated;
el('dg-stat-inprogress').textContent = inProgress;
el('dg-stat-pending').textContent = pending;
el('dg-svc-progress').style.width = (total > 0 ? Math.round((migrated / total) * 100) : 0) + '%';
}
// ── Tab filtering ──
document.getElementById('dg-svc-tabs').addEventListener('click', e => {
if (!e.target.classList.contains('dg-tab')) return;
document.querySelectorAll('#dg-svc-tabs .dg-tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
activeFilter = e.target.dataset.filter;
renderList();
});
// ── Manual add ──
document.getElementById('dg-svc-add').addEventListener('click', () => {
const nameInput = document.getElementById('dg-svc-name');
const name = nameInput.value.trim();
if (!name) return;
const type = document.getElementById('dg-svc-type').value;
const priority = document.getElementById('dg-svc-priority').value;
const services = loadServices();
if (services.some(s => s.name.toLowerCase() === name.toLowerCase())) {
showToast(name + ' already tracked'); return;
}
const domain = getDomain(name);
services.push({
id: genId(), name: name, type: type, priority: priority,
steps: {}, domain: domain, detailUrl: '', iconUrl: '',
addedAt: Date.now(), source: 'manual'
});
saveServices(services);
nameInput.value = '';
renderList();
showToast('Added ' + name);
});
document.getElementById('dg-svc-name').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('dg-svc-add').click();
});
document.getElementById('dg-svc-name').addEventListener('input', e => {
const name = e.target.value.trim();
if (name.length > 2) {
document.getElementById('dg-svc-priority').value = autoClassify(name);
}
});
// ── Export CSV ──
document.getElementById('dg-svc-export').addEventListener('click', () => {
const services = loadServices();
if (services.length === 0) { showToast('No services to export'); return; }
let csv = 'Service Name,Auth Type,Priority,Status,Domain,Login URL,Google Detail URL,Set Password,Update Email,Review Access,Revoke Google,Verified\n';
services.forEach(svc => {
const steps = getSteps(svc.type);
const done = steps.filter(st => svc.steps[st.key]).length;
const status = done === steps.length ? 'Complete' : done > 0 ? 'In Progress' : 'Not Started';
const safeName = svc.name.replace(/"/g, '""');
const domain = svc.domain || getDomain(svc.name) || '';
const loginUrl = svc.type === 'oauth' ? (getLoginUrl(domain) || '') : '';
csv += '"' + safeName + '",' + svc.type + ',' + svc.priority + ',' + status + ',';
csv += '"' + domain + '","' + loginUrl + '","' + (svc.detailUrl || '') + '",';
csv += (svc.steps.password ? 'Yes' : '-') + ',';
csv += (svc.steps.email ? 'Yes' : 'No') + ',';
csv += (svc.steps.review ? 'Yes' : '-') + ',';
csv += (svc.steps.disconnect ? 'Yes' : '-') + ',';
csv += (svc.steps.verified ? 'Yes' : 'No') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'connected-services-audit-' + new Date().toISOString().split('T')[0] + '.csv';
a.click();
URL.revokeObjectURL(url);
showToast('Exported ' + services.length + ' services to CSV');
});
// Initial render
renderList();
}
// ═══════════════════════════════════════════════
// MODULE: GitHub Pages Sync Bridge
// ═══════════════════════════════════════════════
function initPageSync() {
// Send tracked services to the landing page via postMessage
const STORE_KEY = 'dg-connected-services';
let data = [];
try { data = JSON.parse(GM_getValue(STORE_KEY, '[]')); }
catch { data = []; }
if (data.length > 0) {
// postMessage to the page
window.postMessage({ type: 'degoogler-sync', services: data }, '*');
// Also inject as hidden element for fallback
const el = document.createElement('div');
el.id = 'degoogler-userscript-data';
el.style.display = 'none';
el.textContent = JSON.stringify(data);
document.body.appendChild(el);
}
// Listen for requests from the page
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'degoogler-request-sync') {
window.postMessage({ type: 'degoogler-sync', services: data }, '*');
}
});
}
// ── Route to correct module ──
function init() {
GM_addStyle(STYLES);
// Remove anti-FOUC
const af = document.getElementById('degoogler-antifouc');
if (af) af.remove();
const host = location.hostname;
const path = location.pathname;
if (host === 'takeout.google.com') {
initTakeoutHelper();
} else if (host === 'myaccount.google.com') {
if (path.includes('/permissions') || path.includes('/connections')) {
initConnectedServicesAuditor();
} else {
initAccountAudit();
if (path.includes('/security')) {
initConnectedServicesAuditor();
}
}
} else if (host === 'mail.google.com') {
initGmailHelper();
} else if (host === 'www.youtube.com') {
initYouTubeExporter();
} else if (host === 'sysadmindoc.github.io') {
initPageSync();
}
}
// ── Start ──
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();