Panel filter dan sembunyikan soal LeetCode

Menambahkan filter dan kartu progres LeetCode.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();