diff --git a/CHANGELOG.user.md b/CHANGELOG.user.md index 4ee0283..c67f69b 100644 --- a/CHANGELOG.user.md +++ b/CHANGELOG.user.md @@ -1,5 +1,53 @@ # Changelog +**Known Issues:** + +* SVG elements don't load on every webpage: + * _can be blocked by host, external scripts, web extensions, etc._ +* Outlook Website Loading Problem [#70](https://github.com/magicoflolis/Userscript-Plus/issues/70) + * Recommend excluding this userscript on `*.office.com` domains within your userscript manager or switch to the webextension version as it does not injected itself into the page. + * Caused by `Duplicated Default Trusted Types policy`. + +**TODO:** + +* Finish translations. +* Finish blacklist section. +* GitHub engine - filter out UserScript and UserStyles that don't match the host +* Rework TrustedTypePolicy code + +## v7.6.8 + +* Bug fixes: + * ~~Fixed [#70](https://github.com/magicoflolis/Userscript-Plus/issues/70)~~ + +--- + +## v7.6.7 + +* Bug fixes: + * Fixed [#69](https://github.com/magicoflolis/Userscript-Plus/issues/69) + +--- + +## v7.6.6 + +* Config changes: + * `autofetch` is now OFF by default +* Internal changes: + * refactored some code, better readability + * added info icon along with help links +* General changes: + * while `autofetch` is off, clicking the counter for the first time will auto fetch results + * minor bug fixes + +**TODO:** + +* Finish translations. +* Finish blacklist section. +* GitHub engine - filter out UserScript and UserStyles that don't match the host + +--- + ## v7.6.5 * fixed [#67](https://github.com/magicoflolis/Userscript-Plus/issues/67) @@ -51,11 +99,6 @@ * i18n additions (still not localized): * new keys: `code`, `metadata`, `preview_metadata`, `recommend_author`, `recommend_other`, `default_sort` -**Known Issues:** - -* SVG elements don't load on every webpage: - * _can be blocked by host, external scripts, web extensions, etc._ - **TODO:** * Finish translations. @@ -77,7 +120,7 @@ * Config changes, moved `codePreview` value to `preview: code`, recommended resetting your config to clear any old/unused settings. * If you had `Preview Code` enabled, you will need to re-enable it * When `Preview Metadata` and `Preview Code` are both enabled, it will display the entire userscript -* Icon changes, svgs are now from [fontawesome.com](fontawesome.com) +* Icon changes, svgs are now from [fontawesome.com](https://fontawesome.com) * Search engines have been moved to the `Search Engines` section * Improved mobile UI * Improved userscript parsing @@ -92,7 +135,6 @@ * Enabling a engine requires the page to be reloaded, while disabling a engine does not. * This is caused by the cache system. -* SVG elements don't load on every webpage, _can be blocked by the page, external scripts, or web extensions._ * If an item changes after the list is sorted, the sorting won't be updated **TODO:** @@ -112,7 +154,6 @@ * Enabling a engine requires the page to be reloaded, while disabling a engine does not. * This is caused by the cache system. * "Filter out other languages" may not filter _every_ userscript that is not in your language. -* SVG elements don't load on every webpage, _can be blocked by the page, external script, or extensions._ **TODO:** @@ -140,7 +181,6 @@ * Enabling a engine requires the page to be reloaded, while disabling a engine does not. * This is caused by the cache system. * "Filter out other languages" may not filter _every_ userscript that is not in your language. -* SVG elements don't load on every webpage, _can be blocked by the page, external script, or extensions._ **TODO:** @@ -157,13 +197,12 @@ * Added [GreasyFork Bullshit Filter](https://greasyfork.org/scripts/12179) into list. * You can enable it under "List Filters" in the config. * You can also do `filter:` in the search bar to quickly filter the list. - * Full list [here](https://github.com/magicoflolis/Userscript-Plus/tree/master/wiki/README.md) + * Full list here: [Wiki Page](https://github.com/magicoflolis/Userscript-Plus/tree/master/wiki/README.md) * Example: `filter:ascii` => filters out non-ASCII scripts **Known Issues:** * "Filter out other languages" may not filter _every_ userscript that is not in your language. -* SVG elements don't load on every webpage, _can be blocked by the page, external script, or extensions._ **TODO:** @@ -191,7 +230,6 @@ **Known Issues:** * "Filter out other languages" may not filter _every_ userscript that is not in your language. -* SVG elements don't load on every webpage, _can be blocked by the page._ **TODO:** @@ -218,7 +256,6 @@ **Known Issues:** * "Filter out other languages" may not filter _every_ userscript that is not in your language. -* SVG elements don't load on every webpage, _can be blocked by the page._ **TODO:** @@ -250,7 +287,6 @@ **Known Issues:** * "Filter out other languages" may not filter _every_ userscript that is not in your language. -* SVG elements don't load on every webpage, _can be blocked by the page._ **TODO:** diff --git a/README.md b/README.md index 429d657..80923b7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Finds available UserScripts and UserStyles for the current webpage, the power of Save this URL as a bookmark, clicking it will cause the **UserScript version** to inject itself into the current webpage. -```JS +```js filename="src/UserJS/main.js" javascript:(function(){['https://cdn.jsdelivr.net/gh/magicoflolis/Userscript-Plus@master/userscript/dist/magic-userjs.user.js'].map(s=>document.body.appendChild(document.createElement('script')).src=s)})(); ``` diff --git a/UserJS.json b/UserJS.json index a1ea6cc..a814551 100644 --- a/UserJS.json +++ b/UserJS.json @@ -1,5 +1,5 @@ { - "version": "7.6.5", + "version": "7.6.8", "name": "Magic Userscript+ : Show Site All UserJS", "description": "Finds available userscripts for the current webpage.", "author": "Magic ", diff --git a/dist/magic-userjs.meta.js b/dist/magic-userjs.meta.js index 2eceb4b..5924b2f 100644 --- a/dist/magic-userjs.meta.js +++ b/dist/magic-userjs.meta.js @@ -1,5 +1,5 @@ // ==UserScript== -// @version 7.6.5 +// @version 7.6.8 // @name Magic Userscript+ : Show Site All UserJS // @name:ar Magic Userscript+: عرض جميع ملفات UserJS // @name:de Magic Userscript+ : Website anzeigen Alle UserJS diff --git a/dist/magic-userjs.user.js b/dist/magic-userjs.user.js index 66e6efd..ba6c1a6 100644 --- a/dist/magic-userjs.user.js +++ b/dist/magic-userjs.user.js @@ -1,5 +1,5 @@ // ==UserScript== -// @version 7.6.5 +// @version 7.6.8 // @name Magic Userscript+ : Show Site All UserJS // @name:ar Magic Userscript+: عرض جميع ملفات UserJS // @name:de Magic Userscript+ : Website anzeigen Alle UserJS @@ -66,13 +66,11 @@ const inIframe = (() => { try { return window.self !== window.top; - } catch (e) { + } catch { return true; } })(); -if (inIframe) { - return; -} +if (inIframe) return; let userjs = self.userjs; /** * Skip text/plain documents, based on uBlock Origin `vapi.js` file @@ -89,688 +87,692 @@ if ( } else { console.error('[%cMagic Userscript+%c] %cERROR','color: rgb(29, 155, 240);','','color: rgb(249, 24, 128);', `MIME type is not a document, got "${document.contentType || ''}"`); } -if (!(typeof userjs === 'object' && userjs.UserJS)) { - return; +if (!(typeof userjs === 'object' && userjs.UserJS)) return; +{ + /** Native implementation exists */ + const excludePolicy = [ + 'outlook.office.com' + ]; + const hostname = location?.hostname || ''; + if (window.trustedTypes && window.trustedTypes.createPolicy && !hostname.includes(excludePolicy)) window.trustedTypes.createPolicy('default', { createHTML: (string) => string, createScript: (string) => string, createScriptURL: (string) => string }); } -/** Native implementation exists */ -if (window.trustedTypes && window.trustedTypes.createPolicy) window.trustedTypes.createPolicy('default', { createHTML: (string) => string }); /** [i18n directory](https://github.com/magicoflolis/Userscript-Plus/tree/master/src/_locales) */ const translations = { - "ar": { - "createdby": "انشأ من قبل", - "name": "اسم", - "daily_installs": "التثبيت اليومي", - "close": "يغلق", - "filterA": "منقي", - "max": "تحقيق أقصى قدر", - "min": "تصغير", - "search": "يبحث", - "search_placeholder": "بحث في البرامج النصية", - "install": "تثبيت", - "issue": "إصدار جديد", - "version_number": "الإصدار", - "updated": "آخر تحديث", - "total_installs": "إجمالي التثبيت", - "ratings": "التقييمات", - "good": "جيد", - "ok": "جيد", - "bad": "سيء", - "created_date": "تم إنشاؤه", - "redirect": "شوكة دهنية للكبار", - "filter": "تصفية اللغات الأخرى", - "dtime": "عرض المهلة", - "save": "حفظ", - "reset": "إعادة تعيين", - "preview_code": "كود المعاينة", - "saveFile": "احفظ الملف", - "newTab": "علامة تبويب جديدة", - "applies_to": "ينطبق على", - "license": "الترخيص", - "no_license": "لا يوجد", - "antifeatures": "إعلانات", - "userjs_fullscreen": "ملء الشاشة الكاملة التلقائي", - "listing_none": "(لا يوجد)", - "export_config": "تهيئة التصدير", - "export_theme": "تصدير السمة", - "import_config": "استيراد تهيئة الاستيراد", - "import_theme": "استيراد النسق", - "code_size": "حجم الرمز", - "prmpt_css": "التثبيت كأسلوب المستخدم؟", - "userjs_inject": "حقن Userscript+", - "userjs_close": "إغلاق Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Code", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'ar': { + 'createdby': 'انشأ من قبل', + 'name': 'اسم', + 'daily_installs': 'التثبيت اليومي', + 'close': 'يغلق', + 'filterA': 'منقي', + 'max': 'تحقيق أقصى قدر', + 'min': 'تصغير', + 'search': 'يبحث', + 'search_placeholder': 'بحث في البرامج النصية', + 'install': 'تثبيت', + 'issue': 'إصدار جديد', + 'version_number': 'الإصدار', + 'updated': 'آخر تحديث', + 'total_installs': 'إجمالي التثبيت', + 'ratings': 'التقييمات', + 'good': 'جيد', + 'ok': 'جيد', + 'bad': 'سيء', + 'created_date': 'تم إنشاؤه', + 'redirect': 'شوكة دهنية للكبار', + 'filter': 'تصفية اللغات الأخرى', + 'dtime': 'عرض المهلة', + 'save': 'حفظ', + 'reset': 'إعادة تعيين', + 'preview_code': 'كود المعاينة', + 'saveFile': 'احفظ الملف', + 'newTab': 'علامة تبويب جديدة', + 'applies_to': 'ينطبق على', + 'license': 'الترخيص', + 'no_license': 'لا يوجد', + 'antifeatures': 'إعلانات', + 'userjs_fullscreen': 'ملء الشاشة الكاملة التلقائي', + 'listing_none': '(لا يوجد)', + 'export_config': 'تهيئة التصدير', + 'export_theme': 'تصدير السمة', + 'import_config': 'استيراد تهيئة الاستيراد', + 'import_theme': 'استيراد النسق', + 'code_size': 'حجم الرمز', + 'prmpt_css': 'التثبيت كأسلوب المستخدم؟', + 'userjs_inject': 'حقن Userscript+', + 'userjs_close': 'إغلاق Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Code', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "de": { - "createdby": "Erstellt von", - "name": "Name", - "daily_installs": "Tägliche Installationen", - "close": "Schließen Sie", - "filterA": "Filter", - "max": "Maximieren Sie", - "min": "minimieren", - "search": "Suche", - "search_placeholder": "Suche nach Userscripts", - "install": "Installieren Sie", - "issue": "Neue Ausgabe", - "version_number": "Version", - "updated": "Zuletzt aktualisiert", - "total_installs": "Installationen insgesamt", - "ratings": "Bewertungen", - "good": "Gut", - "ok": "Okay", - "bad": "Schlecht", - "created_date": "Erstellt", - "redirect": "Greasy Fork für Erwachsene", - "filter": "Andere Sprachen herausfiltern", - "dtime": "Zeitüberschreitung anzeigen", - "save": "Speichern Sie", - "reset": "Zurücksetzen", - "preview_code": "Vorschau Code", - "saveFile": "Datei speichern", - "newTab": "Neue Registerkarte", - "applies_to": "Gilt für", - "license": "Lizenz", - "no_license": "N/A", - "antifeatures": "Antifeatures", - "userjs_fullscreen": "Automatischer Vollbildmodus", - "listing_none": "(Keine)", - "export_config": "Konfig exportieren", - "export_theme": "Thema exportieren", - "import_config": "Konfig importieren", - "import_theme": "Thema importieren", - "code_size": "Code Größe", - "prmpt_css": "Als UserStyle installieren?", - "userjs_inject": "Userscript+ einfügen", - "userjs_close": "Userscript+ schließen", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Quelltext", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'de': { + 'createdby': 'Erstellt von', + 'name': 'Name', + 'daily_installs': 'Tägliche Installationen', + 'close': 'Schließen Sie', + 'filterA': 'Filter', + 'max': 'Maximieren Sie', + 'min': 'minimieren', + 'search': 'Suche', + 'search_placeholder': 'Suche nach Userscripts', + 'install': 'Installieren Sie', + 'issue': 'Neue Ausgabe', + 'version_number': 'Version', + 'updated': 'Zuletzt aktualisiert', + 'total_installs': 'Installationen insgesamt', + 'ratings': 'Bewertungen', + 'good': 'Gut', + 'ok': 'Okay', + 'bad': 'Schlecht', + 'created_date': 'Erstellt', + 'redirect': 'Greasy Fork für Erwachsene', + 'filter': 'Andere Sprachen herausfiltern', + 'dtime': 'Zeitüberschreitung anzeigen', + 'save': 'Speichern Sie', + 'reset': 'Zurücksetzen', + 'preview_code': 'Vorschau Code', + 'saveFile': 'Datei speichern', + 'newTab': 'Neue Registerkarte', + 'applies_to': 'Gilt für', + 'license': 'Lizenz', + 'no_license': 'N/A', + 'antifeatures': 'Antifeatures', + 'userjs_fullscreen': 'Automatischer Vollbildmodus', + 'listing_none': '(Keine)', + 'export_config': 'Konfig exportieren', + 'export_theme': 'Thema exportieren', + 'import_config': 'Konfig importieren', + 'import_theme': 'Thema importieren', + 'code_size': 'Code Größe', + 'prmpt_css': 'Als UserStyle installieren?', + 'userjs_inject': 'Userscript+ einfügen', + 'userjs_close': 'Userscript+ schließen', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Quelltext', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "en": { - "createdby": "Created by", - "name": "Name", - "daily_installs": "Daily Installs", - "close": "Close", - "filterA": "Filter", - "max": "Maximize", - "min": "Minimize", - "search": "Search", - "search_placeholder": "Search for userscripts", - "install": "Install", - "issue": "New Issue", - "version_number": "Version", - "updated": "Last Updated", - "total_installs": "Total Installs", - "ratings": "Ratings", - "good": "Good", - "ok": "Okay", - "bad": "Bad", - "created_date": "Created", - "redirect": "Greasy Fork for adults", - "filter": "Filter out other languages", - "dtime": "Display Timeout", - "save": "Save", - "reset": "Reset", - "preview_code": "Preview Code", - "saveFile": "Download", - "newTab": "New Tab", - "applies_to": "Applies to", - "license": "License", - "no_license": "N/A", - "antifeatures": "Antifeatures", - "userjs_fullscreen": "Automatic Fullscreen", - "listing_none": "(None)", - "export_config": "Export Config", - "export_theme": "Export Theme", - "import_config": "Import Config", - "import_theme": "Import Theme", - "code_size": "Code Size", - "prmpt_css": "Install as UserStyle?", - "userjs_inject": "Inject Userscript+", - "userjs_close": "Close Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Code", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'en': { + 'createdby': 'Created by', + 'name': 'Name', + 'daily_installs': 'Daily Installs', + 'close': 'Close', + 'filterA': 'Filter', + 'max': 'Maximize', + 'min': 'Minimize', + 'search': 'Search', + 'search_placeholder': 'Search for userscripts', + 'install': 'Install', + 'issue': 'New Issue', + 'version_number': 'Version', + 'updated': 'Last Updated', + 'total_installs': 'Total Installs', + 'ratings': 'Ratings', + 'good': 'Good', + 'ok': 'Okay', + 'bad': 'Bad', + 'created_date': 'Created', + 'redirect': 'Greasy Fork for adults', + 'filter': 'Filter out other languages', + 'dtime': 'Display Timeout', + 'save': 'Save', + 'reset': 'Reset', + 'preview_code': 'Preview Code', + 'saveFile': 'Download', + 'newTab': 'New Tab', + 'applies_to': 'Applies to', + 'license': 'License', + 'no_license': 'N/A', + 'antifeatures': 'Antifeatures', + 'userjs_fullscreen': 'Automatic Fullscreen', + 'listing_none': '(None)', + 'export_config': 'Export Config', + 'export_theme': 'Export Theme', + 'import_config': 'Import Config', + 'import_theme': 'Import Theme', + 'code_size': 'Code Size', + 'prmpt_css': 'Install as UserStyle?', + 'userjs_inject': 'Inject Userscript+', + 'userjs_close': 'Close Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Code', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "en_GB": { - "createdby": "Created by", - "name": "Name", - "daily_installs": "Daily Installs", - "close": "Close", - "filterA": "Filter", - "max": "Maximize", - "min": "Minimize", - "search": "Search", - "search_placeholder": "Search for userscripts", - "install": "Install", - "issue": "New Issue", - "version_number": "Version", - "updated": "Last Updated", - "total_installs": "Total Installs", - "ratings": "Ratings", - "good": "Good", - "ok": "Okay", - "bad": "Bad", - "created_date": "Created", - "redirect": "Greasy Fork for adults", - "filter": "Filter out other languages", - "dtime": "Display Timeout", - "save": "Save", - "reset": "Reset", - "preview_code": "Preview Code", - "saveFile": "Download", - "newTab": "New Tab", - "applies_to": "Applies to", - "license": "License", - "no_license": "N/A", - "antifeatures": "Antifeatures", - "userjs_fullscreen": "Automatic Fullscreen", - "listing_none": "(None)", - "export_config": "Export Config", - "export_theme": "Export Theme", - "import_config": "Import Config", - "import_theme": "Import Theme", - "code_size": "Code Size", - "prmpt_css": "Install as UserStyle?", - "userjs_inject": "Inject Userscript+", - "userjs_close": "Close Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Code", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'en_GB': { + 'createdby': 'Created by', + 'name': 'Name', + 'daily_installs': 'Daily Installs', + 'close': 'Close', + 'filterA': 'Filter', + 'max': 'Maximize', + 'min': 'Minimize', + 'search': 'Search', + 'search_placeholder': 'Search for userscripts', + 'install': 'Install', + 'issue': 'New Issue', + 'version_number': 'Version', + 'updated': 'Last Updated', + 'total_installs': 'Total Installs', + 'ratings': 'Ratings', + 'good': 'Good', + 'ok': 'Okay', + 'bad': 'Bad', + 'created_date': 'Created', + 'redirect': 'Greasy Fork for adults', + 'filter': 'Filter out other languages', + 'dtime': 'Display Timeout', + 'save': 'Save', + 'reset': 'Reset', + 'preview_code': 'Preview Code', + 'saveFile': 'Download', + 'newTab': 'New Tab', + 'applies_to': 'Applies to', + 'license': 'License', + 'no_license': 'N/A', + 'antifeatures': 'Antifeatures', + 'userjs_fullscreen': 'Automatic Fullscreen', + 'listing_none': '(None)', + 'export_config': 'Export Config', + 'export_theme': 'Export Theme', + 'import_config': 'Import Config', + 'import_theme': 'Import Theme', + 'code_size': 'Code Size', + 'prmpt_css': 'Install as UserStyle?', + 'userjs_inject': 'Inject Userscript+', + 'userjs_close': 'Close Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Code', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "es": { - "createdby": "Creado por", - "name": "Nombre", - "daily_installs": "Instalaciones diarias", - "close": "Ya no se muestra", - "filterA": "Filtro", - "max": "Maximizar", - "min": "Minimizar", - "search": "Busque en", - "search_placeholder": "Buscar userscripts", - "install": "Instalar", - "issue": "Nueva edición", - "version_number": "Versión", - "updated": "Última actualización", - "total_installs": "Total de instalaciones", - "ratings": "Clasificaciones", - "good": "Bueno", - "ok": "Ok", - "bad": "Malo", - "created_date": "Creado", - "redirect": "Greasy Fork para adultos", - "filter": "Filtrar otros idiomas", - "dtime": "Mostrar el tiempo de espera", - "save": "Guardar", - "reset": "Reiniciar", - "preview_code": "Vista previa del código", - "saveFile": "Guardar archivo", - "newTab": "Guardar archivo", - "applies_to": "Se aplica a", - "license": "Licencia", - "no_license": "Desconocida", - "antifeatures": "Características indeseables", - "userjs_fullscreen": "Pantalla completa automática", - "listing_none": "(Ninguno)", - "export_config": "Exportar configuración", - "export_theme": "Exportar tema", - "import_config": "Importar configuración", - "import_theme": "Importar tema", - "code_size": "Código Tamaño", - "prmpt_css": "¿Instalar como UserStyle?", - "userjs_inject": "Inyectar Userscript+", - "userjs_close": "Cerrar Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Código", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'es': { + 'createdby': 'Creado por', + 'name': 'Nombre', + 'daily_installs': 'Instalaciones diarias', + 'close': 'Ya no se muestra', + 'filterA': 'Filtro', + 'max': 'Maximizar', + 'min': 'Minimizar', + 'search': 'Busque en', + 'search_placeholder': 'Buscar userscripts', + 'install': 'Instalar', + 'issue': 'Nueva edición', + 'version_number': 'Versión', + 'updated': 'Última actualización', + 'total_installs': 'Total de instalaciones', + 'ratings': 'Clasificaciones', + 'good': 'Bueno', + 'ok': 'Ok', + 'bad': 'Malo', + 'created_date': 'Creado', + 'redirect': 'Greasy Fork para adultos', + 'filter': 'Filtrar otros idiomas', + 'dtime': 'Mostrar el tiempo de espera', + 'save': 'Guardar', + 'reset': 'Reiniciar', + 'preview_code': 'Vista previa del código', + 'saveFile': 'Guardar archivo', + 'newTab': 'Guardar archivo', + 'applies_to': 'Se aplica a', + 'license': 'Licencia', + 'no_license': 'Desconocida', + 'antifeatures': 'Características indeseables', + 'userjs_fullscreen': 'Pantalla completa automática', + 'listing_none': '(Ninguno)', + 'export_config': 'Exportar configuración', + 'export_theme': 'Exportar tema', + 'import_config': 'Importar configuración', + 'import_theme': 'Importar tema', + 'code_size': 'Código Tamaño', + 'prmpt_css': '¿Instalar como UserStyle?', + 'userjs_inject': 'Inyectar Userscript+', + 'userjs_close': 'Cerrar Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Código', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "fr": { - "createdby": "Créé par", - "name": "Nom", - "daily_installs": "Installations quotidiennes", - "close": "Ne plus montrer", - "filterA": "Filtre", - "max": "Maximiser", - "min": "Minimiser", - "search": "Recherche", - "search_placeholder": "Rechercher des userscripts", - "install": "Installer", - "issue": "Nouveau numéro", - "version_number": "Version", - "updated": "Dernière mise à jour", - "total_installs": "Total des installations", - "ratings": "Notations", - "good": "Bon", - "ok": "Ok", - "bad": "Mauvais", - "created_date": "Créé", - "redirect": "Greasy Fork pour les adultes", - "filter": "Filtrer les autres langues", - "dtime": "Délai d'affichage", - "save": "Sauvez", - "reset": "Réinitialiser", - "preview_code": "Prévisualiser le code", - "saveFile": "Enregistrer le fichier", - "newTab": "Nouvel onglet", - "applies_to": "S'applique à", - "license": "Licence", - "no_license": "N/A", - "antifeatures": "Antifeatures", - "userjs_fullscreen": "Plein écran automatique", - "listing_none": "(Aucun)", - "export_config": "Export Config", - "export_theme": "Exporter le thème", - "import_config": "Importer la configuration", - "import_theme": "Importer le thème", - "code_size": "Code Taille", - "prmpt_css": "Installer comme UserStyle ?", - "userjs_inject": "Injecter Userscript+", - "userjs_close": "Fermer Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Code", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'fr': { + 'createdby': 'Créé par', + 'name': 'Nom', + 'daily_installs': 'Installations quotidiennes', + 'close': 'Ne plus montrer', + 'filterA': 'Filtre', + 'max': 'Maximiser', + 'min': 'Minimiser', + 'search': 'Recherche', + 'search_placeholder': 'Rechercher des userscripts', + 'install': 'Installer', + 'issue': 'Nouveau numéro', + 'version_number': 'Version', + 'updated': 'Dernière mise à jour', + 'total_installs': 'Total des installations', + 'ratings': 'Notations', + 'good': 'Bon', + 'ok': 'Ok', + 'bad': 'Mauvais', + 'created_date': 'Créé', + 'redirect': 'Greasy Fork pour les adultes', + 'filter': 'Filtrer les autres langues', + 'dtime': "Délai d'affichage", + 'save': 'Sauvez', + 'reset': 'Réinitialiser', + 'preview_code': 'Prévisualiser le code', + 'saveFile': 'Enregistrer le fichier', + 'newTab': 'Nouvel onglet', + 'applies_to': "S'applique à", + 'license': 'Licence', + 'no_license': 'N/A', + 'antifeatures': 'Antifeatures', + 'userjs_fullscreen': 'Plein écran automatique', + 'listing_none': '(Aucun)', + 'export_config': 'Export Config', + 'export_theme': 'Exporter le thème', + 'import_config': 'Importer la configuration', + 'import_theme': 'Importer le thème', + 'code_size': 'Code Taille', + 'prmpt_css': 'Installer comme UserStyle ?', + 'userjs_inject': 'Injecter Userscript+', + 'userjs_close': 'Fermer Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Code', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "ja": { - "createdby": "によって作成された", - "name": "名前", - "daily_installs": "デイリーインストール", - "close": "表示されなくなりました", - "filterA": "フィルター", - "max": "最大化", - "min": "ミニマム", - "search": "検索", - "search_placeholder": "ユーザースクリプトの検索", - "install": "インストール", - "issue": "新刊のご案内", - "version_number": "バージョン", - "updated": "最終更新日", - "total_installs": "総インストール数", - "ratings": "レーティング", - "good": "グッド", - "ok": "良い", - "bad": "悪い", - "created_date": "作成", - "redirect": "大人のGreasyfork", - "filter": "他の言語をフィルタリングする", - "dtime": "表示タイムアウト", - "save": "拯救", - "reset": "リセット", - "preview_code": "コードのプレビュー", - "saveFile": "ファイルを保存", - "newTab": "新しいタブ", - "applies_to": "適用対象", - "license": "ライセンス", - "no_license": "不明", - "antifeatures": "アンチ機能", - "userjs_fullscreen": "自動フルスクリーン", - "listing_none": "(なし)", - "export_config": "エクスポート設定", - "export_theme": "テーマのエクスポート", - "import_config": "設定のインポート", - "import_theme": "テーマのインポート", - "code_size": "コード・サイズ", - "prmpt_css": "UserStyleとしてインストールしますか?", - "userjs_inject": "Userscript+ を挿入", - "userjs_close": "Userscript+ を閉じる", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "コード", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'ja': { + 'createdby': 'によって作成された', + 'name': '名前', + 'daily_installs': 'デイリーインストール', + 'close': '表示されなくなりました', + 'filterA': 'フィルター', + 'max': '最大化', + 'min': 'ミニマム', + 'search': '検索', + 'search_placeholder': 'ユーザースクリプトの検索', + 'install': 'インストール', + 'issue': '新刊のご案内', + 'version_number': 'バージョン', + 'updated': '最終更新日', + 'total_installs': '総インストール数', + 'ratings': 'レーティング', + 'good': 'グッド', + 'ok': '良い', + 'bad': '悪い', + 'created_date': '作成', + 'redirect': '大人のGreasyfork', + 'filter': '他の言語をフィルタリングする', + 'dtime': '表示タイムアウト', + 'save': '拯救', + 'reset': 'リセット', + 'preview_code': 'コードのプレビュー', + 'saveFile': 'ファイルを保存', + 'newTab': '新しいタブ', + 'applies_to': '適用対象', + 'license': 'ライセンス', + 'no_license': '不明', + 'antifeatures': 'アンチ機能', + 'userjs_fullscreen': '自動フルスクリーン', + 'listing_none': '(なし)', + 'export_config': 'エクスポート設定', + 'export_theme': 'テーマのエクスポート', + 'import_config': '設定のインポート', + 'import_theme': 'テーマのインポート', + 'code_size': 'コード・サイズ', + 'prmpt_css': 'UserStyleとしてインストールしますか?', + 'userjs_inject': 'Userscript+ を挿入', + 'userjs_close': 'Userscript+ を閉じる', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'コード', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "nl": { - "createdby": "Gemaakt door", - "name": "Naam", - "daily_installs": "Dagelijkse Installaties", - "close": "Sluit", - "filterA": "Filter", - "max": "Maximaliseer", - "min": "Minimaliseer", - "search": "Zoek", - "search_placeholder": "Zoeken naar gebruikersscripts", - "install": "Installeer", - "issue": "Nieuw Issue", - "version_number": "Versie", - "updated": "Laatste Update", - "total_installs": "Totale Installaties", - "ratings": "Beoordeling", - "good": "Goed", - "ok": "Ok", - "bad": "Slecht", - "created_date": "Aangemaakt", - "redirect": "Greasy Fork voor volwassenen", - "filter": "Filter andere talen", - "dtime": "Weergave timeout", - "save": "Opslaan", - "reset": "Opnieuw instellen", - "preview_code": "Voorbeeldcode", - "saveFile": "Bestand opslaan", - "newTab": "Nieuw tabblad", - "applies_to": "Geldt voor", - "license": "Licentie", - "no_license": "N.v.t.", - "antifeatures": "Functies voor eigen gewin", - "userjs_fullscreen": "Automatisch volledig scherm", - "listing_none": "(Geen)", - "export_config": "Configuratie exporteren", - "export_theme": "Thema exporteren", - "import_config": "Configuratie importeren", - "import_theme": "Thema importeren", - "code_size": "Code Grootte", - "prmpt_css": "Installeren als UserStyle?", - "userjs_inject": "Injecteer Userscript+", - "userjs_close": "Sluit Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Code", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'nl': { + 'createdby': 'Gemaakt door', + 'name': 'Naam', + 'daily_installs': 'Dagelijkse Installaties', + 'close': 'Sluit', + 'filterA': 'Filter', + 'max': 'Maximaliseer', + 'min': 'Minimaliseer', + 'search': 'Zoek', + 'search_placeholder': 'Zoeken naar gebruikersscripts', + 'install': 'Installeer', + 'issue': 'Nieuw Issue', + 'version_number': 'Versie', + 'updated': 'Laatste Update', + 'total_installs': 'Totale Installaties', + 'ratings': 'Beoordeling', + 'good': 'Goed', + 'ok': 'Ok', + 'bad': 'Slecht', + 'created_date': 'Aangemaakt', + 'redirect': 'Greasy Fork voor volwassenen', + 'filter': 'Filter andere talen', + 'dtime': 'Weergave timeout', + 'save': 'Opslaan', + 'reset': 'Opnieuw instellen', + 'preview_code': 'Voorbeeldcode', + 'saveFile': 'Bestand opslaan', + 'newTab': 'Nieuw tabblad', + 'applies_to': 'Geldt voor', + 'license': 'Licentie', + 'no_license': 'N.v.t.', + 'antifeatures': 'Functies voor eigen gewin', + 'userjs_fullscreen': 'Automatisch volledig scherm', + 'listing_none': '(Geen)', + 'export_config': 'Configuratie exporteren', + 'export_theme': 'Thema exporteren', + 'import_config': 'Configuratie importeren', + 'import_theme': 'Thema importeren', + 'code_size': 'Code Grootte', + 'prmpt_css': 'Installeren als UserStyle?', + 'userjs_inject': 'Injecteer Userscript+', + 'userjs_close': 'Sluit Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Code', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "pl": { - "createdby": "Stworzony przez", - "name": "Nazwa", - "daily_installs": "Codzienne instalacje", - "close": "Zamknij", - "filterA": "Filtr", - "max": "Maksymalizuj", - "min": "Minimalizuj", - "search": "Wyszukiwanie", - "search_placeholder": "Wyszukiwanie skryptów użytkownika", - "install": "Instalacja", - "issue": "Nowy numer", - "version_number": "Wersja", - "updated": "Ostatnia aktualizacja", - "total_installs": "Łączna liczba instalacji", - "ratings": "Oceny", - "good": "Dobry", - "ok": "Ok", - "bad": "Zły", - "created_date": "Utworzony", - "redirect": "Greasy Fork dla dorosłych", - "filter": "Odfiltruj inne języki", - "dtime": "Limit czasu wyświetlania", - "save": "Zapisz", - "reset": "Reset", - "preview_code": "Kod podglądu", - "saveFile": "Zapisz plik", - "newTab": "Nowa karta", - "applies_to": "Dotyczy", - "license": "Licencja", - "no_license": "N/A", - "antifeatures": "Antywzorce", - "userjs_fullscreen": "Automatyczny pełny ekran", - "listing_none": "(Brak)", - "export_config": "Konfiguracja eksportu", - "export_theme": "Motyw eksportu", - "import_config": "Importuj konfigurację", - "import_theme": "Importuj motyw", - "code_size": "Kod Rozmiar", - "prmpt_css": "Zainstalować jako UserStyle?", - "userjs_inject": "Wstrzyknij Userscript+", - "userjs_close": "Zamknij Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Kod", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'pl': { + 'createdby': 'Stworzony przez', + 'name': 'Nazwa', + 'daily_installs': 'Codzienne instalacje', + 'close': 'Zamknij', + 'filterA': 'Filtr', + 'max': 'Maksymalizuj', + 'min': 'Minimalizuj', + 'search': 'Wyszukiwanie', + 'search_placeholder': 'Wyszukiwanie skryptów użytkownika', + 'install': 'Instalacja', + 'issue': 'Nowy numer', + 'version_number': 'Wersja', + 'updated': 'Ostatnia aktualizacja', + 'total_installs': 'Łączna liczba instalacji', + 'ratings': 'Oceny', + 'good': 'Dobry', + 'ok': 'Ok', + 'bad': 'Zły', + 'created_date': 'Utworzony', + 'redirect': 'Greasy Fork dla dorosłych', + 'filter': 'Odfiltruj inne języki', + 'dtime': 'Limit czasu wyświetlania', + 'save': 'Zapisz', + 'reset': 'Reset', + 'preview_code': 'Kod podglądu', + 'saveFile': 'Zapisz plik', + 'newTab': 'Nowa karta', + 'applies_to': 'Dotyczy', + 'license': 'Licencja', + 'no_license': 'N/A', + 'antifeatures': 'Antywzorce', + 'userjs_fullscreen': 'Automatyczny pełny ekran', + 'listing_none': '(Brak)', + 'export_config': 'Konfiguracja eksportu', + 'export_theme': 'Motyw eksportu', + 'import_config': 'Importuj konfigurację', + 'import_theme': 'Importuj motyw', + 'code_size': 'Kod Rozmiar', + 'prmpt_css': 'Zainstalować jako UserStyle?', + 'userjs_inject': 'Wstrzyknij Userscript+', + 'userjs_close': 'Zamknij Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Kod', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "ru": { - "createdby": "Сделано", - "name": "Имя", - "daily_installs": "Ежедневные установки", - "close": "Больше не показывать", - "filterA": "Фильтр", - "max": "Максимизировать", - "min": "Минимизировать", - "search": "Поиск", - "search_placeholder": "Поиск юзерскриптов", - "install": "Установите", - "issue": "Новый выпуск", - "version_number": "Версия", - "updated": "Последнее обновление", - "total_installs": "Всего установок", - "ratings": "Рейтинги", - "good": "Хорошо", - "ok": "Хорошо", - "bad": "Плохо", - "created_date": "Создано", - "redirect": "Greasy Fork для взрослых", - "filter": "Отфильтровать другие языки", - "dtime": "Тайм-аут отображения", - "save": "Сохранить", - "reset": "Перезагрузить", - "preview_code": "Предварительный просмотр кода", - "saveFile": "Сохранить файл", - "newTab": "Новая вкладка", - "applies_to": "Применяется к", - "license": "Лицензия", - "no_license": "Недоступно", - "antifeatures": "Нежелательная функциональность", - "userjs_fullscreen": "Автоматический полноэкранный режим", - "listing_none": "(нет)", - "export_config": "Экспорт конфигурации", - "export_theme": "Экспорт темы", - "import_config": "Импорт конфигурации", - "import_theme": "Импортировать тему", - "code_size": "Код Размер", - "prmpt_css": "Установить как UserStyle?", - "userjs_inject": "Вставить Userscript+", - "userjs_close": "Закрыть Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "Исходный код", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'ru': { + 'createdby': 'Сделано', + 'name': 'Имя', + 'daily_installs': 'Ежедневные установки', + 'close': 'Больше не показывать', + 'filterA': 'Фильтр', + 'max': 'Максимизировать', + 'min': 'Минимизировать', + 'search': 'Поиск', + 'search_placeholder': 'Поиск юзерскриптов', + 'install': 'Установите', + 'issue': 'Новый выпуск', + 'version_number': 'Версия', + 'updated': 'Последнее обновление', + 'total_installs': 'Всего установок', + 'ratings': 'Рейтинги', + 'good': 'Хорошо', + 'ok': 'Хорошо', + 'bad': 'Плохо', + 'created_date': 'Создано', + 'redirect': 'Greasy Fork для взрослых', + 'filter': 'Отфильтровать другие языки', + 'dtime': 'Тайм-аут отображения', + 'save': 'Сохранить', + 'reset': 'Перезагрузить', + 'preview_code': 'Предварительный просмотр кода', + 'saveFile': 'Сохранить файл', + 'newTab': 'Новая вкладка', + 'applies_to': 'Применяется к', + 'license': 'Лицензия', + 'no_license': 'Недоступно', + 'antifeatures': 'Нежелательная функциональность', + 'userjs_fullscreen': 'Автоматический полноэкранный режим', + 'listing_none': '(нет)', + 'export_config': 'Экспорт конфигурации', + 'export_theme': 'Экспорт темы', + 'import_config': 'Импорт конфигурации', + 'import_theme': 'Импортировать тему', + 'code_size': 'Код Размер', + 'prmpt_css': 'Установить как UserStyle?', + 'userjs_inject': 'Вставить Userscript+', + 'userjs_close': 'Закрыть Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': 'Исходный код', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "zh": { - "createdby": "由...制作", - "name": "姓名", - "daily_installs": "日常安装", - "close": "不再显示", - "filterA": "过滤器", - "max": "最大化", - "min": "最小化", - "search": "搜索", - "search_placeholder": "搜索用户脚本", - "install": "安装", - "issue": "新问题", - "version_number": "版本", - "updated": "最后更新", - "total_installs": "总安装量", - "ratings": "评级", - "good": "好的", - "ok": "好的", - "bad": "不好", - "created_date": "创建", - "redirect": "大人的Greasyfork", - "filter": "过滤掉其他语言", - "dtime": "显示超时", - "save": "拯救", - "reset": "重置", - "preview_code": "预览代码", - "saveFile": "保存存档", - "newTab": "新标签", - "applies_to": "适用于", - "license": "许可证", - "no_license": "暂无", - "antifeatures": "可能不受欢迎的功能", - "userjs_fullscreen": "自动全屏", - "listing_none": "(无)", - "export_config": "导出配置", - "export_theme": "导出主题", - "import_config": "导入配置", - "import_theme": "导入主题", - "code_size": "代码 尺寸", - "prmpt_css": "安装为用户风格?", - "userjs_inject": "注入 Userscript+", - "userjs_close": "关闭 Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "代码", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'zh': { + 'createdby': '由...制作', + 'name': '姓名', + 'daily_installs': '日常安装', + 'close': '不再显示', + 'filterA': '过滤器', + 'max': '最大化', + 'min': '最小化', + 'search': '搜索', + 'search_placeholder': '搜索用户脚本', + 'install': '安装', + 'issue': '新问题', + 'version_number': '版本', + 'updated': '最后更新', + 'total_installs': '总安装量', + 'ratings': '评级', + 'good': '好的', + 'ok': '好的', + 'bad': '不好', + 'created_date': '创建', + 'redirect': '大人的Greasyfork', + 'filter': '过滤掉其他语言', + 'dtime': '显示超时', + 'save': '拯救', + 'reset': '重置', + 'preview_code': '预览代码', + 'saveFile': '保存存档', + 'newTab': '新标签', + 'applies_to': '适用于', + 'license': '许可证', + 'no_license': '暂无', + 'antifeatures': '可能不受欢迎的功能', + 'userjs_fullscreen': '自动全屏', + 'listing_none': '(无)', + 'export_config': '导出配置', + 'export_theme': '导出主题', + 'import_config': '导入配置', + 'import_theme': '导入主题', + 'code_size': '代码 尺寸', + 'prmpt_css': '安装为用户风格?', + 'userjs_inject': '注入 Userscript+', + 'userjs_close': '关闭 Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': '代码', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "zh_CN": { - "createdby": "由...制作", - "name": "姓名", - "daily_installs": "日常安装", - "close": "不再显示", - "filterA": "过滤器", - "max": "最大化", - "min": "最小化", - "search": "搜索", - "search_placeholder": "搜索用户脚本", - "install": "安装", - "issue": "新问题", - "version_number": "版本", - "updated": "最后更新", - "total_installs": "总安装量", - "ratings": "评级", - "good": "好的", - "ok": "好的", - "bad": "不好", - "created_date": "创建", - "redirect": "大人的Greasyfork", - "filter": "过滤掉其他语言", - "dtime": "显示超时", - "save": "拯救", - "reset": "重置", - "preview_code": "预览代码", - "saveFile": "保存存档", - "newTab": "新标签", - "applies_to": "适用于", - "license": "许可证", - "no_license": "暂无", - "antifeatures": "可能不受欢迎的功能", - "userjs_fullscreen": "自动全屏", - "listing_none": "(无)", - "export_config": "导出配置", - "export_theme": "导出主题", - "import_config": "导入配置", - "import_theme": "导入主题", - "code_size": "代码 尺寸", - "prmpt_css": "安装为用户风格?", - "userjs_inject": "注入 Userscript+", - "userjs_close": "关闭 Userscript+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "代码", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'zh_CN': { + 'createdby': '由...制作', + 'name': '姓名', + 'daily_installs': '日常安装', + 'close': '不再显示', + 'filterA': '过滤器', + 'max': '最大化', + 'min': '最小化', + 'search': '搜索', + 'search_placeholder': '搜索用户脚本', + 'install': '安装', + 'issue': '新问题', + 'version_number': '版本', + 'updated': '最后更新', + 'total_installs': '总安装量', + 'ratings': '评级', + 'good': '好的', + 'ok': '好的', + 'bad': '不好', + 'created_date': '创建', + 'redirect': '大人的Greasyfork', + 'filter': '过滤掉其他语言', + 'dtime': '显示超时', + 'save': '拯救', + 'reset': '重置', + 'preview_code': '预览代码', + 'saveFile': '保存存档', + 'newTab': '新标签', + 'applies_to': '适用于', + 'license': '许可证', + 'no_license': '暂无', + 'antifeatures': '可能不受欢迎的功能', + 'userjs_fullscreen': '自动全屏', + 'listing_none': '(无)', + 'export_config': '导出配置', + 'export_theme': '导出主题', + 'import_config': '导入配置', + 'import_theme': '导入主题', + 'code_size': '代码 尺寸', + 'prmpt_css': '安装为用户风格?', + 'userjs_inject': '注入 Userscript+', + 'userjs_close': '关闭 Userscript+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': '代码', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' }, - "zh_TW": { - "createdby": "由...制作", - "name": "姓名", - "daily_installs": "日常安装", - "close": "不再显示", - "filterA": "过滤器", - "max": "最大化", - "min": "最小化", - "search": "搜索", - "search_placeholder": "搜索用户脚本", - "install": "安装", - "issue": "新问题", - "version_number": "版本", - "updated": "最后更新", - "total_installs": "总安装量", - "ratings": "评级", - "good": "好的", - "ok": "好的", - "bad": "不好", - "created_date": "创建", - "redirect": "大人的Greasyfork", - "filter": "过滤掉其他语言", - "dtime": "显示超时", - "save": "拯救", - "reset": "重置", - "preview_code": "预览代码", - "saveFile": "保存存档", - "newTab": "新标签", - "applies_to": "适用于", - "license": "许可证", - "no_license": "暂无", - "antifeatures": "可能不受欢迎的功能", - "userjs_fullscreen": "自动全屏", - "listing_none": "(无)", - "export_config": "导出配置", - "export_theme": "导出主题", - "import_config": "导入配置", - "import_theme": "导入主题", - "code_size": "代码 尺寸", - "prmpt_css": "作為使用者樣式安裝?", - "userjs_inject": "注入用戶腳本+", - "userjs_close": "關閉用戶腳本+", - "userjs_sync": "Sync", - "userjs_autoinject": "Inject on load", - "auto_fetch": "Fetch on load", - "code": "代碼", - "metadata": "Metadata", - "preview_metadata": "Preview Metadata", - "recommend_author": "Recommend Author", - "recommend_other": "Recommend Others", - "default_sort": "Default Sort" + 'zh_TW': { + 'createdby': '由...制作', + 'name': '姓名', + 'daily_installs': '日常安装', + 'close': '不再显示', + 'filterA': '过滤器', + 'max': '最大化', + 'min': '最小化', + 'search': '搜索', + 'search_placeholder': '搜索用户脚本', + 'install': '安装', + 'issue': '新问题', + 'version_number': '版本', + 'updated': '最后更新', + 'total_installs': '总安装量', + 'ratings': '评级', + 'good': '好的', + 'ok': '好的', + 'bad': '不好', + 'created_date': '创建', + 'redirect': '大人的Greasyfork', + 'filter': '过滤掉其他语言', + 'dtime': '显示超时', + 'save': '拯救', + 'reset': '重置', + 'preview_code': '预览代码', + 'saveFile': '保存存档', + 'newTab': '新标签', + 'applies_to': '适用于', + 'license': '许可证', + 'no_license': '暂无', + 'antifeatures': '可能不受欢迎的功能', + 'userjs_fullscreen': '自动全屏', + 'listing_none': '(无)', + 'export_config': '导出配置', + 'export_theme': '导出主题', + 'import_config': '导入配置', + 'import_theme': '导入主题', + 'code_size': '代码 尺寸', + 'prmpt_css': '作為使用者樣式安裝?', + 'userjs_inject': '注入用戶腳本+', + 'userjs_close': '關閉用戶腳本+', + 'userjs_sync': 'Sync', + 'userjs_autoinject': 'Inject on load', + 'auto_fetch': 'Fetch on load', + 'code': '代碼', + 'metadata': 'Metadata', + 'preview_metadata': 'Preview Metadata', + 'recommend_author': 'Recommend Author', + 'recommend_other': 'Recommend Others', + 'default_sort': 'Default Sort' } }; /** [source code](https://github.com/magicoflolis/Userscript-Plus/blob/master/src/sass/_main.scss) */ @@ -1546,6 +1548,12 @@ mujs-body table .frame [data-name=license] { .mujs-cfg mujs-section[data-name=theme] select option:hover { background: var(--mujs-even-row, hsl(222, 14%, 18%)) !important; } +.mujs-cfg mujs-section svg { + width: 14px; + height: 14px; + fill: currentColor; + background: transparent; +} .mujs-cfg mujs-section[data-name=exp], .mujs-cfg mujs-section[data-name=blacklist] { display: flex; justify-content: space-between; @@ -1793,21 +1801,33 @@ const con = { }; const { err, info } = con; // #endregion - /** * @type { import("../typings/types.d.ts").config } */ let cfg; - +/** + * @type {URL | undefined} + */ +let url; +try { + // for some reason `window.location.href` isn't always the same as `location.href` + url = new URL(window.location.href ?? BLANK_PAGE); +} catch { + /* empty */ +} +//#region Placeholders const BLANK_FN = function () {}; const BLANK_ASYNC_FN = async function () {}; const BLANK_PAGE = 'about:blank'; +//#endregion /** - * @param {string} hn + * @template {string} S + * @param {S} hn */ const normalizedHostname = (hn) => hn.replace(/^www\./, ''); /** - * @param {string} txt + * @template {string} S + * @param {S} txt */ const formatURL = (txt) => txt @@ -1816,25 +1836,34 @@ const formatURL = (txt) => .join('.') .replace(/\/|https:/g, ''); /** - * @param {string} str + * @template {string} S + * @param {S} str */ const getHostname = (str) => formatURL(normalizedHostname(str)); +// #region Validators /** - * @type {URL | undefined} + * @type { import("../typings/types.d.ts").objToStr } + */ +const objToStr = (obj) => Object.prototype.toString.call(obj).match(/\[object (.*)\]/)[1]; +/** + * @type { import("../typings/types.d.ts").isRegExp } */ -let url; -try { - url = new URL(window.location.href ?? BLANK_PAGE); -} catch { - /* empty */ -} - -// #region Validators -const objToStr = (obj) => Object.prototype.toString.call(obj); const isRegExp = (obj) => /RegExp/.test(objToStr(obj)); +/** + * @type { import("../typings/types.d.ts").isElem } + */ const isElem = (obj) => /Element/.test(objToStr(obj)); -const isHTML = (obj) => /object HTML/.test(objToStr(obj)); +/** + * @type { import("../typings/types.d.ts").isHTML } + */ +const isHTML = (obj) => /HTML/.test(objToStr(obj)); +/** + * @type { import("../typings/types.d.ts").isObj } + */ const isObj = (obj) => /Object/.test(objToStr(obj)); +/** + * @type { import("../typings/types.d.ts").isFN } + */ const isFN = (obj) => /Function/.test(objToStr(obj)); const isUserCSS = (str) => /\.user\.css$/.test(str); const isUserJS = (str) => /\.user\.js$/.test(str); @@ -1863,109 +1892,84 @@ const isEmpty = (obj) => { }; // #endregion // #region Globals -/** - * https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/global-this.js - * @returns {typeof globalThis} - */ -function globalWin() { - const check = function (it) { - return it && it.Math === Math && it; +/** @type { import("../typings/UserJS.d.ts").safeHandles } */ +let _self; +{ + /** + * https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/global-this.js + * @returns {typeof globalThis} + */ + const globalWin = () => { + const check = (it) => it && it.Math === Math && it; + return ( + check(typeof globalThis == 'object' && globalThis) || + check(typeof window == 'object' && window) || + check(typeof self == 'object' && self) + ); }; - return ( - check(typeof globalThis == 'object' && globalThis) || - check(typeof window == 'object' && window) || - check(typeof self == 'object' && self) || - check(typeof this == 'object' && this) || - (function () { - return this; - })() || - Function('return this')() - ); -} -class Safe { - /** @type { import("../typings/UserJS.d.ts").safeHandles } */ - _safe; - create() { - try { - const g = globalWin(); - /** @type { import("../typings/UserJS.d.ts").safeHandles } */ - const safe = { - XMLHttpRequest: g.XMLHttpRequest, - CustomEvent: g.CustomEvent, - createElement: g.document.createElement.bind(g.document), - createElementNS: g.document.createElementNS.bind(g.document), - createTextNode: g.document.createTextNode.bind(g.document), - setTimeout: g.setTimeout, - clearTimeout: g.clearTimeout, - navigator: g.navigator, - scheduler: { - postTask(callback, options) { - if ('scheduler' in g && 'postTask' in g.scheduler) { - return g.scheduler.postTask(callback, options); - } - - options = Object.assign({}, options); - - if (options.delay === undefined) options.delay = 0; - options.delay = Number(options.delay); - if (options.delay < 0) { - return Promise.reject(new TypeError('"delay" must be a positive number.')); - } - return new Promise((resolve) => { - g.setTimeout(() => { - resolve(callback()); - }, options.delay); - }); - }, - yield() { - if ('scheduler' in g && 'yield' in g.scheduler) { - scheduler.yield(); - return g.scheduler.yield(); - } - return new Promise((resolve) => { - g.setTimeout(resolve, 0); - }); + const g = globalWin(); + try { + /** @type { import("../typings/UserJS.d.ts").safeHandles } */ + const safe = { + XMLHttpRequest: g.XMLHttpRequest, + CustomEvent: g.CustomEvent, + createElement: g.document.createElement.bind(g.document), + createElementNS: g.document.createElementNS.bind(g.document), + createTextNode: g.document.createTextNode.bind(g.document), + setTimeout: g.setTimeout, + clearTimeout: g.clearTimeout, + navigator: g.navigator, + scheduler: { + postTask(callback, options) { + if ('scheduler' in g && 'postTask' in g.scheduler) { + return g.scheduler.postTask(callback, options); + } + options = Object.assign({}, options); + if (options.delay === undefined) options.delay = 0; + options.delay = Number(options.delay); + if (options.delay < 0) { + return Promise.reject(new TypeError('"delay" must be a positive number.')); } + return new Promise((resolve) => { + g.setTimeout(() => { + resolve(callback()); + }, options.delay); + }); }, - groupBy(arr, callback) { - if (isFN(Object.groupBy)) { - return Object.groupBy(arr, callback); + yield() { + if ('scheduler' in g && 'yield' in g.scheduler) { + return g.scheduler.yield(); } - /** [Object.groupBy polyfill](https://gist.github.com/gtrabanco/7c97bd41aa74af974fa935bfb5044b6e) */ - return arr.reduce((acc = {}, ...args) => { - const key = callback(...args); - acc[key] ??= []; - acc[key].push(args[0]); - return acc; - }, {}); + return new Promise((resolve) => { + g.setTimeout(resolve, 0); + }); } - }; - for (const [k, v] of Object.entries(safe)) { - if (/scheduler|navigator/.test(k) || isFN(v)) continue; - throw new Error(`Safe handles "${k}" returned "${v}"`, { cause: 'safeSelf' }); + }, + groupBy(items, keySelector) { + if ('groupBy' in Object) { + return Object.groupBy(items, keySelector); + } + /** [Object.groupBy polyfill](https://gist.github.com/gtrabanco/7c97bd41aa74af974fa935bfb5044b6e) */ + return items.reduce((acc = {}, ...args) => { + const key = keySelector(...args); + acc[key] ??= []; + acc[key].push(args[0]); + return acc; + }, {}); } - this._safe = safe; - } catch (e) { - err(e); - this._safe = null; + }; + for (const [k, v] of Object.entries(safe)) { + if (/scheduler|navigator/.test(k) || isFN(v)) continue; + throw new Error(`Safe "${k}" returned "${v}"`, { cause: '_self' }); } - return this._safe; - } - get _self() { - return this._safe ?? this.create(); - } - set _self(obj) { - this._safe = obj; + _self = safe; + } catch (e) { + err(e); + _self = null; } } -const { _self } = new Safe(); // #endregion // #region Constants -/** - * @type { import("../typings/types.d.ts").cfgBase } - */ -const cfgBase = []; -const cfgSec = new Set(); /** Lets highlight me :) */ const authorID = 166061; /** @@ -2020,12 +2024,12 @@ const isMobile = (() => { try { const { navigator } = _self; if (navigator) { - const { userAgent, userAgentData } = navigator; - const { platform, mobile } = userAgentData ? Object(userAgentData) : {}; + const { userAgent = '', userAgentData = {} } = navigator; + const { platform = '', mobile = false } = Object(userAgentData); userjs.isMobile = - /Mobile|Tablet/.test(userAgent ? String(userAgent) : '') || + /Mobile|Tablet/.test(String(userAgent)) || Boolean(mobile) || - /Android|Apple/.test(platform ? String(platform) : ''); + /Android|Apple/.test(String(platform)); } else { userjs.isMobile = false; } @@ -2036,7 +2040,7 @@ const isMobile = (() => { } return userjs.isMobile; })(); -const isGM = typeof GM !== 'undefined'; +const isGM = typeof GM !== 'undefined' || typeof GM_xmlhttpRequest !== 'undefined'; const builtinList = { local: /localhost|router|gov|(\d+\.){3}\d+/, finance: @@ -2053,7 +2057,7 @@ const builtinList = { * @type { import("../typings/types.d.ts").config } */ const DEFAULT_CONFIG = { - autofetch: true, + autofetch: false, autoinject: true, autoSort: 'daily_installs', clearTabCache: true, @@ -2156,51 +2160,46 @@ const DEFAULT_CONFIG = { }; // #endregion // #region i18n -/** @type {Map} */ -const i18nMap = new Map(); -class i18nHandler { - constructor() { - for (const [k, v] of Object.entries(translations)) { - if (!i18nMap.has(k)) i18nMap.set(k, v); - } - } +const Language = class { + static i18nMap = new Map(Object.entries(translations)); /** * @param {string | Date | number} str */ - toDate(str = '') { - const { navigator } = _self; - return new Intl.DateTimeFormat(navigator.language).format( - typeof str === 'string' ? new Date(str) : str - ); + static toDate(str = '') { + const { + navigator: { language } + } = _self; + return new Intl.DateTimeFormat(language).format(typeof str === 'string' ? new Date(str) : str); } /** * @param {number | bigint} number */ - toNumber(number) { - const { navigator } = _self; - return new Intl.NumberFormat(navigator.language).format(number); + static toNumber(number) { + const { + navigator: { language } + } = _self; + return new Intl.NumberFormat(language).format(number); } /** * @type { import("../typings/UserJS.d.ts").i18n$ } */ - i18n$(key) { - const { navigator } = _self; - const current = navigator.language.split('-')[0] ?? 'en'; + static i18n$(key) { try { - return i18nMap.get(current)?.[key] ?? 'Invalid Key'; + return Language.i18nMap.get(Language.current)?.[key] ?? 'Invalid Key'; } catch (e) { err(e); return 'error'; } } - - get current() { - const { navigator } = _self; - return navigator.language.split('-')[0] ?? 'en'; + static get current() { + const { + navigator: { language } + } = _self; + const [current = 'en'] = language.split('-'); + return current; } -} -const language = new i18nHandler(); -const { i18n$ } = language; +}; +const { i18n$ } = Language; // #endregion // #region Utilities const union = (...arr) => [...new Set(arr.flat())]; @@ -2273,22 +2272,22 @@ const ael = (el, type, listener, options = {}) => { * @type { import("../typings/types.d.ts").formAttrs } */ const formAttrs = (elem, attr = {}) => { - if (!elem) { - return elem; - } - for (const key in attr) { - if (typeof attr[key] === 'object') { - formAttrs(elem[key], attr[key]); - } else if (isFN(attr[key])) { + if (!elem) return elem; + for (const [key, value] of Object.entries(attr)) { + if (/^_mujs/i.test(key)) { + elem[key] = value; + } else if (typeof value === 'object') { + formAttrs(elem[key], value); + } else if (isFN(value)) { if (/^on/.test(key)) { - elem[key] = attr[key]; + elem[key] = value; continue; } - ael(elem, key, attr[key]); - } else if (key === 'class') { - elem.className = attr[key]; + ael(elem, key, value); + } else if (/^class/i.test(key)) { + dom.cl.add(elem, value); } else { - elem[key] = attr[key]; + elem[key] = value; } } return elem; @@ -2299,22 +2298,11 @@ const formAttrs = (elem, attr = {}) => { const make = (tagName, cname, attrs) => { let el; try { - const { createElement } = _self; - el = createElement(tagName.toLowerCase()); - if (!isEmpty(cname)) { - if (typeof cname === 'string') { - el.className = cname; - } else if (isObj(cname)) { - formAttrs(el, cname); - } - } - if (!isEmpty(attrs)) { - if (typeof attrs === 'string') { - el.textContent = attrs; - } else if (isObj(attrs)) { - formAttrs(el, attrs); - } - } + el = _self.createElement(tagName.toLowerCase()); + if ((typeof cname === 'string' || Array.isArray(cname)) && !isEmpty(cname)) + dom.cl.add(el, cname); + if (typeof attrs === 'string' && !isEmpty(attrs)) el.textContent = attrs; + formAttrs(el, (isObj(cname) && cname) || (isObj(attrs) && attrs) || {}); } catch (ex) { if (ex instanceof DOMException) throw new Error(`${ex.name}: ${ex.message}`, { cause: 'make' }); ex.cause = 'make'; @@ -2322,7 +2310,6 @@ const make = (tagName, cname, attrs) => { } return el; }; - const $info = (() => { if (isGM) { if (isObj(GM.info)) { @@ -2383,7 +2370,7 @@ const dom = { }, cl: { add(target, token) { - token = normalizeTarget(token, false); + token = normalizeTarget((typeof token === 'string' && token.split(' ')) || token, false); return normalizeTarget(target) .filter(isHTML) .some((elem) => elem.classList.add(...token)); @@ -2442,6 +2429,10 @@ const iconSVG = { viewBox: '0 0 512 512', html: '' }, + info: { + viewBox: '0 0 512 512', + html: '' + }, install: { viewBox: '0 0 512 512', html: '' @@ -2474,12 +2465,9 @@ const iconSVG = { html: '' }, load(type, container) { - const { createElementNS } = _self; - const svgElem = createElementNS('http://www.w3.org/2000/svg', 'svg'); + const svgElem = _self.createElementNS('http://www.w3.org/2000/svg', 'svg'); for (const [k, v] of Object.entries(iconSVG[type])) { - if (k === 'html') { - continue; - } + if (k === 'html') continue; svgElem.setAttributeNS(null, k, v); } try { @@ -2487,13 +2475,14 @@ const iconSVG = { svgElem.innerHTML = iconSVG[type].html; svgElem.setAttribute('id', `mujs_${type ?? 'Unknown'}`); } - // eslint-disable-next-line no-unused-vars - } catch (ex) { + } catch { /* empty */ } - if (container) { + if (isElem(container)) { container.appendChild(svgElem); return svgElem; + } else if (isObj(container)) { + formAttrs(svgElem, container); } return svgElem.outerHTML; } @@ -2677,8 +2666,7 @@ const Network = { } } return await new Promise((resolve, reject) => { - const { XMLHttpRequest } = _self; - const req = new XMLHttpRequest(); + const req = new _self.XMLHttpRequest(); let method = 'GET'; let url = BLANK_PAGE; let body; @@ -2782,14 +2770,15 @@ const Counter = { }; // #region Container /** - * @type { import("../typings/UserJS.d.ts").Container } + * @type { typeof import("../typings/UserJS.d.ts").Container } */ -class Container { +const Container = class { + static prompts = []; webpage; host; injected; shadowRoot; - supported; + shadowSupport; frame; userjsCache; root; @@ -2808,51 +2797,19 @@ class Container { this.host = getHostname(url?.hostname ?? BLANK_PAGE); this.injected = false; this.shadowRoot = undefined; - this.supported = isFN(make('main-userjs').attachShadow); - - // /** furture update */ - // if (!this.supported) throw new Error('Failed to initialize: "attachShadow not supported"', { cause: 'Container' }); - // this.frame = make('main-userjs', { - // style: 'visibility: visible;', - // dataset: { - // insertedBy: $info.script.name, - // role: 'primary-container' - // } - // }); - // this.shadowRoot = this.frame.attachShadow({ mode: 'closed' }); + this.shadowSupport = isFN(make('main-userjs').attachShadow); - this.ready = false; - this.frame = this.supported - ? make('main-userjs', { - style: 'visibility: visible;', - dataset: { - insertedBy: $info.script.name, - role: 'primary-container' - } - }) - : make('iframe', 'mujs-iframe', { - dataset: { - insertedBy: $info.script.name, - role: 'primary-iframe' - }, - loading: 'lazy', - src: BLANK_PAGE, - style: - 'visibility: visible;position: fixed;bottom: 1rem;right: 1rem;height: 525px;width: 90%;margin: 0px 1rem;z-index: 100000000000000020 !important;', - onload: ({ target }) => { - if (target.contentDocument) { - this.shadowRoot = target.contentDocument.documentElement; - this.ready = true; - dom.cl.add([this.shadowRoot, target.contentDocument.body], 'mujs-iframe'); - } - } - }); - if (this.supported) { - this.shadowRoot = this.frame.attachShadow({ mode: 'closed' }); - this.ready = true; - } + if (!this.shadowSupport) + throw new Error('Failed to initialize: "attachShadow not supported"', { cause: 'Container' }); + this.frame = make('main-userjs', { + style: 'visibility: visible;', + dataset: { + insertedBy: $info.script.name, + role: 'primary-container' + } + }); + this.shadowRoot = this.frame.attachShadow({ mode: 'closed' }); - this.hostCache = new Map(); this.userjsCache = new Map(); this.root = make('mujs-root'); this.unsaved = false; @@ -2864,25 +2821,32 @@ class Container { const Timeout = class { constructor() { + /** + * @type {number[]} + */ this.ids = []; } - + /** + * @template R + * @param {number} delay + * @param {R} [reason] + */ set(delay, reason) { - const { setTimeout } = _self; return new Promise((resolve, reject) => { - const id = setTimeout(() => { + const id = _self.setTimeout(() => { Object.is(reason, null) || Object.is(reason, undefined) ? resolve() : reject(reason); this.clear(id); }, delay); this.ids.push(id); }); } - + /** + * @param {...number} ids + */ clear(...ids) { - const { clearTimeout } = _self; this.ids = this.ids.filter((id) => { if (ids.includes(id)) { - clearTimeout(id); + _self.clearTimeout(id); return false; } return true; @@ -2898,20 +2862,13 @@ class Container { window.addEventListener('beforeunload', this.remove); } - /** - * @param { function(): * } callback - * @param { Document } doc - */ - async inject(callback, doc) { + inject(callback, doc) { if (this.checkBlacklist(this.host)) { err(`Blacklisted "${this.host}"`); this.remove(); return; } - if (!this.shadowRoot || doc === null) return; - while (this.ready === false) { - await new Promise((resolve) => requestAnimationFrame(resolve)); - } + if (!this.shadowRoot || isNull(doc)) return; try { doc.documentElement.appendChild(this.frame); if (this.injected) { @@ -2937,7 +2894,7 @@ class Container { Counter.cnt.total.root = this.mainbtn; if (this.countframe) for (const engine of cfg.engines) this.countframe.append(Counter.set(engine)); - const { cfgpage, table, supported, frame, refresh, hostCache, urlBar, host } = this; + const { refresh, urlBar, host, userjsCache, cfgpage, table } = this; class Tabs { /** @@ -2978,17 +2935,11 @@ class Container { * @param {string} hostname */ intFN(hostname) { - const p = this.protoReg.exec(hostname); - if (!p) { - return; - } - if (p[1] === 'settings') { + const [, host] = this.protoReg.exec(hostname) ?? []; + if (host === 'settings') { dom.cl.remove(cfgpage, 'hidden'); dom.cl.add(table, 'hidden'); urlBar.placeholder = 'Search settings'; - if (!supported) { - dom.attr(frame, 'style', 'height: 100%;'); - } } } /** @@ -3020,8 +2971,15 @@ class Container { /** @param { HTMLElement } tab */ close(tab) { if (this.pool.has(tab)) this.pool.delete(tab); - const host = tab.dataset.host; - if (cfg.clearTabCache && hostCache.has(host)) hostCache.delete(host); + if (cfg.clearTabCache) { + const { host } = tab.dataset; + const arr = Array.from(userjsCache.values()).filter(({ _mujs }) => { + return !isEmpty(_mujs) && _mujs.info.host === host; + }); + for (const a of arr) { + arr.splice(arr.indexOf(a), 1); + } + } if (tab.classList.contains('active')) refresh(); const sibling = tab.nextElementSibling ?? tab.previousElementSibling; if (sibling) { @@ -3056,7 +3014,7 @@ class Container { textContent: 'X' }); const tabHost = make('mujs-host'); - const p = this.protoReg.exec(hostname); + const [, host] = this.protoReg.exec(hostname) ?? []; tab.append(tabHost, tabClose); this.el.head.append(tab); this.active(tab, false); @@ -3065,9 +3023,9 @@ class Container { tab.dataset.host = this.blank; tabHost.title = i18n$('newTab'); tabHost.textContent = i18n$('newTab'); - } else if (p) { + } else if (host) { tab.dataset.host = hostname || host; - tabHost.title = p[1] || tab.dataset.host; + tabHost.title = host || tab.dataset.host; tabHost.textContent = tabHost.title; this.intFN(hostname); } else { @@ -3128,7 +3086,12 @@ class Container { this.tabhead = make('thead'); this.header = make('mujs-header'); this.tbody = make('mujs-body'); - this.cfgpage = make('mujs-row', 'mujs-cfg hidden'); + this.cfgpage = make('mujs-row', 'mujs-cfg hidden', { + _mujs: { + base: [], + sections: new Set() + } + }); this.main = make('mujs-main', 'hidden'); this.urlContainer = make('mujs-url'); this.btnframe = make('mujs-column', 'btn-frame'); @@ -3255,7 +3218,6 @@ class Container { return false; } remove() { - this.hostCache.clear(); this.userjsCache.clear(); dom.remove(this.frame); } @@ -3271,28 +3233,25 @@ class Container { * @param { string } name - Name of stylesheet * @return { HTMLStyleElement | undefined } Style element */ - loadCSS(css, name = 'CSS') { + loadCSS(css, name = 'CSS', useGM = true) { try { + if (this.stylesheet) return this.stylesheet; if (typeof name !== 'string') throw new Error('"name" must be a typeof "string"', { cause: 'loadCSS' }); - if (qs(`style[data-role="${name}"]`, this.root)) - return qs(`style[data-role="${name}"]`, this.root); if (typeof css !== 'string') throw new Error('"css" must be a typeof "string"', { cause: 'loadCSS' }); if (isBlank(css)) throw new Error(`"${name}" contains empty CSS string`, { cause: 'loadCSS' }); const parent = isEmpty(this.root.shadowRoot) ? this.root : this.root.shadowRoot; - if (isGM) { - const fn = isFN(GM.addElement) - ? GM.addElement - : isFN(GM_addElement) - ? GM_addElement - : BLANK_FN; + if (useGM && isGM) { + const fn = (isFN(GM.addElement) && GM.addElement) || (isFN(GM_addElement) && GM_addElement); + if (!isFN(fn)) return this.loadCSS(css, name, false); const sty = fn(parent, 'style', { textContent: css }); if (isElem(sty)) { sty.dataset.insertedBy = $info.script.name; sty.dataset.role = name; - return sty; + this.stylesheet = sty; + return this.stylesheet; } } const sty = make('style', { @@ -3303,7 +3262,8 @@ class Container { } }); parent.appendChild(sty); - return sty; + this.stylesheet = sty; + return this.stylesheet; } catch (ex) { err(ex); } @@ -3318,7 +3278,7 @@ class Container { for (const b of normalizeTarget(cfg.blacklist)) { if (typeof b === 'string') { if (b.startsWith('userjs-')) { - const r = /userjs-(\w+)/.exec(b)[1]; + const [, r] = /userjs-(\w+)/.exec(b) ?? []; const biList = builtinList[r]; if (isRegExp(biList)) { if (biList.test(str)) blacklisted = true; @@ -3348,20 +3308,18 @@ class Container { if (theme === DEFAULT_CONFIG.theme) { return; } - const sty = this.root.style; + const { style } = this.root; for (const [k, v] of Object.entries(theme)) { const str = `--mujs-${k}`; - const prop = sty.getPropertyValue(str); + const prop = style.getPropertyValue(str); if (isEmpty(v)) theme[k] = prop; if (prop === v) continue; - sty.removeProperty(str); - sty.setProperty(str, v); + style.removeProperty(str); + style.setProperty(str, v); } } makePrompt(txt, dataset = {}, usePrompt = true) { - for (const elem of normalizeTarget(qsA('.prompt', this.promptElem))) { - if (elem.dataset.prompt) elem.remove(); - } + dom.remove(Container.prompts); const el = make('mu-js', 'prompt', { dataset: { prompt: txt @@ -3398,6 +3356,7 @@ class Container { elPrompt.append(elNo); el.append(elPrompt); } + Container.prompts.push(el); this.promptElem.append(el); return el; } @@ -3412,15 +3371,14 @@ class Container { for (const e of ex) { str += `${typeof e === 'string' ? e : `${e.cause ? `[${e.cause}] ` : ''}${e.message}${e.stack ? ` ${e.stack}` : ''}`}\n`; } - const { createTextNode } = _self; - error.appendChild(createTextNode(str)); + error.appendChild(_self.createTextNode(str)); this.footer.append(error); } refresh() { this.urlBar.placeholder = i18n$('newTab'); Counter.reset(); dom.cl.remove(this.toElem(), 'hidden'); - dom.cl.remove(cfgSec, 'hidden'); + dom.cl.remove(this.cfgpage._mujs.sections, 'hidden'); dom.prop([this.tabbody, this.rateContainer, this.footer], 'innerHTML', ''); } /** @@ -3483,7 +3441,7 @@ class Container { yield userjs; } } -} +}; const container = new Container(); // #endregion // #region Primary Function @@ -3494,49 +3452,34 @@ function primaryFN() { try { const { scheduler } = _self; const { - mainframe, - urlBar, - rateContainer, - footer, - tabbody, - cfgpage, btnfullscreen, + mainframe, main, Tabs, - showError + showError, + cfgpage: { + _mujs: { sections: cfgSec, base: cfgBase } + } } = container; const reloadCfg = () => { for (const base of cfgBase) { - const nm = /^(\w+)-(.+)/.exec(base.value); - const d = (() => { - if (base.tag === 'engine') { - const engine = DEFAULT_CONFIG.engines.find((engine) => engine.name === base.value); - if (engine) { - return engine; - } - } - if (nm) { - return DEFAULT_CONFIG[nm[1]][nm[2]]; - } - return DEFAULT_CONFIG[base.value]; - })(); - const v = (() => { - if (base.tag === 'engine') { - const engine = cfg.engines.find((engine) => engine.name === base.value); - if (engine) { - return engine; - } - } - if (nm) { - return cfg[nm[1]][nm[2]]; - } - return cfg[base.value]; - })(); + const [, CONFIG, SUB_CONFIG] = /^(\w+)-(.+)/.exec(base.value) ?? []; + let d = DEFAULT_CONFIG[base.value]; + let v = cfg[base.value]; + if (base.tag === 'engine') { + const dEngine = DEFAULT_CONFIG.engines.find((engine) => engine.name === base.value); + const vEngine = cfg.engines.find((engine) => engine.name === base.value); + if (dEngine) d = dEngine; + if (vEngine) v = vEngine; + } else if (CONFIG) { + d = DEFAULT_CONFIG[CONFIG][SUB_CONFIG]; + v = cfg[CONFIG][SUB_CONFIG]; + } base.cache = v; if (base.type === 'checkbox') { - if (nm) { - if (nm[1] === 'filters') { - base.elem.checked = cfg[nm[1]][nm[2]].enabled; + if (CONFIG) { + if (CONFIG === 'filters') { + base.elem.checked = cfg[CONFIG][SUB_CONFIG].enabled; } else { base.elem.checked = v; } @@ -3646,11 +3589,11 @@ function primaryFN() { const frameTimeout = container.timeouts.frame; ael(main, isMobile ? 'touchend' : 'click', async (evt) => { try { + if (isNull(evt.target)) return; /** @type { HTMLElement } */ - const target = evt.target.closest('[data-command]'); - if (!target) { - return; - } + const t = evt.target; + const target = t.closest('[data-command]'); + if (isNull(target)) return; let dataset = target.dataset; let cmd = dataset.command; if (/^prompt-/.test(target.dataset.command)) { @@ -3713,7 +3656,9 @@ function primaryFN() { a.href = dataUserJS.code_url; a.click(); } - } else if (cmd === 'open-tab' && dataset.webpage) { + } else if (/open-tab|more-info/.test(cmd) && dataset.webpage) { + if (cmd === 'more-info') evt.preventDefault(); + con.log(cmd); if (isGM) { if (isFN(GM.openInTab)) { return GM.openInTab(dataset.webpage); @@ -3734,7 +3679,11 @@ function primaryFN() { } else if (cmd === 'list-description') { const arr = []; const ignoreTags = new Set(['TD', 'MUJS-A', 'MU-JS']); - for (const node of Object.values(target.parentElement._mujs)) { + /** + * @type { import("../typings/UserJS.d.ts").mujsName } + */ + const p = target.parentElement; + for (const node of Object.values(p._mujs)) { if (ignoreTags.has(node.tagName)) { continue; } @@ -3770,7 +3719,7 @@ function primaryFN() { container.timeoutFrame(); } else if (cmd === 'save') { container.rebuild = true; - dom.prop(rateContainer, 'innerHTML', ''); + dom.prop(container.rateContainer, 'innerHTML', ''); if (!dom.prop(target, 'disabled')) { const config = await container.save(); if (container.rebuild) { @@ -3783,7 +3732,7 @@ function primaryFN() { } } else if (cmd === 'reset') { cfg = DEFAULT_CONFIG; - dom.remove(qsA('.error', footer)); + dom.remove(qsA('.error', container.footer)); container.unsaved = true; container.rebuild = true; reloadCfg(); @@ -3867,11 +3816,13 @@ function primaryFN() { const engine = dataUserJS._mujs.info.engine; let pageURL; if (engine.name.includes('fork')) { - const { navigator } = _self; - const current = navigator.language.split('-')[0] ?? 'en'; + const { + navigator: { language } + } = _self; + const { current } = Language; pageURL = dataUserJS.url.replace( /\/scripts/, - `/${/^(zh|fr|es)/.test(current) ? navigator.language : current}/scripts` + `/${/^(zh|fr|es)/.test(current) ? language : current}/scripts` ); } else if (engine.name.includes('github')) { const page_url = await Network.req(dataUserJS.page_url, 'GET', 'json', { @@ -3887,7 +3838,7 @@ function primaryFN() { return; } const page = await Network.req(page_url.download_url, 'GET', 'text'); - if (container.supported) { + if (container.shadowSupport) { const shadow = pageArea.attachShadow({ mode: 'closed' }); const div = make('div', { innerHTML: page @@ -3924,7 +3875,7 @@ function primaryFN() { } return content; }; - if (container.supported) { + if (container.shadowSupport) { const shadow = pageArea.attachShadow({ mode: 'closed' }); const div = make('div', { style: 'pointer-events: none;', @@ -4063,13 +4014,13 @@ function primaryFN() { dom.attr(elem, 'title', ujs.license ?? i18n$('no_license')); dom.text(elem, `${i18n$('license')}: ${ujs.license ?? i18n$('no_license')}`); } else if (name === 'code_updated_at') { - dom.text(elem, language.toDate(ujs.code_updated_at)); + dom.text(elem, Language.toDate(ujs.code_updated_at)); elem.dataset.value = new Date(ujs.code_updated_at).toISOString(); } else if (name === 'created_date') { - dom.text(elem, `${i18n$('created_date')}: ${language.toDate(ujs.created_at)}`); + dom.text(elem, `${i18n$('created_date')}: ${Language.toDate(ujs.created_at)}`); elem.dataset.value = new Date(ujs.created_at).toISOString(); } else if (name === 'total_installs') { - dom.text(elem, `${i18n$('total_installs')}: ${language.toNumber(ujs.total_installs)}`); + dom.text(elem, `${i18n$('total_installs')}: ${Language.toNumber(ujs.total_installs)}`); } else { dom.text(elem, ujs[name]); } @@ -4182,11 +4133,12 @@ function primaryFN() { } const meta = {}; const meta_block_map = new Map(); + const reg = (this.isUserCSS && /@([a-zA-Z:-]+)\s+(.*)/) || /\/\/\s+@([a-zA-Z:-]+)\s+(.*)/; for (const meta_line of this.get_meta_block().split('\n').filter(Boolean)) { - const meta_match = /\/\/\s+@([a-zA-Z:-]+)\s+(.*)/.exec(meta_line); - if (!meta_match) continue; - const key = meta_match[1].trim(); - const value = meta_match[2].trim(); + let [, key, value] = reg.exec(meta_line) ?? []; + if (!key) continue; + key = key.trim(); + value = value.trim(); if (!meta_block_map.has(key)) meta_block_map.set(key, []); const meta_map = meta_block_map.get(key); meta_map.push(value); @@ -4348,15 +4300,15 @@ function primaryFN() { const { data_meta } = this; if (translate) { - if (data_meta[`name:${language.current}`]) { + if (data_meta[`name:${Language.current}`]) { Object.assign(obj, { - name: data_meta[`name:${language.current}`] + name: data_meta[`name:${Language.current}`] }); this.translated = true; } - if (data_meta[`description:${language.current}`]) { + if (data_meta[`description:${Language.current}`]) { Object.assign(obj, { - description: data_meta[`description:${language.current}`] + description: data_meta[`description:${Language.current}`] }); this.translated = true; } @@ -4367,15 +4319,15 @@ function primaryFN() { if (data_meta.resource) { const obj = {}; if (typeof data_meta.resource === 'string') { - const reg = /(.+)\s+(.+)/.exec(data_meta.resource); - if (reg) { - obj[reg[1].trim()] = reg[2]; + const [, key, value] = /(.+)\s+(.+)/.exec(data_meta.resource) ?? []; + if (key) { + obj[key.trim()] = value; } } else { for (const r of data_meta.resource) { - const reg = /(.+)\s+(http.+)/.exec(r); - if (reg) { - obj[reg[1].trim()] = reg[2]; + const [, key, value] = /(.+)\s+(http.+)/.exec(r) ?? []; + if (key) { + obj[key.trim()] = value; } } } @@ -4422,7 +4374,6 @@ function primaryFN() { }); ujsURLs.append(appliesTo, applyList); root.append(ujsURLs); - const list = obj.list ?? []; if (isEmpty(list)) { const elem = make('mujs-a', { @@ -4484,9 +4435,7 @@ function primaryFN() { badUserJS.includes(ujs.id), badUserJS.includes(ujs.url) ].some((t) => t === true); - if (a) { - return; - } + if (a) return; if (!container.userjsCache.has(ujs.id)) container.userjsCache.set(ujs.id, ujs); const eframe = make('td', 'install-btn'); const uframe = make('td', 'mujs-uframe'); @@ -4497,12 +4446,15 @@ function primaryFN() { } }); const fupdated = make('td', 'mujs-list', { - textContent: language.toDate(ujs.code_updated_at), + textContent: Language.toDate(ujs.code_updated_at), dataset: { name: 'code_updated_at', value: new Date(ujs.code_updated_at).toISOString() } }); + /** + * @type { import("../typings/UserJS.d.ts").mujsName } + */ const fname = make('td', 'mujs-name'); const fmore = make('mujs-column', 'mujs-list hidden', { dataset: { @@ -4525,7 +4477,7 @@ function primaryFN() { textContent: `${i18n$('version_number')}: ${ujs.version}` }); const fcreated = make('mu-js', 'mujs-list', { - textContent: `${i18n$('created_date')}: ${language.toDate(ujs.created_at)}`, + textContent: `${i18n$('created_date')}: ${Language.toDate(ujs.created_at)}`, dataset: { name: 'created_at', value: new Date(ujs.created_at).toISOString() @@ -4539,7 +4491,7 @@ function primaryFN() { } }); const ftotal = make('mu-js', 'mujs-list', { - textContent: `${i18n$('total_installs')}: ${language.toNumber(ujs.total_installs)}`, + textContent: `${i18n$('total_installs')}: ${Language.toNumber(ujs.total_installs)}`, dataset: { name: 'total_installs' } @@ -4597,6 +4549,7 @@ function primaryFN() { }); const tr = make('tr', 'frame', { dataset: { + engine, scriptId: ujs.id } }); @@ -4623,7 +4576,6 @@ function primaryFN() { userjs: ujs.id } }); - tr.dataset.engine = engine; if (!engine.includes('fork') && cfg.recommend.others && goodUserJS.includes(ujs.url)) { tr.dataset.good = 'upsell'; } @@ -4753,22 +4705,24 @@ function primaryFN() { return handles.refresh(); }; // #region List - class List { - intEngines; - intHost; + /** + * @type { typeof import("../typings/UserJS.d.ts").List } + */ + const List = class { + #intEngines; + #intHost; constructor(hostname = undefined) { this.build = this.build.bind(this); this.groupBy = this.groupBy.bind(this); this.dispatch = this.dispatch.bind(this); this.sortRecords = this.sortRecords.bind(this); - this.container = container; - this.intEngines = cfg.engines ?? []; - this.host = hostname; + this.#intEngines = cfg.engines ?? []; + this.setHost(hostname); } setEngines(engines = []) { const { host } = this; - return engines.filter((e) => { + const e = engines.filter((e) => { if (!e.enabled) { return false; } @@ -4780,6 +4734,21 @@ function primaryFN() { } return true; }); + this.#intEngines = e; + return e; + } + + setHost(hostname) { + if (isEmpty(hostname)) hostname = container.host; + this.#intHost = hostname; + this.blacklisted = container.checkBlacklist(hostname); + this.#intEngines = this.setEngines(this.engines); + this.domain = this.getDomain(this.#intHost); + if (this.blacklisted) { + showError(`Blacklisted "${hostname}"`); + container.timeoutFrame(); + } + return hostname; } dispatch(ujs) { @@ -4788,34 +4757,14 @@ function primaryFN() { main.dispatchEvent(customEvent); } - set engines(engines) { - this.intEngines = this.setEngines(engines); - } - get engines() { - return this.intEngines; - } - - set host(hostname) { - if (isEmpty(hostname)) hostname = this.container.host; - this.intHost = hostname; - this.blacklisted = this.container.checkBlacklist(hostname); - this.intEngines = this.setEngines(this.engines); - this.domain = this.getDomain(this.intHost); - if (this.blacklisted) { - showError(`Blacklisted "${hostname}"`); - this.container.timeoutFrame(); - } + return this.#intEngines; } get host() { - return this.intHost; + return this.#intHost; } - /** - * @template { string } S - * @param { S } str - */ getDomain(str = '') { if (str === '*') { return 'all-sites'; @@ -4923,15 +4872,14 @@ function primaryFN() { return; } const { groupBy } = _self; - /** - * @type { {[key: string]: import("../typings/types.d.ts").GSForkQuery[]} } - */ const g = groupBy(dataA.map(_mujs), ({ locale }) => { - return locale.split('-')[0] ?? locale; + const [current = locale] = locale.split('-'); + return current; }); for (const [k, list] of Object.entries(g)) { + if (!list) break; for (const ujs of list) { - if (cfg.filterlang && k !== language.current) { + if (cfg.filterlang && k !== Language.current) { const c = await ujs._mujs.code.request(true); if (!c.translated) continue; } @@ -4968,9 +4916,9 @@ function primaryFN() { showError('Invalid data received from the server, TODO fix this'); return; } - const selected = htmlDocument.documentElement; + const d = htmlDocument.documentElement; if (/openuserjs/gi.test(engine.name)) { - const col = qsA('.col-sm-8 .tr-link', selected) ?? []; + const col = qsA('.col-sm-8 .tr-link', d) ?? []; for (const i of col) { while (isNull(qs('.script-version', i))) { await new Promise((resolve) => requestAnimationFrame(resolve)); @@ -5135,7 +5083,7 @@ function primaryFN() { const txt = make('mujs-row', 'rate-info', { textContent: `${key.toUpperCase()}: ${value}` }); - rateContainer.append(txt); + container.rateContainer.append(txt); } }) .catch(respError); @@ -5154,11 +5102,11 @@ function primaryFN() { } } } else { - for (const ujs of hostCache) tabbody.append(ujs._mujs.root); + for (const ujs of hostCache) container.tabbody.append(ujs._mujs.root); } - urlBar.placeholder = i18n$('search_placeholder'); - urlBar.value = ''; + container.urlBar.placeholder = i18n$('search_placeholder'); + container.urlBar.value = ''; if (isBlank(fetchRecords)) { this.sortRecords(); @@ -5177,29 +5125,28 @@ function primaryFN() { const sortType = cfg.autoSort ?? 'daily_installs'; return b[sortType] - a[sortType]; })) { - if (isElem(ujs._mujs.root)) tabbody.append(ujs._mujs.root); + if (isElem(ujs._mujs.root)) container.tabbody.append(ujs._mujs.root); } for (const [name, value] of Object.entries(this.groupBy(arr))) Counter.update(value.length, { name }); } groupBy() { - const { groupBy } = _self; - return groupBy(Array.from(this), ({ _mujs }) => _mujs.info.engine.name); + return _self.groupBy(Array.from(this), ({ _mujs }) => _mujs.info.engine.name); } *[Symbol.iterator]() { - const { intHost, engines } = this; - const arr = Array.from(this.container).filter( + const { host, engines } = this; + const arr = Array.from(container).filter( ({ _mujs }) => - _mujs.info.host === intHost && + _mujs.info.host === host && engines.find((engine) => engine.enabled && engine.name === _mujs.info.engine.name) ); for (const userjs of arr) { yield userjs; } } - } + }; const MUList = new List(); // #endregion // #region Make Config @@ -5234,7 +5181,7 @@ function primaryFN() { } }); const divDesc = make('mu-js', { - textContent: name + innerHTML: name }); ael(sec, 'click', (evt) => { /** @type { HTMLElement } */ @@ -5254,27 +5201,34 @@ function primaryFN() { }); lb.append(divDesc); sec.append(lb); - cfgpage.append(sec); - !cfgSec.has(sec) && cfgSec.add(sec); + container.cfgpage.append(sec); + if(!cfgSec.has(sec)) cfgSec.add(sec); return sec; }; const sections = { general: makesection('General', 'general'), load: makesection('Automation', 'load'), list: makesection('List', 'list'), - filters: makesection('List Filters', 'filters'), + filters: makesection( + `List Filters ${iconSVG.load('info', { dataset: { command: 'more-info', webpage: 'https://greasyfork.org/scripts/12179' } })}`, + 'filters' + ), blacklist: makesection('Blacklist (WIP)', 'blacklist'), engine: makesection('Search Engines', 'engine'), theme: makesection('Theme Colors', 'theme'), exp: makesection('Import / Export', 'exp') }; const makeRow = (text, value, type = 'checkbox', tag = 'general', attrs = {}) => { + const [name, CONFIG, SUB_CONFIG] = /^(\w+)-(.+)/.exec(value) ?? []; const lb = make('label', 'sub-section hidden', { - textContent: text, dataset: { [tag]: text } }); + const txt = make('mu-js', { + innerHTML: text + }); + lb.append(txt); const getDefault = () => { if (tag === 'engine') { const engine = DEFAULT_CONFIG.engines.find((engine) => engine.name === value); @@ -5282,10 +5236,7 @@ function primaryFN() { return engine; } } - const nm = /^(\w+)-(.+)/.exec(value); - if (nm) { - return DEFAULT_CONFIG[nm[1]][nm[2]]; - } + if (CONFIG) return DEFAULT_CONFIG[CONFIG][SUB_CONFIG]; return DEFAULT_CONFIG[value]; }; const getValue = () => { @@ -5295,10 +5246,7 @@ function primaryFN() { return engine; } } - const nm = /^(\w+)-(.+)/.exec(value); - if (nm) { - return cfg[nm[1]][nm[2]]; - } + if (CONFIG) return cfg[CONFIG][SUB_CONFIG]; return cfg[value]; }; const obj = { @@ -5360,12 +5308,11 @@ function primaryFN() { inlab.append(inp, la); lb.append(inlab); - const nm = /^(\w+)-(.+)/.exec(value); - if (nm) { - if (nm[1] === 'filters') { - inp.checked = cfg[nm[1]][nm[2]].enabled; + if (CONFIG) { + if (CONFIG === 'filters') { + inp.checked = cfg[CONFIG][SUB_CONFIG].enabled; } else { - inp.checked = cfg[nm[1]][nm[2]]; + inp.checked = cfg[CONFIG][SUB_CONFIG]; } } else { inp.checked = cfg[value]; @@ -5375,11 +5322,11 @@ function primaryFN() { if (/filterlang/i.test(value)) { container.rebuild = true; } - if (nm) { - if (nm[1] === 'filters') { - cfg[nm[1]][nm[2]].enabled = evt.target.checked; + if (CONFIG) { + if (CONFIG === 'filters') { + cfg[CONFIG][SUB_CONFIG].enabled = evt.target.checked; } else { - cfg[nm[1]][nm[2]] = evt.target.checked; + cfg[CONFIG][SUB_CONFIG] = evt.target.checked; } } else { cfg[value] = evt.target.checked; @@ -5395,7 +5342,7 @@ function primaryFN() { container.unsaved = true; container.rebuild = true; engine.enabled = evt.target.checked; - MUList.engines = cfg.engines; + MUList.setEngines(cfg.engines); }); if (engine.query) { @@ -5406,7 +5353,7 @@ function primaryFN() { value: decode(engine.query), placeholder: decode(d.query), dataset: { - name: nm, + name, engine: engine.name }, onchange(evt) { @@ -5414,7 +5361,7 @@ function primaryFN() { container.rebuild = true; try { engine.query = encodeURIComponent(new URL(evt.target.value).toString()); - MUList.engines = cfg.engines; + MUList.setEngines(cfg.engines); } catch (ex) { err(ex); } @@ -5436,7 +5383,7 @@ function primaryFN() { container.unsaved = true; container.rebuild = true; engine.token = evt.target.value; - MUList.engines = cfg.engines; + MUList.setEngines(cfg.engines); } }); obj.elemToken = ghToken; @@ -5497,7 +5444,10 @@ function primaryFN() { makeRow(i18n$('userjs_sync'), 'cache'); makeRow(i18n$('userjs_autoinject'), 'autoinject', 'checkbox', 'load'); } - makeRow(i18n$('redirect'), 'sleazyredirect'); + makeRow( + `${i18n$('redirect')} ${iconSVG.load('info', { dataset: { command: 'more-info', webpage: 'https://greasyfork.org/scripts/23840' } })}`, + 'sleazyredirect' + ); makeRow(`${i18n$('dtime')} (ms)`, 'time', 'number', 'general', { defaultValue: 10000, value: cfg.time, @@ -5553,7 +5503,12 @@ function primaryFN() { makeRow('Greasy Fork', 'greasyfork', 'checkbox', 'engine'); makeRow('Sleazy Fork', 'sleazyfork', 'checkbox', 'engine'); makeRow('Open UserJS', 'openuserjs', 'checkbox', 'engine'); - makeRow('GitHub API', 'github', 'checkbox', 'engine'); + makeRow( + `GitHub API ${iconSVG.load('info', { dataset: { command: 'more-info', webpage: 'https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens' } })}`, + 'github', + 'checkbox', + 'engine' + ); for (const [k, v] of Object.entries(cfg.theme)) makeRow(k, v, 'text', 'theme'); @@ -5625,10 +5580,8 @@ function primaryFN() { txt = `Built-in "${s}"`; v = builtinList[s]; } - } else { - if (!key.enabled) { - return; - } + } else if (!key.enabled) { + return; } if (isRegExp(v)) { @@ -5741,59 +5694,64 @@ function primaryFN() { sections.exp.append(v); } } - cfgpage.append(cbtn); + container.cfgpage.append(cbtn); }; // #endregion container.Tabs.custom = (host) => { - MUList.host = host; + MUList.setHost(host); respHandles.build(); }; ael(mainframe, 'mouseenter', (evt) => { evt.preventDefault(); evt.stopPropagation(); + if (isNull(evt.target)) return; evt.target.style.opacity = container.opacityMax; frameTimeout.clear(...frameTimeout.ids); }); ael(mainframe, 'mouseleave', (evt) => { evt.preventDefault(); evt.stopPropagation(); + if (isNull(evt.target)) return; evt.target.style.opacity = container.opacityMin; container.timeoutFrame(); }); + let initClick = true; ael(mainframe, 'click', (evt) => { evt.preventDefault(); frameTimeout.clear(...frameTimeout.ids); + if (initClick && !cfg.autofetch) { + initClick = false; + respHandles.build(); + } dom.cl.remove(main, 'hidden'); dom.cl.add(mainframe, 'hidden'); if (cfg.autoexpand) { - dom.cl.add([btnfullscreen, main], 'expanded'); - dom.prop(btnfullscreen, 'innerHTML', iconSVG.load('collapse')); + dom.cl.add([container.btnfullscreen, main], 'expanded'); + dom.prop(container.btnfullscreen, 'innerHTML', iconSVG.load('collapse')); } }); - ael(urlBar, 'input', (evt) => { + ael(container.urlBar, 'input', (evt) => { evt.preventDefault(); - if (urlBar.placeholder === i18n$('newTab')) { + if (container.urlBar.placeholder === i18n$('newTab')) { return; } /** * @type { string } */ const val = evt.target.value; - const section = qsA('mujs-section[data-name]', cfgpage); if (isEmpty(val)) { - dom.cl.remove(container.toElem(), 'hidden'); - dom.cl.remove(section, 'hidden'); + dom.cl.remove([...container.toElem(), ...cfgSec], 'hidden'); return; } const finds = new Set(); - if (!dom.cl.has(cfgpage, 'hidden')) { + if (!dom.cl.has(container.cfgpage, 'hidden')) { const reg = new RegExp(val, 'gi'); - for (const elem of section) { + for (const elem of cfgSec) { if (!isElem(elem)) continue; if (finds.has(elem)) continue; - if (elem.dataset.name.match(reg)) finds.add(elem); + if (elem.textContent.match(reg)) finds.add(elem); } - dom.cl.add(section, 'hidden'); + dom.cl.add(cfgSec, 'hidden'); dom.cl.remove([...finds], 'hidden'); return; } @@ -5808,21 +5766,17 @@ function primaryFN() { const q_value = val.replace(regExp, ''); const reg = new RegExp(q_value, 'gi'); for (const v of cacheValues) { - let k = v[key]; if (typeof k === 'number') { - k = `${v[key]}`; - } - if (k && k.match(reg)) { - finds.add(v._mujs.root); + if (`${v[key]}`.match(reg)) finds.add(v._mujs.root); } } }; if (val.match(/^(code_url|url):/)) { ezQuery(/^(code_url|url):/, 'code_url'); } else if (val.match(/^(author|users?):/)) { - const parts = /^[\w_]+:(.+)/.exec(val); + const [, parts] = /^[\w_]+:(.+)/.exec(val) ?? []; if (parts) { - const reg = new RegExp(parts[1], 'gi'); + const reg = new RegExp(parts, 'gi'); for (const v of cacheValues.filter((v) => !isEmpty(v.users))) { for (const user of v.users) { for (const value of Object.values(user)) { @@ -5846,17 +5800,17 @@ function primaryFN() { } else if (val.match(/^description:/)) { ezQuery(/^description:/, 'description'); } else if (val.match(/^(search_engine|engine):/)) { - const parts = /^[\w_]+:(\w+)/.exec(val); + const [, parts] = /^[\w_]+:(\w+)/.exec(val) ?? []; if (parts) { - const reg = new RegExp(parts[1], 'gi'); + const reg = new RegExp(parts, 'gi'); for (const { _mujs } of cacheValues) if (_mujs.info.engine.name.match(reg)) finds.add(_mujs.root); } } else if (val.match(/^filter:/)) { - const parts = /^\w+:(.+)/.exec(val); + const [, parts] = /^\w+:(.+)/.exec(val) ?? []; if (parts) { const bsFilter = loadFilters(); - const filterType = bsFilter.get(parts[1].trim().toLocaleLowerCase()); + const filterType = bsFilter.get(parts.trim().toLocaleLowerCase()); if (filterType) { const { reg } = filterType; for (const { name, users, _mujs } of cacheValues) { @@ -5885,31 +5839,32 @@ function primaryFN() { if (/name|desc/i.test(key) && key.match(reg)) finds.add(v._mujs.root); } } - dom.cl.add(qsA('tr[data-engine]', tabbody), 'hidden'); + dom.cl.add(container.toElem(), 'hidden'); dom.cl.remove([...finds], 'hidden'); }); - ael(urlBar, 'change', (evt) => { + ael(container.urlBar, 'change', (evt) => { evt.preventDefault(); - const val = evt.target.value; + if (isNull(evt.target)) return; + const { target } = evt; const tabElem = Tabs.getActive(); - if (urlBar.placeholder === i18n$('newTab') && tabElem) { + if (container.urlBar.placeholder === i18n$('newTab') && tabElem) { const tabHost = tabElem.firstElementChild; - const host = formatURL(normalizedHostname(val)); - if (Tabs.protoReg.test(val)) { - const createdTab = Tabs.getTab(val); + const host = formatURL(normalizedHostname(target.value)); + if (Tabs.protoReg.test(target.value)) { + const createdTab = Tabs.getTab(target.value); Tabs.close(tabElem); if (createdTab) { Tabs.active(createdTab); } else { - Tabs.create(val); + Tabs.create(target.value); } - evt.target.placeholder = i18n$('search_placeholder'); - evt.target.value = ''; + target.placeholder = i18n$('search_placeholder'); + target.value = ''; } else if (host === '*') { tabElem.dataset.host = host; tabHost.title = ''; tabHost.textContent = ''; - MUList.host = host; + MUList.setHost(host); respHandles.build(); } else if (container.checkBlacklist(host)) { showError(`Blacklisted "${host}"`); @@ -5917,7 +5872,7 @@ function primaryFN() { tabElem.dataset.host = host; tabHost.title = host; tabHost.textContent = host; - MUList.host = host; + MUList.setHost(host); respHandles.build(); } } @@ -5942,7 +5897,7 @@ function primaryFN() { } // #endregion /** - * @template { Function } F + * @template F * @param { (this: F, doc: Document) => * } onDomReady */ const loadDOM = (onDomReady) => { diff --git a/src/UserJS/header.js b/src/UserJS/header.js index 109fd04..076c916 100644 --- a/src/UserJS/header.js +++ b/src/UserJS/header.js @@ -5,13 +5,11 @@ const inIframe = (() => { try { return window.self !== window.top; - } catch (e) { + } catch { return true; } })(); -if (inIframe) { - return; -} +if (inIframe) return; let userjs = self.userjs; /** * Skip text/plain documents, based on uBlock Origin `vapi.js` file @@ -28,11 +26,15 @@ if ( } else { console.error('[%cMagic Userscript+%c] %cERROR','color: rgb(29, 155, 240);','','color: rgb(249, 24, 128);', `MIME type is not a document, got "${document.contentType || ''}"`); } -if (!(typeof userjs === 'object' && userjs.UserJS)) { - return; +if (!(typeof userjs === 'object' && userjs.UserJS)) return; +{ + /** Native implementation exists */ + const excludePolicy = [ + 'outlook.office.com' + ]; + const hostname = location?.hostname || ''; + if (window.trustedTypes && window.trustedTypes.createPolicy && !hostname.includes(excludePolicy)) window.trustedTypes.createPolicy('default', { createHTML: (string) => string, createScript: (string) => string, createScriptURL: (string) => string }); } -/** Native implementation exists */ -if (window.trustedTypes && window.trustedTypes.createPolicy) window.trustedTypes.createPolicy('default', { createHTML: (string) => string }); /** [i18n directory](https://github.com/magicoflolis/Userscript-Plus/tree/master/src/_locales) */ const translations = [[languageList]]; /** [source code](https://github.com/magicoflolis/Userscript-Plus/blob/master/src/sass/_main.scss) */ diff --git a/src/sass/_main.scss b/src/sass/_main.scss index 370e4d1..c7e78e9 100644 --- a/src/sass/_main.scss +++ b/src/sass/_main.scss @@ -928,6 +928,13 @@ mujs-body { } } + svg { + width: 14px; + height: 14px; + fill: currentColor; + background: transparent; + } + // &[data-name='theme'], &[data-name='exp'], &[data-name='blacklist'] { diff --git a/src/typings/UserJS.d.ts b/src/typings/UserJS.d.ts index 56cab6e..a3a0cc9 100644 --- a/src/typings/UserJS.d.ts +++ b/src/typings/UserJS.d.ts @@ -1,7 +1,7 @@ // import GM from '@types/greasemonkey'; // import '@types/tampermonkey'; import '@violentmonkey/types'; -import { type GSForkQuery } from './types'; +import { config, FilterLayout, UserJSEngine, type GSForkQuery } from './types'; import './scheduler'; /** [i18n directory](https://github.com/magicoflolis/Userscript-Plus/tree/master/src/_locales) */ @@ -14,20 +14,32 @@ export const translations: { export const main_css: string; export interface safeHandles { - // trustedTypes: { - // createPolicy(): void; - // }; XMLHttpRequest: typeof XMLHttpRequest; CustomEvent: typeof CustomEvent; + customElements: typeof customElements; createElement: typeof document.createElement; createElementNS: typeof document.createElementNS; createTextNode: typeof document.createTextNode; setTimeout: typeof setTimeout; clearTimeout: typeof clearTimeout; navigator: typeof navigator; + // customElements: typeof customElements; /** Taken from [scheduler-polyfill](https://github.com/GoogleChromeLabs/scheduler-polyfill) */ scheduler: typeof scheduler; - groupBy(arr: O[], callback: (this: F, element: O, index: number) => any): O; + /** + * Groups members of an iterable according to the return value of the passed callback. + * @param items An iterable. + * @param keySelector A callback which will be invoked for each item in items. + */ + groupBy( + items: Iterable, + keySelector: (item: T, index: number) => K + ): Partial>; +} + +export class Safe { + public constructor(); + public _self: safeHandles | null; } export interface Translations { @@ -196,37 +208,163 @@ export interface Network { ): Promise>; bscStr(str: S, lowerCase: boolean): S; } - -declare class Timeout { +//#region Testing crap +export class Timeout { + public constructor(); public ids: number[]; - public set(delay: number, reason: any): Promise; + public set(delay: number, reason?: R): Promise; public clear(...ids: number[]): void; } +export class Tabs { + public constructor(root: HTMLElement); + public pool: Set; + public blank: 'about:blank'; + public protocal: 'mujs:'; + public protoReg: RegExp; + public el: { + add: HTMLElement; + head: HTMLElement; + root: HTMLElement; + }; + public custom: () => void; + public getTab(hostname: S): HTMLElement | undefined; + public getActive(): HTMLElement | undefined; + public intFN(hostname: S): void; + public active(tab: T, build?: boolean): void; + public close(tab: T): void; + public create(hostname?: string): HTMLElement | undefined; +} + +export interface mujsName extends HTMLTableCellElement { + _mujs: { + fmore: HTMLElement; + fBtns: HTMLElement; + codeArea: HTMLTextAreaElement; + } +} + +export interface cfgpage extends HTMLElement { + _mujs: { + base: { + text: string; + tag: string; + value: string; + type: HTMLInputElement['type']; + attrs: object; + default: string | boolean | number | UserJSEngine | FilterLayout; + cache: string | boolean | number | UserJSEngine | FilterLayout; + elem: HTMLInputElement | HTMLSelectElement; + elemUrl?: HTMLInputElement; + elemToken?: HTMLInputElement; + }[]; + sections: Set; + } +} + +export class Container { + public static prompts: HTMLElement[]; + public webpage: URL; + public host: string; + public domain: string; + public ready: boolean; + public injected: boolean; + public shadowRoot?: ShadowRoot; + public shadowSupport: boolean; + public frame: HTMLElement; + public hostCache?: Map; + public userjsCache: Map; + public root: HTMLElement; + public unsaved: boolean; + public isBlacklisted: boolean; + public rebuild: boolean; + public opacityMin: string; + public opacityMax: string; + public elementsReady?: boolean; + public timeouts?: { + frame: Timeout; + mouse: Timeout; + }; + public Tabs?: Tabs; + public mainframe?: HTMLElement; + public countframe?: HTMLElement; + public mainbtn?: HTMLElement; + public urlBar?: HTMLInputElement; + public rateContainer?: HTMLElement; + public footer?: HTMLElement; + public tabbody?: HTMLElement; + public promptElem?: HTMLElement; + public toolbar?: HTMLElement; + public table?: HTMLTableElement; + public tabhead?: HTMLTableSectionElement; + public header?: HTMLElement; + public tbody?: HTMLTableSectionElement; + public cfgpage: cfgpage; + public main?: HTMLElement; + public urlContainer?: HTMLElement; + public btnframe?: HTMLElement; + public btnHandles?: HTMLElement; + public btnHide?: HTMLElement; + public btnfullscreen?: HTMLElement; + public closebtn?: HTMLElement; + public btncfg?: HTMLElement; + public btnhome?: HTMLElement; + public btnissue?: HTMLElement; + public btngreasy?: HTMLElement; + public btnnav?: HTMLElement; + public injFN?: () => void; + public inject(callback: (this: this, shadowRoot: this['shadowRoot']) => any, doc?: Document): void; + public initFn(): void; + public init(): boolean; + public remove(): void; + public save(): Promise; + /** + * @param css - CSS to inject + * @param name - Name of stylesheet + * @return Style element + */ + public loadCSS(css: C, name?: N): HTMLStyleElement | undefined; + public checkBlacklist(str: S): boolean; + public setTheme(): void; + public makePrompt(txt: S, dataset?: {}, usePrompt?: boolean): HTMLElement; + public showError(...ex: E[]): void; + public refresh(): void; + /** + * Redirects sleazyfork userscripts from greasyfork.org to sleazyfork.org + * + * Taken from: https://greasyfork.org/scripts/23840 + */ + public redirect(): void; + public timeoutFrame(time?: N): Promise; + public toElem(): HTMLElement[]; + public [Symbol.iterator](): Generator; +} -export interface Container { - webpage: URL; - host: string; - domain: string; - ready: boolean; - injected: boolean; - shadowRoot?: ShadowRoot | HTMLElement; - supported: boolean; - frame: HTMLElement | HTMLIFrameElement; - userjsCache: Map; - root: HTMLElement; - unsaved: boolean; - isBlacklisted: boolean; - rebuild: boolean; - opacityMin: string; - opacityMax: string; - Timeout: new () => Timeout; +export class List { + public constructor(hostname?: string); + private intEngines: UserJSEngine[]; + private intHost: string; + public dispatch(ujs: GSForkQuery): void; + public get engines(): UserJSEngine[]; + public get host(): string; + public setEngines(engines?: E[]): E[]; + public setHost(hostname: S): S; + public getDomain(str?: S): S; + public build(): void; + public sortRecords(): void; + public groupBy(): Record; + public [Symbol.iterator](): Generator; } +//#endregion declare global { let translations: { [i18n: string]: string; }; let userjs: { + /** + * this should always be `true` otherwise the script won't execute + */ + UserJS: boolean; isMobile?: boolean; }; } diff --git a/src/typings/types.d.ts b/src/typings/types.d.ts index 2442bd5..2ff990c 100644 --- a/src/typings/types.d.ts +++ b/src/typings/types.d.ts @@ -231,42 +231,48 @@ export type config = { }; }; -export declare function objToStr(obj: O): string; - +/** + * Object to `[object *]` + */ +export declare function objToStr(obj: any): string; /** * Object is typeof `RegExp` */ -export declare function isRegExp(obj: O): boolean; - +export declare function isRegExp(obj: any): obj is RegExp; +/** + * Object is typeof `HTMLElement` + */ +export declare function isHTML(obj: any): obj is HTMLElement; /** * Object is typeof `Element` */ -export declare function isElem(obj: O): boolean; +export declare function isElem(obj: any): obj is Element; /** * Object is typeof `object` / JSON Object */ -export declare function isObj(obj: O): boolean; +export declare function isObj(obj: any): obj is Object; /** * Object is typeof `Function` */ -export declare function isFN(obj: O): boolean; +export declare function isFN(obj: any): obj is Function; /** * Object is `null` or `undefined` */ -export declare function isNull(obj: O): boolean; +export declare function isNull(obj: any): obj is null; +export declare function isNull(obj: any): obj is undefined; /** - * Object is Blank + * Object is blank */ -export declare function isBlank(obj: O): boolean; +export declare function isBlank(obj: O): boolean; /** - * Object is Empty + * Object is empty */ -export declare function isEmpty(obj: O): boolean; +export declare function isEmpty(obj: O): boolean; /** * Type is not 100% accurate @@ -276,11 +282,6 @@ export declare function normalizeTarget( toQuery?: boolean, root?: Document | Element ): T[]; -// export declare function normalizeTarget( -// target: T, -// toQuery?: boolean, -// root?: Document | Element -// ): T[]; export declare function halt(evt: Event): void; @@ -367,6 +368,7 @@ export declare function make< tagName: K, cname?: | string + | string[] | { [key in A]: Record; }, @@ -400,15 +402,3 @@ export interface dom { has(target: T, token: string | string[]): boolean; }; } -export type cfgBase = { - text: string; - tag: string; - value: string; - type: HTMLInputElement['type']; - attrs: object; - default: string | boolean | number | UserJSEngine | FilterLayout; - cache: string | boolean | number | UserJSEngine | FilterLayout; - elem: HTMLInputElement | HTMLSelectElement; - elemUrl?: HTMLInputElement; - elemToken?: HTMLInputElement; -}[];