리트코드 문제 필터 및 숨김 패널

LeetCode 필터 및 진행 카드 추가.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         LeetCode Problem Hider and Filter Panel
// @name:en      LeetCode Problem Hider and Filter Panel
// @name:es      Panel de filtros y ocultación de problemas de LeetCode
// @name:fr      Panneau de filtrage et de masquage des problèmes LeetCode
// @name:de      LeetCode Problem-Filter- und Ausblendungs-Panel
// @name:it      Pannello filtri e nascondi problemi LeetCode
// @name:pt-BR   Painel de filtros e ocultação de problemas do LeetCode
// @name:ru      Панель фильтров и скрытия задач LeetCode
// @name:ar      لوحة تصفية وإخفاء مسائل LeetCode
// @name:be      Панэль фільтраў і ўтойвання задач LeetCode
// @name:nb      LeetCode problemfilter- og skjulepanel
// @name:bg      Панел за филтриране и скриване на задачи LeetCode
// @name:zh-CN   LeetCode 题目筛选与隐藏面板
// @name:zh-TW   LeetCode 題目篩選與隱藏面板
// @name:hr      LeetCode panel za filtriranje i skrivanje zadataka
// @name:cs      Panel pro filtrování a skrývání úloh LeetCode
// @name:da      LeetCode filter- og skjulepanel
// @name:nl      LeetCode probleemfilter- en verbergpaneel
// @name:eo      LeetCode-problemo-filtrilo kaj kaŝpanelo
// @name:fi      LeetCode-tehtävien suodatus- ja piilopaneeli
// @name:fr-CA   Panneau de filtrage et de masquage LeetCode
// @name:ka      LeetCode ამოცანების ფილტრი და დამალვის პანელი
// @name:el      Πίνακας φίλτρων και απόκρυψης προβλημάτων LeetCode
// @name:he      לוח סינון והסתרת בעיות של LeetCode
// @name:hu      LeetCode feladat szűrő és elrejtő panel
// @name:id      Panel filter dan sembunyikan soal LeetCode
// @name:ja      LeetCode 問題フィルター&非表示パネル
// @name:ko      리트코드 문제 필터 및 숨김 패널
// @name:mr      LeetCode समस्या फिल्टर आणि लपविणे पॅनेल
// @name:pl      Panel filtrów i ukrywania zadań LeetCode
// @name:ro      Panou de filtrare și ascundere a problemelor LeetCode
// @name:sr      LeetCode панел за филтрирање и скривање задатака
// @name:sk      Panel na filtrovanie a skrývanie úloh LeetCode
// @name:sv      LeetCode filter- och döljpanel
// @name:th      แผงกรองและซ่อนปัญหา LeetCode
// @name:tr      LeetCode problem filtreleme ve gizleme paneli
// @name:ug      LeetCode مەسىلە سۈزۈش ۋە يوشۇرۇش تاختىسى
// @name:uk      Панель фільтрів і приховування задач LeetCode
// @name:vi      Bảng lọc và ẩn bài toán LeetCode
// @name:ckb     پانێلی فلتەرکردن و شاردنەوەی کێشەکانی LeetCode

// @version      1.4

