Furaffinity-Custom-Settings

Library to create Custom settings on Furaffinitiy

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/475041/1760932/Furaffinity-Custom-Settings.js을(를) 사용하여 포함하는 라이브러리입니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Furaffinity-Custom-Settings
// @namespace   Violentmonkey Scripts
// @grant       none
// @version     4.3.3
// @author      Midori Dragon
// @description Library to create Custom settings on Furaffinitiy
// @icon        https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png
// @license     MIT
// @homepageURL https://greasyfork.org/scripts/475041-furaffinity-custom-settings
// @supportURL  https://greasyfork.org/scripts/475041-furaffinity-custom-settings/feedback
// ==/UserScript==
// jshint esversion: 11
(function (exports) {
    'use strict';

    var LogLevel;
    (function (LogLevel) {
        LogLevel[LogLevel["Error"] = 1] = "Error";
        LogLevel[LogLevel["Warning"] = 2] = "Warning";
        LogLevel[LogLevel["Info"] = 3] = "Info";
    })(LogLevel || (LogLevel = {}));
    class Logger {
        static log(logLevel = LogLevel.Warning, ...args) {
            if (window.__FF_GLOBAL_LOG_LEVEL__ == null) {
                window.__FF_GLOBAL_LOG_LEVEL__ = LogLevel.Error;
            }
            if (logLevel > window.__FF_GLOBAL_LOG_LEVEL__) {
                return;
            }
            switch (logLevel) {
                case LogLevel.Error:
                    console.error(...args);
                    break;
                case LogLevel.Warning:
                    console.warn(...args);
                    break;
                case LogLevel.Info:
                    console.log(...args);
                    break;
            }
        }
        static setLogLevel(logLevel) {
            window.__FF_GLOBAL_LOG_LEVEL__ = logLevel;
        }
        static logError(...args) {
            Logger.log(LogLevel.Error, ...args);
        }
        static logWarning(...args) {
            Logger.log(LogLevel.Warning, ...args);
        }
        static logInfo(...args) {
            Logger.log(LogLevel.Info, ...args);
        }
    }

    var SettingType;
    (function (SettingType) {
        SettingType[SettingType["Number"] = 0] = "Number";
        SettingType[SettingType["Boolean"] = 1] = "Boolean";
        SettingType[SettingType["Action"] = 2] = "Action";
        SettingType[SettingType["Text"] = 3] = "Text";
        SettingType[SettingType["Option"] = 4] = "Option";
    })(SettingType || (SettingType = {}));

    function makeIdCompatible(id) {
        const sanitizedString = id
            .replace(/[^a-zA-Z0-9-_\.]/g, '-')
            .replace(/^-+|-+$/g, '')
            .replace(/^-*(?=\d)/, 'id-');
        return /^[0-9]/.test(sanitizedString) ? 'id-' + sanitizedString : sanitizedString;
    }

    class SettingAction extends EventTarget {
        id;
        type;
        name;
        description = '';
        defaultValue;
        settingElem;
        _onInput;
        _settingInputElem;
        constructor(providerId, name) {
            super();
            Object.setPrototypeOf(this, SettingAction.prototype);
            this.name = name;
            this.id = providerId + '-' + makeIdCompatible(this.name);
            this.type = SettingType.Action;
            this.defaultValue = '';
            this.loadFromSyncedStorage();
            this.settingElem = this._settingInputElem = this.create();
        }
        get value() {
            return this._settingInputElem.textContent ?? '';
        }
        set value(newValue) {
            this._settingInputElem.textContent = newValue;
        }
        get onInput() {
            return this._onInput;
        }
        set onInput(handler) {
            this._onInput = handler;
        }
        create() {
            const settingElem = document.createElement('button');
            settingElem.id = this.id;
            settingElem.type = 'button';
            settingElem.className = 'button standard mobile-fix';
            settingElem.textContent = this.name;
            settingElem.addEventListener('click', this.invokeInput.bind(this));
            return settingElem;
        }

        loadFromSyncedStorage() { }
        toString() {
            return `${this.name} = ${this.value}`;
        }
        invokeInput() {
            this._onInput?.(this._settingInputElem);
            this.dispatchEvent(new Event('input'));
        }
    }

    class GMInfo {
        static isBrowserEnvironment() {
            return (typeof browser !== 'undefined' && typeof browser.runtime !== 'undefined') || (typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined');
        }
        static getBrowserAPI() {
            if (typeof GM_info !== 'undefined' && GM_info != null) {
                // For userscripts
                return GM_info;
            }
            else if (typeof browser !== 'undefined' && typeof browser.runtime !== 'undefined') {
                // Firefox or browsers using WebExtension API
                return browser;
            }
            else if (typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined') {
                // Chrome or Chromium-based browsers
                return chrome;
            }
            else {
                throw new Error('Unsupported browser for SyncedStorage.');
            }
        }
        static get scriptName() {
            if (GMInfo.isBrowserEnvironment()) {
                return GMInfo.getBrowserAPI().runtime.getManifest().name;
            }
            else {
                return GMInfo.getBrowserAPI().script.name;
            }
        }
        static get scriptVersion() {
            if (GMInfo.isBrowserEnvironment()) {
                return GMInfo.getBrowserAPI().runtime.getManifest().version;
            }
            else {
                return GMInfo.getBrowserAPI().script.version;
            }
        }
        static get scriptDescription() {
            if (GMInfo.isBrowserEnvironment()) {
                return GMInfo.getBrowserAPI().runtime.getManifest().description;
            }
            else {
                return GMInfo.getBrowserAPI().script.description;
            }
        }
        static get scriptAuthor() {
            if (GMInfo.isBrowserEnvironment()) {
                return GMInfo.getBrowserAPI().runtime.getManifest().author;
            }
            else {
                return GMInfo.getBrowserAPI().script.author;
            }
        }
        static get scriptNamespace() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions don't have namespace
            }
            else {
                return GMInfo.getBrowserAPI().script.namespace;
            }
        }
        static get scriptSource() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions don't have source
            }
            else {
                return GMInfo.getBrowserAPI().script.source;
            }
        }
        static get scriptIcon() {
            if (GMInfo.isBrowserEnvironment()) {
                const manifest = GMInfo.getBrowserAPI().runtime.getManifest();
                let largestIcon = 0;
                for (const key of Object.keys(manifest.icons)) {
                    const size = parseInt(key);
                    if (size > largestIcon) {
                        largestIcon = size;
                    }
                }
                return manifest.icons[largestIcon.toString()];
            }
            else {
                return GMInfo.getBrowserAPI().script.icon;
            }
        }
        static get scriptIcon64() {
            if (GMInfo.isBrowserEnvironment()) {
                const manifest = GMInfo.getBrowserAPI().runtime.getManifest();
                return manifest.icons == null ? undefined : manifest.icons['64'];
            }
            else {
                return GMInfo.getBrowserAPI().script.icon64;
            }
        }
        static get scriptAntifeature() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions don't have antifeature
            }
            else {
                return GMInfo.getBrowserAPI().script.antifeature;
            }
        }
        static get scriptOptions() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions don't have script options
            }
            else {
                return GMInfo.getBrowserAPI().script.options;
            }
        }
        static get scriptMetaStr() {
            if (GMInfo.isBrowserEnvironment()) {
                return JSON.stringify(GMInfo.getBrowserAPI().runtime.getManifest());
            }
            else {
                return GMInfo.getBrowserAPI().scriptMetaStr;
            }
        }
        static get scriptHandler() {
            if (GMInfo.isBrowserEnvironment()) {
                return typeof browser !== 'undefined' ? 'Firefox' : 'Chrome';
            }
            else {
                return GMInfo.getBrowserAPI().scriptHandler;
            }
        }
        static get scriptUpdateURL() {
            if (GMInfo.isBrowserEnvironment()) {
                return GMInfo.getBrowserAPI().runtime.getManifest().update_url;
            }
            else {
                return GMInfo.getBrowserAPI().scriptUpdateURL;
            }
        }
        static get scriptWillUpdate() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions handle updates differently
            }
            else {
                return GMInfo.getBrowserAPI().scriptWillUpdate;
            }
        }
        static get scriptResources() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions don't have script resources
            }
            else {
                return GMInfo.getBrowserAPI().scriptResources;
            }
        }
        static get downloadMode() {
            if (GMInfo.isBrowserEnvironment()) {
                return undefined; // Browser extensions don't have download mode
            }
            else {
                return GMInfo.getBrowserAPI().downloadMode;
            }
        }
    }

    
    class SyncedStorage {
        static warningSentCount = 0;
        static maxWarningCount = 10;
        static async setItem(key, value) {
            if (!GMInfo.isBrowserEnvironment()) {
                if (this.warningSentCount < this.maxWarningCount) {
                    Logger.logWarning('SyncedStorage is only available in browser extensions.');
                    this.warningSentCount++;
                }
                return;
            }
            Logger.logInfo(`Setting item in synced storage: ${key}=${value}`);
            const api = GMInfo.getBrowserAPI();
            if (api.storage != null) {
                return new Promise((resolve, reject) => {
                    api.storage.sync.set({ [key]: value }, () => {
                        if (api.runtime.lastError != null) {
                            return reject(api.runtime.lastError);
                        }
                        resolve();
                    });
                });
            }
            else {
                Logger.logError('Unsupported storage API.');
            }
        }
        static async getItem(key) {
            if (!GMInfo.isBrowserEnvironment()) {
                if (this.warningSentCount < this.maxWarningCount) {
                    Logger.logWarning('SyncedStorage is only available in browser extensions.');
                    this.warningSentCount++;
                }
                return;
            }
            Logger.logInfo(`Getting item from synced storage: ${key}`);
            const api = GMInfo.getBrowserAPI();
            if (api.storage != null) {
                return new Promise((resolve, reject) => {
                    api.storage.sync.get(key, (result) => {
                        if (api.runtime.lastError != null) {
                            return reject(api.runtime.lastError);
                        }
                        resolve(result[key]);
                    });
                });
            }
            else {
                Logger.logError('Unsupported storage API.');
            }
        }
        static async getAllItems() {
            if (!GMInfo.isBrowserEnvironment()) {
                if (this.warningSentCount < this.maxWarningCount) {
                    Logger.logWarning('SyncedStorage is only available in browser extensions.');
                    this.warningSentCount++;
                }
                return {};
            }
            Logger.logInfo('Getting all items from synced storage');
            const api = GMInfo.getBrowserAPI();
            if (api.storage != null) {
                return new Promise((resolve, reject) => {
                    api.storage.sync.get(null, (result) => {
                        if (api.runtime.lastError != null) {
                            return reject(api.runtime.lastError);
                        }
                        resolve(result);
                    });
                });
            }
            else {
                Logger.logError('Unsupported storage API.');
                return {};
            }
        }
        static async removeItem(key) {
            if (!GMInfo.isBrowserEnvironment()) {
                if (this.warningSentCount < this.maxWarningCount) {
                    Logger.logWarning('SyncedStorage is only available in browser extensions.');
                    this.warningSentCount++;
                }
                return;
            }
            Logger.logInfo(`Removing item from synced storage: ${key}`);
            const api = GMInfo.getBrowserAPI();
            if (api.storage != null) {
                return new Promise((resolve, reject) => {
                    api.storage.sync.remove(key, () => {
                        if (api.runtime.lastError != null) {
                            return reject(api.runtime.lastError);
                        }
                        resolve();
                    });
                });
            }
            else {
                Logger.logError('Unsupported storage API.');
            }
        }
    }

    class SettingBoolean extends EventTarget {
        id;
        type;
        name;
        description = '';
        settingElem;
        _onInput;
        _defaultValue;
        _settingInputElem;
        constructor(providerId, name) {
            super();
            Object.setPrototypeOf(this, SettingBoolean.prototype);
            this.name = name;
            this.id = providerId + '-' + makeIdCompatible(this.name);
            this.type = SettingType.Boolean;
            this._defaultValue = false;
            this.loadFromSyncedStorage();
            this.settingElem = this.create();
            this._settingInputElem = this.settingElem.querySelector('input');
        }
        get value() {
            const localValue = localStorage.getItem(this.id);
            if (localValue == null) {
                return this.defaultValue;
            }
            return localValue === 'true' || localValue === '1';
        }
        set value(newValue) {
            if (newValue === this.defaultValue) {
                localStorage.removeItem(this.id);
                void SyncedStorage.removeItem(this.id);
            }
            else {
                localStorage.setItem(this.id, newValue.toString());
                void SyncedStorage.setItem(this.id, newValue);
            }
            this._settingInputElem.checked = newValue;
            this.invokeInput(this._settingInputElem);
        }
        get defaultValue() {
            return this._defaultValue;
        }
        set defaultValue(value) {
            this._defaultValue = value;
            this.value = this.value;
        }
        get onInput() {
            return this._onInput;
        }
        set onInput(handler) {
            this._onInput = handler;
        }
        create() {
            const container = document.createElement('div');
            const settingElem = document.createElement('input');
            settingElem.id = this.id;
            settingElem.type = 'checkbox';
            settingElem.style.cursor = 'pointer';
            settingElem.style.marginRight = '4px';
            settingElem.addEventListener('change', () => this.value = settingElem.checked);
            container.appendChild(settingElem);
            const settingElemLabel = document.createElement('label');
            settingElemLabel.textContent = this.name;
            settingElemLabel.style.cursor = 'pointer';
            settingElemLabel.style.userSelect = 'none';
            settingElemLabel.htmlFor = this.id;
            container.appendChild(settingElemLabel);
            return container;
        }
        loadFromSyncedStorage() {
            void SyncedStorage.getItem(this.id).then((value) => {
                if (value != null) {
                    localStorage.setItem(this.id, value.toString());
                }
            });
        }
        toString() {
            return `${this.name} = ${this.value}`;
        }
        invokeInput(elem) {
            this.onInput?.(elem);
            this.dispatchEvent(new CustomEvent('input', { detail: elem }));
        }
    }

    class SettingNumber extends EventTarget {
        id;
        type;
        name;
        description = '';
        min;
        max;
        step;
        settingElem;
        _onInput;
        _defaultValue;
        _settingInputElem;
        constructor(providerId, name) {
            super();
            Object.setPrototypeOf(this, SettingNumber.prototype);
            this.name = name;
            this.id = providerId + '-' + makeIdCompatible(this.name);
            this.type = SettingType.Number;
            this._defaultValue = 0;
            this.min = 0;
            this.max = 32767;
            this.step = 1;
            this.loadFromSyncedStorage();
            this.settingElem = this._settingInputElem = this.create();
        }
        get value() {
            return parseInt(localStorage.getItem(this.id) ?? this.defaultValue.toString()) || this.defaultValue;
        }
        set value(newValue) {
            newValue = Math.min(Math.max(newValue, this.min), this.max);
            if (newValue === this.defaultValue) {
                localStorage.removeItem(this.id);
                void SyncedStorage.removeItem(this.id);
            }
            else {
                localStorage.setItem(this.id, newValue.toString());
                void SyncedStorage.setItem(this.id, newValue);
            }
            this._settingInputElem.value = newValue.toString();
            this.invokeInput(this._settingInputElem);
        }
        get defaultValue() {
            return this._defaultValue;
        }
        set defaultValue(value) {
            this._defaultValue = value;
            this.value = this.value;
        }
        get onInput() {
            return this._onInput;
        }
        set onInput(handler) {
            this._onInput = handler;
        }
        create() {
            const settingElem = document.createElement('input');
            settingElem.id = this.id;
            settingElem.type = 'text';
            settingElem.className = 'textbox';
            settingElem.addEventListener('keydown', (event) => {
                const currentValue = parseInt(settingElem.value) || this.defaultValue;
                if (event.key === 'ArrowUp') {
                    this.value = Math.min(currentValue + this.step, this.max);
                }
                else if (event.key === 'ArrowDown') {
                    this.value = Math.max(currentValue - this.step, this.min);
                }
            });
            settingElem.addEventListener('input', () => {
                const inputValue = settingElem.value.replace(/[^0-9]/g, '');
                const numericValue = parseInt(inputValue) || this.defaultValue;
                this.value = Math.min(Math.max(numericValue, this.min), this.max);
            });
            return settingElem;
        }
        loadFromSyncedStorage() {
            void SyncedStorage.getItem(this.id).then((value) => {
                if (value != null) {
                    localStorage.setItem(this.id, value.toString());
                }
            });
        }
        toString() {
            return `${this.name} = ${this.value}`;
        }
        invokeInput(elem) {
            this.onInput?.(elem);
            this.dispatchEvent(new CustomEvent('input', { detail: elem }));
        }
    }

    class SettingText extends EventTarget {
        id;
        type;
        name;
        description = '';
        settingElem;
        verifyRegex;
        _onInput;
        _defaultValue;
        _settingInputElem;
        _errorMessage;
        constructor(providerId, name) {
            super();
            Object.setPrototypeOf(this, SettingText.prototype);
            this.name = name;
            this.id = providerId + '-' + makeIdCompatible(this.name);
            this.type = SettingType.Text;
            this._defaultValue = '';
            this.loadFromSyncedStorage();
            this.settingElem = this.create();
            this._settingInputElem = this.settingElem.querySelector('input');
            this._errorMessage = this.settingElem.querySelector('.error-message');
        }
        get value() {
            return localStorage.getItem(this.id) ?? this.defaultValue;
        }
        set value(newValue) {
            if (this.verifyRegex != null && !this.verifyRegex.test(newValue)) {
                this._errorMessage.style.display = 'block';
                return;
            }
            this._errorMessage.style.display = 'none';
            try {
                if (newValue === this.defaultValue) {
                    localStorage.removeItem(this.id);
                    void SyncedStorage.removeItem(this.id);
                }
                else {
                    localStorage.setItem(this.id, newValue);
                    void SyncedStorage.setItem(this.id, newValue);
                }
            }
            catch (error) {
                Logger.logError(error);
            }
            this._settingInputElem.value = newValue;
            this.invokeInput(this._settingInputElem);
        }
        get defaultValue() {
            return this._defaultValue;
        }
        set defaultValue(value) {
            this._defaultValue = value;
            this.value = this.value;
        }
        get onInput() {
            return this._onInput;
        }
        set onInput(handler) {
            this._onInput = handler;
        }
        create() {
            const container = document.createElement('div');
            container.style.position = 'relative';
            const settingElem = document.createElement('input');
            settingElem.id = this.id;
            settingElem.type = 'text';
            settingElem.className = 'textbox';
            settingElem.addEventListener('input', () => {
                if (this.verifyRegex != null && !this.verifyRegex.test(settingElem.value)) {
                    this._errorMessage.style.display = 'block';
                }
                else {
                    this._errorMessage.style.display = 'none';
                }
                this.value = settingElem.value;
            });
            container.appendChild(settingElem);
            const errorMessage = document.createElement('div');
            errorMessage.className = 'error-message';
            errorMessage.style.color = 'red';
            errorMessage.style.display = 'none';
            errorMessage.style.position = 'absolute';
            errorMessage.style.top = '100%';
            errorMessage.style.left = '0';
            errorMessage.textContent = 'Invalid input';
            container.appendChild(errorMessage);
            return container;
        }
        loadFromSyncedStorage() {
            void SyncedStorage.getItem(this.id).then((value) => {
                if (value != null) {
                    localStorage.setItem(this.id, value.toString());
                }
            });
        }
        toString() {
            return `${this.name} = ${this.value}`;
        }
        invokeInput(elem) {
            this.onInput?.(elem);
            this.dispatchEvent(new CustomEvent('input', { detail: elem }));
        }
    }

    function styleInject(css, ref) {
      if ( ref === void 0 ) ref = {};
      var insertAt = ref.insertAt;

      if (typeof document === 'undefined') { return; }

      var head = document.head || document.getElementsByTagName('head')[0];
      var style = document.createElement('style');
      style.type = 'text/css';

      if (insertAt === 'top') {
        if (head.firstChild) {
          head.insertBefore(style, head.firstChild);
        } else {
          head.appendChild(style);
        }
      } else {
        head.appendChild(style);
      }

      if (style.styleSheet) {
        style.styleSheet.cssText = css;
      } else {
        style.appendChild(document.createTextNode(css));
      }
    }

    var css_248z = ".switch-cs {\n    position: relative;\n    display: inline-block;\n    width: 52px;\n    height: 28px;\n    margin: 6px 8px 6px 0;\n}\n\n.switch-cs input {\n    opacity: 0;\n    width: 0;\n    height: 0;\n}\n\n.slider-cs {\n    position: absolute;\n    cursor: pointer;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-color: #ccc;\n    transition: .4s;\n    border-radius: 34px;\n}\n\n.slider-cs:before {\n    position: absolute;\n    content: \"\";\n    height: 20px;\n    width: 20px;\n    left: 4px;\n    bottom: 4px;\n    background-color: white;\n    transition: .4s;\n    border-radius: 50%;\n}\n\ninput:checked+.slider-cs {\n    background-color: #4CAF50;\n}\n\ninput:checked+.slider-cs:before {\n    transform: translateX(26px);\n}\n\n.section-header.cs {\n    display: flex;\n    align-items: center;\n}\n\n.section-body.cs {\n    opacity: 1;\n    transition: opacity 0.3s linear;\n}\n\n.section-body.cs.collapsed {\n    opacity: 0.4;\n    pointer-events: none;\n}\n";
    styleInject(css_248z);

    class SettingOption extends EventTarget {
        id;
        type;
        name;
        description = '';
        settingElem;
        _onInput;
        _defaultValue;
        _settingInputElem;
        constructor(providerId, name) {
            super();
            Object.setPrototypeOf(this, SettingOption.prototype);
            this.name = name;
            this.id = providerId + '-' + makeIdCompatible(this.name);
            this.type = SettingType.Option;
            this._defaultValue = '0';
            this.loadFromSyncedStorage();
            this.settingElem = this.create();
            this._settingInputElem = this.settingElem.querySelector('select');
        }
        get value() {
            return localStorage.getItem(this.id) ?? this.defaultValue;
        }
        set value(newValue) {
            try {

                if (newValue == this.defaultValue) {
                    localStorage.removeItem(this.id);
                    void SyncedStorage.removeItem(this.id);
                }
                else {
                    localStorage.setItem(this.id, newValue.toString());
                    void SyncedStorage.setItem(this.id, newValue.toString());
                }
            }
            catch (error) {
                Logger.logError(error);
            }
            this._settingInputElem.value = newValue.toString();
            this.invokeInput(this._settingInputElem);
        }
        get defaultValue() {
            return this._defaultValue;
        }
        set defaultValue(value) {
            this._defaultValue = value;
            this.value = this.value;
        }
        get options() {
            const options = {};
            for (const option of Array.from(this._settingInputElem.options)) {
                options[option.value] = option.textContent ?? '';
            }
            return options;
        }
        set options(newValue) {
            const currValue = this.value;
            this._settingInputElem.innerHTML = '';
            for (const [value, text] of Object.entries(newValue)) {
                this.addOption(value, text);
            }
            if (Array.from(this._settingInputElem.options).some(x => x.value === currValue.toString())) {
                this.value = currValue;
            }
            else {
                this.value = this.defaultValue;
            }
        }
        get onInput() {
            return this._onInput;
        }
        set onInput(handler) {
            this._onInput = handler;
        }
        addOption(value, text) {
            if (this._settingInputElem.options.namedItem(value.toString()) != null) {
                Logger.logWarning(`Option with value "${value}" already exists.`);
                return;
            }
            const option = document.createElement('option');
            option.value = value.toString();
            option.textContent = text.toString();
            this._settingInputElem.options.add(option);
        }
        removeOption(value) {
            const option = this._settingInputElem.options.namedItem(value.toString());
            if (option == null) {
                Logger.logWarning(`Option with value "${value}" does not exist.`);
                return;
            }
            this._settingInputElem.removeChild(option);
        }
        create() {
            const container = document.createElement('div');
            container.style.position = 'relative';
            const settingElem = document.createElement('select');
            settingElem.id = this.id;
            settingElem.className = 'forminput styled';
            settingElem.addEventListener('change', () => {
                this.value = settingElem.value;
            });
            container.appendChild(settingElem);
            return container;
        }
        loadFromSyncedStorage() {
            void SyncedStorage.getItem(this.id).then((value) => {
                if (value != null) {
                    localStorage.setItem(this.id, value.toString());
                }
            });
        }
        toString() {
            return `${this.name} = ${this.value}`;
        }
        invokeInput(elem) {
            this.onInput?.(elem);
            this.dispatchEvent(new CustomEvent('input', { detail: elem }));
        }
    }

    
    class StorageWrapper {
        static async setItemAsync(key, value, retry = false) {
            try {
                if (retry) {
                    return await StorageWrapper.setItemAsyncWithRetry(key, value);
                }
                localStorage.setItem(key, value);
                await SyncedStorage.setItem(key, value);
                return true;
            }
            catch {
                Logger.logError(`Failed to set item in storage: ${key}=${value}`);
                return false;
            }
        }
        static async setItemAsyncWithRetry(key, value) {
            return new Promise((resolve) => {
                const attemptSave = async () => {
                    const success = await StorageWrapper.setItemAsync(key, value);
                    if (success) {
                        resolve(true);
                    }
                    else {
                        Logger.logWarning(`Failed to save item ${key}, retrying in 1000ms...`);
                        setTimeout(() => void attemptSave(), 1000);
                    }
                };
                void attemptSave();
            });
        }
        static async getItemAsync(key) {
            try {
                const valueLocal = localStorage.getItem(key);
                const valueSynced = await SyncedStorage.getItem(key);
                if (valueSynced === valueLocal) {
                    return valueSynced;
                }
                return valueSynced ?? valueLocal;
            }
            catch {
                Logger.logError(`Failed to get item from storage: ${key}`);
                return null;
            }
        }
        static async removeItemAsync(key) {
            try {
                localStorage.removeItem(key);
                await SyncedStorage.removeItem(key);
                return true;
            }
            catch {
                Logger.logError(`Failed to remove item from storage: ${key}`);
                return false;
            }
        }
        static async getAllItemsAsync() {
            try {
                const localStorageItems = {};
                for (let i = 0; i < localStorage.length; i++) {
                    const key = localStorage.key(i);
                    if (key == null)
                        continue;
                    const value = localStorage.getItem(key);
                    localStorageItems[key] = value;
                }
                const syncedItems = await SyncedStorage.getAllItems();
                return { ...localStorageItems, ...syncedItems };
            }
            catch {
                Logger.logError('Failed to get all items from storage');
                return {};
            }
        }
    }

    class Settings {
        settings = {};
        showFeatureEnabledSetting = true;
        _menuName;
        _menuNameId;
        _provider;
        _providerId;
        _headerName;
        _isFeatureEnabledSetting;
        _settingClassMapping = {
            [SettingType.Number]: SettingNumber,
            [SettingType.Boolean]: SettingBoolean,
            [SettingType.Action]: SettingAction,
            [SettingType.Text]: SettingText,
            [SettingType.Option]: SettingOption
        };
        constructor(provider, headerName) {
            this._menuName = 'Extension Settings';
            this._menuNameId = makeIdCompatible('Extension Settings');
            this._provider = provider;
            this._providerId = makeIdCompatible(provider);
            this._headerName = headerName;
            this._isFeatureEnabledSetting = new SettingBoolean(this.providerId, `${headerName} Enabled`);
            this._isFeatureEnabledSetting.defaultValue = true;
            let currSettingsJson = localStorage.getItem('ff-registered-settings');
            currSettingsJson ??= '[]';
            const currSettings = JSON.parse(currSettingsJson);
            if (!currSettings.includes(this.providerId)) {
                currSettings.push(this.providerId);
                localStorage.setItem('ff-registered-settings', JSON.stringify(currSettings));
            }
        }
        get menuName() {
            return this._menuName;
        }
        get menuNameId() {
            return this._menuNameId;
        }
        get provider() {
            return this._provider;
        }
        get providerId() {
            return this._providerId;
        }
        get headerName() {
            return this._headerName;
        }
        get isFeatureEnabled() {
            return this._isFeatureEnabledSetting.value;
        }
        newSetting(type, name) {
            const classConstructor = this._settingClassMapping[type];
            const newSetting = new classConstructor(this.providerId, name);
            this.settings[name] = newSetting;
            return newSetting;
        }
        loadSettings() {
            try {
                this.addExSettingsMenu(this.menuName, this.provider, this.menuNameId, this.providerId);
                if (window.location.toString().includes('controls/settings')) {
                    this.addExSettingsMenuSidebar(this.menuName, this.provider, this.menuNameId, this.providerId);
                    if (window.location.toString().includes('?extension=' + this.providerId)) {
                        this.loadSettingValues(this.headerName, Object.values(this.settings));
                    }
                }
            }
            catch (error) {
                Logger.logError(error);
            }
        }
        async exportSettings() {
            try {
                let registeredSettingsJson = localStorage.getItem('ff-registered-settings');
                registeredSettingsJson ??= '[]';
                const registeredSettings = JSON.parse(registeredSettingsJson);
                if (registeredSettings.length === 0) {
                    return;
                }
                let settings = await StorageWrapper.getAllItemsAsync();
                if (settings == null) {
                    return;
                }
                settings = Object.entries(settings)
                    .filter(([key]) => registeredSettings.some(setting => key.startsWith(setting)))
                    .reduce((obj, [key, value]) => {
                    obj[key] = value;
                    return obj;
                }, {});
                Logger.logInfo('Exporting Settings');
                Logger.logInfo(settings);
                if (Object.keys(settings).length === 0) {
                    return;
                }
                const settingsString = JSON.stringify(settings, null, 2);
                const blob = new Blob([settingsString], { type: 'application/json' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `${this.providerId}_settings.json`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            }
            catch (error) {
                Logger.logError(error);
            }
        }
        async importSettings(settingsJson) {
            try {
                Logger.logInfo('Importing Settings');
                Logger.logInfo(settingsJson);
                settingsJson ??= '{}';
                const settings = JSON.parse(settingsJson);
                for (const [key, value] of Object.entries(settings)) {
                    await StorageWrapper.setItemAsync(key, value);
                }
            }
            catch (error) {
                Logger.logError(error);
            }
        }
        loadSettingValues(headerName, settings) {
            if (settings.length === 0) {
                return;
            }
            const settingsContainerPresent = document.getElementById(headerName + '_settingscontainer') != null;
            if (settingsContainerPresent) {
                return;
            }
            const columnPage = document.getElementById('columnpage');
            const content = columnPage?.querySelector('div[class="content"]');
            if (content == null) {
                Logger.logError('Failed to load settings. No content found.');
                return;
            }
            const nonExSettings = content.querySelectorAll('section:not([class="exsettings"])');
            for (const section of Array.from(nonExSettings ?? [])) {
                section.parentNode?.removeChild(section);
            }
            const section = document.createElement('section');
            section.id = headerName + '_settingscontainer';
            section.className = 'exsettings';
            const headerContainer = document.createElement('div');
            headerContainer.className = 'section-header cs';
            const header = document.createElement('h2');
            header.textContent = headerName;
            const bodyContainer = document.createElement('div');
            bodyContainer.className = 'section-body cs';
            if (this._isFeatureEnabledSetting.value) {
                bodyContainer.classList.remove('collapsed');
            }
            else {
                bodyContainer.classList.add('collapsed');
            }
            if (this.showFeatureEnabledSetting) {
                headerContainer.appendChild(this.createFeatureEnableSetting(bodyContainer));
            }
            headerContainer.appendChild(header);
            section.appendChild(headerContainer);
            for (const setting of settings) {
                bodyContainer.appendChild(this.createSettingContainer(setting));
            }
            section.appendChild(bodyContainer);
            content.appendChild(section);
        }
        createFeatureEnableSetting(bodyContainer) {
            const enableFeatureSettingContainerElem = document.createElement('label');
            enableFeatureSettingContainerElem.classList.add('switch-cs');
            const enableFeatureSettingInput = document.createElement('input');
            enableFeatureSettingInput.type = 'checkbox';
            enableFeatureSettingInput.id = 'toggleSwitch';
            enableFeatureSettingInput.checked = this._isFeatureEnabledSetting.value;
            enableFeatureSettingInput.addEventListener('input', () => {
                this._isFeatureEnabledSetting.value = enableFeatureSettingInput.checked;
                if (enableFeatureSettingInput.checked) {
                    bodyContainer.classList.remove('collapsed');
                }
                else {
                    bodyContainer.classList.add('collapsed');
                }
            });
            const enableFeatureSettingSpan = document.createElement('span');
            enableFeatureSettingSpan.classList.add('slider-cs');
            enableFeatureSettingContainerElem.appendChild(enableFeatureSettingInput);
            enableFeatureSettingContainerElem.appendChild(enableFeatureSettingSpan);
            return enableFeatureSettingContainerElem;
        }
        toString() {
            if (Object.keys(this.settings).length === 0) {
                return `${this.menuName} has no settings.`;
            }
            let settingsString = '(';
            Object.keys(this.settings).forEach((key) => {
                if (this.settings[key].type !== SettingType.Action) {
                    settingsString += `"${this.settings[key].toString()}", `;
                }
            });
            settingsString = settingsString.slice(0, -2) + ')';
            return settingsString;
        }
        createSettingContainer(setting) {
            const settingContainer = document.createElement('div');
            settingContainer.className = 'control-panel-item-container';
            const settingName = document.createElement('div');
            settingName.className = 'control-panel-item-name';
            const settingNameText = document.createElement('h4');
            settingNameText.textContent = setting.name;
            settingName.appendChild(settingNameText);
            settingContainer.appendChild(settingName);
            const settingDesc = document.createElement('div');
            settingDesc.className = 'control-panel-item-description';
            const settingDescText = document.createTextNode(setting.description);
            settingDesc.appendChild(settingDescText);
            settingContainer.appendChild(settingDesc);
            if (showResetButtonSetting.value && setting.type !== SettingType.Action) {
                settingDesc.appendChild(document.createElement('br'));
                settingDesc.appendChild(this.createSettingReset(setting));
            }
            const settingOption = document.createElement('div');
            settingOption.className = 'control-panel-item-options';
            settingOption.appendChild(setting.settingElem);
            settingContainer.appendChild(settingOption);
            return settingContainer;
        }
        createSettingReset(setting) {
            const settingDescReset = document.createElement('a');
            settingDescReset.id = setting.id + '_settingreset';
            settingDescReset.textContent = 'Reset this Setting';
            settingDescReset.style.cursor = 'pointer';
            settingDescReset.style.color = 'aqua';
            settingDescReset.style.textDecoration = 'underline';
            settingDescReset.style.fontStyle = 'italic';
            settingDescReset.style.fontSize = '14px';
            settingDescReset.addEventListener('click', () => {
                setting.value = setting.defaultValue;
            });
            return settingDescReset;
        }
        addExSettingsMenu(name, provider, nameId, providerId) {
            try {
                const navBar = document.querySelector('ul[class="navhideonmobile"]');
                const settings = navBar?.querySelector('a[href="/controls/settings/"]')?.parentNode;
                if (settings == null) {
                    Logger.logError(`Failed to add extension ${name} to settings menu`);
                    return;
                }
                const exSettingNamePresent = document.getElementById(nameId) != null;
                const exSettingProviderPresent = document.getElementById(providerId) != null;
                if (!exSettingNamePresent) {
                    const exSettingsHeader = document.createElement('h3');
                    exSettingsHeader.id = nameId;
                    exSettingsHeader.textContent = name;
                    settings.appendChild(exSettingsHeader);
                }
                if (!exSettingProviderPresent) {
                    const currExSettings = document.createElement('a');
                    currExSettings.id = providerId;
                    currExSettings.textContent = provider;
                    currExSettings.href = '/controls/settings?extension=' + providerId;
                    currExSettings.style.cursor = 'pointer';
                    settings.appendChild(currExSettings);
                }
            }
            catch (error) {
                Logger.logError(error);
            }
        }
        addExSettingsMenuSidebar(name, provider, nameId, providerId) {
            try {
                const settings = document.getElementById('controlpanelnav');
                if (settings == null) {
                    Logger.logError(`Failed to add extension ${name} to settings sidebar`);
                    return;
                }
                const exSettingNamePresent = document.getElementById(nameId + '_side') != null;
                const exSettingProviderPresent = document.getElementById(providerId + '_side') != null;
                if (!exSettingNamePresent) {
                    const exSettingsHeader = document.createElement('h3');
                    exSettingsHeader.id = nameId + '_side';
                    exSettingsHeader.textContent = name;
                    settings.appendChild(exSettingsHeader);
                }
                if (!exSettingProviderPresent) {
                    const currExSettings = document.createElement('a');
                    currExSettings.id = providerId + '_side';
                    currExSettings.textContent = provider;
                    currExSettings.href = '/controls/settings?extension=' + providerId;
                    currExSettings.style.cursor = 'pointer';
                    settings.appendChild(currExSettings);
                    settings.appendChild(document.createElement('br'));
                }
            }
            catch (error) {
                Logger.logError(error);
            }
        }
    }

    Object.defineProperties(window, {
        FACustomSettings: { get: () => Settings },
        FASettingType: { get: () => SettingType },
    });
    const customSettings = new Settings('Custom Furaffinity Settings', 'Global Custom Furaffinity Settings');
    customSettings.showFeatureEnabledSetting = false;
    const loggingSetting = customSettings.newSetting(window.FASettingType.Option, 'Logging');
    loggingSetting.description = 'Sets the logging level.';
    loggingSetting.defaultValue = LogLevel.Error;
    loggingSetting.options = {
        [LogLevel.Error]: LogLevel[LogLevel.Error],
        [LogLevel.Warning]: LogLevel[LogLevel.Warning],
        [LogLevel.Info]: LogLevel[LogLevel.Info]
    };
    loggingSetting.addEventListener('input', () => Logger.setLogLevel(parseInt(loggingSetting.value.toString())));
    Logger.setLogLevel(parseInt(loggingSetting.value.toString()));
    const showResetButtonSetting = customSettings.newSetting(SettingType.Boolean, 'Show Reset Button');
    showResetButtonSetting.description = 'Set wether the "Reset this Setting" button is shown in other Settings.';
    showResetButtonSetting.defaultValue = true;
    const importSettingsSetting = customSettings.newSetting(SettingType.Action, 'Import Settings');
    importSettingsSetting.description = 'Imports the Settings from a JSON file.';
    importSettingsSetting.addEventListener('input', () => {
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.json';
        fileInput.addEventListener('change', () => {
            const file = fileInput.files?.[0];
            if (file) {
                void file.text().then((content) => {
                    void customSettings.importSettings(content);
                    location.reload();
                });
            }
        });
        fileInput.click();
    });
    const exportSettingsSetting = customSettings.newSetting(SettingType.Action, 'Export Settings');
    exportSettingsSetting.description = 'Exports the current Settings to a JSON file.';
    exportSettingsSetting.addEventListener('input', () => void customSettings.exportSettings());
    customSettings.loadSettings();
    let color = 'color: blue';
    if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
        color = 'color: aqua';
    }
    const settingString = `GlobalSettings: ${customSettings.toString()}`;
    console.info(`%c${settingString}`, color);

    exports.exportSettingsSetting = exportSettingsSetting;
    exports.importSettingsSetting = importSettingsSetting;
    exports.loggingSetting = loggingSetting;
    exports.showResetButtonSetting = showResetButtonSetting;

    return exports;

})({});