Greasy Fork is available in English.

BetterLevelFoldersPop

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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