// @description        Adds advanced filtering for LeetCode problemset including difficulty filters (Easy/Medium/Hard), hiding locked/completed/daily questions, and a live progress card in the sidebar.
// @description:en     Adds advanced filtering for LeetCode problemset including difficulty filters (Easy/Medium/Hard), hiding locked/completed/daily questions, and a live progress card in the sidebar.
// @description:es     Añade filtros avanzados para LeetCode incluyendo dificultad (Fácil/Medio/Difícil), ocultar problemas bloqueados/completados/diarios y una tarjeta de progreso en la barra lateral.
// @description:fr     Ajoute des filtres avancés pour LeetCode incluant difficulté (Facile/Moyen/Difficile), masquage des problèmes verrouillés/terminés/quotidiens et une carte de progression.
// @description:de     Fügt erweiterte Filter für LeetCode hinzu einschließlich Schwierigkeit (Leicht/Mittel/Schwer), Ausblenden gesperrter/gelöster/täglicher Aufgaben und Fortschrittskarte.
// @description:it     Aggiunge filtri avanzati per LeetCode con difficoltà (Facile/Medio/Difficile), nascondere problemi bloccati/completati/quotidiani e scheda progresso.
// @description:pt-BR  Adiciona filtros avançados ao LeetCode incluindo dificuldade (Fácil/Médio/Difícil), ocultar bloqueados/concluídos/diários e cartão de progresso.
// @description:ru     Добавляет фильтры LeetCode: сложность (Easy/Medium/Hard), скрытие заблокированных/решённых/ежедневных задач и карточку прогресса.
// @description:ar     يضيف فلاتر متقدمة لـ LeetCode تشمل الصعوبة وإخفاء الأسئلة وبطاقة التقدم.
// @description:be     Дадае фільтры LeetCode з утойваннем задач і карткай прагрэсу.
// @description:nb     Legger til filtre for LeetCode med progresjonskort.
// @description:bg     Добавя филтри за LeetCode и карта за прогрес.
// @description:zh-CN  添加 LeetCode 高级筛选(难度/隐藏题目/进度卡片)。
// @description:zh-TW  添加 LeetCode 進階篩選(難度/隱藏題目/進度卡片)。
// @description:hr     Dodaje LeetCode filtere i karticu napretka.
// @description:cs     Přidává filtry LeetCode a kartu postupu.
// @description:da     Tilføjer filtre og progresskort til LeetCode.
// @description:nl     Voegt LeetCode filters en voortgangskaart toe.
// @description:eo     Aldonas filtrilojn kaj progreskarton por LeetCode.
// @description:fi     Lisää LeetCode-suodattimet ja etenemiskortin.
// @description:fr-CA  Ajoute filtres et carte de progression pour LeetCode.
// @description:ka     ამატებს LeetCode ფილტრებს და პროგრესის ბარათს.
// @description:el     Προσθέτει φίλτρα και κάρτα προόδου στο LeetCode.
// @description:he     מוסיף מסננים וכרטיס התקדמות ל-LeetCode.
// @description:hu     Szűrőket és haladási kártyát ad a LeetCode-hoz.
// @description:id     Menambahkan filter dan kartu progres LeetCode.
// @description:ja     LeetCodeのフィルターと進捗カードを追加。
// @description:ko     LeetCode 필터 및 진행 카드 추가.
// @description:mr     LeetCode साठी फिल्टर आणि प्रगती कार्ड जोडतो.
// @description:pl     Dodaje filtry i kartę postępu LeetCode.
// @description:ro     Adaugă filtre și card de progres pentru LeetCode.
// @description:sr     Додаје филтере и картицу напретка.
// @description:sk     Pridáva filtre a kartu postupu.
// @description:sv     Lägger till filter och progresskort.
// @description:th     เพิ่มตัวกรองและการ์ดความคืบหน้า LeetCode.
// @description:tr     LeetCode için filtreler ve ilerleme kartı ekler.
// @description:ug     LeetCode سۈزگۈچ ۋە ئىلگىرىلەش كارتىسى قوشىدۇ.
// @description:uk     Додає фільтри та картку прогресу.
// @description:vi     Thêm bộ lọc và thẻ tiến độ LeetCode.
// @description:ckb    زیادکردنی فلتەر و کارت پیشرفت بۆ LeetCode.

// @match        https://leetcode.com/problemset/*
// @grant        none
// @run-at       document-idle
// @namespace https://greasyfork.org/users/1591397
// ==/UserScript==

/*
====================================================================
  LEETCODE FILTER + PROGRESS USERSCRIPT
====================================================================

  PURPOSE:
  This script enhances LeetCode’s problem list by adding:
  1. Filters (Easy / Medium / Hard)
  2. Hide Locked Problems (Premium)
  3. Hide Completed Problems
  4. Hide Daily Challenge
  5. Live progress circle (Solved / Total)
  6. Sidebar control panel UI

  SAFETY:
  - No external network requests
  - No data collection
  - Only reads local page DOM
  - Only stores user settings in localStorage
  - Runs fully client-side

====================================================================
*/

