// ==UserScript==
// @name ChatGPT Sidebar GPTs — Fold/Unfold List
// @name:fr ChatGPT Barre latérale — Plier/Déplier la liste des GPTs
// @namespace https://omega.tools/userscripts
// @version 1.0.4
// @description Fold ChatGPT custom GPTs list in the sidebar (show only 3 by default, add “See more (n)” button).
// @description:fr Plie la liste des GPTs personnalisés de ChatGPT dans la barre latérale (affiche seulement 3 par défaut, ajoute un bouton « Voir plus (n) »).
// @author Omega
// @license MIT
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @match https://www.chatgpt.com/*
// @match https://openai.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
/* eslint-env browser */
/* eslint-disable no-console */
(function () {
"use strict";
const MAX_VISIBLE = 3;
const NS = "gpts-fold";
const LOG_PREFIX = "[gpts-fold]";
let mo = null;
let observe = null;
let observerPaused = false;
let pauseTimerId = null;
function log() {
console.log.apply(console, [LOG_PREFIX].concat(Array.prototype.slice.call(arguments)));
}
function warn() {
console.warn.apply(console, [LOG_PREFIX].concat(Array.prototype.slice.call(arguments)));
}
function error() {
console.error.apply(console, [LOG_PREFIX].concat(Array.prototype.slice.call(arguments)));
}
function ensureStyle() {
const id = NS + "-style";
if (document.getElementById(id)) return;
const style = document.createElement("style");
style.id = id;
style.textContent =
"." + NS + "-toggle{display:flex;align-items:center;gap:8px;width:100%;border:none;background:transparent;cursor:pointer;padding:8px 12px;border-radius:12px;font:inherit;text-align:left;}" +
"." + NS + "-toggle:hover{background:rgba(127,127,127,.08);}" +
"." + NS + "-toggle:focus{outline:2px solid rgba(127,127,127,.35);outline-offset:2px;}" +
"." + NS + "-hidden-row{display:none!important;}" +
"." + NS + "-chev{margin-left:auto;}";
document.head.appendChild(style);
}
function isVisible(el) {
if (!el || !(el instanceof Element)) return false;
if (el.offsetParent === null) return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
function normalizeText(s) {
return String(s == null ? "" : s).replace(/\s+/g, " ").trim();
}
function isHeadingLike(el) {
if (!el || !isVisible(el)) return false;
const tag = el.tagName;
if (tag && /^H[1-6]$/.test(tag)) return true;
if (el.getAttribute("role") === "heading") return true;
const txt = normalizeText(el.textContent);
if (txt.length >= 2 && txt.length <= 30) {
try {
const cs = window.getComputedStyle(el);
const fw = parseInt(cs.fontWeight, 10);
if (!Number.isNaN(fw) && fw >= 600) return true;
if (cs.textTransform === "uppercase" && txt === txt.toUpperCase()) return true;
} catch (e) { /* ignore */ }
}
return false;
}
function findGptHeader() {
const candidates = document.querySelectorAll("h1,h2,h3,h4,div,span");
for (let i = 0; i < candidates.length; i++) {
const el = candidates[i];
if (!isVisible(el)) continue;
const txt = normalizeText(el.textContent);
if (txt === "GPT") return el;
}
return null;
}
function collectGptAnchors() {
const header = findGptHeader();
if (!header) return [];
let root = header.closest("nav,aside");
if (!root) root = header.parentElement || document.body;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
let inGpt = false;
const anchors = [];
while (walker.nextNode()) {
const el = walker.currentNode;
if (el === header) {
inGpt = true;
continue;
}
if (!inGpt) continue;
if (isHeadingLike(el)) {
break;
}
if ((el.tagName === "A" || el.getAttribute("role") === "link") && isVisible(el)) {
anchors.push(el);
}
}
return anchors;
}
function anchorToRow(anchor) {
return anchor.closest("li") || anchor;
}
function cleanup() {
Array.prototype.forEach.call(document.querySelectorAll("." + NS + "-toggle"), function (btn) {
if (btn.parentElement) btn.remove();
});
Array.prototype.forEach.call(document.querySelectorAll("." + NS + "-hidden-row"), function (row) {
row.classList.remove(NS + "-hidden-row");
});
}
function pauseObserver(ms) {
if (!mo) return;
observerPaused = true;
if (pauseTimerId) window.clearTimeout(pauseTimerId);
pauseTimerId = window.setTimeout(function () {
observerPaused = false;
}, ms || 400);
}
function insertToggle(afterNode, hiddenRows) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = NS + "-toggle";
btn.setAttribute("aria-expanded", "false");
btn.dataset.state = "collapsed";
const label = document.createElement("span");
const chev = document.createElement("span");
chev.className = NS + "-chev";
chev.textContent = "▾";
function setCollapsedLabel() {
label.textContent = "Voir plus (" + hiddenRows.length + ")";
chev.textContent = "▾";
}
function setExpandedLabel() {
label.textContent = "Voir moins";
chev.textContent = "▴";
}
setCollapsedLabel();
btn.appendChild(label);
btn.appendChild(chev);
btn.addEventListener("click", function () {
pauseObserver(500);
const expanded = btn.dataset.state === "expanded";
if (expanded) {
Array.prototype.forEach.call(hiddenRows, function (el) {
el.classList.add(NS + "-hidden-row");
});
btn.dataset.state = "collapsed";
btn.setAttribute("aria-expanded", "false");
setCollapsedLabel();
} else {
Array.prototype.forEach.call(hiddenRows, function (el) {
el.classList.remove(NS + "-hidden-row");
});
btn.dataset.state = "expanded";
btn.setAttribute("aria-expanded", "true");
setExpandedLabel();
}
});
const parent = afterNode.parentElement || document.body;
try {
afterNode.insertAdjacentElement("afterend", btn);
} catch (e) {
parent.insertBefore(btn, afterNode.nextSibling);
}
}
function applyFold() {
try {
ensureStyle();
cleanup();
const anchors = collectGptAnchors();
log("Items GPT détectés (section bornée):", anchors.length);
if (anchors.length <= MAX_VISIBLE) return;
const rows = [];
const seen = new Set();
anchors.forEach(function (a) {
const row = anchorToRow(a);
if (row && !seen.has(row)) {
seen.add(row);
rows.push(row);
}
});
if (rows.length <= MAX_VISIBLE) return;
const hiddenRows = rows.slice(MAX_VISIBLE);
hiddenRows.forEach(function (row) {
row.classList.add(NS + "-hidden-row");
});
insertToggle(rows[MAX_VISIBLE - 1], hiddenRows);
const hiddenCount = hiddenRows.filter(function (r) {
return r.classList.contains(NS + "-hidden-row");
}).length;
log("Pliage appliqué. Rangées masquées:", hiddenCount);
} catch (e) {
error("Échec de l'application du pliage:", e);
}
}
function debounce(fn, delay) {
let t = null;
return function () {
if (t) window.clearTimeout(t);
t = window.setTimeout(function () {
try { fn(); } catch (e) { error("Erreur debounced:", e); }
}, delay);
};
}
observe = debounce(function () {
if (observerPaused) {
log("Observation ignorée (pause).");
return;
}
applyFold();
}, 250);
function startObserver() {
if (mo) return;
try {
mo = new MutationObserver(observe);
mo.observe(document.body, { childList: true, subtree: true });
} catch (e) {
error("MutationObserver indisponible:", e);
}
}
applyFold();
startObserver();
document.addEventListener("visibilitychange", function () {
if (!document.hidden) applyFold();
});
})();