BetterLevelFoldersPop

Client-side folders for gpop.io user levels list (drag & drop, persistent, export/import)

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         BetterLevelFoldersPop
// @namespace    https://gpop.io
// @version      1.2.0
// @description  Client-side folders for gpop.io user levels list (drag & drop, persistent, export/import)
// @author       Purrfect
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gpop.io
// @match        https://gpop.io
// @match        https://gpop.io/user/*
// @match        https://gpop.io/user/*?*
// @match        https://gpop.io/user/?*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const MOD_FLAG = "__betterlevelfolderspop_loaded__";
  if (window[MOD_FLAG]) return;
  window[MOD_FLAG] = true;

  const PREFIX_USER = "__blfp_v1__";
  const KEY_GLOBAL = "__blfp_global_v1__";

  // ---------------------------
  // Helpers
  // ---------------------------
  const $ = (sel, root = document) => root.querySelector(sel);
  const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
  const now = () => Date.now();

  function safeRead(key) { try { return localStorage.getItem(key); } catch { return null; } }
  function safeWrite(key, value) { try { localStorage.setItem(key, value); } catch {} }
  function safeRemove(key) { try { localStorage.removeItem(key); } catch {} }

  function getQueryParam(name) {
    try { return new URL(location.href).searchParams.get(name); } catch { return null; }
  }

  function getUserSlug() {
    const u = getQueryParam("u");
    if (u && String(u).trim()) return String(u).trim();
    const m = location.pathname.match(/\/user\/([^/]+)/i);
    if (m && m[1]) return decodeURIComponent(m[1]);
    return "unknown";
  }

  function keyForUser(user) { return `${PREFIX_USER}${user}`; }

  function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }

  function normalizeFolderName(name) {
    const s = String(name ?? "").trim();
    if (!s) return null;
    return s.slice(0, 40);
  }

  function getLevelIdFromCard(card) {
    const a = card.querySelector('a[href^="/play/"]');
    if (!a) return null;
    const href = a.getAttribute("href") || "";
    const m = href.match(/^\/play\/([^/?#]+)/i);
    return m ? m[1] : null;
  }

  function escapeHtml(s) {
    return String(s)
      .replaceAll("&", "&")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#039;");
  }

  function listUserKeys() {
    const out = [];
    try {
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k && k.startsWith(PREFIX_USER)) out.push(k);
      }
    } catch {}
    return out.sort();
  }

  // ---------------------------
  // Storage Model
  // ---------------------------
  const DEFAULT_STATE = Object.freeze({
    folders: ["Favorites", "Unsorted"],
    map: {}, // levelId -> folderName
    ui: {
      active: "All",
      collapsed: {},      // sectionKey -> bool
      hiddenFolders: {},  // folderName -> bool
      showBadge: true,    // show folder badge on cards
    },
    meta: { updatedAt: 0 },
  });

  function loadState(storeKey) {
    try {
      const raw = safeRead(storeKey);
      if (!raw) return deepClone(DEFAULT_STATE);
      const parsed = JSON.parse(raw);
      const st = deepClone(DEFAULT_STATE);

      if (parsed && typeof parsed === "object") {
        if (Array.isArray(parsed.folders)) st.folders = parsed.folders.map(normalizeFolderName).filter(Boolean);
        if (parsed.map && typeof parsed.map === "object") st.map = parsed.map;
        if (parsed.ui && typeof parsed.ui === "object") {
          st.ui.active = typeof parsed.ui.active === "string" ? parsed.ui.active : st.ui.active;
          st.ui.collapsed = parsed.ui.collapsed && typeof parsed.ui.collapsed === "object" ? parsed.ui.collapsed : {};
          st.ui.hiddenFolders = parsed.ui.hiddenFolders && typeof parsed.ui.hiddenFolders === "object" ? parsed.ui.hiddenFolders : {};
          st.ui.showBadge = typeof parsed.ui.showBadge === "boolean" ? parsed.ui.showBadge : st.ui.showBadge;
        }
        st.meta.updatedAt = Number.isFinite(+parsed?.meta?.updatedAt) ? +parsed.meta.updatedAt : 0;
      }

      st.folders = Array.from(new Set(st.folders)).filter(n => n !== "All");
      return st;
    } catch {
      return deepClone(DEFAULT_STATE);
    }
  }

  function saveState(storeKey, state) {
    try {
      state.meta.updatedAt = now();
      safeWrite(storeKey, JSON.stringify(state));
    } catch {}
  }

  // ---------------------------
  // Styling
  // ---------------------------
  function injectCSS() {
    if ($("#__blfp_css")) return;

    const css = `
      :root{
        --blfp-bg: rgba(20, 20, 24, 0.78);
        --blfp-bg2: rgba(28, 28, 36, 0.72);
        --blfp-border: rgba(255,255,255,0.12);
        --blfp-border2: rgba(255,255,255,0.18);
        --blfp-text: rgba(248,255,253,0.92);
        --blfp-dim: rgba(248,255,253,0.65);
        --blfp-accent: rgba(180, 200, 255, 0.90);
        --blfp-danger: rgba(255, 70, 90, 0.95);
      }

      #__blfp_wrap{
        margin-top: 14px;
        margin-bottom: 14px;
        background: var(--blfp-bg);
        border: 1px solid var(--blfp-border);
        border-bottom: 3px solid var(--blfp-border2);
        border-radius: 16px;
        box-shadow: 0 14px 40px rgba(0,0,0,0.28);
        backdrop-filter: blur(8px);
        overflow: hidden;
      }

      #__blfp_head{
        display:flex;
        align-items:center;
        justify-content:space-between;
        gap:10px;
        padding: 10px 12px;
        background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
        border-bottom: 1px solid rgba(255,255,255,0.10);
        user-select:none;
      }

      #__blfp_title{
        display:flex;
        align-items:center;
        gap:10px;
        font-weight: 850;
        letter-spacing: 0.2px;
        font-size: 13px;
        color: var(--blfp-text);
      }

      #__blfp_title small{
        font-weight: 700;
        color: var(--blfp-dim);
      }

      #__blfp_actions{
        display:flex;
        align-items:center;
        gap:8px;
      }

      .blfp-btn{
        all: unset;
        cursor: pointer;
        padding: 6px 10px;
        border-radius: 999px;
        background: rgba(255,255,255,0.10);
        border: 1px solid rgba(255,255,255,0.10);
        color: rgba(248,255,253,0.88);
        font-size: 12px;
        font-weight: 800;
        user-select: none;
        line-height: 1;
        white-space: nowrap;
      }
      .blfp-btn:hover{ background: rgba(255,255,255,0.14); }
      .blfp-btn:active{ transform: translateY(1px); }

      .blfp-btn-danger{
        background: rgba(255, 70, 90, 0.20) !important;
        border-color: rgba(255, 70, 90, 0.55) !important;
        color: rgba(255, 220, 225, 0.96) !important;
      }
      .blfp-btn-dangerArm{
        background: rgba(255, 70, 90, 0.32) !important;
        border-color: rgba(255, 70, 90, 0.75) !important;
      }

      .blfp-tabs{
        display:flex;
        gap:8px;
        flex-wrap: wrap;
        padding: 10px 12px;
        border-bottom: 1px solid rgba(255,255,255,0.10);
        background: rgba(0,0,0,0.06);
      }

      .blfp-tab{
        position: relative;
        all: unset;
        cursor: pointer;
        padding: 7px 12px;
        border-radius: 999px;
        background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.10);
        color: rgba(248,255,253,0.82);
        font-size: 12px;
        font-weight: 800;
        user-select: none;
        line-height: 1;
      }
      .blfp-tab:hover{ background: rgba(255,255,255,0.12); }
      .blfp-tab[data-active="1"]{
        color: rgba(248,255,253,0.95);
        background: rgba(180,200,255,0.16);
        border-color: rgba(180,200,255,0.30);
        box-shadow: 0 0 0 1px rgba(180,200,255,0.10) inset;
      }

      .blfp-dropHint{
        outline: 2px dashed rgba(180,200,255,0.70);
        outline-offset: 2px;
      }

      .blfp-section.blfp-dropHint{
        outline: 2px dashed rgba(180,200,255,0.75);
        outline-offset: 3px;
      }
      .blfp-sectionHeader.blfp-dropHint{
        outline: 2px dashed rgba(180,200,255,0.75);
        outline-offset: 3px;
      }

      #__blfp_body{ padding: 12px; }

      .blfp-section{
        margin-bottom: 14px;
        background: var(--blfp-bg2);
        border: 1px solid rgba(255,255,255,0.10);
        border-radius: 14px;
        overflow: hidden;
      }

      .blfp-sectionHeader{
        display:flex;
        align-items:center;
        justify-content:space-between;
        gap:10px;
        padding: 10px 12px;
        background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.02));
        border-bottom: 1px solid rgba(255,255,255,0.10);
        user-select:none;
      }

      .blfp-sectionHeaderLeft{
        display:flex;
        align-items:center;
        gap:10px;
        font-weight: 900;
        letter-spacing: 0.2px;
        color: rgba(248,255,253,0.92);
        font-size: 12px;
      }

      .blfp-pill{
        padding: 2px 8px;
        border-radius: 999px;
        background: rgba(255,255,255,0.10);
        border: 1px solid rgba(255,255,255,0.12);
        color: rgba(248,255,253,0.75);
        font-weight: 900;
        font-size: 11px;
      }

      .blfp-collapse{
        all: unset;
        cursor:pointer;
        padding: 6px 10px;
        border-radius: 999px;
        background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.10);
        color: rgba(248,255,253,0.82);
        font-weight: 900;
        font-size: 11px;
      }

      .blfp-sectionContent{ padding: 12px 10px 10px; }

      .blfp-grid{
        display:flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 10px;
      }

      .blfp-draggable{ cursor: grab; }
      .blfp-draggable:active{ cursor: grabbing; }

      .blfp-tag{
        position:absolute;
        right: 6px;
        top: 6px;
        z-index: 6;
        padding: 3px 7px;
        border-radius: 999px;
        background: rgba(0,0,0,0.35);
        border: 1px solid rgba(255,255,255,0.16);
        color: rgba(248,255,253,0.85);
        font-size: 10px;
        font-weight: 900;
        pointer-events: none;
      }

      #__blfp_overlay{
        position: fixed;
        inset: 0;
        z-index: 2147483647;
        display:none;
        background: rgba(0,0,0,0.45);
        backdrop-filter: blur(4px);
        overscroll-behavior: contain;
      }


      #__blfp_modalHeader{
  position: sticky;
  top: 0;
  z-index: 5;

  display:flex;
  align-items:center;
  justify-content:space-between;
  padding: 12px 12px;

  border-bottom: 1px solid rgba(255,255,255,0.10);
  background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.04));
  backdrop-filter: blur(8px);

  user-select:none;
}

      #__blfp_modalTitle{
        font-weight: 950;
        font-size: 13px;
        letter-spacing: 0.2px;
        color: rgba(248,255,253,0.94);
      }

      #__blfp_modal{
  position:absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 560px;
  max-width: calc(100vw - 28px);

  /* IMPORTANT: make the modal itself the scroll container */
  max-height: min(86vh, 760px);
  overflow: auto;

  background: var(--blfp-bg);
  border: 1px solid var(--blfp-border);
  border-bottom: 3px solid var(--blfp-border2);
  border-radius: 16px;
  box-shadow: 0 20px 70px rgba(0,0,0,0.55);

  color: var(--blfp-text);
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
}

#__blfp_modalBody{
  padding: 12px;
  display:flex;
  flex-direction: column;
  gap: 12px;

  /* remove max-height here completely */
  max-height: none;
  overflow: visible;
}

      .blfp-row{
        display:flex;
        gap:10px;
        align-items:center;
        justify-content:space-between;
      }

      .blfp-label{
        font-size: 12px;
        font-weight: 900;
        color: rgba(248,255,253,0.88);
      }

      .blfp-sub{
        font-size: 11px;
        color: rgba(248,255,253,0.62);
        margin-top: 2px;
      }

      .blfp-input{
        all: unset;
        padding: 7px 10px;
        border-radius: 10px;
        border: 1px solid rgba(255,255,255,0.12);
        background: rgba(0,0,0,0.22);
        color: rgba(248,255,253,0.92);
        font-size: 12px;
        min-width: 220px;
        text-align: center;
      }

      .blfp-textarea{
        width: 100%;
        min-height: 120px;
        resize: vertical;
        padding: 10px;
        border-radius: 12px;
        border: 1px solid rgba(255,255,255,0.12);
        background: rgba(0,0,0,0.22);
        color: rgba(248,255,253,0.92);
        font-size: 12px;
        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
        outline: none;
        box-sizing: border-box;
      }

      .blfp-divider{
        height: 1px;
        background: rgba(255,255,255,0.10);
        margin: 2px 0;
      }

      .blfp-chipRow{
        display:flex;
        flex-wrap:wrap;
        gap:8px;
        align-items:center;
        justify-content:flex-end;
      }

      .blfp-miniBtn{
        all: unset;
        cursor: pointer;
        width: 28px;
        height: 28px;
        border-radius: 10px;
        display:flex;
        align-items:center;
        justify-content:center;
        background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.12);
        color: rgba(248,255,253,0.85);
        font-weight: 900;
        user-select:none;
      }
      .blfp-miniBtn:hover{ background: rgba(255,255,255,0.12); }
      .blfp-miniBtn:active{ transform: translateY(1px); }

      /* Switch */
      .blfp-switch{
        display:flex;
        align-items:center;
        gap:10px;
      }
      .blfp-switchTrack{
        position: relative;
        width: 46px;
        height: 24px;
        border-radius: 999px;
        background: rgba(255,255,255,0.10);
        border: 1px solid rgba(255,255,255,0.14);
        cursor: pointer;
        flex: 0 0 auto;
      }
      .blfp-switchTrack[data-on="1"]{
        background: rgba(180,200,255,0.20);
        border-color: rgba(180,200,255,0.28);
      }
      .blfp-switchKnob{
        position:absolute;
        top: 50%;
        transform: translateY(-50%);
        width: 18px;
        height: 18px;
        border-radius: 999px;
        background: rgba(248,255,253,0.90);
        left: 3px;
        transition: left 120ms ease;
      }
      .blfp-switchTrack[data-on="1"] .blfp-switchKnob{
        left: 25px;
      }
    `;

    const style = document.createElement("style");
    style.id = "__blfp_css";
    style.textContent = css;
    document.head.appendChild(style);
  }

  // ---------------------------
  // UI Build
  // ---------------------------
  function buildUI(levelsRoot, state, storeKey, userSlug) {
    injectCSS();

    const stash = document.createElement("div");
    stash.id = "__blfp_stash";
    stash.style.display = "none";

    // Wrapper inserted above the levels list
    const wrap = document.createElement("div");
    wrap.id = "__blfp_wrap";

    const head = document.createElement("div");
    head.id = "__blfp_head";

    const title = document.createElement("div");
    title.id = "__blfp_title";
    title.innerHTML = `Folders <small></small>`;

    const actions = document.createElement("div");
    actions.id = "__blfp_actions";

    const btnNew = document.createElement("button");
    btnNew.className = "blfp-btn";
    btnNew.textContent = "New folder";

    const btnManage = document.createElement("button");
    btnManage.className = "blfp-btn";
    btnManage.textContent = "Manage";

    actions.appendChild(btnNew);
    actions.appendChild(btnManage);

    head.appendChild(title);
    head.appendChild(actions);

    const tabs = document.createElement("div");
    tabs.className = "blfp-tabs";

    const body = document.createElement("div");
    body.id = "__blfp_body";

    wrap.appendChild(head);
    wrap.appendChild(tabs);
    wrap.appendChild(body);

    // modal
    const overlay = document.createElement("div");
    overlay.id = "__blfp_overlay";
    overlay.innerHTML = `
      <div id="__blfp_modal" role="dialog" aria-modal="true">
        <div id="__blfp_modalHeader">
          <div id="__blfp_modalTitle">Manage folders</div>
          <button class="blfp-btn" id="__blfp_close">Close</button>
        </div>
        <div id="__blfp_modalBody"></div>
      </div>
    `;
    document.body.appendChild(overlay);

    const modalBody = $("#__blfp_modalBody", overlay);
    const closeBtn = $("#__blfp_close", overlay);

    function openModal() {
      renderManage();
      overlay.style.display = "block";
      document.documentElement.style.overflow = "hidden";
    }
    function closeModal() {
      overlay.style.display = "none";
      document.documentElement.style.overflow = "";
    }

    overlay.addEventListener("mousedown", (e) => {
      if (e.target === overlay) closeModal();
    });
    closeBtn.addEventListener("click", closeModal);
    document.addEventListener("keydown", (e) => {
      if (overlay.style.display === "block" && e.key === "Escape") closeModal();
    });

    function visibleFolders() {
      return state.folders.filter(f => !state.ui.hiddenFolders?.[f]);
    }

    function setActive(name) {
      state.ui.active = name;
      saveState(storeKey, state);
      renderTabs();
      renderSections();
    }

    function renderTabs() {
      tabs.innerHTML = "";

      const makeTab = (name, droppableKey) => {
        const b = document.createElement("button");
        b.className = "blfp-tab";
        b.textContent = name;
        b.dataset.active = (state.ui.active === name) ? "1" : "0";

        b.addEventListener("click", () => setActive(name));

        // Drop target
        b.addEventListener("dragover", (e) => {
          e.preventDefault();
          b.classList.add("blfp-dropHint");
        });
        b.addEventListener("dragleave", () => b.classList.remove("blfp-dropHint"));
        b.addEventListener("drop", (e) => {
          e.preventDefault();
          b.classList.remove("blfp-dropHint");
          const id = e.dataTransfer?.getData("text/blfp-levelid") || "";
          if (!id) return;

          if (droppableKey === "__all__") return;

            if (droppableKey === "Unsorted") delete state.map[id];
            else state.map[id] = droppableKey;

          saveState(storeKey, state);
          renderSections();
        });

        return b;
      };

      tabs.appendChild(makeTab("All", "__all__"));

        for (const f of visibleFolders()) {
            tabs.appendChild(makeTab(f, f));
        }

      // if active points to hidden folder, back to All
        if (state.ui.active !== "All" && !visibleFolders().includes(state.ui.active)) {
            state.ui.active = "All";
            saveState(storeKey, state);
        }
    }

    function ensureFolder(name) {
      const n = normalizeFolderName(name);
      if (!n) return null;
      if (n === "All" || n === "Unsorted" || n === "Favorites") return null;
      if (!state.folders.includes(n)) state.folders.push(n);
      state.folders = Array.from(new Set(state.folders));
      saveState(storeKey, state);
      return n;
    }

    function removeFolder(name) {
      state.folders = state.folders.filter(f => f !== name);
      for (const [id, folder] of Object.entries(state.map)) {
        if (folder === name) delete state.map[id];
      }
      delete state.ui.collapsed[name];
      delete state.ui.hiddenFolders[name];
      if (state.ui.active === name) state.ui.active = "All";
      saveState(storeKey, state);
    }

    function renameFolder(oldName, newName) {
      const nn = normalizeFolderName(newName);
      if (!nn || nn === "All" || nn === "Unsorted") return null;
      if (state.folders.includes(nn) && nn !== oldName) return null;

      state.folders = state.folders.map(f => (f === oldName ? nn : f));
      for (const [id, folder] of Object.entries(state.map)) {
        if (folder === oldName) state.map[id] = nn;
      }

      if (state.ui.active === oldName) state.ui.active = nn;
      if (state.ui.collapsed[oldName] != null) {
        state.ui.collapsed[nn] = !!state.ui.collapsed[oldName];
        delete state.ui.collapsed[oldName];
      }
      if (state.ui.hiddenFolders[oldName] != null) {
        state.ui.hiddenFolders[nn] = !!state.ui.hiddenFolders[oldName];
        delete state.ui.hiddenFolders[oldName];
      }

      saveState(storeKey, state);
      return nn;
    }

    function moveFolder(oldIndex, newIndex) {
      const arr = state.folders.slice();
      if (oldIndex < 0 || oldIndex >= arr.length) return;
      newIndex = Math.max(0, Math.min(arr.length - 1, newIndex));
      const [item] = arr.splice(oldIndex, 1);
      arr.splice(newIndex, 0, item);
      state.folders = arr;
      saveState(storeKey, state);
    }

    function makeSwitch(label, value, onChange, subText) {
      const row = document.createElement("div");
      row.className = "blfp-row";

      const left = document.createElement("div");
      left.innerHTML = `
        <div class="blfp-label">${escapeHtml(label)}</div>
        ${subText ? `<div class="blfp-sub">${escapeHtml(subText)}</div>` : ""}
      `;

      const right = document.createElement("div");
      right.className = "blfp-switch";

      const track = document.createElement("div");
      track.className = "blfp-switchTrack";
      track.dataset.on = value ? "1" : "0";

      const knob = document.createElement("div");
      knob.className = "blfp-switchKnob";
      track.appendChild(knob);

      function setOn(v) {
        track.dataset.on = v ? "1" : "0";
      }

      track.addEventListener("click", () => {
        const newVal = track.dataset.on !== "1";
        setOn(newVal);
        onChange(newVal);
      });

      right.appendChild(track);

      row.appendChild(left);
      row.appendChild(right);

      return row;
    }

    function resetCurrentUser() {
      safeRemove(storeKey);
      // reload state in-place
      const fresh = deepClone(DEFAULT_STATE);
      state.folders = fresh.folders;
      state.map = fresh.map;
      state.ui = fresh.ui;
      saveState(storeKey, state);
      renderTabs();
      renderSections();
    }

    function resetAllProfiles() {
      // Remove all user states + global key
      for (const k of listUserKeys()) safeRemove(k);
      safeRemove(KEY_GLOBAL);
    }

      function exportStateCurrentUser() {
          const payload = {
              type: "blfp-export",
              version: "1.2.0",
              scope: "user",
              user: userSlug,
              data: {
                  folders: Array.isArray(state.folders) ? state.folders.slice() : [],
                  map: (state.map && typeof state.map === "object") ? state.map : {},
              },
              exportedAt: new Date().toISOString(),
          };
          return JSON.stringify(payload, null, 2);
      }

      function isDefaultState(st) {
          try {
              const d = deepClone(DEFAULT_STATE);

              // Normalize folders
              const folders = Array.isArray(st?.folders) ? st.folders.slice() : [];
              const defFolders = Array.isArray(d?.folders) ? d.folders.slice() : [];

              // Check folder structure
              if (folders.length !== defFolders.length) return false;
              for (let i = 0; i < folders.length; i++) {
                  if (folders[i] !== defFolders[i]) return false;
              }

              // Check level assignments
              const map = (st?.map && typeof st.map === "object") ? st.map : {};
              if (Object.keys(map).length !== 0) return false;

              // Ignore ALL UI settings and meta
              return true;
          } catch {
              return false;
          }
      }



      function exportStateAllProfiles() {
          const users = {};

          for (const k of listUserKeys()) {
              const u = k.slice(PREFIX_USER.length);
              const st = loadState(k);

              // Skip profiles that are still default (folders+map only)
              if (isDefaultState(st)) continue;

              users[u] = {
                  folders: Array.isArray(st.folders) ? st.folders.slice() : [],
                  map: (st.map && typeof st.map === "object") ? st.map : {},
              };
          }

          const payload = {
              type: "blfp-export",
              version: "1.2.0",
              scope: "all",
              users,
              exportedAt: new Date().toISOString(),
          };

          return JSON.stringify(payload, null, 2);
      }


    function importPayload(raw, mode, scopeTarget) {
      // mode: "merge" | "replace"
      // scopeTarget: "current" | "all"
      let data;
      try {
        data = JSON.parse(raw);
      } catch {
        return { ok: false, msg: "Invalid JSON." };
      }

      const isExport = data && typeof data === "object" && data.type === "blfp-export";
      if (!isExport) return { ok: false, msg: "Not a BLFP export JSON." };

      const applyOne = (targetKey, incomingState) => {
          const incoming = incomingState || {};
          // Accept minimal payloads: {folders, map}
          const incomingFolders = Array.isArray(incoming.folders) ? incoming.folders : [];
          const incomingMap = (incoming.map && typeof incoming.map === "object") ? incoming.map : {};


        if (mode === "replace") {
            const base = loadState(targetKey);

            base.folders = Array.isArray(incomingFolders) ? incomingFolders.slice() : base.folders;
            base.map = incomingMap;

            saveState(targetKey, base);
            return;
        }


        // merge
        const cur = loadState(targetKey);

        // folders: keep order of existing, append new (in incoming order)
          const curSet = new Set(cur.folders);
          const mergedFolders = cur.folders.slice();

          for (const f of incomingFolders) {
              const nf = normalizeFolderName(f);
              if (!nf) continue;
              if (!curSet.has(nf)) {
                  mergedFolders.push(nf);
                  curSet.add(nf);
              }
          }

          cur.folders = mergedFolders.filter(n => n !== "All");

        // Ensure base folders exist and stay on top
          cur.folders = Array.isArray(cur.folders) ? cur.folders : [];
          cur.folders = cur.folders.filter(f => f !== "Favorites" && f !== "Unsorted");
          cur.folders.unshift("Unsorted");
          cur.folders.unshift("Favorites");
          cur.folders = Array.from(new Set(cur.folders)).filter(n => n !== "All");

        // map: incoming overwrites same level ids
          for (const [id, folder] of Object.entries(incomingMap)) {
              const fn = normalizeFolderName(folder);
              if (!fn) continue;
              cur.map[id] = fn;
          }

        saveState(targetKey, cur);
      };

      if (data.scope === "user") {
        const incomingState = data.data || data.state;


        if (scopeTarget === "all") {
          for (const k of listUserKeys()) applyOne(k, incomingState);
          applyOne(storeKey, incomingState);
          return { ok: true, msg: "Imported to all profiles." };
        } else {
          applyOne(storeKey, incomingState);
          return { ok: true, msg: "Imported to current profile." };
        }
      }

      if (data.scope === "all") {
        const users = data.users && typeof data.users === "object" ? data.users : null;
        if (!users) return { ok: false, msg: "Missing users payload." };

        if (scopeTarget === "all") {
          for (const [user, st] of Object.entries(users)) {
            const k = keyForUser(user);
            applyOne(k, st);
          }
          return { ok: true, msg: "Imported to all profiles." };
        } else {
          const st = users[userSlug] || null;
          if (!st) {
            return { ok: false, msg: "This export does not contain the current user." };
          }
          applyOne(storeKey, st);
          return { ok: true, msg: "Imported to current profile." };
        }
      }

      return { ok: false, msg: "Unknown export scope." };
    }

    function renderManage() {
      const SYSTEM = new Set(["Favorites", "Unsorted"]);
      modalBody.innerHTML = "";

      // Section: Behavior
      {
        const sec = document.createElement("div");
        sec.className = "blfp-section";

        const h = document.createElement("div");
        h.className = "blfp-sectionHeader";
        h.innerHTML = `<div class="blfp-sectionHeaderLeft">Appearance <span class="blfp-pill">UI</span></div><div></div>`;

        const c = document.createElement("div");
        c.className = "blfp-sectionContent";

        const sw = makeSwitch(
          "Show folder badge on cards",
          !!state.ui.showBadge,
          (v) => {
            state.ui.showBadge = !!v;
            saveState(storeKey, state);
            renderSections();
          },
          "Tiny label on the top-right of each level card."
        );

        c.appendChild(sw);
        sec.appendChild(h);
        sec.appendChild(c);
        modalBody.appendChild(sec);
      }

      // Section: Folders
      {
        const sec = document.createElement("div");
        sec.className = "blfp-section";

        const h = document.createElement("div");
        h.className = "blfp-sectionHeader";
        h.innerHTML = `
          <div class="blfp-sectionHeaderLeft">Folders <span class="blfp-pill">${state.folders.length}</span></div>
          <div></div>
        `;

        const c = document.createElement("div");
        c.className = "blfp-sectionContent";

        const list = document.createElement("div");
        list.style.display = "flex";
        list.style.flexDirection = "column";
        list.style.gap = "10px";

        for (let idx = 0; idx < state.folders.length; idx++) {
          const f = state.folders[idx];
          const isSystem = SYSTEM.has(f);
          const isHidden = !!state.ui.hiddenFolders?.[f];

          const row = document.createElement("div");
          row.className = "blfp-row";
          row.innerHTML = `
            <div style="min-width: 200px;">
              <div class="blfp-label">${escapeHtml(f)} ${isHidden ? `<span class="blfp-pill">hidden</span>` : ""}</div>
              <div class="blfp-sub">Order affects tabs and sections. Hidden folders are not shown.</div>
            </div>

            <div class="blfp-chipRow">
              <button class="blfp-miniBtn" data-act="up" title="Move up">▲</button>
              <button class="blfp-miniBtn" data-act="down" title="Move down">▼</button>
              <button class="blfp-btn" data-act="hide">${isHidden ? "Unhide" : "Hide"}</button>
              ${isSystem ? "" : `<button class="blfp-btn" data-act="rename">Rename</button>`}
              ${isSystem ? "" : `<button class="blfp-btn blfp-btn-danger" data-act="delete">Delete</button>`}
            </div>
          `;

          row.querySelector('[data-act="up"]').addEventListener("click", () => {
            moveFolder(idx, idx - 1);
            renderTabs(); renderSections(); renderManage();
          });

          row.querySelector('[data-act="down"]').addEventListener("click", () => {
            moveFolder(idx, idx + 1);
            renderTabs(); renderSections(); renderManage();
          });

          row.querySelector('[data-act="hide"]').addEventListener("click", () => {
            state.ui.hiddenFolders = state.ui.hiddenFolders || {};
            state.ui.hiddenFolders[f] = !state.ui.hiddenFolders[f];
            saveState(storeKey, state);

            // if active folder is hidden -> back to All
            if (state.ui.hiddenFolders[f] && state.ui.active === f) {
              state.ui.active = "All";
              saveState(storeKey, state);
            }

            renderTabs(); renderSections(); renderManage();
          });


            const renameBtn = row.querySelector('[data-act="rename"]');
            if (renameBtn) {
                renameBtn.addEventListener("click", () => {
                    const nn = prompt(`Rename folder "${f}" to:`, f);
                    if (nn == null) return;
                    const res = renameFolder(f, nn);
                    if (!res) alert("Invalid name (or already exists).");
                    renderTabs(); renderSections(); renderManage();
                });
            }

            const deleteBtn = row.querySelector('[data-act="delete"]');
            if (deleteBtn) {
                deleteBtn.addEventListener("click", () => {
                    const ok = confirm(`Delete folder "${f}"? Levels inside will go to Unsorted.`);
                    if (!ok) return;
                    removeFolder(f);
                    renderTabs(); renderSections(); renderManage();
                });
            }


          list.appendChild(row);

          const div = document.createElement("div");
          div.className = "blfp-divider";
          list.appendChild(div);
        }

        // Create folder
        const createRow = document.createElement("div");
        createRow.className = "blfp-row";
        createRow.innerHTML = `
          <div>
            <div class="blfp-label">Create folder</div>
            <div class="blfp-sub">New folder will appear at the bottom. Active tab stays on "All".</div>
          </div>
          <div style="display:flex; gap:8px; align-items:center;">
            <input class="blfp-input" id="__blfp_newName" placeholder="Folder name" />
            <button class="blfp-btn" id="__blfp_createNow">Create</button>
          </div>
        `;

        c.appendChild(list);
        c.appendChild(createRow);

        sec.appendChild(h);
        sec.appendChild(c);
        modalBody.appendChild(sec);

        const inp = $("#__blfp_newName", createRow);
        const btn = $("#__blfp_createNow", createRow);
        btn.addEventListener("click", () => {
          const name = inp.value;
          const made = ensureFolder(name);
          if (!made) return alert("Invalid folder name.");
          inp.value = "";

          state.ui.active = "All";
          saveState(storeKey, state);

          renderTabs(); renderSections(); renderManage();
        });
      }

      // Section: Export / Import
      {
        const sec = document.createElement("div");
        sec.className = "blfp-section";

        const h = document.createElement("div");
        h.className = "blfp-sectionHeader";
        h.innerHTML = `<div class="blfp-sectionHeaderLeft">Export / Import <span class="blfp-pill">JSON</span></div><div></div>`;

        const c = document.createElement("div");
        c.className = "blfp-sectionContent";

        const row1 = document.createElement("div");
        row1.className = "blfp-row";
        row1.innerHTML = `
          <div>
            <div class="blfp-label">Export</div>
            <div class="blfp-sub">Copy your folder layout to share it or backup.</div>
          </div>
          <div style="display:flex; gap:8px; align-items:center;">
            <button class="blfp-btn" id="__blfp_exp_user">Export current user</button>
            <button class="blfp-btn" id="__blfp_exp_all">Export all profiles</button>
          </div>
        `;

        const ta = document.createElement("textarea");
        ta.className = "blfp-textarea";
        ta.id = "__blfp_json";
        ta.placeholder = "Export will appear here. Paste an export here to import.";

        const row2 = document.createElement("div");
        row2.className = "blfp-row";
        row2.innerHTML = `
          <div>
            <div class="blfp-label">Import</div>
            <div class="blfp-sub">Merge adds/overwrites fields. Replace wipes and uses the export as-is.</div>
          </div>
          <div style="display:flex; gap:8px; align-items:center;">
            <button class="blfp-btn" id="__blfp_imp_merge_cur">Merge → current</button>
            <button class="blfp-btn" id="__blfp_imp_replace_cur">Replace → current</button>
          </div>
        `;

        const row3 = document.createElement("div");
        row3.className = "blfp-row";
        row3.innerHTML = `
          <div>
            <div class="blfp-label">Import to all profiles</div>
            <div class="blfp-sub">Apply the same layout to every saved profile (dangerous if you replace).</div>
          </div>
          <div style="display:flex; gap:8px; align-items:center;">
            <button class="blfp-btn" id="__blfp_imp_merge_all">Merge → all</button>
            <button class="blfp-btn blfp-btn-danger" id="__blfp_imp_replace_all">Replace → all</button>
          </div>
        `;

        const msg = document.createElement("div");
        msg.className = "blfp-sub";
        msg.style.marginTop = "6px";
        msg.id = "__blfp_msg";

        c.appendChild(row1);
        c.appendChild(ta);
        c.appendChild(row2);
        c.appendChild(row3);
        c.appendChild(msg);

        sec.appendChild(h);
        sec.appendChild(c);
        modalBody.appendChild(sec);

        const expUser = $("#__blfp_exp_user");
        const expAll = $("#__blfp_exp_all");
        const impMergeCur = $("#__blfp_imp_merge_cur");
        const impReplaceCur = $("#__blfp_imp_replace_cur");
        const impMergeAll = $("#__blfp_imp_merge_all");
        const impReplaceAll = $("#__blfp_imp_replace_all");
        const out = $("#__blfp_json");
        const outMsg = $("#__blfp_msg");

        function setMsg(t) { outMsg.textContent = t; }

        expUser.addEventListener("click", async () => {
          const s = exportStateCurrentUser();
          out.value = s;
          try { await navigator.clipboard.writeText(s); setMsg("Export copied to clipboard."); }
          catch { setMsg("Export generated (copy manually)."); }
        });

        expAll.addEventListener("click", async () => {
          const s = exportStateAllProfiles();
          out.value = s;
          try { await navigator.clipboard.writeText(s); setMsg("Export copied to clipboard."); }
          catch { setMsg("Export generated (copy manually)."); }
        });

        function doImport(mode, scopeTarget) {
          const raw = out.value.trim();
          if (!raw) { setMsg("Paste JSON first."); return; }

          if (mode === "replace" && scopeTarget === "all") {
            const ok = confirm("This will REPLACE every profile. Continue?");
            if (!ok) return;
          }

          const res = importPayload(raw, mode, scopeTarget);
          setMsg(res.ok ? res.msg : `Import failed: ${res.msg}`);

          // reload current state from storage after import
          const re = loadState(storeKey);
          state.folders = re.folders;
          state.map = re.map;
          state.ui = re.ui;
          state.meta = re.meta;

          renderTabs();
          renderSections();
          renderManage();
        }

        impMergeCur.addEventListener("click", () => doImport("merge", "current"));
        impReplaceCur.addEventListener("click", () => doImport("replace", "current"));
        impMergeAll.addEventListener("click", () => doImport("merge", "all"));
        impReplaceAll.addEventListener("click", () => doImport("replace", "all"));
      }

      // Section: Reset
      {
        const sec = document.createElement("div");
        sec.className = "blfp-section";

        const h = document.createElement("div");
        h.className = "blfp-sectionHeader";
        h.innerHTML = `<div class="blfp-sectionHeaderLeft">Danger Zone <span class="blfp-pill">reset</span></div><div></div>`;

        const c = document.createElement("div");
        c.className = "blfp-sectionContent";

        const row1 = document.createElement("div");
        row1.className = "blfp-row";
        row1.innerHTML = `
          <div>
            <div class="blfp-label">Reset current user</div>
            <div class="blfp-sub">Deletes folder layout for <b>@${escapeHtml(userSlug)}</b> only (local).</div>
          </div>
          <div style="display:flex; gap:8px; align-items:center;">
            <button class="blfp-btn blfp-btn-danger" id="__blfp_reset_user">Reset</button>
          </div>
        `;

        const row2 = document.createElement("div");
        row2.className = "blfp-row";
        row2.innerHTML = `
          <div>
            <div class="blfp-label">Reset ALL profiles</div>
            <div class="blfp-sub">Deletes every BLFP profile stored in this browser.</div>
          </div>
          <div style="display:flex; gap:8px; align-items:center;">
            <button class="blfp-btn blfp-btn-danger" id="__blfp_reset_all">Reset all</button>
          </div>
        `;

        c.appendChild(row1);
        c.appendChild(row2);
        sec.appendChild(h);
        sec.appendChild(c);
        modalBody.appendChild(sec);

        const btnResetUser = $("#__blfp_reset_user");
        const btnResetAll = $("#__blfp_reset_all");

        btnResetUser.addEventListener("click", () => {
          const ok = confirm("Reset current user folders? (Local only)");
          if (!ok) return;
          resetCurrentUser();
          renderManage();
        });

        // double confirm
        let armedAt = 0;
        btnResetAll.addEventListener("click", () => {
          const t = now();
          if (t - armedAt > 6500) {
            armedAt = t;
            btnResetAll.classList.add("blfp-btn-dangerArm");
            btnResetAll.textContent = "Confirm reset all";
            setTimeout(() => {
              if (now() - armedAt > 6500) {
                armedAt = 0;
                btnResetAll.classList.remove("blfp-btn-dangerArm");
                btnResetAll.textContent = "Reset all";
              }
            }, 6600);
            return;
          }

          const ok = confirm("Last warning: delete ALL BLFP profiles from this browser?");
          if (!ok) return;

          resetAllProfiles();
          resetCurrentUser();

          armedAt = 0;
          btnResetAll.classList.remove("blfp-btn-dangerArm");
          btnResetAll.textContent = "Reset all";

          renderManage();
        });
      }
    }

    // Sections display
    function makeSection(titleText, keyName, cards) {
      const section = document.createElement("div");
      section.className = "blfp-section";

      const collapsed = !!state.ui.collapsed[keyName];

      const header = document.createElement("div");
      header.className = "blfp-sectionHeader";

      const left = document.createElement("div");
      left.className = "blfp-sectionHeaderLeft";
      left.innerHTML = `${escapeHtml(titleText)} <span class="blfp-pill">${cards.length}</span>`;

      const right = document.createElement("div");
      right.style.display = "flex";
      right.style.gap = "8px";
      right.style.alignItems = "center";

      const btnCollapse = document.createElement("button");
      btnCollapse.className = "blfp-collapse";
      btnCollapse.textContent = collapsed ? "Expand" : "Collapse";
      btnCollapse.addEventListener("click", () => {
        state.ui.collapsed[keyName] = !state.ui.collapsed[keyName];
        saveState(storeKey, state);
        renderSections();
      });

      right.appendChild(btnCollapse);

      header.appendChild(left);
      header.appendChild(right);

      const content = document.createElement("div");
      content.className = "blfp-sectionContent";
      content.style.display = collapsed ? "none" : "block";

      const grid = document.createElement("div");
      grid.className = "blfp-grid";
      for (const c of cards) grid.appendChild(c);

      content.appendChild(grid);
      section.appendChild(header);
      section.appendChild(content);

      // Drop target: section + header
      const isDroppable =
            (typeof keyName === "string" && keyName.length > 0 && keyName !== "All");

        function applyDrop(levelId) {
            if (!levelId) return;

            if (keyName === "Unsorted") delete state.map[levelId];
            else state.map[levelId] = keyName;

            saveState(storeKey, state);

            if (state.ui.collapsed[keyName]) {
                state.ui.collapsed[keyName] = false;
                saveState(storeKey, state);
            }

            renderSections();
        }

      function onDragOver(e) {
        if (!isDroppable) return;
        e.preventDefault();
        section.classList.add("blfp-dropHint");
      }
      function onDragLeave() {
        section.classList.remove("blfp-dropHint");
      }
      function onDrop(e) {
        if (!isDroppable) return;
        e.preventDefault();
        section.classList.remove("blfp-dropHint");
        const id = e.dataTransfer?.getData("text/blfp-levelid") || "";
        applyDrop(id);
      }

      header.addEventListener("dragover", onDragOver);
      header.addEventListener("dragleave", onDragLeave);
      header.addEventListener("drop", onDrop);

      section.addEventListener("dragover", onDragOver);
      section.addEventListener("dragleave", onDragLeave);
      section.addEventListener("drop", onDrop);

      return section;
    }

    // ---- Card pool
    const orderIndex = new Map(); // card -> index
    const cardsAll = [];          // stable references

    function addCardToPool(card) {
      if (!card || card.nodeType !== 1) return;
      if (!card.classList.contains("pixii-level")) return;
      if (card.__blfpPooled) return;
      card.__blfpPooled = true;

      orderIndex.set(card, orderIndex.size);
      cardsAll.push(card);
      stash.appendChild(card);
    }

    function ingestFromLevelsRoot() {
      const fresh = $$(".pixii-level", levelsRoot);
      for (const c of fresh) addCardToPool(c);
      levelsRoot.innerHTML = "";
    }

    function tagCard(card, folderName) {
      const old = card.querySelector(".__blfp_tag");
      if (old) old.remove();

      if (!state.ui.showBadge) return;
      if (!folderName) return;

      const style = getComputedStyle(card);
      if (style.position === "static") card.style.position = "relative";

      const t = document.createElement("div");
      t.className = "blfp-tag __blfp_tag";
      t.textContent = folderName;
      card.appendChild(t);
    }

    function enableDragOnce(card, levelId) {
      if (card.__blfpDragReady) return;
      card.__blfpDragReady = true;

      card.setAttribute("draggable", "true");
      card.classList.add("blfp-draggable");

      card.addEventListener("dragstart", (e) => {
        e.dataTransfer.setData("text/blfp-levelid", levelId);
        e.dataTransfer.effectAllowed = "move";
      });
    }

    function renderSections() {
      ingestFromLevelsRoot();

      const byFolder = new Map();
      const all = [];

      for (const card of cardsAll) {
        const id = getLevelIdFromCard(card);
        if (!id) continue;

        all.push(card);
        const folder = normalizeFolderName(state.map[id] || "") || "Unsorted";
          if (!byFolder.has(folder)) byFolder.set(folder, []);
          byFolder.get(folder).push(card);
      }

      const sortByOriginal = (arr) =>
        arr.sort((a, b) => (orderIndex.get(a) ?? 0) - (orderIndex.get(b) ?? 0));

      sortByOriginal(all);
      for (const arr of byFolder.values()) sortByOriginal(arr);

      for (const c of all) stash.appendChild(c);

      for (const card of all) {
        const id = getLevelIdFromCard(card);
        const folder = normalizeFolderName(state.map[id] || "");
        tagCard(card, folder || "");
        enableDragOnce(card, id);
      }

      body.innerHTML = "";

      const active = state.ui.active;
      const visFolders = visibleFolders();

      if (active === "All") {
          for (const f of visFolders) {
              const arr = byFolder.get(f) || [];
              body.appendChild(makeSection(f, f, arr));
          }
      } else {
          // only show if not hidden
          if (!visFolders.includes(active)) {
              state.ui.active = "All";
              saveState(storeKey, state);
              renderTabs();
              return renderSections();
          }
          body.appendChild(makeSection(active, active, byFolder.get(active) || []));
      }

      levelsRoot.style.display = "none";
    }

    function rebuildAll() {
      renderTabs();
      renderSections();
    }

    // Buttons
    btnNew.addEventListener("click", () => {
      const name = prompt("New folder name:", "New Folder");
      if (name == null) return;

      const made = ensureFolder(name);
      if (!made) return alert("Invalid folder name.");

      state.ui.active = "All";
      saveState(storeKey, state);

      renderTabs();
      renderSections();
    });

    btnManage.addEventListener("click", openModal);

    // Insert UI + stash
    levelsRoot.parentElement.insertBefore(wrap, levelsRoot);
    levelsRoot.parentElement.insertBefore(stash, levelsRoot);

    // Initial pool ingest + render
    ingestFromLevelsRoot();
    rebuildAll();

    const mo = new MutationObserver(() => {
      const cardsNow = $$(".pixii-level", levelsRoot);
      if (cardsNow.length) {
        ingestFromLevelsRoot();
        renderSections();
      }
    });
    mo.observe(levelsRoot, { childList: true, subtree: false });
  }

  // ---------------------------
  // Boot
  // ---------------------------
  function boot() {
    const user = getUserSlug();
    const storeKey = keyForUser(user);

    const levelsRoot = $(".levelspage-levels");
    if (!levelsRoot) return;

    const state = loadState(storeKey);

    const allowed = new Set(state.folders);
    for (const [id, f] of Object.entries(state.map)) {
      if (!f) continue;
      if (!allowed.has(f)) delete state.map[id];
    }

    // Keep UI sane
    if (!state.ui) state.ui = deepClone(DEFAULT_STATE.ui);
    if (typeof state.ui.showBadge !== "boolean") state.ui.showBadge = true;
    if (!state.ui.hiddenFolders || typeof state.ui.hiddenFolders !== "object") state.ui.hiddenFolders = {};
      // Ensure base folders exist and have a sane default order
      state.folders = Array.isArray(state.folders) ? state.folders : [];
      state.folders = state.folders.filter(f => f !== "Favorites" && f !== "Unsorted");
      state.folders.unshift("Unsorted");
      state.folders.unshift("Favorites");
      state.folders = Array.from(new Set(state.folders)).filter(n => n !== "All");

      const anyVisible = state.folders.some(f => !state.ui.hiddenFolders?.[f]);
      if (!anyVisible) {
          state.ui.hiddenFolders["Favorites"] = false;
          state.ui.hiddenFolders["Unsorted"] = false;
          state.ui.active = "All";
      }


    saveState(storeKey, state);
    buildUI(levelsRoot, state, storeKey, user);
  }

  // Wait for levels
  const start = now();
  const t = setInterval(() => {
    const levelsRoot = $(".levelspage-levels");
    const cards = levelsRoot ? $$(".pixii-level", levelsRoot) : [];
    if (levelsRoot && cards.length) {
      clearInterval(t);
      boot();
      return;
    }
    if (now() - start > 15000) {
      clearInterval(t);
      boot();
    }
  }, 200);
})();