Dreadcast Script Manager

Centralize all dreadcast scripts in one single source, integrated to the game.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name        Dreadcast Script Manager
// @namespace   Dreadcast
// @match       https://www.dreadcast.net/Main
// @match       https://www.dreadcast.net/Forum
// @match       https://www.dreadcast.net/Forum/*
// @match       https://www.dreadcast.net/EDC
// @match       https://www.dreadcast.net/EDC/*
// @version     1.4.0
// @author      Pelagia/Isilin
// @description Centralize all dreadcast scripts in one single source, integrated to the game.
// @license     https://github.com/Isilin/dreadcast-scripts?tab=GPL-3.0-1-ov-file
// @require     https://update.greasyfork.org/scripts/507382/Dreadcast%20Development%20Kit.user.js?version=1533476
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @connect     update.greasyfork.org
// @connect     docs.google.com
// @connect     googleusercontent.com
// @connect     sheets.googleapis.com
// @connect     raw.githubusercontent.com
// ==/UserScript==

// TODO remplacer petit à petit les scripts par les versions locales nettoyées.
// TODO use a recent jquery with noConflict
// TODO Reset button in Com'Back reset all the settings from DCSM (including scripts then).

// Offline copy of the script list, used as a last resort when both the cache
// and the remote list are unavailable. It may lag behind data/scripts.json.
// === DCSM_FALLBACK_START - generated by tools/sync-fallback.mjs, do not edit by hand ===
const DCSM_FALLBACK_LIST = JSON.parse(`
[
  {
    "id": "comback",
    "name": "Com'Back",
    "description": "Il ajoute des fonctions d'en-tête aux messages, ainsi que de blocs pré-écrits, et rend les liens cliquables.",
    "authors": "Valion, Naugriim, Solon, Harlinde",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/17200/Com'back.user.js?version=1074459",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mailing"
    ],
    "experimental": false
  },
  {
    "id": "comcount",
    "name": "Com'Count",
    "description": "Il permet d'ajuster manuellement le nombre de messages non lus, lorsque celui-ci se désynchronise.",
    "authors": "Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/395938/Com'Count.user.js?version=830889",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mailing"
    ],
    "experimental": false
  },
  {
    "id": "vousavezunmessage",
    "name": "\\"Vous avez un message\\"",
    "description": "Il émet une alerte sonore à la réception d'un nouveau message.",
    "authors": "Gideon, Sÿ, Odul, Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/396697/VousAvezUnMessage.user.js?version=830556",
    "doc": "",
    "rp": "",
    "contact": "Odul",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mailing"
    ],
    "experimental": false
  },
  {
    "id": "messagerietitreperso",
    "name": "Messagerie : Titres Perso",
    "description": "Personnalisation des titres et des avatars d'aperçu, sur la messagerie.",
    "authors": "Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/415294/Messagerie%20:%20Titres%20Perso.user.js?version=1559798",
    "doc": "",
    "rp": "",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mailing"
    ],
    "experimental": false
  },
  {
    "id": "lockddmail",
    "name": "LockD&DMail",
    "description": "Bloque le drag and drop sur la messagerie.",
    "authors": "Odul, Pelagia",
    "icon": "",
    "url": "https://raw.githubusercontent.com/Isilin/dreadcast-scripts/main/src/scripts/messagerie/lockd%26dmail.js",
    "doc": "",
    "rp": "",
    "contact": "Odul, Pelagia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mailing"
    ],
    "experimental": false
  },
  {
    "id": "mailprotect",
    "name": "DC - Mail Protect",
    "description": "Ajoute un mode permettant de demander la confirmation avant de fermer un message en cours de réaction.",
    "authors": "Nasty, Odul, Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/480707/DC%20-%20Mail%20Protect.user.js?version=1285527",
    "doc": "",
    "rp": "",
    "contact": "Nasty, Odul",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mailing"
    ],
    "experimental": false
  },
  {
    "id": "roll",
    "name": "Roll",
    "description": "Faire des jets avec prise en compte des stats via des compétences.",
    "authors": "Odul, Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/447640/Roll.user.js?version=1070850",
    "doc": "https://www.dreadcast.net/Forum/2-56453-script-roll#1",
    "rp": "",
    "contact": "Odul",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "chat"
    ],
    "experimental": false
  },
  {
    "id": "dcce",
    "name": "DreadCast Chat Enhancer",
    "description": "Il ajoute de nombreuses fonctionnalités pour le chat et des options de customisation.",
    "authors": "Valion, Odul, Ladoria, Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/21359/Dreadcast%20Chat%20Enhancer.user.js?version=1068608",
    "doc": "https://www.dreadcast.net/Forum/2-67353-script-dc-chat-enhancer?5",
    "rp": "",
    "contact": "Odul, Pelagia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "chat"
    ],
    "experimental": false
  },
  {
    "id": "chatinputdc",
    "name": "ChatInputDC",
    "description": "Automatically 'closing' the following characters in the chat input area: \\", *, [, (, or {.",
    "authors": "Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/388245/ChatInputDC.user.js?version=722725",
    "doc": "",
    "rp": "",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "chat"
    ],
    "experimental": false
  },
  {
    "id": "skinsilhouette",
    "name": "SkinSilhouette",
    "description": "Voir des silhouettes personnalisées.",
    "authors": "Odul, Lorkah, Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/6081/SkinSilhouette.user.js?version=1271247",
    "doc": "",
    "rp": "",
    "contact": "Odul",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "silhouette"
    ],
    "experimental": false
  },
  {
    "id": "showsilhouettebyisilin",
    "name": "ShowSilhouette - By Isilin",
    "description": "Un skin réorganisant l'inventaire pour mieux voir les silhouettes.",
    "authors": "Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/415259/ShowSilhouette%20-%20By%20Isilin.user.js?version=864933",
    "doc": "",
    "rp": "",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "silhouette"
    ],
    "experimental": false
  },
  {
    "id": "showsilhouettevalion",
    "name": "Show Silhouette Valion",
    "description": "Un skin réorganisant l'inventaire pour mieux voir les silhouettes.",
    "authors": "Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/415246/Show%20Silhouette%20Valion.user.js?version=864762",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "silhouette"
    ],
    "experimental": false
  },
  {
    "id": "stopmultiplemessageforumonly",
    "name": "StopMultipleMessageForumOnly",
    "description": "Empêche les messages doublons accidentels sur le forum.",
    "authors": "Kmaschta, Valion, Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/508039/StopMultipleMessageForumOnly.user.js?version=1446184",
    "doc": "",
    "rp": "",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "forum"
    ],
    "category": [
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "séparationsujets",
    "name": "Séparation sujets",
    "description": "Separe le RP du HRP dans la section 'Derniers Sujets",
    "authors": "Aversiste, MockingJay, Odul, Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/508226/S%C3%A9paration%20sujets.user.js?version=1448853",
    "doc": "",
    "rp": "",
    "contact": "Odul, Pelagia",
    "settings": false,
    "section": [
      "forum"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "breakinglinecode",
    "name": "BreakingLineCode",
    "description": "Ajoute un retour à la ligne automatique pour la balise code.",
    "authors": "Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/493103/BreakingLineCode.user.js?version=1363923",
    "doc": "https://www.dreadcast.net/Forum/2-108077-themefix-dc-forum-fix?1#2",
    "rp": "",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "forum"
    ],
    "category": [
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "mapoverlay",
    "name": "DC Map Overlay",
    "description": "Modification de la carte initiale dans DC, par remplacement et/ou ajout d'une surcouche.",
    "authors": "Radium",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/394964/DC_MapOverlay%20fouilleur.user.js?version=764590",
    "doc": "https://www.dreadcast.net/Forum/2-83878-script-dc_mapoverlay?1",
    "rp": "",
    "contact": "Radium",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "nightmode",
    "name": "DC Nightmode",
    "description": "Un script qui enlève certains éléments graphiques pour un thème plus sombre.",
    "authors": "Lorkah",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/370999/DC%20-%20Nightmode.user.js?version=945864",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "datefixed",
    "name": "DC - Date fixed",
    "description": "Affiche la date en jeu, et des commandes sur le deck.",
    "authors": "Ianouf, Ladoria, Nasty",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/453281/DC%20-%20Date%20fixed.user.js?version=1106311",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "compteursac",
    "name": "Compteur Sac",
    "description": "Affiche sur le sac le nombre d'objets qui y est. Se rafraichit toutes les 3 secondes.",
    "authors": "Anon, Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/25490/Compteur%20Sac.user.js?version=750393",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "blockuse",
    "name": "DC - BlockUse",
    "description": "Bloque l'utilisation de consommables par défaut. Faut déverouiller le petit cadenas 'BU' pour pouvoir.",
    "authors": "Odul, Lorkah",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/412356/DC%20-%20BlockUse%20Fixed.user.js?version=888873",
    "doc": "",
    "rp": "",
    "contact": "Odul",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "autorefresh",
    "name": "DC - AutoRefresh",
    "description": "Tente automatiquement de rafraîchir le jeu si la connexion est perdue, et affiche le ping du serveur.",
    "authors": "Ladoria, Lorkah",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/415271/DC%20-%20AutoRefresh%20Fixed.user.js?version=864870",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "bulletsalert",
    "name": "Bullets Alert",
    "description": "Joue un son lorsque votre chargeur est vide.",
    "authors": "Ladoria, Lorkah",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/404024/DC%20-%20Bullets_alert_fixed.user.js?version=888872",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "bougetafeuille",
    "name": "DC - Bouge ta feuille",
    "description": "Rend la lecture plus agréable en permettant de déplacer plus facilement une feuille.",
    "authors": "Nasty",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/453476/DC%20-%20Bouge%20ta%20feuille.user.js?version=1107622",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "copyterminal",
    "name": "Copy Terminal",
    "description": "Copie rapidement le contenu d'une page du TP (STV, Cryo, Historiques des entreprises et OI).",
    "authors": "RedLine",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/486524/copyTerminal.user.js?version=1322828",
    "doc": "",
    "rp": "",
    "contact": "Redline",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "neuvopackoverhaul",
    "name": "Neuvopack Overhaul",
    "description": "Modifie l'interface des neuvopacks pour un usage plus agréable (suivi en direct du contenu, extraction des condensés rapide).",
    "authors": "Altaïr",
    "icon": "https://www.dreadcast.net/images/objets/NEUVOPACK_262px.png",
    "url": "https://update.greasyfork.org/scripts/468141/Neuvopack%20Overhaul.user.js?version=1491022",
    "doc": "https://www.dreadcast.net/Forum/2-144257-script-neuvopack-overhaul?1",
    "rp": "",
    "contact": "Altaïr",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "tpoverhaul",
    "name": "TP Overhaul",
    "description": "Modernisation de l'interface du Terminal Portable.",
    "authors": "Altaïr",
    "icon": "https://www.dreadcast.net/images/objets/mini/TERMINAL%20PORTABLE2.png",
    "url": "https://update.greasyfork.org/scripts/488053/TP%20Overhaul.user.js?version=1558625",
    "doc": "https://www.dreadcast.net/Forum/2-145540-script-tp-overhaul",
    "rp": "",
    "contact": "Altaïr",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "visio3d",
    "name": "Visio 3D",
    "description": "Utilitaire pour récupérer les plans et FW d'un bâtiment.",
    "authors": "Pelagia",
    "icon": "https://www.dreadcast.net/images/objets/decker-casque2.png",
    "url": "https://update.greasyfork.org/scripts/481981/Visio%203D.user.js?version=1533889",
    "doc": "https://www.dreadcast.net/Forum/2-144695-ns-v13612u-visio-3d---logiciel-utilitaire-pour-la-decoration-dinterieur?1",
    "rp": "https://www.dreadcast.net/Forum/2-144695-ns-v13612u-visio-3d---logiciel-utilitaire-pour-la-decoration-dinterieur?1",
    "contact": "Pelagia",
    "settings": true,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "eccb",
    "name": "Editeur de Commentaires de Conteneurs en Banque",
    "description": "Permet de mettre un commentaire sur ses sacs en banque pour mieux s'y retrouver!",
    "authors": "Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/488837/ECCB.user.js?version=1337142",
    "doc": "https://www.dreadcast.net/Forum/2-144702-ns-e13612u-eccb---editeur-de-commentaires-de-conteneurs-en-banque?1",
    "rp": "https://www.dreadcast.net/Forum/2-136024--aequilibrium-financial-#10",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "autostock",
    "name": "AutoStock",
    "description": "Permet le réajustement des stocks en vente ou les prix dans l'usine de production pour DC.",
    "authors": "Amane-Mochizuki",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/493495/DC%20-%20AutoStock%20%28channel%20Beta%29.user.js?version=1366383",
    "doc": "https://www.dreadcast.net/Forum/2-145375-script-autostock?1",
    "rp": "",
    "contact": "Amane-Mochizuki",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "silmerionwindowfix",
    "name": "Silmerion Window Fix",
    "description": "Règle le problème faisant que la fenêtre du Silmerion empêche le clic ailleurs sur la page.",
    "authors": "Arrakis",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/371206/Silmerion%20Window%20Fix.user.js?version=641774",
    "doc": "",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "skwikker",
    "name": "Skwikker",
    "description": "Pour accéder au célèbre réseau social Skwikker.",
    "authors": "Valion",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/30182/Skwikker.user.js?version=201187",
    "doc": "https://www.dreadcast.net/Forum/2-77724-script-skwikker",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "forum",
      "edc"
    ],
    "category": [
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "silhouettePlus",
    "name": "Silhouette+",
    "description": "Pour personnaliser entièrement sa feuille RP avec silhouette et plus. Réunit les scripts SkinSilhouette et ShowSilhouette.",
    "authors": "Isilin/Pelagia",
    "icon": "",
    "url": "https://raw.githubusercontent.com/Isilin/dreadcast-scripts/main/src/scripts/silhouette/silhouette-plus.js",
    "doc": "https://www.dreadcast.net/Forum/2-150257-script-silhouette",
    "rp": "",
    "contact": "Pelagia",
    "settings": true,
    "section": [
      "game"
    ],
    "category": [
      "silhouette"
    ],
    "experimental": false
  },
  {
    "id": "vitrineStock",
    "name": "Vitrine vs Stocks",
    "description": "Check stock et vitrine / Check quantité",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/527017/Vitrine%20vs%20Stocks.user.js?version=1584063",
    "doc": "https://greasyfork.org/en/scripts/527017-vitrine-vs-stocks",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "objectLinking",
    "name": "Partage d'objet chat",
    "description": "Permet de partager un objet dans le chat facilement",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/528245/Partage%20d%27objet%20chat.user.js?version=1583032",
    "doc": "https://greasyfork.org/en/scripts/528245-partage-d-objet-chat",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "chat"
    ],
    "experimental": false
  },
  {
    "id": "copyPasteAll",
    "name": "Copy Paste all",
    "description": "Ajoute un bouton qui vous permet de copier tous les posts d'une page forum dans votre presse-papier.",
    "authors": "Silly",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/529290/%5BDC%5D%20Copy%20Paste%20All%20-%20Forum.user.js?version=1550441",
    "doc": "https://www.dreadcast.net/Forum/2-151693-script-copy-paste-all---forum",
    "rp": "",
    "contact": "Silly",
    "settings": false,
    "section": [
      "forum"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "panFenestra",
    "name": "P.A.N. Fenestra",
    "description": "Un nouveau système d'exploitation pour votre espace matriciel personnel.",
    "authors": "Isilin/Pelagia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/530381/PAN%20Fenestra.user.js?version=1557231",
    "doc": "https://www.dreadcast.net/Forum/2-152039-script-pan-fenestra?1",
    "rp": "",
    "contact": "Pelagia",
    "settings": false,
    "section": [
      "forum"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "deckExportData",
    "name": "DeckExportData",
    "description": "Permet de copier les données du deck",
    "authors": "Amane-Mochizuki",
    "icon": "https://i.imgur.com/Wn8zYJ8.png",
    "url": "https://update.greasyfork.org/scripts/530389-dc-deckexportdata.user.js?version=1563381",
    "doc": "https://www.dreadcast.net/Forum/2-152205-script-deck-export-data?1",
    "rp": "",
    "contact": "Amane-Mochizuki",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "fixUsine",
    "name": "Fix Usine",
    "description": "Empêche la fenêtre d'usine de se fermer quand on achète",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/527127-fix-usine.user.js?version=1554902",
    "doc": "",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "holovision",
    "name": "HoloVision",
    "description": "Historique chat malléable",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/532962/Holovision.user.js?version=1573401",
    "doc": "https://www.dreadcast.net/Forum/2-152896-script-holovision?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "chat"
    ],
    "experimental": false
  },
  {
    "id": "neuvosmart",
    "name": "NeuvoSmart",
    "description": "Son quand le neuvopack est plein / UI de suivi",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/544413/NeuvoSmart.user.js?version=1634380",
    "doc": "https://www.dreadcast.net/Forum/2-155651-script-neuvosmart?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "mech"
    ],
    "experimental": false
  },
  {
    "id": "dcmobilefix",
    "name": "DC Mobile Fix",
    "description": "Divers fix mobile",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/537990/Mobile%20DC%20Fix.user.js?version=1600207",
    "doc": "https://www.dreadcast.net/Forum/2-155650-script-dc-mobile---fix?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "fix"
    ],
    "experimental": true
  },
  {
    "id": "aitlDuPremierCoup",
    "name": "AITL - Du premier coup ! ",
    "description": "Preview de l'annonce AITL",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/533831/AITL%20-%20Du%20premier%20coup%20%21.user.js?version=1677497",
    "doc": "https://www.dreadcast.net/Forum/2-153038-script-aitl---du-premier-coup?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "details",
    "name": "Details!",
    "description": "Log de combat détaillé",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/535028/Details%21.user.js?version=1687389",
    "doc": "https://www.dreadcast.net/Forum/2-153293-scripts-details?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "chatBulle",
    "name": "ChatBulle",
    "description": "Affiche une bulle de tchat au-dessus des personnages",
    "authors": "Asalia",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/554530/ChatBulle.user.js?version=1696150",
    "doc": "https://www.dreadcast.net/Forum/2-158092-script-chatbulle?1",
    "rp": "",
    "contact": "Asalia",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "chat"
    ],
    "experimental": false
  },
  {
    "id": "dcnotif",
    "name": "DC Notif'",
    "description": "Notifications traitées dans une interface",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/534899/DC%20Notif%27.user.js?version=1590384",
    "doc": "https://www.dreadcast.net/Forum/2-153271-script-dc-notif?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui",
      "fix"
    ],
    "experimental": false
  },
  {
    "id": "pimpmypion",
    "name": "Pimp My Pion",
    "description": "Remplace les pions bleus par les avatars des joueurs et ajoute des paramètres de personnalisation",
    "authors": "Darlene",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/556113/Dreadcast%20-%20PimpMyPion%20-%20Public%20v%20063.user.js?version=1709845",
    "doc": "https://www.dreadcast.net/Forum/2-158238-script-pimpmypion?1",
    "rp": "",
    "contact": "Darlene",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "fouilletracker",
    "name": "DC Fouille Tracker",
    "description": "Minuteur fouille draggable, lock/unlock, historique déroulant, vide de l'historique, copier de l'historique (thème bleu)",
    "authors": "Yenzelle",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/557196/DC%20-%20Fouille%20Tracker%20%28Bleu%29.user.js?version=1703636",
    "doc": "https://www.dreadcast.net/Forum/2-158505-script-dc-fouille-tracker?1",
    "rp": "",
    "contact": "",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "ddm",
    "name": "DC Dynamic Message V2",
    "description": "Messagerie version dynamique et réactive",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/557947/DDM%20V2.user.js?version=1800579",
    "doc": "https://www.dreadcast.net/Forum/2-158665-script-ddm-v2?4",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": true
  },
  {
    "id": "fpl",
    "name": "FPLive",
    "description": "Notifs en jeu lorsqu'un FP suivi reçoit un message",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/565766/FPLive.user.js?version=1752030",
    "doc": "https://www.dreadcast.net/Forum/2-159824-script-fpl?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game",
      "forum"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "igforum",
    "name": "IGForum",
    "description": "Permet d'accéder au forum depuis le jeu",
    "authors": "Ciel",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/565953/IGForum.user.js?version=1752886",
    "doc": "https://www.dreadcast.net/Forum/2-159840-script-igforum?1",
    "rp": "",
    "contact": "Ciel",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "gpsrouge",
    "name": "GPS Rouge",
    "description": "Remplace la flèche bleue du GPS par une flèche rouge.",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/532463/GPS%20Rouge.user.js?version=1571171",
    "doc": "https://www.dreadcast.net/Forum/2-152684-script---sommaire-lain?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "ui"
    ],
    "experimental": false
  },
  {
    "id": "kobsteak",
    "name": "Kobsteak!",
    "description": "Un snake à l'intérieur de Dreadcast. Quand l'autre met trop de temps à répondre pendant un RPQ.",
    "authors": "Laïn",
    "icon": "",
    "url": "https://update.greasyfork.org/scripts/531236/KobSteak%21.user.js?version=1571171",
    "doc": "https://www.dreadcast.net/Forum/2-152684-script---sommaire-lain?1",
    "rp": "",
    "contact": "Laïn",
    "settings": false,
    "section": [
      "game"
    ],
    "category": [
      "mech"
    ],
    "experimental": false
  }
]
`);
// === DCSM_FALLBACK_END ===

