Since the original creator aamiaa does not want new features or improvements added to their code, I created this gist with some quality-of-life improvements instead.
My version still fully respects the Discord API and does not bypass or abuse it.
Patch Notes
- Added a simple and intuitive UI
- Implemented quest queueing
- Enabled multitasking between different quests
- Added proper handling for HTTP 429 (rate limiting)
- Added safeguards to prevent multitasking a lot when quests are of the same type
- Added support for starting individual quests manually
- Added multi-language support (i18n)
- Changed cursor to blocked icon when buttons are disabled
How to use this script:
- Accept a quest under Discover -> Quests
- Press Ctrl+Shift+I to open DevTools
- Go to the
Consoletab - Paste the following code and hit enter:
Click to expand
{
// --- 0. Cleanup Previous Instances ---
const existingUI = document.getElementById('autoquest-overlay');
if (existingUI) existingUI.remove();
const existingCanvas = document.getElementById('aq-confetti');
if (existingCanvas) existingCanvas.remove();
// --- 1. Discord Internals Setup ---
delete window.$;
let wpRequire = webpackChunkdiscord_app.push([[Symbol()], {}, r => r]);
webpackChunkdiscord_app.pop();
let ApplicationStreamingStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getStreamerActiveStreamMetadata).exports.Z;
let RunningGameStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getRunningGames).exports.ZP;
let QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getQuest).exports.Z;
let ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getAllThreadsForParent).exports.Z;
let GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getSFWDefaultChannel).exports.ZP;
let FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.flushWaitQueue).exports.Z;
let api = Object.values(wpRequire.c).find(x => x?.exports?.tn?.get).exports.tn;
const isApp = typeof DiscordNative !== "undefined";
const SLEEP_BUFFER = 5000;
const AllTranslations = {
"en-US": {
name: "English (US)",
headerTitle: "AutoQuest",
scanning: "Scanning quests...",
noQuests: "No active quests found.<br>Make sure you accepted them!",
startBtnReady: "Start All Quests",
startBtnRunning: "Tasks Running...",
startBtnQueued: "Tasks Queued...",
startBtnDone: "All Quests Completed",
statusReady: "Ready to start",
statusQueued: "Queued...",
statusWatching: "Watching...",
statusActivity: "Active...",
statusInActivity: "In Activity...",
statusGame: "Playing Game...",
statusStream: "Streaming...",
statusRetrying: "Retrying...",
statusCompleted: "Completed!",
statusError: "Error Occurred",
statusDone: "Done",
errorVoice: "Error: No Voice Channel found",
errorDesktop: "Error: Desktop App Required",
refreshTooltip: "Refresh Quests",
minimizeTooltip: "Minimize",
closeTooltip: "Close",
playTooltip: "Start This Quest",
langTooltip: "Change Language",
creditsTooltip: "Credits & Patch Notes"
},
"en-GB": {
name: "English (UK)",
headerTitle: "AutoQuest",
scanning: "Scanning quests...",
noQuests: "No active quests found.<br>Ensure you have accepted them!",
startBtnReady: "Start All Quests",
startBtnRunning: "Tasks Running...",
startBtnQueued: "Tasks Queued...",
startBtnDone: "All Quests Completed",
statusReady: "Ready to start",
statusQueued: "Queued...",
statusWatching: "Watching...",
statusActivity: "Active...",
statusInActivity: "In Activity...",
statusGame: "Playing Game...",
statusStream: "Streaming...",
statusRetrying: "Retrying...",
statusCompleted: "Completed!",
statusError: "Error Occurred",
statusDone: "Done",
errorVoice: "Error: No Voice Channel found",
errorDesktop: "Error: Desktop App Required",
refreshTooltip: "Refresh Quests",
minimizeTooltip: "Minimize",
closeTooltip: "Close",
playTooltip: "Start This Quest",
langTooltip: "Change Language",
creditsTooltip: "Credits & Patch Notes"
},
"pt-BR": {
name: "Português (Brasil)",
headerTitle: "AutoQuest",
scanning: "Escaneando missões...",
noQuests: "Nenhuma missão ativa encontrada.<br>Certifique-se de tê-las aceito!",
startBtnReady: "Iniciar Todas",
startBtnRunning: "Tarefas em Andamento...",
startBtnQueued: "Tarefas na Fila...",
startBtnDone: "Todas Missões Completas",
statusReady: "Pronto para iniciar",
statusQueued: "Na fila...",
statusWatching: "Assistindo...",
statusActivity: "Ativo...",
statusInActivity: "Em Atividade...",
statusGame: "Jogando...",
statusStream: "Transmitindo...",
statusRetrying: "Tentando novamente...",
statusCompleted: "Concluído!",
statusError: "Ocorreu um Erro",
statusDone: "Feito",
errorVoice: "Erro: Nenhum Canal de Voz encontrado",
errorDesktop: "Erro: App Desktop Necessário",
refreshTooltip: "Atualizar Missões",
minimizeTooltip: "Minimizar",
closeTooltip: "Fechar",
playTooltip: "Iniciar Esta Missão",
langTooltip: "Mudar Idioma",
creditsTooltip: "Créditos e Notas"
},
"es-ES": {
name: "Español",
headerTitle: "AutoQuest",
scanning: "Escaneando misiones...",
noQuests: "No se encontraron misiones activas.<br>¡Asegúrate de haberlas aceptado!",
startBtnReady: "Iniciar Todas",
startBtnRunning: "Tareas en curso...",
startBtnQueued: "Tareas en cola...",
startBtnDone: "Todas las misiones completadas",
statusReady: "Listo para iniciar",
statusQueued: "En cola...",
statusWatching: "Viendo...",
statusActivity: "Activo...",
statusInActivity: "En actividad...",
statusGame: "Jugando...",
statusStream: "Transmitiendo...",
statusRetrying: "Reintentando...",
statusCompleted: "¡Completado!",
statusError: "Ocurrió un error",
statusDone: "Hecho",
errorVoice: "Error: No se encontró canal de voz",
errorDesktop: "Error: Se requiere App de Escritorio",
refreshTooltip: "Actualizar Misiones",
minimizeTooltip: "Minimizar",
closeTooltip: "Cerrar",
playTooltip: "Iniciar Esta Misión",
langTooltip: "Cambiar Idioma",
creditsTooltip: "Créditos y Notas"
},
"de-DE": {
name: "Deutsch",
headerTitle: "AutoQuest",
scanning: "Scanne Quests...",
noQuests: "Keine aktiven Quests gefunden.<br>Stelle sicher, dass du sie angenommen hast!",
startBtnReady: "Alle starten",
startBtnRunning: "Aufgaben laufen...",
startBtnQueued: "Aufgaben in Warteschlange...",
startBtnDone: "Alle Quests abgeschlossen",
statusReady: "Bereit zum Starten",
statusQueued: "In Warteschlange...",
statusWatching: "Schaue zu...",
statusActivity: "Aktiv...",
statusInActivity: "In Aktivität...",
statusGame: "Spiele...",
statusStream: "Streame...",
statusRetrying: "Wiederhole...",
statusCompleted: "Abgeschlossen!",
statusError: "Fehler aufgetreten",
statusDone: "Fertig",
errorVoice: "Fehler: Kein Sprachkanal gefunden",
errorDesktop: "Fehler: Desktop-App erforderlich",
refreshTooltip: "Quests aktualisieren",
minimizeTooltip: "Minimieren",
closeTooltip: "Schließen",
playTooltip: "Diese Quest starten",
langTooltip: "Sprache ändern",
creditsTooltip: "Credits & Hinweise"
},
"fr-FR": {
name: "Français",
headerTitle: "AutoQuest",
scanning: "Recherche de quêtes...",
noQuests: "Aucune quête active trouvée.<br>Assurez-vous de les avoir acceptées !",
startBtnReady: "Tout Démarrer",
startBtnRunning: "Tâches en cours...",
startBtnQueued: "Tâches en file d'attente...",
startBtnDone: "Toutes les quêtes terminées",
statusReady: "Prêt à démarrer",
statusQueued: "En file d'attente...",
statusWatching: "Regarde...",
statusActivity: "Actif...",
statusInActivity: "En activité...",
statusGame: "Joue...",
statusStream: "Stream...",
statusRetrying: "Nouvelle tentative...",
statusCompleted: "Terminé !",
statusError: "Erreur survenue",
statusDone: "Fait",
errorVoice: "Erreur : Aucun salon vocal trouvé",
errorDesktop: "Erreur : Application de bureau requise",
refreshTooltip: "Actualiser les quêtes",
minimizeTooltip: "Réduire",
closeTooltip: "Fermer",
playTooltip: "Démarrer cette quête",
langTooltip: "Changer de langue",
creditsTooltip: "Crédits et Notes"
},
"it-IT": {
name: "Italiano",
headerTitle: "AutoQuest",
scanning: "Scansione missioni...",
noQuests: "Nessuna missione attiva trovata.<br>Assicurati di averle accettate!",
startBtnReady: "Avvia Tutto",
startBtnRunning: "Compiti in corso...",
startBtnQueued: "Compiti in coda...",
startBtnDone: "Tutte le missioni completate",
statusReady: "Pronto per iniziare",
statusQueued: "In coda...",
statusWatching: "Guardando...",
statusActivity: "Attivo...",
statusInActivity: "In attività...",
statusGame: "Giocando...",
statusStream: "In streaming...",
statusRetrying: "Riprovo...",
statusCompleted: "Completato!",
statusError: "Si è verificato un errore",
statusDone: "Fatto",
errorVoice: "Errore: Nessun canale vocale trovato",
errorDesktop: "Errore: App Desktop richiesta",
refreshTooltip: "Aggiorna missioni",
minimizeTooltip: "Minimizza",
closeTooltip: "Chiudi",
playTooltip: "Avvia questa missione",
langTooltip: "Cambia lingua",
creditsTooltip: "Crediti e Note"
},
"zh-CN": {
name: "简体中文",
headerTitle: "AutoQuest",
scanning: "正在扫描任务...",
noQuests: "未找到活动任务。<br>请确保您已接受它们!",
startBtnReady: "全部开始",
startBtnRunning: "任务运行中...",
startBtnQueued: "任务排队中...",
startBtnDone: "所有任务已完成",
statusReady: "准备开始",
statusQueued: "排队中...",
statusWatching: "观看中...",
statusActivity: "活跃中...",
statusInActivity: "活动进行中...",
statusGame: "正在游戏...",
statusStream: "正在直播...",
statusRetrying: "正在重试...",
statusCompleted: "已完成!",
statusError: "发生错误",
statusDone: "完成",
errorVoice: "错误:未找到语音频道",
errorDesktop: "错误:需要桌面应用程序",
refreshTooltip: "刷新任务",
minimizeTooltip: "最小化",
closeTooltip: "关闭",
playTooltip: "开始此任务",
langTooltip: "更改语言",
creditsTooltip: "致谢与更新"
},
"zh-TW": {
name: "繁體中文",
headerTitle: "AutoQuest",
scanning: "正在掃描任務...",
noQuests: "未找到活動任務。<br>請確保您已接受它們!",
startBtnReady: "全部開始",
startBtnRunning: "任務執行中...",
startBtnQueued: "任務排隊中...",
startBtnDone: "所有任務已完成",
statusReady: "準備開始",
statusQueued: "排隊中...",
statusWatching: "觀看中...",
statusActivity: "活躍中...",
statusInActivity: "活動進行中...",
statusGame: "正在遊戲...",
statusStream: "正在直播...",
statusRetrying: "正在重試...",
statusCompleted: "已完成!",
statusError: "發生錯誤",
statusDone: "完成",
errorVoice: "錯誤:未找到語音頻道",
errorDesktop: "錯誤:需要桌面應用程序",
refreshTooltip: "刷新任務",
minimizeTooltip: "最小化",
closeTooltip: "關閉",
playTooltip: "開始此任務",
langTooltip: "更改語言",
creditsTooltip: "致謝與更新"
},
"ja-JP": {
name: "日本語",
headerTitle: "AutoQuest",
scanning: "クエストをスキャン中...",
noQuests: "アクティブなクエストが見つかりません。<br>承認していることを確認してください!",
startBtnReady: "すべて開始",
startBtnRunning: "タスク実行中...",
startBtnQueued: "タスク待機中...",
startBtnDone: "全クエスト完了",
statusReady: "開始準備完了",
statusQueued: "待機中...",
statusWatching: "視聴中...",
statusActivity: "アクティブ...",
statusInActivity: "アクティビティ中...",
statusGame: "ゲーム中...",
statusStream: "配信中...",
statusRetrying: "再試行中...",
statusCompleted: "完了!",
statusError: "エラー発生",
statusDone: "終了",
errorVoice: "エラー:ボイスチャンネルが見つかりません",
errorDesktop: "エラー:デスクトップアプリが必要です",
refreshTooltip: "クエストを更新",
minimizeTooltip: "最小化",
closeTooltip: "閉じる",
playTooltip: "このクエストを開始",
langTooltip: "言語を変更",
creditsTooltip: "クレジットとメモ"
},
"ko-KR": {
name: "한국어",
headerTitle: "AutoQuest",
scanning: "퀘스트 스캔 중...",
noQuests: "진행 중인 퀘스트가 없습니다.<br>수락했는지 확인하세요!",
startBtnReady: "모두 시작",
startBtnRunning: "작업 진행 중...",
startBtnQueued: "대기열에 추가됨...",
startBtnDone: "모든 퀘스트 완료",
statusReady: "시작 준비 완료",
statusQueued: "대기 중...",
statusWatching: "시청 중...",
statusActivity: "활동 중...",
statusInActivity: "활동 수행 중...",
statusGame: "게임 중...",
statusStream: "방송 중...",
statusRetrying: "재시도 중...",
statusCompleted: "완료!",
statusError: "오류 발생",
statusDone: "완료",
errorVoice: "오류: 음성 채널을 찾을 수 없음",
errorDesktop: "오류: 데스크톱 앱 필요",
refreshTooltip: "퀘스트 새로 고침",
minimizeTooltip: "최소화",
closeTooltip: "닫기",
playTooltip: "이 퀘스트 시작",
langTooltip: "언어 변경",
creditsTooltip: "크레딧 및 노트"
},
"ru-RU": {
name: "Русский",
headerTitle: "AutoQuest",
scanning: "Сканирование квестов...",
noQuests: "Активные квесты не найдены.<br>Убедитесь, что вы их приняли!",
startBtnReady: "Запустить все",
startBtnRunning: "Выполнение задач...",
startBtnQueued: "В очереди...",
startBtnDone: "Все квесты завершены",
statusReady: "Готово к запуску",
statusQueued: "В очереди...",
statusWatching: "Просмотр...",
statusActivity: "Активность...",
statusInActivity: "В активности...",
statusGame: "В игре...",
statusStream: "Стриминг...",
statusRetrying: "Повторная попытка...",
statusCompleted: "Завершено!",
statusError: "Ошибка",
statusDone: "Готово",
errorVoice: "Ошибка: Голосовой канал не найден",
errorDesktop: "Ошибка: Требуется приложение для ПК",
refreshTooltip: "Обновить квесты",
minimizeTooltip: "Свернуть",
closeTooltip: "Закрыть",
playTooltip: "Запустить этот квест",
langTooltip: "Изменить язык",
creditsTooltip: "Кредиты и заметки"
},
"tr-TR": {
name: "Türkçe",
headerTitle: "AutoQuest",
scanning: "Görevler taranıyor...",
noQuests: "Aktif görev bulunamadı.<br>Kabul ettiğinizden emin olun!",
startBtnReady: "Hepsini Başlat",
startBtnRunning: "Görevler Çalışıyor...",
startBtnQueued: "Sırada...",
startBtnDone: "Tüm Görevler Tamamlandı",
statusReady: "Başlamaya Hazır",
statusQueued: "Sırada...",
statusWatching: "İzleniyor...",
statusActivity: "Aktif...",
statusInActivity: "Etkinlikte...",
statusGame: "Oynanıyor...",
statusStream: "Yayınlanıyor...",
statusRetrying: "Tekrar deneniyor...",
statusCompleted: "Tamamlandı!",
statusError: "Hata Oluştu",
statusDone: "Bitti",
errorVoice: "Hata: Ses Kanalı bulunamadı",
errorDesktop: "Hata: Masaüstü Uygulaması Gerekli",
refreshTooltip: "Görevleri Yenile",
minimizeTooltip: "Küçült",
closeTooltip: "Kapat",
playTooltip: "Bu Görevi Başlat",
langTooltip: "Dili Değiştir",
creditsTooltip: "Krediler ve Notlar"
},
"pl-PL": {
name: "Polski",
headerTitle: "AutoQuest",
scanning: "Skanowanie zadań...",
noQuests: "Nie znaleziono aktywnych zadań.<br>Upewnij się, że je zaakceptowałeś!",
startBtnReady: "Rozpocznij wszystkie",
startBtnRunning: "Zadania w toku...",
startBtnQueued: "Zadania w kolejce...",
startBtnDone: "Wszystkie zadania ukończone",
statusReady: "Gotowy do startu",
statusQueued: "W kolejce...",
statusWatching: "Oglądanie...",
statusActivity: "Aktywny...",
statusInActivity: "W trakcie aktywności...",
statusGame: "W grze...",
statusStream: "Streamowanie...",
statusRetrying: "Ponawianie...",
statusCompleted: "Ukończono!",
statusError: "Wystąpił błąd",
statusDone: "Gotowe",
errorVoice: "Błąd: Nie znaleziono kanału głosowego",
errorDesktop: "Błąd: Wymagana aplikacja desktopowa",
refreshTooltip: "Odśwież zadania",
minimizeTooltip: "Zminimalizuj",
closeTooltip: "Zamknij",
playTooltip: "Rozpocznij to zadanie",
langTooltip: "Zmień język",
creditsTooltip: "Twórcy i Notatki"
}
};
let Strings = AllTranslations["en-US"]; // Default
// --- 2. VFX: Confetti Engine ---
const fireConfetti = (duration = 2000, amount = 100) => {
const canvas = document.createElement('canvas');
canvas.id = 'aq-confetti';
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100vw';
canvas.style.height = '100vh';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '100000';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const colors = ['#5865F2', '#57F287', '#EB459E', '#FEE75C', '#ffffff'];
for (let i = 0; i < amount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height - canvas.height,
vx: Math.random() * 4 - 2,
vy: Math.random() * 4 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 8 + 4,
rotation: Math.random() * 360
});
}
let start = null;
const animate = (timestamp) => {
if (!start) start = timestamp;
const progress = timestamp - start;
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.rotation += 2;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation * Math.PI / 180);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
if (p.y > canvas.height) p.y = -20;
});
if (progress < duration) {
requestAnimationFrame(animate);
} else {
canvas.remove();
}
};
requestAnimationFrame(animate);
};
// --- 3. UI Builder & State Management ---
const colors = {
bg: '#2b2d31',
header: '#1e1f22',
primary: '#5865F2',
success: '#23a559',
text: '#dbdee1',
subtext: '#949ba4',
barBg: '#3f4147'
};
const styles = `
#autoquest-overlay {
position: fixed;
top: 100px;
left: 100px;
width: 400px;
height: auto;
min-width: 300px;
min-height: 100px;
max-height: 80vh;
background: rgba(43, 45, 49, 0.98);
border-radius: 12px;
box-shadow: 0 12px 32px rgba(0,0,0,0.4);
font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: ${colors.text};
z-index: 99999;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid rgba(255,255,255,0.08);
backdrop-filter: blur(12px);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-radius 0.3s,
opacity 0.2s,
transform 0.2s;
}
/* Minimized State */
#autoquest-overlay.minimized {
width: 60px !important;
height: 60px !important;
min-width: 0 !important;
min-height: 0 !important;
border-radius: 50% !important;
cursor: pointer;
overflow: hidden;
background: ${colors.primary};
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4);
border: 2px solid rgba(255,255,255,0.2);
}
#autoquest-overlay.minimized #autoquest-header,
#autoquest-overlay.minimized #autoquest-content,
#autoquest-overlay.minimized #autoquest-footer,
#autoquest-overlay.minimized .aq-resize-handle,
#autoquest-overlay.minimized #aq-credits-modal {
display: none;
}
#autoquest-overlay.minimized::after {
content: "AQ";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-weight: 900;
font-size: 20px;
pointer-events: none;
}
#autoquest-overlay.minimized:hover {
transform: scale(1.1);
}
#autoquest-header {
background: rgba(30, 31, 34, 0.8);
padding: 14px 16px;
font-weight: 700;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255,255,255,0.05);
cursor: default;
user-select: none;
flex-shrink: 0;
}
.header-controls { display: flex; gap: 8px; position: relative; }
/* Language Dropdown */
.aq-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 5px;
background: #1e1f22;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px;
padding: 5px;
z-index: 100000;
display: none;
max-height: 200px;
width: 150px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.aq-dropdown.visible { display: block; }
.aq-lang-item {
padding: 6px 12px;
cursor: pointer;
color: #dbdee1;
font-size: 13px;
border-radius: 3px;
text-align: left;
}
.aq-lang-item:hover { background: #5865F2; color: #fff; }
.aq-lang-item.active { background: rgba(88, 101, 242, 0.3); color: #fff; font-weight: bold; }
#autoquest-content {
padding: 16px;
flex: 1;
overflow-y: auto;
min-height: 0;
}
#autoquest-content::-webkit-scrollbar { width: 6px; }
#autoquest-content::-webkit-scrollbar-track { background: transparent; }
#autoquest-content::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.4); border-radius: 4px; }
.quest-card {
position: relative;
background: rgba(30, 31, 34, 0.4);
border-radius: 8px;
padding: 16px;
margin-bottom: 10px;
border: 1px solid rgba(255,255,255,0.05);
transition: transform 0.2s, border-color 0.2s;
display: flex;
gap: 12px;
align-items: center;
overflow: hidden;
}
.quest-card:hover {
border-color: rgba(255,255,255,0.2);
background: rgba(40, 42, 45, 0.6);
transform: translateY(-2px);
}
.quest-info {
flex: 1;
min-width: 0;
z-index: 1;
}
.quest-actions {
z-index: 2;
flex-shrink: 0;
}
.quest-title {
font-weight: 700;
font-size: 15px;
margin-bottom: 8px;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quest-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.quest-type {
font-size: 10px;
color: #fff;
text-transform: uppercase;
font-weight: 800;
letter-spacing: 0.5px;
background: rgba(255,255,255,0.1);
padding: 4px 8px;
border-radius: 4px;
}
.quest-timer {
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
color: ${colors.text};
font-variant-numeric: tabular-nums;
background: rgba(0,0,0,0.4);
padding: 2px 6px;
border-radius: 4px;
}
.progress-track {
height: 8px;
background: rgba(0,0,0,0.4);
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: ${colors.primary};
width: 0%;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transform: translateX(-100%);
animation: shimmer 2s infinite;
}
@keyframes shimmer { 100% { transform: translateX(100%); } }
.status-text {
font-size: 12px;
color: #efefef;
margin-top: 6px;
display: flex;
justify-content: space-between;
font-weight: 500;
}
#autoquest-footer {
padding: 16px;
background: rgba(30, 31, 34, 0.8);
display: flex;
gap: 10px;
border-top: 1px solid rgba(255,255,255,0.05);
flex-shrink: 0;
}
.aq-btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 6px;
color: #fff;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.aq-btn:active { transform: scale(0.97); }
.aq-btn:hover { opacity: 0.9; }
.aq-btn-start {
background: linear-gradient(90deg, ${colors.primary}, #4752C4);
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3);
}
.aq-btn-icon {
width: 28px;
height: 28px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
flex: 0 0 auto;
background: rgba(255,255,255,0.1);
color: #dbdee1;
border: none;
font-family: monospace;
font-size: 16px;
}
.aq-btn-icon:hover { background: rgba(255,255,255,0.2); color: #fff; }
.aq-btn-close:hover { background: #da373c; }
.aq-btn-play {
background: rgba(88, 101, 242, 0.2);
color: #5865F2;
width: 32px;
height: 32px;
border: 1px solid rgba(88, 101, 242, 0.3);
}
.aq-btn-play:hover:not(:disabled) {
background: #5865F2;
color: white;
transform: scale(1.05);
}
.aq-btn-play:disabled {
opacity: 0.3;
cursor: not-allowed;
filter: grayscale(1);
}
/* Credits Modal */
#aq-credits-modal {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(43, 45, 49, 0.98);
z-index: 2000;
padding: 24px;
display: none;
flex-direction: column;
backdrop-filter: blur(16px);
overflow-y: auto;
}
#aq-credits-modal.visible { display: flex; }
.credits-section { margin-bottom: 20px; }
.credits-header { font-size: 18px; font-weight: bold; margin-bottom: 12px; color: #fff; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px; }
.credits-text { font-size: 14px; color: #dbdee1; line-height: 1.6; }
.credits-text strong { color: #fff; }
details {
background: rgba(0,0,0,0.2);
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid rgba(255,255,255,0.05);
}
summary {
padding: 10px;
cursor: pointer;
font-weight: 600;
color: #fff;
list-style: none;
}
summary::-webkit-details-marker { display: none; }
summary:hover { background: rgba(255,255,255,0.05); }
.patch-content { padding: 10px; border-top: 1px solid rgba(255,255,255,0.05); font-size: 13px; color: #dbdee1; }
.patch-content ul { margin: 0; padding-left: 20px; }
.patch-content li { margin-bottom: 4px; }
.aq-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
background: radial-gradient(circle at bottom right, rgba(255,255,255,0.2) 0%, transparent 50%);
border-bottom-right-radius: 12px;
z-index: 10;
}
`;
// Inject Styles
const styleEl = document.createElement('style');
styleEl.textContent = styles;
document.head.appendChild(styleEl);
// Create Main Container
const container = document.createElement('div');
container.id = 'autoquest-overlay';
container.innerHTML = `
<div id="autoquest-header">
<span style="display:flex; align-items:center; gap:10px;">
<img src="https://cdn.discordapp.com/embed/avatars/0.png" style="width:24px; height:24px; border-radius:50%;" id="aq-header-icon">
<span id="aq-title">${Strings.headerTitle}</span>
</span>
<div class="header-controls">
<button class="aq-btn-icon" id="aq-credits-btn" title="${Strings.creditsTooltip || 'Credits'}">ℹ</button>
<button class="aq-btn-icon" id="aq-lang-btn" title="${Strings.langTooltip}">🌐</button>
<div id="aq-lang-menu" class="aq-dropdown"></div>
<button class="aq-btn-icon" id="aq-refresh-btn" title="${Strings.refreshTooltip}">↻</button>
<button class="aq-btn-icon" id="aq-min-btn" title="${Strings.minimizeTooltip}">_</button>
<button class="aq-btn-icon aq-btn-close" title="${Strings.closeTooltip}">×</button>
</div>
</div>
<div id="autoquest-content">
<div style="text-align: center; color: #949ba4; padding: 40px 20px;">
<div style="margin-bottom:10px; font-size:24px;">🔍</div>
<span id="aq-scanning">${Strings.scanning}</span>
</div>
</div>
<div id="autoquest-footer">
<button id="aq-start-btn" class="aq-btn aq-btn-start">${Strings.startBtnReady}</button>
</div>
<!-- Credits Modal -->
<div id="aq-credits-modal">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin:0; color:#fff; font-size: 20px;">Credits</h2>
<button class="aq-btn-icon" id="aq-credits-close">×</button>
</div>
<div class="credits-section">
<div class="credits-text">
Original creator: <strong>aamiaa</strong><br>
Edited by: <strong>Rakise</strong>
</div>
</div>
<div class="credits-header">Patch Notes</div>
<details>
<summary>Patch 0.1.2</summary>
<div class="patch-content">
<ul>
<li>Added multi-language support (i18n)</li>
<li>Changed cursor to blocked icon when buttons are disabled</li>
</ul>
</div>
</details>
<details>
<summary>Patch 0.1.1</summary>
<div class="patch-content">
<ul>
<li>Added support for starting individual quests manually</li>
</ul>
</div>
</details>
<details>
<summary>Patch 0.1</summary>
<div class="patch-content">
<ul>
<li>Added proper handling for HTTP 429 (rate limiting)</li>
<li>Added safeguards to prevent multitasking a lot when quests are of the same type</li>
</ul>
</div>
</details>
<details>
<summary>Patch 0.0</summary>
<div class="patch-content">
<ul>
<li>Added a simple and intuitive UI</li>
<li>Implemented quest queueing</li>
<li>Enabled multitasking between different quests</li>
</ul>
</div>
</details>
</div>
<div class="aq-resize-handle"></div>
`;
document.body.appendChild(container);
// --- State & Interaction Logic ---
const header = document.getElementById('autoquest-header');
const resizeHandle = container.querySelector('.aq-resize-handle');
const minBtn = document.getElementById('aq-min-btn');
const refreshBtn = document.getElementById('aq-refresh-btn');
const langBtn = document.getElementById('aq-lang-btn');
const langMenu = document.getElementById('aq-lang-menu');
const creditsBtn = document.getElementById('aq-credits-btn');
const creditsModal = document.getElementById('aq-credits-modal');
const creditsClose = document.getElementById('aq-credits-close');
let isDragging = false, isResizing = false, isMinimized = false;
let dragOffset = { x: 0, y: 0 };
let resizeStart = { w: 0, h: 0, x: 0, y: 0 };
let rafId = null;
let savedSize = { w: 380, h: 500 };
let hasMoved = false;
// Credits Logic
creditsBtn.onclick = (e) => {
e.stopPropagation();
creditsModal.classList.add('visible');
};
creditsClose.onclick = (e) => {
e.stopPropagation();
creditsModal.classList.remove('visible');
};
// Language Menu Toggle
langBtn.onclick = (e) => {
e.stopPropagation();
langMenu.classList.toggle('visible');
};
// Close menu when clicking elsewhere
document.addEventListener('click', (e) => {
if (!e.target.closest('#aq-lang-btn') && !e.target.closest('#aq-lang-menu')) {
langMenu.classList.remove('visible');
}
});
const toggleMinimize = () => {
isMinimized = !isMinimized;
container.classList.toggle('minimized', isMinimized);
if (isMinimized) {
savedSize = { w: container.offsetWidth, h: container.offsetHeight };
} else {
container.style.width = `${savedSize.w}px`;
container.style.height = `${savedSize.h}px`;
}
};
minBtn.onclick = (e) => { e.stopPropagation(); toggleMinimize(); };
container.onmousedown = (e) => {
if (!isMinimized) return;
e.preventDefault();
isDragging = true;
hasMoved = false;
const rect = container.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
container.style.cursor = 'grabbing';
};
container.onclick = (e) => {
if(isMinimized && e.target === container && !hasMoved) toggleMinimize();
};
header.onmousedown = (e) => {
if(e.target.closest('button') || e.target.closest('.aq-dropdown')) return;
isDragging = true;
hasMoved = false;
const rect = container.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
header.style.cursor = 'grabbing';
};
resizeHandle.onmousedown = (e) => {
e.stopPropagation();
isResizing = true;
const rect = container.getBoundingClientRect();
resizeStart = { w: rect.width, h: rect.height, x: e.clientX, y: e.clientY };
};
document.onmousemove = (e) => {
if (!isDragging && !isResizing) return;
if (isDragging) hasMoved = true;
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
if (isDragging) {
const newLeft = e.clientX - dragOffset.x;
const newTop = e.clientY - dragOffset.y;
container.style.left = `${newLeft}px`;
container.style.top = `${newTop}px`;
}
else if (isResizing && !isMinimized) {
const deltaX = e.clientX - resizeStart.x;
const deltaY = e.clientY - resizeStart.y;
container.style.width = `${Math.max(300, resizeStart.w + deltaX)}px`;
container.style.height = `${Math.max(200, resizeStart.h + deltaY)}px`;
}
});
};
document.onmouseup = () => {
isDragging = false;
isResizing = false;
header.style.cursor = 'default';
container.style.cursor = isMinimized ? 'pointer' : 'default';
if (rafId) cancelAnimationFrame(rafId);
};
container.querySelector('.aq-btn-close').onclick = (e) => {
e.stopPropagation();
container.remove();
styleEl.remove();
if(autoRefreshInterval) clearInterval(autoRefreshInterval);
};
// --- UI Helpers ---
const contentArea = document.getElementById('autoquest-content');
const timers = new Map();
let autoRefreshInterval = null;
const formatTime = (seconds) => {
if (seconds <= 0) return "00:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
const updateUI = (questId, percent, status, remainingSecs = null, isDone = false) => {
const card = document.getElementById(`quest-card-${questId}`);
if (!card) return;
const bar = card.querySelector('.progress-fill');
const statusEl = card.querySelector('.status-detail');
const timerEl = card.querySelector('.quest-timer');
const playBtn = card.querySelector('.aq-btn-play');
if(bar) bar.style.width = `${Math.min(100, percent)}%`;
if(statusEl) statusEl.innerText = status;
if (remainingSecs !== null && timerEl) timerEl.innerText = formatTime(remainingSecs);
// If status implies running/queued, disable button
if(playBtn && !isDone && (status.includes("Running") || status.includes("Queued") || status.includes("Starting") || status.includes("Active") || status.includes("Watching"))) {
playBtn.disabled = true;
playBtn.innerHTML = `
<div style="width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:aq-spin 1s linear infinite;"></div>
<style>@keyframes aq-spin{to{transform:rotate(360deg)}}</style>
`;
}
if (isDone) {
if(bar) { bar.style.background = colors.success; bar.style.boxShadow = "none"; }
if(statusEl) { statusEl.style.color = colors.success; statusEl.innerText = Strings.statusCompleted; }
if(timerEl) timerEl.innerText = Strings.statusDone;
if(playBtn) { playBtn.innerText = "✓"; playBtn.disabled = true; playBtn.style.color = colors.success; playBtn.style.borderColor = colors.success; }
if (Math.random() > 0.7) fireConfetti(1000, 30);
if (timers.has(questId)) { clearInterval(timers.get(questId)); timers.delete(questId); }
}
};
const startLocalTimer = (questId, initialSeconds) => {
if (timers.has(questId)) clearInterval(timers.get(questId));
let remaining = initialSeconds;
const card = document.getElementById(`quest-card-${questId}`);
const timerEl = card?.querySelector('.quest-timer');
if(timerEl) timerEl.innerText = formatTime(remaining);
const interval = setInterval(() => {
remaining--;
if (remaining < 0) remaining = 0;
if(timerEl) timerEl.innerText = formatTime(remaining);
if(!document.getElementById(`quest-card-${questId}`)) clearInterval(interval);
}, 1000);
timers.set(questId, interval);
};
// --- Task Queue Scheduler ---
const Scheduler = {
video: { active: 0, max: 2, queue: [] },
game: { active: 0, max: 1, queue: [] },
stream: { active: 0, max: 1, queue: [] },
activity: { active: 0, max: 1, queue: [] },
enqueue: (task) => {
let type = 'video';
if(task.type.includes('PLAY_ON_DESKTOP')) type = 'game';
else if(task.type.includes('STREAM')) type = 'stream';
else if(task.type.includes('ACTIVITY')) type = 'activity';
Scheduler[type].queue.push(task);
updateUI(task.q.id, (task.done/task.needed)*100, Strings.statusQueued);
Scheduler.process(type);
},
process: async (type) => {
const pool = Scheduler[type];
if (pool.active >= pool.max || pool.queue.length === 0) return;
pool.active++;
const task = pool.queue.shift();
try {
// Execute based on type
if (type === 'video') {
await handleVideoTask(task.q, task.type, task.needed, task.done);
} else if (type === 'activity') {
await handleActivityTask(task.q, task.type, task.needed, task.done);
} else if (type === 'game') {
await handleDesktopPlayTask(task.q, task.appId, task.appName, task.needed, task.pid);
} else if (type === 'stream') {
await handleDesktopStreamTask(task.q, task.appId, task.appName, task.needed, task.pid);
}
} catch (e) {
console.error(e);
} finally {
pool.active--;
// Add delay before next to avoid rate limits
setTimeout(() => Scheduler.process(type), 1500);
}
}
};
const createQuestCard = (q, type) => {
const div = document.createElement('div');
div.className = 'quest-card';
div.id = `quest-card-${q.id}`;
let typeClass = 'badge-game';
if(type.includes("VIDEO")) typeClass = 'badge-video';
if(type.includes("STREAM")) typeClass = 'badge-stream';
if(type.includes("ACTIVITY")) typeClass = 'badge-activity';
div.innerHTML = `
<div class="quest-info">
<div class="quest-meta">
<span class="quest-type ${typeClass}">${type.replace(/_/g, ' ')}</span>
<span class="quest-timer">--:--</span>
</div>
<div class="quest-title">${q.config.messages.questName}</div>
<div class="progress-track">
<div class="progress-fill" style="width: 0%"></div>
</div>
<div class="status-text">
<span class="status-detail">${Strings.statusReady}</span>
</div>
</div>
<div class="quest-actions">
<button class="aq-btn-icon aq-btn-play" title="${Strings.playTooltip}">▶</button>
</div>
`;
// Attach individual start handler
div.querySelector('.aq-btn-play').onclick = function() {
this.disabled = true;
const pid = Math.floor(Math.random() * 30000) + 1000;
const taskConfig = q.config.taskConfig ?? q.config.taskConfigV2;
const secondsNeeded = taskConfig.tasks[type].target;
const secondsDone = q.userStatus?.progress?.[type]?.value ?? 0;
Scheduler.enqueue({
q: q,
type: type,
needed: secondsNeeded,
done: secondsDone,
appId: q.config.application.id,
appName: q.config.application.name,
pid: pid
});
};
return div;
};
// --- 4. Task Logic (Smart Throttle) ---
// Retry Helper
const safePost = async (url, body) => {
try {
return await api.post({ url, body });
} catch (e) {
// Check for 429
if (e.status === 429 || (e.body && e.body.retry_after)) {
const retryAfter = (e.body?.retry_after || 5) * 1000;
console.log(`[Rate Limit] Waiting ${retryAfter}ms...`);
await new Promise(r => setTimeout(r, retryAfter + 1000));
return await api.post({ url, body });
}
throw e;
}
};
const handleVideoTask = async (quest, taskName, secondsNeeded, secondsDone) => {
let remaining = secondsNeeded - secondsDone;
updateUI(quest.id, (secondsDone/secondsNeeded)*100, Strings.statusWatching, remaining);
startLocalTimer(quest.id, remaining);
const maxFuture = 10, speed = 7, interval = 2; // Slower interval
const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
await new Promise(r => setTimeout(r, Math.random() * 2000));
while (true) {
const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
const diff = maxAllowed - secondsDone;
const timestamp = secondsDone + speed;
if (diff >= speed) {
try {
const res = await safePost(`/quests/${quest.id}/video-progress`, { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) });
secondsDone = Math.min(secondsNeeded, timestamp);
updateUI(quest.id, (secondsDone/secondsNeeded)*100, Strings.statusWatching);
if (res.body.completed_at != null || secondsDone >= secondsNeeded) break;
} catch(e) {
updateUI(quest.id, (secondsDone/secondsNeeded)*100, Strings.statusRetrying);
}
}
await new Promise(r => setTimeout(r, interval * 1000));
}
await safePost(`/quests/${quest.id}/video-progress`, { timestamp: secondsNeeded });
updateUI(quest.id, 100, Strings.statusCompleted, 0, true);
};
const handleActivityTask = async (quest, taskName, secondsNeeded, secondsDone) => {
const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id
?? Object.values(GuildChannelStore.getAllGuilds()).find(x => x != null && x.VOCAL.length > 0)?.VOCAL[0].channel.id;
if (!channelId) {
updateUI(quest.id, 0, Strings.errorVoice);
return;
}
let remaining = secondsNeeded - secondsDone;
const streamKey = `call:${channelId}:1`;
updateUI(quest.id, 0, Strings.statusActivity, remaining);
startLocalTimer(quest.id, remaining);
while (true) {
try {
const res = await safePost(`/quests/${quest.id}/heartbeat`, { stream_key: streamKey, terminal: false });
const progress = res.body.progress.PLAY_ACTIVITY.value;
updateUI(quest.id, (progress/secondsNeeded)*100, Strings.statusInActivity);
if (progress >= secondsNeeded) {
await safePost(`/quests/${quest.id}/heartbeat`, { stream_key: streamKey, terminal: true });
break;
}
} catch(e) {
updateUI(quest.id, 0, Strings.statusRetrying);
}
await new Promise(r => setTimeout(r, 20 * 1000));
}
updateUI(quest.id, 100, Strings.statusCompleted, 0, true);
};
const handleDesktopPlayTask = (quest, applicationId, applicationName, secondsNeeded, pid) => {
return new Promise((resolve) => {
if (!isApp) { updateUI(quest.id, 0, Strings.errorDesktop); return resolve(); }
updateUI(quest.id, 0, Strings.statusGame, secondsNeeded);
api.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
const appData = res.body[0];
const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", "");
const fakeGame = { cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`, exeName, exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`, hidden: false, isLauncher: false, id: applicationId, name: appData.name, pid: pid, pidPath: [pid], processName: appData.name, start: Date.now() };
const realGetRunningGames = RunningGameStore.getRunningGames;
const realGetGameForPID = RunningGameStore.getGameForPID;
const realGames = RunningGameStore.getRunningGames();
RunningGameStore.getRunningGames = () => [fakeGame];
RunningGameStore.getGameForPID = (pid) => pid === fakeGame.pid ? fakeGame : null;
FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: [fakeGame] });
const fn = (data) => {
let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value);
const rem = Math.max(0, secondsNeeded - progress);
if (!timers.has(quest.id)) startLocalTimer(quest.id, rem);
updateUI(quest.id, (progress/secondsNeeded)*100, Strings.statusGame, rem);
if (progress >= secondsNeeded) {
RunningGameStore.getRunningGames = realGetRunningGames;
RunningGameStore.getGameForPID = realGetGameForPID;
FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] });
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
updateUI(quest.id, 100, Strings.statusCompleted, 0, true);
resolve();
}
};
FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
});
});
};
const handleDesktopStreamTask = (quest, applicationId, applicationName, secondsNeeded, pid) => {
return new Promise((resolve) => {
if (!isApp) { updateUI(quest.id, 0, Strings.errorDesktop); return resolve(); }
updateUI(quest.id, 0, Strings.statusStream, secondsNeeded);
const realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata;
ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({ id: applicationId, pid, sourceName: null });
const fn = (data) => {
let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value);
const rem = Math.max(0, secondsNeeded - progress);
if (!timers.has(quest.id)) startLocalTimer(quest.id, rem);
updateUI(quest.id, (progress/secondsNeeded)*100, Strings.statusStream, rem);
if (progress >= secondsNeeded) {
ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc;
FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
updateUI(quest.id, 100, Strings.statusCompleted, 0, true);
resolve();
}
};
FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
});
};
// --- 5. Initialization ---
const scanQuests = () => {
const rawQuests = QuestsStore.quests;
const questsList = (rawQuests instanceof Map) ? [...rawQuests.values()] : Object.values(rawQuests);
contentArea.innerHTML = '';
let count = 0;
for (const q of questsList) {
const isEnrolled = !!q.userStatus?.enrolledAt;
const isCompleted = !!q.userStatus?.completedAt;
const isExpired = new Date(q.config.expiresAt).getTime() <= Date.now();
if (q.id === "1412491570820812933") continue;
if (isCompleted || isExpired || !isEnrolled) continue;
const taskConfig = q.config.taskConfig ?? q.config.taskConfigV2;
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE"].find(x => taskConfig.tasks[x] != null);
if (!taskName) continue;
const secondsNeeded = taskConfig.tasks[taskName].target;
const secondsDone = q.userStatus?.progress?.[taskName]?.value ?? 0;
if (secondsDone >= secondsNeeded) continue;
contentArea.appendChild(createQuestCard(q, taskName));
const rem = Math.max(0, secondsNeeded - secondsDone);
const tEl = document.getElementById(`quest-card-${q.id}`).querySelector('.quest-timer');
if(tEl) tEl.innerText = formatTime(rem);
count++;
}
if(count === 0) { contentArea.innerHTML = `<div style="text-align:center; padding: 40px; color: #949ba4;">${Strings.noQuests}</div>`; return; }
};
const startAllQuests = async (btn) => {
btn.innerText = Strings.startBtnQueued;
btn.disabled = true;
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
// Trigger clicks on all visible play buttons
const playBtns = document.querySelectorAll('.aq-btn-play');
playBtns.forEach(b => {
if(!b.disabled) b.click();
});
};
// Apply a specific language
const applyLanguage = (langCode) => {
if (!AllTranslations[langCode]) return;
Strings = AllTranslations[langCode];
// Update Static Elements
document.getElementById('aq-title').innerText = Strings.headerTitle;
// Check if scanning text element still exists
const scanningEl = document.getElementById('aq-scanning');
if(scanningEl) scanningEl.innerHTML = Strings.scanning;
document.getElementById('aq-start-btn').innerText = Strings.startBtnReady;
document.getElementById('aq-refresh-btn').title = Strings.refreshTooltip;
document.getElementById('aq-min-btn').title = Strings.minimizeTooltip;
document.querySelector('.aq-btn-close').title = Strings.closeTooltip;
document.getElementById('aq-credits-btn').title = Strings.creditsTooltip;
document.getElementById('aq-lang-btn').title = Strings.langTooltip;
scanQuests();
document.getElementById('aq-lang-menu').classList.remove('visible');
};
const buildLangMenu = () => {
const menu = document.getElementById('aq-lang-menu');
menu.innerHTML = '';
Object.keys(AllTranslations).forEach(code => {
const item = document.createElement('div');
item.className = 'aq-lang-item';
item.innerText = AllTranslations[code].name;
item.onclick = () => applyLanguage(code);
menu.appendChild(item);
});
};
const init = async () => {
// Load Translations first
try {
const resp = await fetch(TRANSLATION_URL);
if (resp.ok) {
const data = await resp.json();
AllTranslations = { ...AllTranslations, ...data };
console.log(`AutoQuest: Loaded remote translations`);
}
} catch (e) {
console.log("AutoQuest: Remote translations unavailable, using built-in");
}
// Initialize UI with detected language
const userLang = (navigator.language || navigator.userLanguage);
let targetLang = "en-US";
if (AllTranslations[userLang]) targetLang = userLang;
else {
const shortLang = userLang.split('-')[0];
const found = Object.keys(AllTranslations).find(k => k.startsWith(shortLang));
if (found) targetLang = found;
}
Strings = AllTranslations[targetLang];
// Build UI elements
document.getElementById('aq-title').innerText = Strings.headerTitle;
const scanningEl = document.getElementById('aq-scanning');
if(scanningEl) scanningEl.innerHTML = Strings.scanning;
document.getElementById('aq-start-btn').innerText = Strings.startBtnReady;
document.getElementById('aq-refresh-btn').title = Strings.refreshTooltip;
document.getElementById('aq-min-btn').title = Strings.minimizeTooltip;
document.querySelector('.aq-btn-close').title = Strings.closeTooltip;
document.getElementById('aq-credits-btn').title = Strings.creditsTooltip;
document.getElementById('aq-lang-btn').title = Strings.langTooltip;
buildLangMenu();
scanQuests();
const startBtn = document.getElementById('aq-start-btn');
if(startBtn) startBtn.onclick = () => startAllQuests(startBtn);
if(refreshBtn) {
refreshBtn.onclick = () => {
refreshBtn.style.transform = 'rotate(360deg)';
refreshBtn.style.transition = 'transform 0.5s';
scanQuests();
setTimeout(() => { refreshBtn.style.transform = 'none'; }, 500);
};
}
autoRefreshInterval = setInterval(() => {}, 30000);
};
init();
console.log("AutoQuest UI Loaded");
}

Thanks for doing that! Improvements are welcome! i rly like what u did with the project!