Greasy Fork

Greasy Fork is available in English.

DUO_KEEPSTREAK

Automatically maintains the daily streak on Duolingo (NEW UPDATE)

当前为 2025-03-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         DUO_KEEPSTREAK
// @namespace    ´꒳`ⓎⒶⓂⒾⓈⒸⓇⒾⓅⓉ×͜×
// @version      v1.0.2
// @description  Automatically maintains the daily streak on Duolingo (NEW UPDATE)
// @author       ´꒳`ⓎⒶⓂⒾⓈⒸⓇⒾⓅⓉ×͜×
// @match        https://*.duolingo.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// ==/UserScript==

const getToken = () => {
    const tokenRow = document.cookie.split('; ').find(row => row.startsWith('jwt_token='));
    return tokenRow ? tokenRow.split('=')[1] : null;
};

const parseJwt = (token) => {
    try {
        return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
    } catch (e) {
        console.error("JWT parsing error", e);
        return null;
    }
};

const getHeaders = (token) => ({
    "Content-Type": "application/json",
    "Authorization": `Bearer ${token}`,
    "User-Agent": navigator.userAgent
});

const fetchUserData = async (userId, headers) => {
    try {
        const response = await fetch(`https://www.duolingo.com/2017-06-30/users/${userId}?fields=fromLanguage,learningLanguage,streakData`, { headers });
        if (!response.ok) throw new Error("Failed to fetch user data");
        return response.json();
    } catch (error) {
        console.error("Error fetching user data:", error);
        return null;
    }
};

const hasStreakToday = (data) => {
    const today = new Date().toISOString().split('T')[0];
    return data?.streakData?.currentStreak?.endDate === today;
};

const startSession = async (fromLang, learningLang, headers) => {
    try {
        const payload = {
            challengeTypes: ["translate", "match", "tapComplete", "reverseAssist", "judge"],
            fromLanguage: fromLang,
            learningLanguage: learningLang,
            type: "GLOBAL_PRACTICE"
        };
        const response = await fetch("https://www.duolingo.com/2017-06-30/sessions", {
            method: 'POST',
            headers,
            body: JSON.stringify(payload)
        });
        if (!response.ok) throw new Error("Failed to start session");
        return response.json();
    } catch (error) {
        console.error("Error starting session:", error);
        return null;
    }
};

const completeSession = async (session, headers) => {
    try {
        const payload = { ...session, heartsLeft: 0, failed: false, shouldLearnThings: true };
        const response = await fetch(`https://www.duolingo.com/2017-06-30/sessions/${session.id}`, {
            method: 'PUT',
            headers,
            body: JSON.stringify(payload)
        });
        if (!response.ok) throw new Error("Failed to complete session");
        return response.json();
    } catch (error) {
        console.error("Error completing session:", error);
        return null;
    }
};

// 🎉 Hiệu ứng giấy màu rơi xuống 🎉
const createConfetti = () => {
    const confettiCount = 100;
    for (let i = 0; i < confettiCount; i++) {
        const confetti = document.createElement("div");
        confetti.classList.add("confetti");
        document.body.appendChild(confetti);

        // Tạo màu sắc ngẫu nhiên
        confetti.style.backgroundColor = `hsl(${Math.random() * 360}, 100%, 50%)`;
        confetti.style.width = `${6 + Math.random() * 6}px`;
        confetti.style.height = confetti.style.width;
        confetti.style.position = "fixed";
        confetti.style.top = "-10px";
        confetti.style.left = `${Math.random() * 100}vw`;
        confetti.style.opacity = "0.8";
        confetti.style.borderRadius = "50%";

        // Random tốc độ rơi
        const duration = 2 + Math.random() * 3;
        confetti.style.animation = `fall ${duration}s ease-out forwards, spin ${duration}s linear infinite`;

        // Xóa sau khi animation kết thúc
        setTimeout(() => {
            confetti.remove();
        }, duration * 1000);
    }
};

// Thêm CSS animation
const addConfettiStyle = () => {
    const style = document.createElement("style");
    style.innerHTML = `
        .confetti {
            position: fixed;
            top: 0;
        }
        @keyframes fall {
            to {
                transform: translateY(100vh);
                opacity: 0;
            }
        }
        @keyframes spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
    `;
    document.head.appendChild(style);
};

const attemptStreak = async () => {
    const token = getToken();
    if (!token) return alert("❌ You are not logged into Duolingo!");

    const userId = parseJwt(token)?.sub;
    if (!userId) return alert("❌ Error retrieving user ID.");

    const headers = getHeaders(token);
    const userData = await fetchUserData(userId, headers);
    if (!userData) return alert("⚠️ Error fetching user data, try again!");

    if (hasStreakToday(userData)) return alert("✅ You have already maintained your streak today!");

    const session = await startSession(userData.fromLanguage, userData.learningLanguage, headers);
    if (!session) return alert("⚠️ Error starting session, try again!");

    const completed = await completeSession(session, headers);
    if (completed) {
        alert("🎉 Streak has been maintained! Reload the page to check.");
        createConfetti();
    } else {
        alert("⚠️ Error maintaining streak, try again!");
    }
};

const addButton = () => {
    if (document.getElementById("get-streak-btn")) return;

    const button = document.createElement("button");
    button.id = "get-streak-btn";
    button.innerText = "🔥 Get Streak 🔥";
    button.style.position = "fixed";
    button.style.bottom = "20px";
    button.style.right = "20px";
    button.style.padding = "14px 24px";
    button.style.backgroundColor = "#58cc02";
    button.style.color = "white";
    button.style.fontSize = "18px";
    button.style.fontWeight = "bold";
    button.style.border = "none";
    button.style.borderRadius = "30px";
    button.style.boxShadow = "0px 6px 12px rgba(0, 0, 0, 0.2)";
    button.style.cursor = "pointer";
    button.style.zIndex = "1000";
    button.style.transition = "all 0.2s ease-in-out";

    button.onmouseover = () => {
        button.style.backgroundColor = "#46a102";
        button.style.transform = "scale(1.1)";
    };

    button.onmouseout = () => {
        button.style.backgroundColor = "#58cc02";
        button.style.transform = "scale(1)";
    };

    button.onclick = attemptStreak;
    document.body.appendChild(button);
};

window.onload = () => {
    addConfettiStyle();
    setTimeout(addButton, 2000);
};