$(() => {
  // To check if a script is used in a DSM context.
  Util.isDSM = () => true;

  const LIST_TAG = 'dcsm_list';
  const ALL_DISABLED_TAG = 'dcsm_all_disabled';
  const INTRO_TAG = 'dcsm_intro_disabled';
  const DEV_MODE_TAG = 'dcsm_dev_mode';
  const CACHE_TAG = 'dcsm_scripts_cache';

  const LIST_URL =
    'https://raw.githubusercontent.com/Isilin/dreadcast-scripts/main/data/scripts.json';
  const CACHE_TTL = 60 * 60 * 1000; // 1 hour.
  const FETCH_TIMEOUT = 8000;

  let settings, allDisabled, introDisabled, devMode;
  let newSettings, newAllDisabled, newDevMode;

  // ===== CORE =====

  const initPersistence = () => {
    // Init persistent memory if needed.
    DC.LocalMemory.init(LIST_TAG, {});
    DC.LocalMemory.init(ALL_DISABLED_TAG, false);
    DC.LocalMemory.init(INTRO_TAG, false);
    DC.LocalMemory.init(DEV_MODE_TAG, false);

    // TODO to delete at next major version.
    if (DC.LocalMemory.get('dcm_list') !== undefined) {
      DC.LocalMemory.set(LIST_TAG, DC.LocalMemory.get('dcm_list'));
      DC.LocalMemory.delete('dcm_list');
    }
    if (DC.LocalMemory.get('dcm_all_disabled') !== undefined) {
      DC.LocalMemory.set(
        ALL_DISABLED_TAG,
        DC.LocalMemory.get('dcm_all_disabled'),
      );
      DC.LocalMemory.delete('dcm_all_disabled');
    }

    // Load the current settings.
    settings = DC.LocalMemory.get(LIST_TAG);
    allDisabled = DC.LocalMemory.get(ALL_DISABLED_TAG);
    introDisabled = DC.LocalMemory.get(INTRO_TAG);
    devMode = DC.LocalMemory.get(DEV_MODE_TAG);
  };

  const isValidList = (list) =>
    Util.isArray(list) &&
    list.length > 0 &&
    list.every(
      (script) =>
        Util.isObject(script) &&
        Util.isString(script.id) &&
        script.id !== '' &&
        Util.isString(script.url) &&
        script.url !== '' &&
        Util.isArray(script.section) &&
        Util.isArray(script.category),
    );

  // Undefined when there is nothing usable in persistent memory: a missing
  // cache and a corrupted one are handled the same way.
  const readCache = () => {
    const cache = DC.LocalMemory.get(CACHE_TAG);

    if (
      !Util.isObject(cache) ||
      !Number.isFinite(cache.ts) ||
      !isValidList(cache.scripts)
    ) {
      return undefined;
    }

    return cache;
  };

  const writeCache = (scripts) => {
    // Single entry rather than two keys, so the list and its timestamp can
    // never end up out of sync.
    const cache = { ts: Date.now(), scripts };
    DC.LocalMemory.set(CACHE_TAG, cache);

    return cache;
  };

  const fetchList = async () => {
    // 'timeout' is forwarded as is to GM_xmlhttpRequest, whose 'ontimeout' is
    // wired to reject: without it, an host that hangs would leave the request
    // pending forever and nothing would ever be loaded.
    const list = await DC.Network.fetch({
      method: 'GET',
      url: LIST_URL,
      headers: { 'Content-Type': 'application/json' },
      responseType: 'json',
      timeout: FETCH_TIMEOUT,
    });

    // The request resolves whatever the HTTP status is, so a 4xx/5xx comes back
    // here as null or as an error page: only the payload tells us the truth.
    if (!isValidList(list)) {
      throw new Error('the remote list is empty or malformed');
    }

    return list;
  };

  // Cache younger than an hour, else remote list, else cache whatever its age,
  // else the copy embedded in this userscript.
  const resolveList = async () => {
    const cache = readCache();

    if (cache !== undefined && Date.now() - cache.ts < CACHE_TTL) {
      console.info('DCSM - List of scripts loaded from the cache.');
      return { scripts: cache.scripts, source: 'cache', ts: cache.ts };
    }

    try {
      const scripts = await fetchList();
      console.info('DCSM - List of scripts updated from the remote source.');
      return { scripts, source: 'remote', ts: writeCache(scripts).ts };
    } catch (err) {
      if (cache !== undefined) {
        // The timestamp is left untouched, so the update is attempted again on
        // the next page load instead of being held off for a full hour.
        console.warn(
          'DCSM - Error updating the list of scripts, keeping the cached one: ' +
            err,
        );
        return { scripts: cache.scripts, source: 'cache-stale', ts: cache.ts };
      }

      if (!isValidList(DCSM_FALLBACK_LIST)) {
        throw new Error('no cache, no remote list and no usable embedded list');
      }

      console.error(
        'DCSM - Error loading the list of scripts, using the embedded one: ' +
          err,
      );
      return {
        scripts: DCSM_FALLBACK_LIST,
        source: 'embedded',
        ts: writeCache(DCSM_FALLBACK_LIST).ts,
      };
    }
  };

  // 'prune' should only be true when 'scripts' comes from the remote list. On a
  // degraded list (cache or embedded fallback), removing the unknown entries
  // would silently drop the settings of the scripts missing from it.
  const synchronizeSettings = (settings, scripts, prune) => {
    let tmp = settings;

    scripts.forEach((script) => {
      if (!Object.hasOwn(tmp, script.id)) {
        // Update the settings, if there is new scripts.
        tmp[script.id] = false;
      }
    });

    if (prune) {
      // Remove in settings, scripts that doesn't exist anymore.
      tmp = Object.keys(tmp)
        .filter(
          (key) => scripts.find((script) => script.id === key) !== undefined,
        )
        .reduce((obj, key) => {
          obj[key] = tmp[key];
          return obj;
        }, {});
    }

    // Save the new settings in persistent memory.
    DC.LocalMemory.set(LIST_TAG, tmp);

    return tmp;
  };

  const createScriptLine = (script, index) => {
    const line = $(`
      <tr style="border-top: 1px solid white; border-left: 1px solid white; border-right: 1px solid white;">
        <td style="padding: 5px 0 0 5px" rowspan="2">${index}</td>
        ${
          script.icon && script.icon !== ''
            ? `<td style="padding: 5px" rowspan="2"><img src="${script.icon}" width="48" height="48" /></td>`
            : '<td class="short" style="width: 58px;" rowspan="2" />'
        }
        <td style="padding: 5px 0; min-width: 120px; text-align: left;">${
          script.experimental ? '<span style="color: red;">[DEV]</span>' : ''
        } ${script.name || ''}</td>
        <td style="padding: 5px 0; min-width: 120px; text-align: left;"><small>${
          script.authors || ''
        }</small></td>
        <td class="enabled_cell" style="padding: 5px 0; display: flex; justify-content: center;"></td>
        <td class="setting_cell" style="padding: 5px 5px 0 0;"></td>
        <td class="doc_cell" style="padding: 5px 5px 0 0;"></td>
        <td class="rp_cell" style="padding: 5px 5px 0 0;"></td>
        <td class="contact_cell" style="padding: 5px 5px 0 0;"></td>
      </tr>
      <tr style="border-bottom: 1px solid white; border-left: 1px solid white; border-right: 1px solid white;">
        <td colspan="7" style="padding: 0 5px 5px 5px; text-align: left;"><small><em class="couleur5">${
          script.description || ''
        }</em></small></td>
      </tr>
    `);
    $('.enabled_cell', line).append(
      DC.UI.Tooltip(
        'Activer/Désactiver le script ne perdra pas sa configuration.',
        DC.UI.Checkbox(
          `${script.id}_check`,
          newSettings[script.id],
          () => (newSettings[script.id] = !newSettings[script.id]),
        ),
      ),
    );
    if (script.settings) {
      $('.setting_cell', line).append(
        DC.UI.Tooltip(
          'Settings',
          DC.UI.Button(
            `${script.id}_setting`,
            '<i class="fas fa-cog"></i>',
            () => {},
          ),
        ),
      );
    }
    if (script.doc && script.doc !== '') {
      $('.doc_cell', line).append(
        DC.UI.Tooltip(
          'Documentation',
          DC.UI.Button(`${script.id}_doc`, '<i class="fas fa-book"></i>', () =>
            window.open(script.doc, '_blank'),
          ),
        ),
      );
    }
    if (script.rp && script.rp !== '') {
      $('.rp_cell', line).append(
        DC.UI.Tooltip(
          'Topic RP',
          DC.UI.Button(
            `${script.id}_rp`,
            '<div class=""gridCenter>RP</div>',
            () => window.open(script.doc, '_blank'),
          ),
        ),
      );
    }
    if (script.contact && script.contact !== '') {
      $('.contact_cell', line).append(
        DC.UI.Tooltip(
          'Contact',
          DC.UI.Button(
            `${script.id}_rp`,
            '<i class="fas fa-envelope"></i>',
            () => nav.getMessagerie().newMessage(script.contact),
          ),
        ),
      );
    }

    return line;
  };

  const createIntro = () => {
    if (Util.isGame() && !introDisabled) {
      introDisabled = true;
      DC.LocalMemory.set(INTRO_TAG, introDisabled);
      DC.UI.PopUp(
        'dcsm_intro',
        'Bienvenue sur le Dreadcast Script Manager !',
        $(`
          <div style="color: white;">
            <h3>Merci d'avoir installé le DreaCast Script Manager (DCSM).</h3><br />
            <p>Cet utilitaire va vous permettre de gérer vos scripts directement en jeu. Pensez à désactiver/désinstaller dans votre GreaseMonkey/TamperMonkey (ou équivalent), les scripts que vous activerez dans le DCSM, pour éviter des doublons.</p><br />
            <p>La suite se passe dans Paramètres > Scripts & Skins.</p><br />
            <p>Vous pourrez obtenir des réponses à vos questions sur le <a href="https://github.com/Isilin/dreadcast-scripts/wiki">wiki</a>, sur le forum, ou en me contactant directement par Com' HRP : (<em>JD Pelagia</em>).</p><br />
            <p>Bon jeu ! (Vous ne verrez plus cette fenêtre d'information par la suite).</p>
          </div>
        `),
      );
    }
  };

  // Tells where the displayed list comes from, so a missing script can be told
  // apart from a script that is simply disabled.
  const createListStatus = (meta) => {
    const date = new Date(meta.ts).toLocaleString('fr-FR', {
      dateStyle: 'short',
      timeStyle: 'short',
    });

    const messages = {
      remote: `Liste à jour (${date}).`,
      cache: `Liste en cache (${date}).`,
      'cache-stale': `⚠ Source injoignable : liste en cache du ${date}.`,
      embedded:
        '⚠ Source injoignable : liste de secours embarquée, potentiellement incomplète.',
    };

    const degraded =
      meta.source === 'cache-stale' || meta.source === 'embedded';
    const decoration = degraded ? 'style="color: red;"' : 'class="couleur5"';

    return $(
      `<p style="margin-bottom: 1rem;"><small><em ${decoration}>${
        messages[meta.source]
      }</em></small></p>`,
    );
  };

  const createUI = (scripts, settings, meta) => {
    DC.UI.addSubMenuTo(
      'Paramètres ▾',
      DC.UI.SubMenu(
        'Scripts & Skins',
        () => {
          // On récupère une config temporaire qu'on appliquera uniquement si sauvegardée.
          newSettings = settings;
          newAllDisabled = allDisabled;
          newDevMode = devMode;

          const sections = [
            { id: 'all', label: 'Tous' },
            { id: 'game', label: 'Jeu' },
            { id: 'forum', label: 'Forum' },
            { id: 'edc', label: 'EDC' },
          ];

          const categories = [
            { id: 'all', label: 'Tous' },
            { id: 'mailing', label: 'Messagerie' },
            { id: 'chat', label: 'Chat' },
            { id: 'silhouette', label: 'Silhouette' },
            { id: 'ui', label: 'UI' },
            { id: 'mech', label: 'Mécaniques' },
            { id: 'fix', label: 'Correctifs' },
            { id: 'misc', label: 'Autres' },
          ];

          const content = $(`<div style="color: white;">
          <div id="developper_mode_switch" style="display: flex; justify-content: flex-begin;gap: 1rem;margin-bottom: 1rem;">
            <p>Mode développeur</p>
          </div>
          <div style="display: flex; justify-content: space-between">
            <div id="scripts_all_switch" style="display: flex;gap: 1rem;margin-bottom: 1rem;">
              <p>Tout désactiver</p>
            </div>
            <div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
                <label for="search_script">Recherche</label>
                <input id="search_script" name="search_script" type="text" size="50" style="color: white;" />
            </div>
          </div>
          <div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
            <legend style="margin-right: 1rem; min-width: 60px;">Filtrer :</legend>
            <div style="display: flex; gap: 5%; flex-wrap: wrap; width: 100%;">
              ${sections
                .map(
                  (section, index) => `
                  <div>
                    <input type="radio" id="${
                      section.id
                    }_section" name ="section" value ="${section.id}" ${
                    index === 0 ? 'checked' : ''
                  } />
                    <label for="${section.id}_section">${section.label}</label>
                  </div>
              `,
                )
                .join('')}
            </div>
          </div>
          <div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
            <legend style="margin-right: 1rem; min-width: 60px;">Filtrer :</legend>
            <div style="display: flex; gap: 5%; flex-wrap: wrap; width: 100%;">
              ${categories
                .map(
                  (category, index) => `
                  <div>
                    <input type="radio" id="${
                      category.id
                    }_category" name ="category" value ="${category.id}" ${
                    index === 0 ? 'checked' : ''
                  } />
                    <label for="${category.id}_category">${
                    category.label
                  }</label>
                  </div>
              `,
                )
                .join('')}
            </div>
          </div>
          <div style="overflow-y: scroll; overflow-x: hidden; max-height: 350px;">
            <table style="border-collapse: collapse; width: 100%; border: 1px solid white; padding: 5px; font-size: 15px; text-align: center;">
              <thead>
                <th style="padding: 5px 0 5px 5px" scope="col">#</th>
                <th class="short" style="width:58px;" />
                <th style="padding: 5px 0 5px 0" scope="col">Nom</th>
                <th style="padding: 5px 0 5px 0" scope="col">Auteurs</th>
                <th style="padding: 5px 0 5px 0" scope="col">Actif</th>
                <th class="short" style="width: 40px;" />
                <th class="short" style="width: 40px;" />
                <th class="short" style="width: 40px;" />
                <th class="short" style="width: 40px;" />
              </thead>
              <tbody></tbody>
            </table>
          </div>
        </div>`);

          content.prepend(createListStatus(meta));

          $(document).on('change', "input[name='category']", (e) => {
            const category = e.target.value;
            const section = $("input[name='section']:checked").val();
            const search = $("input[name='search_script']").val().toLowerCase();

            // Empty the table
            $('tbody', content).empty();
            // Add filtered lines
            scripts
              .filter(
                (script) =>
                  (script.section.includes(section) || section === 'all') &&
                  (script.category.includes(category) || category === 'all') &&
                  (script.name.toLowerCase().includes(search) ||
                    script.description.toLowerCase().includes(search)),
              )
              .forEach((script, index) => {
                const line = createScriptLine(script, index);
                $('tbody', content).append(line);
              });
          });

          $(document).on('change', "input[name='section']", (e) => {
            const section = e.target.value;
            const category = $("input[name='category']:checked").val();
            const search = $("input[name='search_script']").val().toLowerCase();

            // Empty the table
            $('tbody', content).empty();
            // Add filtered lines
            scripts
              .filter(
                (script) =>
                  (script.section.includes(section) || section === 'all') &&
                  (script.category.includes(category) || category === 'all') &&
                  (script.name.toLowerCase().includes(search) ||
                    script.description.toLowerCase().includes(search)),
              )
              .forEach((script, index) => {
                const line = createScriptLine(script, index);
                $('tbody', content).append(line);
              });
          });

          $(document).on('input', "input[name='search_script']", (e) => {
            const search = e.target.value.toLowerCase();
            const category = $("input[name='category']:checked").val();
            const section = $("input[name='section']:checked").val();

            // Empty the table
            $('tbody', content).empty();
            // Add filtered lines
            scripts
              .filter(
                (script) =>
                  (script.section.includes(section) || section === 'all') &&
                  (script.category.includes(category) || category === 'all') &&
                  (script.name.toLowerCase().includes(search) ||
                    script.description.toLowerCase().includes(search)),
              )
              .forEach((script, index) => {
                const line = createScriptLine(script, index);
                $('tbody', content).append(line);
              });
          });

          // Sauvegarder les paramètres.
          content.append(
            DC.UI.TextButton('scripts_refresh', 'Sauvegarder', () => {
              settings = newSettings;
              allDisabled = newAllDisabled;
              devMode = newDevMode;
              DC.LocalMemory.set(LIST_TAG, settings);
              DC.LocalMemory.set(ALL_DISABLED_TAG, allDisabled);
              DC.LocalMemory.set(DEV_MODE_TAG, devMode);
              location.replace('https://www.dreadcast.net/Main'); // Better than reload() with Chrome.
            }),
          );
          content.append(
            $(
              `<p><em class="couleur5">⚠ Sauvegarder votre configuration va raffraichir la page.<br />
         Pensez à sauvegarder votre travail en cours avant.</em></p>`,
            ),
          );

          const resetConfig = () => {
            const list = DC.LocalMemory.list();
            list.forEach((key) => {
              DC.LocalMemory.delete(key);
            });
          };

          // Import/Export
          content.append(
            $(
              '<div id="config_buttons" style="display: flex; justify-content: end; gap: 1rem; margin-bottom: 1rem;"></div>',
            ),
          );
          $('#config_buttons', content).append(
            DC.UI.TextButton(
              'config_reset',
              '<i class="fas fa-undo"></i> Réinitialiser',
              () => {
                resetConfig();
                location.replace('https://www.dreadcast.net/Main');
              },
            ),
          );
          $('#config_buttons', content).append(
            DC.UI.TextButton(
              'config_import',
              '<i class="fas fa-upload"></i> Importer la configuration',
              () => {
                resetConfig();

                const anchor = document.createElement('input');
                anchor.style.display = 'none';
                anchor.type = 'file';
                anchor.accept = 'application.json';
                anchor.onchange = (e) => {
                  var reader = new FileReader();
                  reader.onload = (e) => {
                    const data = JSON.parse(e.target.result);
                    Object.keys(data)
                      // Never import someone else's cached list: it would be
                      // restored with their timestamp and used as ours.
                      .filter((key) => key !== CACHE_TAG)
                      .forEach((key) => {
                        DC.LocalMemory.set(key, data[key]);
                      });
                  };
                  reader.readAsText(e.target.files[0]);
                  document.body.removeChild(anchor);
                  location.replace('https://www.dreadcast.net/Main');
                };
                document.body.appendChild(anchor);
                anchor.click();
              },
            ),
          );
          $('#config_buttons', content).append(
            DC.UI.TextButton(
              'config_export',
              '<i class="fas fa-download"></i> Exporter la configuration',
              function () {
                const list = DC.LocalMemory.list();
                let data = {};
                // The cached list is rebuilt on demand: it has nothing to do in
                // a configuration file, and would only bloat it.
                list
                  .filter((key) => key !== CACHE_TAG)
                  .forEach((key) => {
                    data[key] = DC.LocalMemory.get(key);
                  });

                const anchor = document.createElement('a');
                anchor.style.display = 'none';
                anchor.href = URL.createObjectURL(
                  new Blob([JSON.stringify(data)], {
                    type: 'application/json',
                  }),
                );
                anchor.download = 'dcsm_config.json';
                document.body.appendChild(anchor);
                anchor.click();
                document.body.removeChild(anchor);
              },
            ),
          );

          // Switch button pour désactiver tous les scripts.
          $('#scripts_all_switch', content).append(
            DC.UI.Checkbox(
              'scripts_all_check',
              newAllDisabled,
              () => (newAllDisabled = !newAllDisabled),
            ),
          );

          // Switch button pour le développeur mode.
          $('#developper_mode_switch', content).append(
            DC.UI.Tooltip(
              'Attention, ces scripts sont encore en développement !',
              DC.UI.Checkbox(
                'developper_mode_check',
                newDevMode,
                () => (newDevMode = !newDevMode),
              ),
            ),
          );

          scripts
            .filter((script) => devMode || !script.experimental)
            .forEach((script, index) => {
              const line = createScriptLine(script, index);
              $('tbody', content).append(line);
            });

          return DC.UI.PopUp('scripts_modal', 'Scripts & Skins', content);
        },
        true,
      ),
      5,
    );
  };

  // ===============
  $(document).ready(() => {
    initPersistence();
    createIntro();

    // Load list of scripts
    resolveList()
      .then(({ scripts, source, ts }) => {
        settings = synchronizeSettings(settings, scripts, source === 'remote');

        // Create the interface.
        if (Util.isGame()) {
          createUI(scripts, settings, { source, ts });
        }

        // Load the scripts
        if (!allDisabled) {
          const context = Util.getContext();

          scripts
            .filter((script) => script.section.includes(context))
            .filter((script) => devMode || !script.experimental)
            .forEach((script) => {
              if (settings[script.id]) {
                DC.Network.loadScript(script.url)
                  .then(() => {
                    console.info(
                      `DCSM - '${script.name}' script has been loaded successfully.`,
                    );
                  })
                  .catch((err) => {
                    console.error(
                      `DCSM - Error loading '${script.name}' script: ` + err,
                    );
                  });
              }
            });
        }
      })
      .catch((err) => {
        console.error('DCSM - Error loading the list of scripts :' + err);
      });
  });
});