🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Greasy Fork is available in English.

📂 缓存分发状态(共享加速已生效)
🕒 页面同步时间:2026/01/12 04:16:27
🔄 下次更新时间:2026/01/12 05:16:27
手动刷新缓存

LeetSession – Code obsession

Work-around for LeetCode’s removed “Session Management” feature (see issue #22883). It emulates the legacy behaviour by creating a self-updating favorite list that contains every problem and automatically syncs it.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         LeetSession – Code obsession
// @namespace    https://github.com/wallandteen
// @version      1.0.0
// @description  Work-around for LeetCode’s removed “Session Management” feature (see issue #22883). It emulates the legacy behaviour by creating a self-updating favorite list that contains every problem and automatically syncs it.
// @author       Valentin Chizhov
// @license      MIT
// @homepageURL  https://github.com/wallandteen/leetsession#readme
// @supportURL   https://github.com/wallandteen/leetsession/issues
// @icon         https://raw.githubusercontent.com/wallandteen/leetsession/main/assets/icon48.ico
// @match        https://leetcode.com/*
// @run-at       document-end
// @noframes
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  const MARK = "[LS]";
  const SESSION_FLAGS = {
    CREATING: "[CREATING]", 
  };
  
  const SESSION_DESCRIPTION = `Customise freely but keep [LS] in the name for auto-sync. Give me a ⭐: https://github.com/wallandteen/leetsession`;

  const CONFIG = Object.freeze({
    BTN_ID: "leet-session-btn",
    CHUNK: 1000, 
    MAX_PAR: 6, 
    LAST_SYNC_KEY: "leetSession_lastSync_v1", 
  });

  const MESSAGES = Object.freeze({
    TOAST: {
      ALREADY_CREATING: "⚠️ You are already creating a session. Please wait for it to complete.",
      CREATING_SESSION: "⏳ Creating new session... Please wait.",
      SESSION_CREATED: "✅ Session created successfully!",
      SESSION_FAILED: (error) => `❌ Failed to create session: ${error}`,
      SYNCED_PROBLEMS: "✅ All problems synchronized.",
      INCOMPLETE_SESSIONS: "⚠️ Found incomplete sessions. Syncing to complete...",
    },
    
    UI: {
      BUTTON_TEXT: "New Session",
      BEFORE_UNLOAD: "Session creation is in progress. Are you sure you want to leave?"
    }
  });

  const log = (...args) => console.log("[LeetSession]", ...args);


  class Toast {
    static _ensureStyle() {
      if (document.getElementById("leet-toast-css")) return;
      const style = document.createElement("style");
      style.id = "leet-toast-css";
      style.textContent = `
      .leet{position:fixed;top:20px;right:20px;z-index:90000;background:#fff;border-radius:8px;
        padding:12px 16px;margin-top:8px;box-shadow:0 4px 12px rgba(0,0,0,.15);max-width:420px;
        font-family:system-ui,'Segoe UI',sans-serif;display:flex;gap:12px;animation:in .3s;color:#000;}
      .leet.i{border-left:4px solid #2196f3}.leet.s{border-left:4px solid #4caf50}
      .leet.w{border-left:4px solid #ff9800}.leet.e{border-left:4px solid #f44336}
      .leet .x{margin-left:auto;background:none;border:none;font:16px monospace;cursor:pointer;color:#777}
      @keyframes in{from{opacity:0;transform:translateX(100%)}to{opacity:1;transform:none}}
      @keyframes out{from{opacity:1}to{opacity:0;transform:translateX(100%)}}`;
      document.head.appendChild(style);
    }

    static _show(message, type, duration) {
      this._ensureStyle();
      const toast = document.createElement("div");
      toast.className = `leet ${type}`;
      toast.innerHTML = `<span>${message}</span><button class="x">×</button>`;
      toast.querySelector(".x").onclick = () => toast.remove();
      document.body.appendChild(toast);
      
      if (duration > 0) {
        setTimeout(() => {
          toast.style.animation = "out .3s forwards";
          setTimeout(() => toast.remove(), 300);
        }, duration);
      }
    }

    static info(msg, d = 4e3) {
      this._show(msg, "i", d);
    }
    static success(msg, d = 4e3) {
      this._show(msg, "s", d);
    }
    static warn(msg, d = 4e3) {
      this._show(msg, "w", d);
    }
    static error(msg, d = 6e3) {
      this._show(msg, "e", d);
    }
  }


  class GQL {
    static _csrf() {
      return (
        document.cookie.split("; ").find((c) => c.startsWith("csrftoken="))?.split("=")[1] ||
        ""
      );
    }

    static async request(query, variables = {}, operationName = "") {
      log("🌐", operationName || query.split(/[({]/)[0].trim(), variables);
      const response = await fetch("https://leetcode.com/graphql/", {
        method: "POST",
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
          "x-csrftoken": this._csrf(),
        },
        body: JSON.stringify({ query, variables, operationName }),
      });

      if (!response.ok) throw Error(`HTTP ${response.status}`);
      const json = await response.json();
      if (json.errors) throw Error(json.errors.map((e) => e.message).join("; "));
      return json.data;
    }
  }


  const Lists = {
    create: ({ name, description = "", pub = false }) =>
      GQL.request(
        `mutation createEmptyFavorite($name: String!, $description: String, $favoriteType: FavoriteTypeEnum!, $isPublicFavorite: Boolean) {
          createEmptyFavorite(
            name: $name,
            description: $description,
            favoriteType: $favoriteType,
            isPublicFavorite: $isPublicFavorite
          ) {
            ok
            error
            favoriteSlug
          }
        }`,
        {
          name,
          description: `${description}`,
          favoriteType: "NORMAL",
          isPublicFavorite: pub,
        },
        "createEmptyFavorite"
      ),

    add: (favoriteSlug, questionSlugs) =>
      GQL.request(
        `mutation batchAddQuestionsToFavorite($favoriteSlug: String!, $questionSlugs: [String]!) {
          batchAddQuestionsToFavorite(
            favoriteSlug: $favoriteSlug,
            questionSlugs: $questionSlugs
          ) {
            ok
            error
          }
        }`,
        { 
            favoriteSlug, 
            questionSlugs 
        },
        "batchAddQuestionsToFavorite"
      ),

    reset: (favoriteSlug) =>
      GQL.request(
        `mutation resetFavoriteSessionV2($favoriteSlug: String!, $deleteSyncedCode: Boolean) {
          resetFavoriteSessionV2(
            favoriteSlug: $favoriteSlug,
            deleteSyncedCode: $deleteSyncedCode
          ) {
            ok
            error
          }
        }`,
        { 
            favoriteSlug, 
            deleteSyncedCode: true 
        },
        "resetFavoriteSessionV2"
      ),

    mine: () =>
      GQL.request(
        `query myFavoriteList {
          myCreatedFavoriteList {
            favorites {
              name
              slug
            }
          }
        }`,
        {},
        "myFavoriteList"
      ),

    questions: async (favoriteSlug) => {
      const data = await GQL.request(
        `query favoriteQuestionList($favoriteSlug: String!) {
          favoriteQuestionList(favoriteSlug: $favoriteSlug, limit: 10000) {
            questions {
              titleSlug
            }
          }
        }`,
        { 
            favoriteSlug 
        },
        "favoriteQuestionList"
      );
      return data.favoriteQuestionList.questions.map((q) => q.titleSlug);
    },

    updateSessionName: (favoriteSlug, newName) => {
      log(`🔄 Updating session name: ${favoriteSlug} → "${newName}"`);
      return GQL.request(
        `mutation updateFavoriteNameDescriptionV2($favoriteSlug: String!, $name: String!, $description: String) {
          updateFavoriteNameDescriptionV2(
            favoriteSlug: $favoriteSlug,
            name: $name,
            description: $description
          ) {
            ok
            error
          }
        }`,
        {
          favoriteSlug,
          name: newName,
          description: SESSION_DESCRIPTION,
        },
        "updateFavoriteNameDescriptionV2"
      ).then(result => {
        if (result.updateFavoriteNameDescriptionV2.ok) {
          log(`✅ Successfully updated session name to: "${newName}"`);
        } else {
          log(`❌ Failed to update session name: ${result.updateFavoriteNameDescriptionV2.error}`);
        }
        return result;
      });
    },
  };


  class SessionManager {

    static async getMine() {
      return (await Lists.mine()).myCreatedFavoriteList.favorites
        .filter((f) => f.name?.includes(MARK));
    }

    static async getIncompleteSessions() {
      return (await SessionManager.getMine()).filter(f => f.name?.includes(SESSION_FLAGS.CREATING));
    }

    static async hasIncompleteSessions() {
      return (await SessionManager.getIncompleteSessions()).length > 0;
    }
    
    static async fetchAllSlugs() {
      log("📡 Loading problems...");
      const json = await (await fetch("https://leetcode.com/api/problems/all/")).json();
      const slugs = json.stat_status_pairs.map((p) => p.stat.question__title_slug);
      log(`✅ Loaded ${slugs.length} problems`);
      return slugs;
    }

    static async generateUniqueSessionName(baseName, state = SESSION_FLAGS.CREATING) {
      const existingSessions = (await Lists.mine()).myCreatedFavoriteList.favorites
        .filter(f => f.name?.includes(MARK))
        .map(f => f.name);
      
      // Find all sessions that start with baseName (with or without state flags)
      const matchingSessions = existingSessions.filter(name => name.startsWith(baseName));
      
      let sessionNumber = 1;
      let sessionName = `${baseName} ${state}`;
      
      if (matchingSessions.length > 0) {
        const numbers = matchingSessions.map(name => {
          const match = name.match(new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} #(\\d+).*`));
          return match ? parseInt(match[1]) : 0;
        });
        
        const maxNumber = Math.max(...numbers);
        sessionNumber = maxNumber + 1;
        sessionName = `${baseName} #${sessionNumber} ${state}`;
      }
      
      log(`📝 Generated session name: "${sessionName}" (existing: ${existingSessions.length}, max number: ${sessionNumber - 1})`);
      return sessionName;
    }

    static async addProblemsToSession(favoriteSlug, problemsToAdd, sessionName = "Session") {
      if (!problemsToAdd.length) {
        log(`✅ No problems to add to ${sessionName}`);
        return 0;
      }

      log(`📦 Adding ${problemsToAdd.length} problems to ${sessionName}...`);
      
      let concurrency = CONFIG.MAX_PAR;
      let addedCount = 0;

      for (let i = 0; i < problemsToAdd.length; i += CONFIG.CHUNK * concurrency) {
        const group = [];
        for (let j = 0; j < concurrency && i + j * CONFIG.CHUNK < problemsToAdd.length; j++) {
          group.push(problemsToAdd.slice(i + j * CONFIG.CHUNK, i + (j + 1) * CONFIG.CHUNK));
        }

        try {
          await Promise.all(group.map((a) => Lists.add(favoriteSlug, a)));
          addedCount += Math.min(i + CONFIG.CHUNK * concurrency, problemsToAdd.length) - i;
          const currentProgress = Math.min(i + CONFIG.CHUNK * concurrency, problemsToAdd.length);
          log(`➕ Added ${currentProgress}/${problemsToAdd.length} problems to ${sessionName}`);
        } catch (e) {
          if (e.message.includes("429") && concurrency > 1) {
            concurrency--;
            i -= CONFIG.CHUNK * concurrency; // retry current window with reduced concurrency
            continue;
          }
          throw e;
        }
      }

      log(`✅ Successfully added ${addedCount} problems to ${sessionName}`);
      return addedCount;
    }

    static async create() {
      if (await SessionManager.hasIncompleteSessions()) {
        Toast.warn(MESSAGES.TOAST.ALREADY_CREATING);
        return;
      }
      
      // Show toast immediately when button is clicked
      Toast.info(MESSAGES.TOAST.CREATING_SESSION, 6000);
      
      try {
        const dateLabel = new Date().toLocaleDateString("en-GB", {
          day: "2-digit",
          month: "short",
          year: "numeric"
        }).replace(",", "");
        const listName = await SessionManager.generateUniqueSessionName(`${dateLabel} ${MARK}`, SESSION_FLAGS.CREATING);

        const {
          createEmptyFavorite: { ok, favoriteSlug },
        } = await Lists.create({ 
          name: listName,
          description: SESSION_DESCRIPTION
        });
        if (!ok) throw Error("createEmptyFavorite failed");
        
        log(`📝 Created session: ${listName} (${favoriteSlug})`);

        // Add all problems to the new session
        const slugs = await SessionManager.fetchAllSlugs();
        await SessionManager.addProblemsToSession(favoriteSlug, slugs, listName);
        log(`✅ Successfully synchronized session "${listName}"`);

        await Lists.reset(favoriteSlug);
        log("✅ Session progress reset successfully");
        
        // Mark session as ready by removing the CREATING flag
        const finalName = listName.replace(SESSION_FLAGS.CREATING, "");
        await Lists.updateSessionName(favoriteSlug, finalName);
        log(`✅ Session marked as ready: "${finalName}"`);
        
        // Remove the "creating" toast and show success
        const creatingToast = document.querySelector(".leet.i");
        if (creatingToast) creatingToast.remove();
        
        Toast.success(MESSAGES.TOAST.SESSION_CREATED, 6000);
        
        // Navigate to the created session
        window.location.href = `https://leetcode.com/problem-list/${favoriteSlug}`;
      } catch (e) {
        console.error(e);
        Toast.error(MESSAGES.TOAST.SESSION_FAILED(e.message));
        
        // Force sync if session creation was interrupted
        if (await SessionManager.hasIncompleteSessions()) {
          log("⚠️ Session creation was interrupted. Forcing sync to complete...");
          await SessionManager.sync();
        }
      } finally {
      }
    }

    static async sync() {
      const todayUTC = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
      const lastSyncDay = localStorage.getItem(CONFIG.LAST_SYNC_KEY);
      
      const incompleteSessions = await SessionManager.getIncompleteSessions();
      const forceSync = incompleteSessions.length > 0;
      
      if (lastSyncDay === todayUTC && !forceSync) {
        log("sync skipped: already done today", todayUTC);
        return;
      }

      if (forceSync) {
        log("🔄 Force sync due to incomplete sessions or interrupted creation");
        if (incompleteSessions.length > 0) {
          log(`⚠️ Found ${incompleteSessions.length} incomplete session(s):`);
          incompleteSessions.forEach(session => {
            log(`   - "${session.name}" (${session.slug})`);
          });
        }
      }

      const mine = await SessionManager.getMine();
      
      if (!mine.length) return;

      const slugs = await SessionManager.fetchAllSlugs();
      let addedTotal = 0;

      for (const fav of mine) {
        log(`🔄 Processing session: "${fav.name}" (${fav.slug})`);
        const haveArr = await Lists.questions(fav.slug);
        const have = new Set(haveArr);
        const diff = slugs.filter((x) => !have.has(x));
        
        log(`   - Has ${haveArr.length} problems, ${diff.length} new problems to add`);
        
        // Add missing problems to the session if any
        if (diff.length > 0) {
          const addedCount = await SessionManager.addProblemsToSession(fav.slug, diff, fav.name);
          addedTotal += addedCount;
          log(`🔄 ${fav.name}: +${addedCount} problems`);
        } else {
          log(`   - No new problems to add`);
        }
        
        // If this was an incomplete session, reset progress and mark it as ready
        if (fav.name?.includes(SESSION_FLAGS.CREATING)) {
          log(`🔄 Resetting progress for incomplete session: "${fav.name}"`);
          await Lists.reset(fav.slug);
          
          const finalName = fav.name.replace(SESSION_FLAGS.CREATING, "");
          log(`🔄 Marking session as ready: "${fav.name}" → "${finalName}"`);
          await Lists.updateSessionName(fav.slug, finalName);
          log(`✅ Successfully marked session as ready: ${finalName}`);
        } else {
          log(`   - Session "${fav.name}" is already ready (no CREATING flag)`);
        }
      }

      if (addedTotal) {
        Toast.success(MESSAGES.TOAST.SYNCED_PROBLEMS, 5000);
        log(`✅ Total synced: +${addedTotal} problems`);
      }

      if (forceSync) {
        log("🧹 Cleared interrupted session tracking");
      }

      localStorage.setItem(CONFIG.LAST_SYNC_KEY, todayUTC);
    }
  }

  class UI {
    static _insertButton() {
      const currentPath = window.location.pathname;
      const allowedPaths = ['/problemset', '/problem-list', '/studyplan'];
      const isAllowedPage = allowedPaths.some(path => currentPath.startsWith(path));
      
      if (!isAllowedPage) {
        log(`Skipping button insertion on path: ${currentPath}`);
        return;
      }
      
      if (document.getElementById(CONFIG.BTN_ID)) {
        log("Button already exists, skipping...");
        return; // Don't disconnect observer - keep watching for changes
      }

      const allDivs = document.querySelectorAll("div.flex.flex-col.gap-1 > div");
      const studyPlanButton = [...allDivs].find(
        (d) => d.textContent.trim() === "Study Plan"
      );
      
      if (studyPlanButton) {
        const btn = document.createElement("div");
        btn.id = CONFIG.BTN_ID;
        btn.className = "rounded-sd-sm hover:bg-sd-accent flex h-10 cursor-pointer items-center gap-2 py-2 pl-2 transition-all";
        btn.onclick = SessionManager.create;
        
        const iconContainer = document.createElement("div");
        iconContainer.className = "relative text-[16px] leading-[normal] p-1 before:block before:h-4 before:w-4";
        
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("aria-hidden", "true");
        svg.setAttribute("focusable", "false");
        svg.setAttribute("class", "svg-inline--fa fa-refresh absolute left-1/2 top-1/2 h-[1em] -translate-x-1/2 -translate-y-1/2 align-[-0.125em]");
        svg.setAttribute("role", "img");
        svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        svg.setAttribute("viewBox", "0 0 24 24");
        
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("fill", "currentColor");
        path.setAttribute("d", "M23,12A11,11,0,1,1,12,1a10.9,10.9,0,0,1,5.882,1.7l1.411-1.411A1,1,0,0,1,21,2V6a1,1,0,0,1-1,1H16a1,1,0,0,1-.707-1.707L16.42,4.166A8.9,8.9,0,0,0,12,3a9,9,0,1,0,9,9,1,1,0,0,1,2,0Z");
        
        svg.appendChild(path);
        iconContainer.appendChild(svg);
        
        const textContainer = document.createElement("div");
        textContainer.className = "select-none text-base font-semibold";
        textContainer.textContent = MESSAGES.UI.BUTTON_TEXT;
        
        btn.appendChild(iconContainer);
        btn.appendChild(textContainer);
        studyPlanButton.parentNode.insertBefore(btn, studyPlanButton.nextSibling);
      }
    }
  }

  const observer = new MutationObserver(() => {
    // Only try to insert if button doesn't exist
    if (!document.getElementById(CONFIG.BTN_ID)) {
      UI._insertButton();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
  UI._insertButton(); // initial check


  setTimeout(SessionManager.sync, 1000);

  // Check for interrupted session creation on page load
  (async () => {
    if (await SessionManager.hasIncompleteSessions()) {
              log("⚠️ Found incomplete sessions. Syncing to complete...");
      Toast.warn(MESSAGES.TOAST.INCOMPLETE_SESSIONS, 4000);
    }
  })();

})();