(function () {
    'use strict';

    /* ------------------------------------------------------------
       PREVENT DUPLICATE EXECUTION
       ------------------------------------------------------------
       LeetCode is a Single Page Application (SPA).
       This script may be injected multiple times.
       This flag ensures it only runs once.
    ------------------------------------------------------------ */
    if (window.__lc_ui_suite_opt) return;
    window.__lc_ui_suite_opt = true;

    /* ------------------------------------------------------------
       CONSTANTS
    ------------------------------------------------------------ */

    // CSS class used to hide elements
    const HIDDEN = 'lc-hidden';

    // Key used in browser localStorage
    const STORE_KEY = 'lc_pro_suite_settings_v6';

    /* ------------------------------------------------------------
       DEFAULT USER SETTINGS
       ------------------------------------------------------------
       These values are used if user has never saved settings.
    ------------------------------------------------------------ */
    const defaultSettings = {
        hideLocked: true, // hide premium locked problems
        hideCompleted: false, // hide solved problems
        hideDaily: false, // hide daily challenge
        easy: true, // show easy problems
        medium: true, // show medium problems
        hard: true, // show hard problems
        collapsed: false // sidebar open/closed
    };

    /* ------------------------------------------------------------
       LOAD SAVED SETTINGS
    ------------------------------------------------------------ */
    let settings =
        JSON.parse(localStorage.getItem(STORE_KEY) || 'null') ||
        defaultSettings;

    /* Save settings whenever user changes a toggle */
    function saveSettings() {
        localStorage.setItem(STORE_KEY, JSON.stringify(settings));
    }

    /* ------------------------------------------------------------
       STYLE INJECTION (CSS)
       ------------------------------------------------------------
       We inject CSS dynamically so:
       - No manual installation needed
       - Works instantly on page load
    ------------------------------------------------------------ */
    const style = document.createElement('style');

    style.textContent = `
/* Hidden rows */
.${HIDDEN}{display:none!important}

/* Sidebar container */
#lc-suite{
  margin-top:10px;
  border-radius:14px;
  background:rgba(255,255,255,0.04);
  border:1px solid rgba(255,255,255,0.06);
  overflow:hidden;
}

/* Header bar */
#lc-header{
  padding:12px;
  cursor:pointer;
  display:flex;
  justify-content:space-between;
  font-weight:700;
  font-size:13px;
  color:#e5e7eb;
}

/* Panel body */
#lc-body{padding:12px}

/* Collapsed state hides body */
#lc-suite.collapsed #lc-body{display:none}

/* Toggle button styling */
.lc-btn{
  display:flex;
  justify-content:space-between;
  padding:10px;
  margin:6px 0;
  border-radius:10px;
  cursor:pointer;
  background:rgba(255,255,255,0.03);
  transition:0.15s;
  user-select:none;
}

.lc-btn:hover{transform:translateY(-1px)}
.lc-active{background:rgba(34,197,94,0.18)}

/* Progress card container */
.lc-progress-card{
  border-radius:13px;
  border:1px solid rgba(255,255,255,0.08);
  background:rgba(255,255,255,0.03);
  width:100%;
  padding:12px;
  margin:6px 0 10px 0;
  display:block;
  text-decoration:none;
  color:inherit;
}

/* Layout inside progress card */
.lc-progress-inner{
  display:flex;
  align-items:center;
  gap:10px;
}

/* Progress text */
.lc-progress-text{
  font-size:13px;
  color:#9ca3af;
  font-weight:500;
}
`;

    document.head.appendChild(style);

    /* ------------------------------------------------------------
       ROW SELECTION SYSTEM
       ------------------------------------------------------------
       We select ALL possible problem rows because LeetCode
       uses different layouts depending on page state.
    ------------------------------------------------------------ */
    const ROW_SELECTOR =
          '[role="row"], tr, [data-cy="question-card"], a[href*="/problems/"]';

    /* ------------------------------------------------------------
       DIFFICULTY DETECTION
       ------------------------------------------------------------ */
    function getDifficulty(el) {
        if (!el) return null;

        // Best case: LeetCode uses explicit CSS classes
        if (el.querySelector('.text-sd-easy')) return 'easy';
        if (el.querySelector('.text-sd-medium')) return 'medium';
        if (el.querySelector('.text-sd-hard')) return 'hard';

        // Fallback: text-based detection
        const t = el.textContent;
        if (!t) return null;

        const lower = t.toLowerCase();
        if (lower.includes('easy')) return 'easy';
        if (lower.includes('med')) return 'medium';
        if (lower.includes('hard')) return 'hard';

        return null;
    }

    /* Detect locked premium problems */
    function isLocked(el) {
        return !!el.querySelector('svg[data-icon="lock"]');
    }

    /* Detect solved problems */
    function isCompleted(el) {
        return !!el.querySelector('svg[data-icon="check"], [class*="check"]');
    }

    /* Detect daily challenge rows */
    function isDaily(el) {
        return !!el.querySelector('svg[data-icon="calendar"]');
    }

    /* ------------------------------------------------------------
       FILTER ENGINE
       ------------------------------------------------------------
       This decides whether a row should be hidden or shown.
    ------------------------------------------------------------ */
    function shouldHide(row) {

        if (isDaily(row) && settings.hideDaily) return true;

        const diff = getDifficulty(row);

        if (diff === 'easy' && !settings.easy) return true;
        if (diff === 'medium' && !settings.medium) return true;
        if (diff === 'hard' && !settings.hard) return true;

        if (settings.hideLocked && isLocked(row)) return true;
        if (settings.hideCompleted && isCompleted(row)) return true;

        return false;
    }

    /* Apply visibility changes to a row */
    function applyRow(row) {
        if (!row) return;

        const target =
              row.closest?.('[role="row"], tr, [data-cy="question-card"]') || row;

        const hide = shouldHide(target);

        // Avoid unnecessary DOM updates (performance optimization)
        if (target.classList.contains(HIDDEN) === hide) return;

        target.classList.toggle(HIDDEN, hide);
    }

    /* ------------------------------------------------------------
       FULL PAGE SCAN
       ------------------------------------------------------------ */
    function scanAll() {
        const rows = document.querySelectorAll(ROW_SELECTOR);

        for (let i = 0; i < rows.length; i++) {
            applyRow(rows[i]);
        }
    }

    /* ------------------------------------------------------------
       MUTATION OBSERVER
       ------------------------------------------------------------
       LeetCode loads content dynamically.
       This watches for new problems being added.
    ------------------------------------------------------------ */
    const observer = new MutationObserver((mutations) => {

        for (let i = 0; i < mutations.length; i++) {
            const m = mutations[i];

            for (let j = 0; j < m.addedNodes.length; j++) {
                const node = m.addedNodes[j];

                if (!(node instanceof HTMLElement)) continue;

                if (node.matches?.(ROW_SELECTOR)) applyRow(node);

                const children = node.querySelectorAll?.(ROW_SELECTOR);
                if (children?.length) {
                    for (let k = 0; k < children.length; k++) {
                        applyRow(children[k]);
                    }
                }
            }
        }
    });

    function startObserver() {
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    /* ------------------------------------------------------------
       PROGRESS PARSER
       ------------------------------------------------------------
       Reads text like:
       "123 / 350 Solved"
    ------------------------------------------------------------ */
    function parseProgress() {
        const el =
              document.querySelector('a[href="/progress/"] .text-sd-muted-foreground') ||
              document.querySelector('a[href="/progress/"] div');

        const text = el?.innerText?.trim() || '';
        const match = text.match(/(\d+)\s*\/\s*(\d+)/);

        if (!match) return { solved: 0, total: 0, raw: text };

        return {
            solved: +match[1],
            total: +match[2],
            raw: text
        };
    }

    /* ------------------------------------------------------------
       PROGRESS CIRCLE UI
       ------------------------------------------------------------ */
    function createProgressCard() {

        const a = document.createElement('a');
        a.href = '/progress/';
        a.className = 'lc-progress-card';

        const wrap = document.createElement('div');
        wrap.className = 'lc-progress-inner';

        // SVG setup
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 100 100');
        svg.style.width = '42px';
        svg.style.height = '42px';

        const r = 42;
        const C = 2 * Math.PI * r; // circumference formula

        // Background circle (empty track)
        const bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');

        bg.setAttribute('cx', '50');
        bg.setAttribute('cy', '50');
        bg.setAttribute('r', '42');
        bg.setAttribute('fill', 'none');
        bg.setAttribute('stroke', '#2a2a2a');
        bg.setAttribute('stroke-width', '10');

        // Foreground progress circle
        const fg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');

        fg.setAttribute('cx', '50');
        fg.setAttribute('cy', '50');
        fg.setAttribute('r', '42');
        fg.setAttribute('fill', 'none');
        fg.setAttribute('stroke', '#22c55e');
        fg.setAttribute('stroke-width', '10');
        fg.setAttribute('stroke-linecap', 'round');

        // Rotate so progress starts from top
        fg.style.transform = 'rotate(-90deg)';
        fg.style.transformOrigin = '50% 50%';

        // Stroke-based progress system
        fg.setAttribute('stroke-dasharray', String(C));
        fg.setAttribute('stroke-dashoffset', String(C));

        svg.appendChild(bg);
        svg.appendChild(fg);

        const text = document.createElement('div');
        text.className = 'lc-progress-text';

        let last = '';

        function update() {
            const { solved, total, raw } = parseProgress();

            if (!total) {
                if (raw !== last) text.textContent = raw || 'Loading...';
                return;
            }

            const pct = solved / total;

            // Update circular progress
            fg.style.strokeDashoffset = C * (1 - pct);

            const newText = `${solved}/${total} Solved`;

            if (newText !== last) {
                text.textContent = newText;
                last = newText;
            }
        }

        update();
        setInterval(update, 2500);

        wrap.append(svg, text);
        a.appendChild(wrap);

        return a;
    }

    /* ------------------------------------------------------------
       UI BUTTON SYSTEM
    ------------------------------------------------------------ */
    function btn(label, key) {
        const el = document.createElement('div');
        el.className = 'lc-btn';

        const render = () => {
            el.textContent = '';
            el.innerHTML = `<span>${label}</span><b>${settings[key] ? 'ON' : 'OFF'}</b>`;
            el.classList.toggle('lc-active', settings[key]);
        };

        el.onclick = () => {
            settings[key] = !settings[key];

            // prevent disabling all difficulty filters
            if (['easy', 'medium', 'hard'].includes(key)) {
                if (!settings.easy && !settings.medium && !settings.hard) {
                    settings[key] = true;
                }
            }

            saveSettings();
            render();
            scanAll();
        };

        render();
        return el;
    }

    /* ------------------------------------------------------------
       SIDEBAR INJECTION
    ------------------------------------------------------------ */
    function insertSidebar() {
        if (document.getElementById('lc-suite')) return true;

        const sidebar = document.querySelector('#sidebarWidthContainer');
        if (!sidebar) return false;

        const wrap = document.createElement('div');
        wrap.id = 'lc-suite';

        const header = document.createElement('div');
        header.id = 'lc-header';
        header.textContent = 'LeetCode Panel';

        header.onclick = () => {
            settings.collapsed = !settings.collapsed;
            wrap.classList.toggle('collapsed');
            saveSettings();
        };

        const body = document.createElement('div');
        body.id = 'lc-body';

        body.append(
            createProgressCard(),
            btn('Hide Locked', 'hideLocked'),
            btn('Hide Completed', 'hideCompleted'),
            btn('Hide Daily', 'hideDaily'),
            btn('Easy', 'easy'),
            btn('Medium', 'medium'),
            btn('Hard', 'hard')
        );

        wrap.append(header, body);
        sidebar.prepend(wrap);

        return true;
    }

    /* ------------------------------------------------------------
       INITIALIZATION FLOW
    ------------------------------------------------------------ */
    function waitForSidebarThenInit() {
        const tryInit = () => {
            const sidebar = document.querySelector('#sidebarWidthContainer');

            if (sidebar) {
                scanAll();
                startObserver();
                insertSidebar(); // try immediately
                return true;
            }
            return false;
        };

        // Try immediately first
        if (tryInit()) return;

        // Then watch DOM until sidebar appears (THIS is the fix)
        const bootObserver = new MutationObserver(() => {
            if (tryInit()) {
                bootObserver.disconnect();
            }
        });

        bootObserver.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    function init() {
        waitForSidebarThenInit();
    }

    // start script
    if (document.body) init();
    else window.addEventListener('DOMContentLoaded', init);

})();