diff --git a/snippet/DescrGen.js b/snippet/DescrGen.js new file mode 100644 index 0000000..52e5fbe --- /dev/null +++ b/snippet/DescrGen.js @@ -0,0 +1,30 @@ +class DescrGen { + constructor(width = 79) { + this._lines = []; + this._width = width; + } + + addLine(line) { + this._lines.push(line); + } + + addCenterLine(line) { + const padding = Math.floor((this._width - line.length) / 2); + const centeredLine = ' '.repeat(padding) + line; + this._lines.push(centeredLine); + } + + addPadRight(field, padSize, padChar, sep, value) { + const paddedField = field + padChar.repeat(padSize - field.length); + const line = paddedField + sep + value; + this._lines.push(line); + } + + addSeparator(char = '-', length = this._width) { + this._lines.push(char.repeat(length)); + } + + generate() { + return this._lines.join('\n'); + } +} diff --git a/snippet/DescrGen.test.js b/snippet/DescrGen.test.js new file mode 100644 index 0000000..f2620f4 --- /dev/null +++ b/snippet/DescrGen.test.js @@ -0,0 +1,31 @@ +describe('DescrGen', () => { + test('should generate description file from various inputs', () => { + const descrGen = new DescrGen(79); + + descrGen.addSeparator('='); + descrGen.addCenterLine('This is a test description file.'); + descrGen.addSeparator('='); + + descrGen.addLine(''); + descrGen.addLine('Normal line'); + descrGen.addLine(''); + + descrGen.addPadRight('Field1', 20, '.', ': ', 'Value1'); + descrGen.addPadRight('LongerFieldName', 20, '.', ': ', 'Value2'); + + descrGen.addLine(''); + + descrGen.addSeparator(); + + expect(descrGen.generate()).toBe(`=============================================================================== + This is a test description file. +=============================================================================== + +Normal line + +Field1..............: Value1 +LongerFieldName.....: Value2 + +-------------------------------------------------------------------------------`); + }); +}); diff --git a/snippet/HookableValue.js b/snippet/HookableValue.js new file mode 100644 index 0000000..9ec5788 --- /dev/null +++ b/snippet/HookableValue.js @@ -0,0 +1,74 @@ +/** + * A class representing a value that can have hooks on change + * @template T The type of the value + */ +class HookableValue { + /** + * Constructor + * @param {string} name The name of the hook + * @param {T|null} defaultValue The default value + */ + constructor(name, defaultValue = null) { + this._name = name; + this._value = defaultValue; + this.callbacks = []; + } + + /** + * Sets the value and calls the hooks if the value changed + * + * @param {T} newValue The new value + * @returns {void} + */ + async setValue(newValue) { + const oldValue = this.value; + if (oldValue !== newValue) { + this._value = newValue; + for (const callback of this.callbacks) { + await callback(newValue, oldValue); + } + } + } + + /** + * Gets the value + * + * @returns {T} The current value + */ + getValue() { + return this._value; + } + + /** + * Register a callback to be called when the value changes + * @param {(newValue:T, oldValue:T)=>Promise} callback The callback (that may be async) + * @returns {()=>void} The unregister function + */ + register(callback) { + this.callbacks.push(callback); + return () => { + this.callbacks = this.callbacks.filter(cb => cb !== callback); + } + } + + /** + * Clears all registered callbacks + * @returns {void} + */ + clearCallbacks() { + this.callbacks = []; + } + + get value() { + return this.getValue(); + } + + set value(newValue) { + this.setValue(newValue); + } + + get name() { + return this._name; + } +} +/** @typedef {HookableValue} HookableValue */ \ No newline at end of file diff --git a/snippet/RegistrationManager.js b/snippet/RegistrationManager.js new file mode 100644 index 0000000..da4dd6c --- /dev/null +++ b/snippet/RegistrationManager.js @@ -0,0 +1,40 @@ +/** + * A simple class to manage the registration and cleanup of the different event listeners and mutations observers using the register pattern. + * + * This class is useful when several registrations need to be done at different times and cleaned up together. + */ +class RegistrationManager { + /** + * Create a new RegistrationManager instance. + * + * @param {Objet} options additional options + * @param {boolean} [options.autoCleanupOnAfterFirstCleanup=false] automatically call cleanup after the first call of cleanupAll (This instance of RegistrationManager will be only used once, but any late registration will be automatically cleaned up) + */ + constructor(options) { + this.cleanupFunctions = [] + this.options = options || {} + this.autoCleanupOnAfterFirstCleanup = this.options.autoCleanupOnAfterFirstCleanup || false + this.hasBeenCleanedUp = false + } + + /** + * Add a new cleanup function + * @param {() => void} cleanupFunction + */ + onRegistration(cleanupFunction) { + if (this.autoCleanupOnAfterFirstCleanup && this.hasBeenCleanedUp) { + cleanupFunction() + } else { + this.cleanupFunctions.push(cleanupFunction) + } + } + + /** + * Cleanup all the cleanup functions. + */ + cleanupAll() { + this.hasBeenCleanedUp = true + this.cleanupFunctions.forEach(cleanup => cleanup()) + this.cleanupFunctions.length = 0 + } +} diff --git a/snippet/addStyle.js b/snippet/addStyle.js new file mode 100644 index 0000000..0bade03 --- /dev/null +++ b/snippet/addStyle.js @@ -0,0 +1,29 @@ +/** + * Add a new css string to the page + * + * @param {string} styleText The CSS string to pass + * @returns {void} + */ +const addStyle = (() => { + let styleElement = null; + let styleContent = null; + + /** + * Add a new css string to the page + * + * @param {string} styleText The CSS string to pass + * @returns {void} + */ + return (styleText) => { + if (styleElement === null) { + styleElement = document.createElement('style'); + styleContent = ""; + document.head.appendChild(styleElement); + } else { + styleContent += "\n"; + } + + styleContent += styleText; + styleElement.textContent = styleContent; + }; +})(); diff --git a/snippet/bindOnChange.js b/snippet/bindOnChange.js new file mode 100644 index 0000000..dad061a --- /dev/null +++ b/snippet/bindOnChange.js @@ -0,0 +1,21 @@ +/** + * Bind an onChange handler an element. Returns uninstall handler + * + * @param {HTMLElement} element The element to bind the handler + * @param {()=>boolean|undefined} callback The onChange handler + * @returns {()=>{}} + */ +const bindOnChange = (element, callback) => { + const onChange = (e) => { + const result = callback() + if (result !== false) { + e.preventDefault() + e.stopImmediatePropagation() + } + } + element.addEventListener('change', onChange, true); + + return () => { + element.removeEventListener('change', onChange, true); + } +} diff --git a/snippet/bindOnClick.js b/snippet/bindOnClick.js new file mode 100644 index 0000000..669fc7e --- /dev/null +++ b/snippet/bindOnClick.js @@ -0,0 +1,30 @@ +/** + * Bind an onClick handler an element. Returns uninstall handler + * + * @param {HtmlElement} element The element to bind the handler + * @param {()=>boolean|undefined} callback The onClick handler + * @param {()=>boolean|undefined} callbackCtrl The onClick handler for ctrl+click + * @returns {()=>{}} + */ +const bindOnClick = (element, callback, callbackCtrl) => { + const onClick = (e) => { + let callbackToExecute = null; + if (e.ctrlKey && callbackCtrl) { + callbackToExecute = callbackCtrl; + } else { + callbackToExecute = callback; + } + if (callbackToExecute) { + const result = callbackToExecute() + if (result !== false) { + e.preventDefault() + e.stopImmediatePropagation() + } + } + } + element.addEventListener('click', onClick, true); + + return () => { + element.removeEventListener('click', onClick, true); + } +} diff --git a/snippet/capitalize.js b/snippet/capitalize.js new file mode 100644 index 0000000..338ab57 --- /dev/null +++ b/snippet/capitalize.js @@ -0,0 +1,7 @@ +/** + * Capitalizes the first letter of a string. + * + * @param {string} data The string to capitalize + * @returns {string} The capitalized string + */ +const capitalize = (data) => data.length > 0 ? data[0].toUpperCase() + data.slice(1) : ""; diff --git a/snippet/capitalize.test.js b/snippet/capitalize.test.js new file mode 100644 index 0000000..4e86e19 --- /dev/null +++ b/snippet/capitalize.test.js @@ -0,0 +1,12 @@ +describe('capitalize', () => { + test('should capitalize the first letter of a string', () => { + expect(capitalize('hello')).toBe('Hello'); + expect(capitalize('world')).toBe('World'); + expect(capitalize('javaScript')).toBe('JavaScript'); + expect(capitalize('a')).toBe('A'); + }); + + test('should handle empty strings', () => { + expect(capitalize('')).toBe(''); + }); +}); \ No newline at end of file diff --git a/snippet/cleanupString.js b/snippet/cleanupString.js new file mode 100644 index 0000000..30bda7b --- /dev/null +++ b/snippet/cleanupString.js @@ -0,0 +1,8 @@ +/** + * Cleanup string by removing leading and trailing whitespaces and replacing multiple whitespaces with a single whitespace + * + * @param {string} string The string to cleanup + * @returns {string} The cleaned string + */ +const cleanupString = (string) => string?.trim()?.replace(/\s+/g, ' ') + diff --git a/snippet/cleanupString.test.js b/snippet/cleanupString.test.js new file mode 100644 index 0000000..176bc0a --- /dev/null +++ b/snippet/cleanupString.test.js @@ -0,0 +1,13 @@ +describe('cleanupString', () => { + test('should cleanup string by removing leading and trailing whitespaces and replacing multiple whitespaces with a single whitespace', () => { + expect(cleanupString(' hello ')).toBe('hello'); + expect(cleanupString(' world ')).toBe('world'); + expect(cleanupString(' hello world ')).toBe('hello world'); + expect(cleanupString(' javaScript ')).toBe('javaScript'); + expect(cleanupString(' a ')).toBe('a'); + }); + + test('should handle empty strings', () => { + expect(cleanupString('')).toBe(''); + }); +}); \ No newline at end of file diff --git a/snippet/computeSha256.js b/snippet/computeSha256.js new file mode 100644 index 0000000..047f8bd --- /dev/null +++ b/snippet/computeSha256.js @@ -0,0 +1,20 @@ +/** + * Compute the SHA-256 hash of the given data. + * + * @param {string|Uint8Array} data The data to hash + * @param {Object|null} options The options + * @param {string} options.encoding The encoding to use on the text data if provided + * @returns {Promise} The SHA-256 hash as a hexadecimal string + */ +const computeSha256 = async (data, options) => { + if (!options) { + options = {} + } + if (options.encoding) { + data = new TextEncoder(options.encoding).encode(data) + } + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return hashHex; +} diff --git a/snippet/copyTextToClipboard.js b/snippet/copyTextToClipboard.js new file mode 100644 index 0000000..78c1680 --- /dev/null +++ b/snippet/copyTextToClipboard.js @@ -0,0 +1,14 @@ +/** + * Copy some text to clipboard + * + * @param {string} text text to copy to clipboard + * @returns + */ +const copyTextToClipboard = async (text) => { + if (!navigator.clipboard) { + console.log(`Can't copy [${test}] : No navigator.clipboard API`) + return; + } + + await navigator.clipboard.writeText(text) +} diff --git a/snippet/counter.js b/snippet/counter.js new file mode 100644 index 0000000..127f332 --- /dev/null +++ b/snippet/counter.js @@ -0,0 +1,12 @@ +/** + * Create a counter generator starting from "start" using a "step" + * @param {number} start + * @param {number} step + */ +const counter = function* (start, step) { + let value = start; + while (true) { + yield value; + value+=step; + } +} diff --git a/snippet/counter.test.js b/snippet/counter.test.js new file mode 100644 index 0000000..236c142 --- /dev/null +++ b/snippet/counter.test.js @@ -0,0 +1,24 @@ +describe('counter', () => { + test('should generate numbers starting from start with step increments', () => { + const c = counter(5, 3); + expect(c.next().value).toBe(5); + expect(c.next().value).toBe(8); + expect(c.next().value).toBe(11); + expect(c.next().value).toBe(14); + }); + + test('should work with negative steps', () => { + const c = counter(10, -2); + expect(c.next().value).toBe(10); + expect(c.next().value).toBe(8); + expect(c.next().value).toBe(6); + expect(c.next().value).toBe(4); + }); + + test('should work with zero step', () => { + const c = counter(7, 0); + expect(c.next().value).toBe(7); + expect(c.next().value).toBe(7); + expect(c.next().value).toBe(7); + }); +}); \ No newline at end of file diff --git a/snippet/createElementExtended.js b/snippet/createElementExtended.js new file mode 100644 index 0000000..c868b54 --- /dev/null +++ b/snippet/createElementExtended.js @@ -0,0 +1,81 @@ +/** + * Create a new element, and add some properties to it + * + * @param {string} name The name of the element to create + * @param {object} params The parameters to tweek the new element + * @param {object.} params.attributes The propeties of the new element + * @param {object.} params.style The style properties of the new element + * @param {string} params.text The textContent of the new element + * @param {HTMLElement[]} params.children The children of the new element + * @param {HTMLElement} params.parent The parent of the new element + * @param {string[]} params.classnames The classnames of the new element + * @param {string} params.id The classnames of the new element + * @param {HTMLElement} params.prevSibling The previous sibling of the new element (to insert after) + * @param {HTMLElement} params.nextSibling The next sibling of the new element (to insert before) + * @param {(element:HTMLElement)=>{}} params.onCreated called when the element is fully created + * @returns {HTMLElement} The created element + */ +const createElementExtended = (name, params) => { + /** @type{HTMLElement} */ + const element = document.createElement(name) + if (!params) { + params = {} + } + const { attributes, text, children, parent, prependIn, classnames, id, style, prevSibling, nextSibling, onCreated } = params + if (attributes) { + for (let attributeName in attributes) { + element.setAttribute(attributeName, attributes[attributeName]) + } + } + if (style) { + for (let key in style) { + element.style[key] = style[key]; + } + } + if (text) { + element.textContent = text; + } + if (children) { + const addChild = (child) => { + if (child) { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)) + } else if (Array.isArray(child)) { + for (let subChild of child) { + addChild(subChild) + } + } else { + element.appendChild(child) + } + } + } + + for (let child of children) { + addChild(child) + } + } + if (parent) { + parent.appendChild(element) + } + if (prependIn) { + prependIn.prepend(element) + } + if (classnames) { + for (let classname of classnames) { + element.classList.add(classname) + } + } + if (id) { + element.id = id + } + if (prevSibling) { + prevSibling.parentElement.insertBefore(element, prevSibling.nextSibling) + } + if (nextSibling) { + nextSibling.parentElement.insertBefore(element, nextSibling) + } + if (onCreated) { + onCreated(element) + } + return element +} diff --git a/snippet/delay.js b/snippet/delay.js new file mode 100644 index 0000000..966f14e --- /dev/null +++ b/snippet/delay.js @@ -0,0 +1,11 @@ +/** + * @param {Number} timeout The timeout in ms + * @param {Object} data The data to return after the timeout + * @returns + */ +const delay = (timeout, data) => + new Promise((resolve) => + setTimeout(() => + resolve(data), timeout + ) + ) diff --git a/snippet/downloadData.js b/snippet/downloadData.js new file mode 100644 index 0000000..b03aeaa --- /dev/null +++ b/snippet/downloadData.js @@ -0,0 +1,30 @@ +// @import{createElementExtended} + +/** + * Download data as a file + * + * @param {string} filename - The name of the file + * @param {string} data - The data to download + * @param {object} options - The options + * @param {string} options.mimetype - The mimetype of the data + * @param {string} options.encoding - The encoding to use on the text data if provided + */ +const downloadData = (filename, data, options) => { + if (!options) { + options = {} + } + let { mimetype, encoding } = options + if (!mimetype) { + mimetype = 'application/octet-stream' + } + if (encoding) { + data = new TextEncoder(encoding).encode(data) + } + const element = createElementExtended('a', { + attributes: { + href: URL.createObjectURL(new Blob([data], { type: mimetype })), + download: filename, + } + }) + element.click() +} diff --git a/snippet/downloadDataUrl.js b/snippet/downloadDataUrl.js new file mode 100644 index 0000000..1138acb --- /dev/null +++ b/snippet/downloadDataUrl.js @@ -0,0 +1,19 @@ +// @import{createElementExtended} + +/** + * Downloads a data url as a file + * + * @param {String} url The data url to download + * @param {String} filename The filename to use to save the file + * @returns + */ +const downloadDataUrl = async (url, filename) => { + const a = createElementExtended('a', { + attributes: { + href: url, + target: '_blank', + download: filename, + }, + }) + a.click() +} \ No newline at end of file diff --git a/snippet/downloadText.js b/snippet/downloadText.js new file mode 100644 index 0000000..c855cd2 --- /dev/null +++ b/snippet/downloadText.js @@ -0,0 +1,14 @@ +/** + * Download a text file with the given filename and content + * + * @param {string} fileName The text filename to download + * @param {string} content The text content as a string + * @param {Object} options The download options + * @param {string} option.mimeType The mime type of the text content + */ +const downloadText = async (fileName, content, options = {}) => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(new Blob([content], { type: options.mimeType || 'text/plain' })); + link.download = fileName; + link.click(); +} diff --git a/snippet/downloadUrl.js b/snippet/downloadUrl.js new file mode 100644 index 0000000..a55b783 --- /dev/null +++ b/snippet/downloadUrl.js @@ -0,0 +1,22 @@ +// @import{createElementExtended} + +/** + * Downloads a url as a file + * + * @param {String} url The url to download + * @param {String} filename The filename to use to save the file + * @returns + */ +const downloadUrl = async (url, filename) => { + const response = await fetch(url); + const blob = await response.blob(); + const urlContent = window.URL.createObjectURL(blob); + const a = createElementExtended('a', { + attributes: { + href: urlContent, + target: '_blank', + download: filename, + }, + }) + a.click() +} \ No newline at end of file diff --git a/snippet/downloadZip.js b/snippet/downloadZip.js new file mode 100644 index 0000000..1e7d148 --- /dev/null +++ b/snippet/downloadZip.js @@ -0,0 +1,24 @@ +// @require{https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js} +/** + * Download a zip file with the given filename and content provided by the async generator + * + * @param {string} fileName The zip filename to download + * @param {() => AsyncGenerator<{path: string, data: Uint8Array}>} contentProvider + */ +const downloadZip = async (fileName, contentProvider) => { + const zip = new JSZip(); + for await (const { path, data, date } of contentProvider()) { + const options = {}; + + if (date) { + options.date = date; + } + + zip.file(path, data, options); + } + const content = await zip.generateAsync({ type: "blob" }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(content); + link.download = fileName; + link.click(); +} diff --git a/snippet/getElements.js b/snippet/getElements.js new file mode 100644 index 0000000..0d9606c --- /dev/null +++ b/snippet/getElements.js @@ -0,0 +1,9 @@ +// @import{getSubElements} + +/** + * Request some elements from the current document + * + * @param {string} query The query + * @returns {[HtmlElement]} + */ +const getElements = (query) => getSubElements(document, query) diff --git a/snippet/getMiBSize.js b/snippet/getMiBSize.js new file mode 100644 index 0000000..de1171b --- /dev/null +++ b/snippet/getMiBSize.js @@ -0,0 +1,9 @@ +/** + * Get the size in Mo from bytes + * + * @param {Number} sizeInBytes The size in bytes + * @returns {string} The size in MiB + */ +const getMiBSize = (sizeInBytes) => { + return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MiB`; +} \ No newline at end of file diff --git a/snippet/getMiBSize.test.js b/snippet/getMiBSize.test.js new file mode 100644 index 0000000..1df0445 --- /dev/null +++ b/snippet/getMiBSize.test.js @@ -0,0 +1,14 @@ +describe('getMiBSize', () => { + const testExample = (input, expected) => { + test(`should convert ${input} bytes to "${expected}"`, () => { + expect(getMiBSize(input)).toBe(expected); + }); + } + testExample(1048576, '1.00 MiB'); // 1 MB + testExample(5242880, '5.00 MiB'); // 5 MB + testExample(15728640, '15.00 MiB'); // 15 MB + testExample(123456789, '117.74 MiB'); // ~117.74 MB + testExample(0, '0.00 MiB'); // 0 MB + testExample(100000, '0.10 MiB'); // 0.10 MB + testExample(700000, '0.67 MiB'); // 0.67 MB +}); \ No newline at end of file diff --git a/snippet/getSlug.js b/snippet/getSlug.js new file mode 100644 index 0000000..23784f0 --- /dev/null +++ b/snippet/getSlug.js @@ -0,0 +1,20 @@ +/** + * Get a slug from a name removing diacritics and special characters + * @param {string} name The name to convert + * @returns {string} The slugified version of the name + */ +const getSlug = (name, options = {}) => { + let currentName = name; + if (! options.case) { + currentName = currentName.toLocaleLowerCase(); + } + + currentName = currentName.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Remove diacritics + currentName = currentName.replace(/ß/g, "ss"); // Special case for German sharp S + currentName = currentName.replace(/[^A-Za-z0-9]/g, '-').replace(/--+/g, '-').replace(/^-+|-+$/g, ''); + + if (options.separator) { + currentName = currentName.replace(/-/g, options.separator); + } + return currentName; +} diff --git a/snippet/getSlug.test.js b/snippet/getSlug.test.js new file mode 100644 index 0000000..76b65db --- /dev/null +++ b/snippet/getSlug.test.js @@ -0,0 +1,36 @@ +describe('getSlug', () => { + const testExample = (input, expected, options) => { + test(`should convert "${input}" (with options: ${JSON.stringify(options)}) to "${expected}"`, () => { + expect(getSlug(input, options)).toBe(expected); + }); + } + testExample('Hello World!', 'hello-world'); + testExample(' Leading and trailing spaces ', 'leading-and-trailing-spaces'); + testExample('Special #$&* Characters', 'special-characters'); + testExample('Multiple Spaces', 'multiple-spaces'); + testExample('UPPERCASE to lowercase', 'uppercase-to-lowercase'); + testExample('Accented éàü Characters', 'accented-eau-characters'); + testExample('Mixed-Separators_and Spaces', 'mixed-separators-and-spaces'); + testExample('', ''); + testExample('ßpecial', 'sspecial'); + + testExample('Hello World!', 'Hello-World', {case: true}); + testExample(' Leading and trailing spaces ', 'Leading-and-trailing-spaces', {case: true}); + testExample('Special #$&* Characters', 'Special-Characters', {case: true}); + testExample('Multiple Spaces', 'Multiple-Spaces', {case: true}); + testExample('UPPERCASE to lowercase', 'UPPERCASE-to-lowercase', {case: true}); + testExample('Accented éàü Characters', 'Accented-eau-Characters', {case: true}); + testExample('Mixed-Separators_and Spaces', 'Mixed-Separators-and-Spaces', {case: true}); + testExample('', ''); + testExample('ßpecial', 'sspecial', {case: true}); + + testExample('Hello World!', 'Hello.World', {case: true, separator: '.'}); + testExample(' Leading and trailing spaces ', 'Leading.and.trailing.spaces', {case: true, separator: '.'}); + testExample('Special #$&* Characters', 'Special.Characters', {case: true, separator: '.'}); + testExample('Multiple Spaces', 'Multiple.Spaces', {case: true, separator: '.'}); + testExample('UPPERCASE to lowercase', 'UPPERCASE.to.lowercase', {case: true, separator: '.'}); + testExample('Accented éàü Characters', 'Accented.eau.Characters', {case: true, separator: '.'}); + testExample('Mixed-Separators_and Spaces', 'Mixed.Separators.and.Spaces', {case: true, separator: '.'}); + testExample('', ''); + testExample('ßpecial', 'sspecial', {case: true}); +}) \ No newline at end of file diff --git a/snippet/getSsmHookableValue.js b/snippet/getSsmHookableValue.js new file mode 100644 index 0000000..669f609 --- /dev/null +++ b/snippet/getSsmHookableValue.js @@ -0,0 +1,15 @@ +// @import{HookableValue} +// @import{getSsmLocalState} +/** + * Gets a SSM hookable value + * @template T + * @param {string} localName The SSM local state name + * @param {string} name The hookable value name + * @param {T|null} defaultValue The default value + * @returns {HookableValue} The SSM hookable value + */ +const getSsmHookableValue = (localName, name, defaultValue = null) => { + const ssmLocalState = getSsmLocalState(localName); + ssmLocalState[name] = ssmLocalState[name] || new HookableValue(name, defaultValue); + return ssmLocalState[name]; +} diff --git a/snippet/getSsmHookableValue.test.js b/snippet/getSsmHookableValue.test.js new file mode 100644 index 0000000..515b5b5 --- /dev/null +++ b/snippet/getSsmHookableValue.test.js @@ -0,0 +1,48 @@ +unsafeWindow = {} + +describe('getSsmHookableValue', () => { + beforeEach(() => { + Object.keys(unsafeWindow).forEach((key)=>{ delete unsafeWindow[key]; }) + }); + + test('should return the same HookableValue instance for the same localName and name', () => { + const hv1 = getSsmHookableValue('local1', 'value1', 42); + const hv2 = getSsmHookableValue('local1', 'value1', 100); + expect(hv1).toBe(hv2); + expect(hv1.value).toBe(42); + }); + + test('should return different HookableValue instances for different names', () => { + const hv1 = getSsmHookableValue('local1', 'value1', 'foo'); + const hv2 = getSsmHookableValue('local1', 'value2', 'bar'); + expect(hv1).not.toBe(hv2); + expect(hv1.value).toBe('foo'); + expect(hv2.value).toBe('bar'); + }); + + test('should return different HookableValue instances for different localNames', () => { + const hv1 = getSsmHookableValue('local1', 'value1', true); + const hv2 = getSsmHookableValue('local2', 'value1', false); + expect(hv1).not.toBe(hv2); + expect(hv1.value).toBe(true); + expect(hv2.value).toBe(false); + }); + + test('should use null as default value if none is provided', () => { + const hv = getSsmHookableValue('local1', 'value1'); + expect(hv.value).toBeNull(); + }); + + test('should execute hook when value changes', () => { + const hv = getSsmHookableValue('local1', 'value1', 10); + let hookCalled = false; + hv.register((newValue, oldValue) => { + hookCalled = true; + expect(oldValue).toBe(10); + expect(newValue).toBe(20); + }); + expect(hookCalled).toBe(false); + hv.value = 20; + expect(hookCalled).toBe(true); + }); +}); \ No newline at end of file diff --git a/snippet/getSsmHookableValueMonkeyGetSet.js b/snippet/getSsmHookableValueMonkeyGetSet.js new file mode 100644 index 0000000..c109494 --- /dev/null +++ b/snippet/getSsmHookableValueMonkeyGetSet.js @@ -0,0 +1,18 @@ +// @import{getSsmHookableValue} +// @import{monkeyGetSetValue} +// @import{monkeySetValue} +/** + * Gets a SSM hookable value with monkey get/set integration + * @template T + * @param {string} localName The SSM local state name + * @param {string} name The hookable value name + * @param {T|null} defaultValue The default value + * @returns {HookableValue} The SSM hookable value + */ +const getSsmHookableValueMonkeyGetSet = (localName, name, defaultValue = null) => { + const hookableValue = getSsmHookableValue(localName, name, monkeyGetSetValue(name, defaultValue)); + hookableValue.register(async (newValue) => { + await monkeySetValue(name, newValue); + }); + return hookableValue; +} diff --git a/snippet/getSsmLocalState.js b/snippet/getSsmLocalState.js new file mode 100644 index 0000000..b313014 --- /dev/null +++ b/snippet/getSsmLocalState.js @@ -0,0 +1,12 @@ +// @import{getSsmState} + +/** + * Gets a local SSM state + * @param {string} localName The local state name + * @returns {Object} The local SSM state + */ +const getSsmLocalState = (localName) => { + const ssmState = getSsmState(); + ssmState[localName] = ssmState[localName] || {}; + return ssmState[localName]; +} diff --git a/snippet/getSsmState.js b/snippet/getSsmState.js new file mode 100644 index 0000000..c601df1 --- /dev/null +++ b/snippet/getSsmState.js @@ -0,0 +1,8 @@ +/** + * Gets the global SSM state + * @returns {Object} The SSM state + */ +const getSsmState = () => { + unsafeWindow.ssmState = unsafeWindow.ssmState || {}; + return unsafeWindow.ssmState; +} diff --git a/snippet/getSsmValue.js b/snippet/getSsmValue.js new file mode 100644 index 0000000..61f1867 --- /dev/null +++ b/snippet/getSsmValue.js @@ -0,0 +1,14 @@ +// @import{getSsmLocalState} +/** + * Gets a SSM const value + * @template T + * @param {string} localName The SSM local state name + * @param {string} name The hookable value name + * @param {()=>T} defaultValueGenerator The const value + * @returns {T} The const value + */ +const getSsmValue = (localName, name, defaultValueGenerator) => { + const ssmLocalState = getSsmLocalState(localName); + ssmLocalState[name] = ssmLocalState[name] || defaultValueGenerator(); + return ssmLocalState[name]; +} diff --git a/snippet/getSubElements.js b/snippet/getSubElements.js new file mode 100644 index 0000000..9130a31 --- /dev/null +++ b/snippet/getSubElements.js @@ -0,0 +1,8 @@ +/** + * Request some sub elements from an element + * + * @param {HTMLElement} element The element to query + * @param {string} query The query + * @returns {[HTMLElement]} + */ +const getSubElements = (element, query) => [...element.querySelectorAll(query)] diff --git a/snippet/monkeyGetSetValue.js b/snippet/monkeyGetSetValue.js new file mode 100644 index 0000000..95b52b4 --- /dev/null +++ b/snippet/monkeyGetSetValue.js @@ -0,0 +1,17 @@ +// @grant{GM_getValue} +// @grant{GM_setValue} +/** + * Récupère ou définit une valeur dans le stockage de Tampermonkey/Greasemonkey/Violentmonkey/etc. + * + * @param {String} key La clé de la valeur à récupérer ou définir + * @param {String} value La valeur par défaut à définir si la clé n'existe pas (optionnel) + * @returns {String} La valeur récupérée + */ +const monkeyGetSetValue = (key, value) => { + const storedValue = GM_getValue(key); + if (storedValue === undefined && value !== undefined) { + GM_setValue(key, value); + return value; + } + return storedValue; +} diff --git a/snippet/monkeySetValue.js b/snippet/monkeySetValue.js new file mode 100644 index 0000000..22e6557 --- /dev/null +++ b/snippet/monkeySetValue.js @@ -0,0 +1,9 @@ +// @grant{GM_setValue} +/** + * Définit une valeur dans le stockage de Tampermonkey/Greasemonkey/Violentmonkey/etc. + * Juste un alias pour GM_setValue, pour etre cohérent avec monkeyGetSetValue. + * + * @param {String} key La clé de la valeur à définir + * @param {String} value La valeur à définir + */ +const monkeySetValue = (key, value) => GM_setValue(key, value); diff --git a/snippet/onHashChanged.js b/snippet/onHashChanged.js new file mode 100644 index 0000000..9ec6739 --- /dev/null +++ b/snippet/onHashChanged.js @@ -0,0 +1,57 @@ +const {setHashChanged, setLocationHash, getCurrentLocationHash} = (() => { + /** + * A callback that should be called each time the hash has changed, or the first time the callback is installed + * @type {(hash: string) => void} + */ + let onHashChanged = null; + + /** + * The current hash in the page + * @type {string} + */ + let currentHash = null; + + /** + * Force execute the callback "onHashChanged" if any + */ + const executeHashChanged = () => { + if (onHashChanged) { + onHashChanged(currentHash); + } + } + /** + * Update the current hash with a new value + * @param {string} hash The new value of the hash to set. + */ + const updateCurrentHash = (hash) => { + currentHash = hash; + executeHashChanged(); + } + /** + * Set the location hash, executing the callback only if the new hash is different from the old one. + * @param {string} hash + */ + const setLocationHash = (hash) => { + if (hash !== currentHash) { + location.hash = hash; + updateCurrentHash(location.hash); + } + } + // Install the hash change callback + window.onhashchange = () => { + if (location.hash !== currentHash) { + updateCurrentHash(location.hash); + } + }; + currentHash = location.hash; + /** + * Add a callback when the hash has changed. + * @param {(hash: string) => void} event The callback to call when the hash has changed + */ + const setHashChanged = (event) => { + onHashChanged = event; + executeHashChanged(); + } + const getCurrentLocationHash = () => currentHash; + return {setHashChanged, setLocationHash, getCurrentLocationHash}; +})(); diff --git a/snippet/openLinkInNewTab.js b/snippet/openLinkInNewTab.js new file mode 100644 index 0000000..0cda012 --- /dev/null +++ b/snippet/openLinkInNewTab.js @@ -0,0 +1,15 @@ +// @import{createElementExtended} + +/** + * Open a link in a new tab + * @param {string} url + */ +const openLinkInNewTab = (url) => { + const link = createElementExtended('a', { + attributes: { + href: url, + target: '_blank', + }, + }) + link.click(); +} \ No newline at end of file diff --git a/snippet/parentsElement.js b/snippet/parentsElement.js new file mode 100644 index 0000000..0e24c0e --- /dev/null +++ b/snippet/parentsElement.js @@ -0,0 +1,15 @@ +/** + * Get the depth'th parent of the element + * + * @param {HTMLElement} element + * @param {Number} depth + * @returns {HTMLElement} + */ +const parentsElement = (element, depth) => { + let current_element = element + for (let index = 0; index < depth; index++) { + current_element = current_element?.parentElement + } + return current_element +} + diff --git a/snippet/range.js b/snippet/range.js new file mode 100644 index 0000000..87269c7 --- /dev/null +++ b/snippet/range.js @@ -0,0 +1,39 @@ +/** + * Create a generator to simulate a range of numbers + * @param {number} start The first value of the range + * @param {number?} stop The stop value of the range (if any) + * @param {number?} step The step to use (if any) + * @returns + */ +const range = function* (start, stop, step) { + let cond = null; + if (stop === undefined) { + if (step === undefined) { + stop = start + start = 0 + } else { + cond = () => true; + } + } + if (step === undefined) { + if (start 0) { + cond = (i) => i i>stop; + } else { + return; + } + } + let i=start; + while (step > 0 ? istop ) { + yield i; + i+=step; + } +} diff --git a/snippet/registerDomNodeMutated.js b/snippet/registerDomNodeMutated.js new file mode 100644 index 0000000..b7110fe --- /dev/null +++ b/snippet/registerDomNodeMutated.js @@ -0,0 +1,24 @@ +/** + * Call the callback when the document change + * Handle the fact that the callback can't be called while aleady being called (no stackoverflow). + * Use the register pattern thus return the unregister function as a result + * @param {()=>()} callback + * @return {()=>{}} The unregister function + */ +const registerDomNodeMutated = (callback) => { + let callbackInProgress = false + + const action = () => { + if (!callbackInProgress) { + callbackInProgress = true + callback() + callbackInProgress = false + } + } + + const mutationObserver = new MutationObserver((mutationsList, observer) => { action() }); + action() + mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); + + return () => mutationObserver.disconnect() +} diff --git a/snippet/registerDomNodeMutatedUnique.js b/snippet/registerDomNodeMutatedUnique.js new file mode 100644 index 0000000..929d1ea --- /dev/null +++ b/snippet/registerDomNodeMutatedUnique.js @@ -0,0 +1,40 @@ +// @import{registerDomNodeMutated} +/** + * Call the callback once per element provided by the elementProvider when the document change + * Handle the fact that the callback can't be called while aleady being called (no stackoverflow). + * Use the register pattern thus return the unregister function as a result + * + * Ensure that when an element matching the query elementProvider, the callback is called with the element + * exactly once for each element + * @param {()=>[HTMLElement]} elementProvider + * @param {(element: HTMLElement, options: {currentIteration: number, indexElement: number})=>{}} callback + * @param {(element: HTMLElement, options: {currentIteration: number})=>{}} [callbackOnNotHere] called when an element is not here anymore (not provided by the elementProvider anymore) + */ +const registerDomNodeMutatedUnique = (elementProvider, callback, callbackOnNotHere) => { + const domNodesHandled = new Map() + let indexIteration = 0 + + return registerDomNodeMutated(() => { + indexIteration++; + let currentIteration = indexIteration + let indexElement = 0 + for (let element of elementProvider()) { + if (!domNodesHandled.has(element)) { + domNodesHandled.set(element, { element, indexIteration: currentIteration }) + const result = callback(element, { currentIteration, indexElement }) + if (result === false) { + domNodesHandled.delete(element) + } + } else { + domNodesHandled.get(element).indexIteration = currentIteration + } + indexElement++; + } + for (let item of domNodesHandled.values().filter(item => item.indexIteration !== currentIteration)) { + if (callbackOnNotHere) { + callbackOnNotHere(item.element, { currentIteration }) + } + domNodesHandled.delete(item.element) + } + }) +} diff --git a/snippet/registerEventListener.js b/snippet/registerEventListener.js new file mode 100644 index 0000000..cb2ca26 --- /dev/null +++ b/snippet/registerEventListener.js @@ -0,0 +1,24 @@ +/** + * Wrap addEventListener and removeEventListener using a pattern where the unregister function is returned + * + * @param {HTMLElement|EventTarget} element The object on which to register the event + * @param {string} eventType The event type + * @param {EventListenerOrEventListenerObject} callback The callback to call when the event is triggered + * @param {boolean|AddEventListenerOptions=} options The options to pass to addEventListener + * @return {()=>{}} The unregister function + */ +const registerEventListener = (element, eventType, callback, options) => { + if (element.addEventListener) { + element.addEventListener(eventType, callback, options); + if (typeof options === 'object' && !Array.isArray(options) && options !== null) { + if (options.executeAtRegister) { + setTimeout(()=>callback(),0) + } + } + } + return () => { + if (element.removeEventListener) { + element.removeEventListener(eventType, callback, options); + } + } +} diff --git a/snippet/registerLocationChange.js b/snippet/registerLocationChange.js new file mode 100644 index 0000000..fdf092d --- /dev/null +++ b/snippet/registerLocationChange.js @@ -0,0 +1,37 @@ +/** + * Registers a callback to be called when the location changes (SPA navigation) + * + * @param {(Location)=>void} callback A callback called when the location changes + * @returns {()=>void} The unregister function + */ +const registerLocationChange = (callback) => { + const normalizeLocation = (location) => { + const { href, origin, protocol, host, hostname, port, pathname, search, hash } = location; + const pathParts = pathname.split('/') + if (pathParts.length > 0 && pathParts[0] === '') { + pathParts.shift(); + } + const isFolder = pathParts.length === 0 || pathParts[pathParts.length - 1] === ''; + if (isFolder) { + pathParts.pop(); + } + + return { href, origin, protocol, host, hostname, port, pathname, pathParts, isFolder, search, hash }; + } + let currentLocation = normalizeLocation(location); + + const observer = new MutationObserver(() => { + const newLocation = normalizeLocation(location); + if (newLocation.href !== currentLocation.href) { + currentLocation = newLocation; + callback(currentLocation); + } + }); + + callback(currentLocation); + observer.observe(document, { subtree: true, childList: true }); + + return () => { + observer.disconnect(); + }; +} diff --git a/snippet/setSsmValue.js b/snippet/setSsmValue.js new file mode 100644 index 0000000..0b3db25 --- /dev/null +++ b/snippet/setSsmValue.js @@ -0,0 +1,12 @@ +// @import{getSsmLocalState} +/** + * Sets a SSM const value + * @template T + * @param {string} localName The SSM local state name + * @param {string} name The name of the value + * @param {T|null} value The const value + */ +const setSsmValue = (localName, name, value = null) => { + const ssmLocalState = getSsmLocalState(localName); + ssmLocalState[name] = ssmLocalState[name] || value; +} diff --git a/src/common.props.json b/src/common.props.json new file mode 100644 index 0000000..a1abf23 --- /dev/null +++ b/src/common.props.json @@ -0,0 +1,9 @@ +{ + "namespace": "https://github.com/kabertools/userscripts/", + "author": "kaberly", + "homepage": "https://github.com/kabertools/userscripts/", + "supportURL": "https://github.com/kabertools/userscripts/", + "grant": [ + "none" + ] +} diff --git a/src/la-cale/common.props.json b/src/la-cale/common.props.json new file mode 100644 index 0000000..693f221 --- /dev/null +++ b/src/la-cale/common.props.json @@ -0,0 +1,6 @@ +{ + "match": [ + "https://la-cale.space/*" + ], + "iconFromDomain" : "la-cale.space" +} \ No newline at end of file diff --git a/src/la-cale/la-cale-change-image-format-for-some-categories.user.js b/src/la-cale/la-cale-change-image-format-for-some-categories.user.js new file mode 100644 index 0000000..8b84684 --- /dev/null +++ b/src/la-cale/la-cale-change-image-format-for-some-categories.user.js @@ -0,0 +1,49 @@ +// @import{getElements} +// @import{getSubElements} +// @import{registerDomNodeMutatedUnique} +// @import{registerLocationChange} +// @import{RegistrationManager} +// @import{monkeyGetSetValue} + +const categoriesToChangeVertical = monkeyGetSetValue('categoriesToChangeVertical', ['BD','Comics','Divers','Livres','Mangas','Presse','Audiobooks']) +const categoriesToChangeSquare = monkeyGetSetValue('categoriesToChangeSquare', ['Musique','Podcast']) + +const categoriesToChange = [ + { + list: categoriesToChangeVertical, + newClass: 'aspect-[2/3]', + oldClass: 'aspect-video', + }, + { + list: categoriesToChangeSquare, + newClass: 'aspect-square', + oldClass: 'aspect-video', + }, +] + +const main = async () => { + const registrationManager = new RegistrationManager() + registerLocationChange((location) => { + registrationManager.cleanupAll() + + const pathParts = location.pathname.split('/') + if (pathParts.length === 3 && pathParts[1] === 'torrents') { + registrationManager.onRegistration(registerDomNodeMutatedUnique( + () => getElements('.flex.flex-wrap.items-center.gap-2.mb-3>div.bg-brand-primary'), + (node) => { + for (const { list, newClass, oldClass } of categoriesToChange) { + if (list.indexOf(node.textContent) > -1) { + const imageContainer = getElements(`.${oldClass}`)[0] + if (imageContainer) { + imageContainer.classList.remove(oldClass) + imageContainer.classList.add(newClass) + } + } + } + } + )) + } + }) +} + +main() diff --git a/src/la-cale/la-cale-easy-upload.user.js b/src/la-cale/la-cale-easy-upload.user.js new file mode 100644 index 0000000..2bfc572 --- /dev/null +++ b/src/la-cale/la-cale-easy-upload.user.js @@ -0,0 +1,191 @@ +// @import{registerLocationChange} +// @import{getSubElements} +// @import{getElements} +// @import{registerEventListener} +// @import{addStyle} +// @import{delay} +// @import{registerDomNodeMutatedUnique} +// @import{RegistrationManager} + +const setReactElementProperty = (element, eventName, propertyName, value) => { + const nativeElementPropertySetter = + Object.getOwnPropertyDescriptor( + element.__proto__, + propertyName + ).set; + + nativeElementPropertySetter.call(element, value); + + element.dispatchEvent(new Event(eventName, { bubbles: true })); +} +const setReactInputValue = (input, value) => setReactElementProperty(input, 'input', 'value', value) +const setReactTextareaValue = (textarea, value) => setReactElementProperty(textarea, 'input', 'value', value) +const setReactInputFiles = (input, files) => setReactElementProperty(input, 'change', 'files', files) + +const fileToFileList = (file) => { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + return dataTransfer.files; +} + +const getPrezContent = async (prezButton, prezZone) => { + prezButton.click(); + await delay(0) + let prezTextarea = getSubElements(prezZone, 'textarea')[0] + if (!prezTextarea) { + await delay(1000); + prezButton.click(); + await delay(0); + prezTextarea = getSubElements(prezZone, 'textarea')[0] + if (!prezTextarea) { + throw new Error(`Impossible de trouver la textarea de présentation`); + } + } + return prezTextarea; +} + +const showPrezContent = async (prezButton, prezZone) => { + let prezTextarea = getSubElements(prezZone, 'textarea')[0] + if (prezTextarea) { + prezButton.click(); + await delay(0); + } +} + +const registrationManager = new RegistrationManager() + +const installEasyUpload = async () => { + registerDomNodeMutatedUnique( + () => getElements('.p-6.pt-6'), + async (dropElement, { indexElement }) => { + if (indexElement === 0) { + const spacey2s = getElements('div.space-y-2'); + if (spacey2s.length !== 8) { + throw new Error(`Impossible de trouver les éléments de space (trouvé ${spacey2s.length} éléments, attendu 8)`); + } + const quaiZone = spacey2s[2] + const emplacementZone = spacey2s[3] + const titreZone = spacey2s[6] + const prezZone = spacey2s[7] + + console.log({ quaiZone, emplacementZone, titreZone, prezZone }) + + const prezButtons = getSubElements(prezZone, '.flex.items-center.gap-1 > button') + if (prezButtons.length !== 4) { + throw new Error(`Impossible de trouver les boutons de prez (trouvé ${prezButtons.length} éléments, attendu 4)`); + } + const prezButton = prezButtons[1] + + if (!prezButton) { + throw new Error(`Impossible de trouver le bouton de présentation`); + } + + const prezShowContentButton = prezButtons[3] + + if (!prezShowContentButton) { + throw new Error(`Impossible de trouver le bouton d'affichage de la présentation`); + } + + let prezTextarea = await getPrezContent(prezButton, prezZone); + + const titreInput = getSubElements(titreZone, 'input')[0] + + console.log({ prezButton, prezTextarea, titreInput }) + + const inputFiles = [...document.querySelectorAll('input[type=file]')] + if (inputFiles.length !== 2) { + throw new Error(`Impossible de trouver les champs de fichier (trouvé ${inputFiles.length} éléments, attendu 2)`); + } + const [inputFileTorrent, inputFileNfo] = inputFiles; + + const eventNames = ['dragenter', 'dragover', 'dragleave', 'drop']; + const actionsByEventName = { + dragenter: (element, eventName) => element.classList.add('x-dragover'), + dragover: (element, eventName) => element.classList.add('x-dragover'), + dragleave: (element, eventName) => element.classList.remove('x-dragover'), + drop: async (element, eventName, event) => { + element.classList.remove('x-dragover') + if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) { + const fileCount = event.dataTransfer.files.length; + console.log(`Fichiers déposés : ${fileCount}`); + let prezFile = null; + let tagsFile = null; + for (let i = 0; i < fileCount; i++) { + console.log(`Fichiers déposés : ${fileCount} (${i + 1}/${fileCount})`); + const filename = event.dataTransfer.files[i].name; + console.log(`Fichier déposé : ${filename}`); + if (filename.endsWith('.nfo')) { + setReactInputFiles(inputFileNfo, fileToFileList(event.dataTransfer.files[i])); + } else if (filename.endsWith('.torrent')) { + setReactInputFiles(inputFileTorrent, fileToFileList(event.dataTransfer.files[i])); + } else if (filename.endsWith('.prez')) { + prezFile = event.dataTransfer.files[i]; + const basename = filename.substring(0, filename.length - '.prez'.length); + setReactInputValue(titreInput, basename); + } else if (filename.endsWith('.tags')) { + tagsFile = event.dataTransfer.files[i]; + } + } + if (prezFile !== null) { + const prezContent = await prezFile.text(); + prezTextarea = await getPrezContent(prezButton, prezZone); + setReactTextareaValue(prezTextarea, prezContent); + await showPrezContent(prezShowContentButton, prezZone); + } + if (tagsFile !== null) { + const tagsContent = await tagsFile.text(); + console.log(`Contenu du fichier .tags : ${tagsContent}`); + const tags = tagsContent.split('\n').map(tag => tag.trim()) + console.log({ tags }); + + if (tags.length > 2) { + getSubElements(quaiZone, 'button')[0].click() + await delay(0) + let options = getElements('[data-radix-popper-content-wrapper] [role=option]') + options.filter(x => x.textContent === tags[0])[0].dispatchEvent(new Event('click', { bubbles: true })) + await delay(0) + getSubElements(emplacementZone, 'button')[0].click() + await delay(0) + options = getElements('[data-radix-popper-content-wrapper] [role=option]') + options.filter(x => x.textContent === tags[1])[0].dispatchEvent(new Event('click', { bubbles: true })) + await delay(0) + const groupButtonsToDisable = getElements('.group.cursor-pointer.border-brand-primary') + groupButtonsToDisable.forEach(x => x.click()) + const groupButtons = getElements('.group.cursor-pointer') + groupButtons.filter(x => tags.slice(2).map(x => x.toLocaleUpperCase()).indexOf(x.textContent.toLocaleUpperCase()) >= 0).forEach(x => x.click()) + } + } + } + }, + } + + eventNames.forEach((eventName) => { + const unreg = registerEventListener(dropElement, eventName, (event) => { + // console.log(`Event ${eventName} déclenché`); + event.preventDefault(); + event.stopPropagation(); + actionsByEventName[eventName](dropElement, eventName, event); + }); + registrationManager.onRegistration(unreg); + }); + } + } + ) +} + +const main = () => { + addStyle(`.x-dragover { background-color: rgba(255, 0, 0, 0.3) !important; }`); + registerLocationChange((currentLocation) => { + if (currentLocation.pathname === '/upload') { + try { + installEasyUpload(); + } catch (error) { + alert(`Une erreur est survenue : ${error.message}`); + } + } else { + registrationManager.cleanupAll(); + } + }); +} + +main() diff --git a/src/la-cale/la-cale-musardine-en-rose.user.js b/src/la-cale/la-cale-musardine-en-rose.user.js new file mode 100644 index 0000000..0bd8b3a --- /dev/null +++ b/src/la-cale/la-cale-musardine-en-rose.user.js @@ -0,0 +1,36 @@ +// @import{getElements} +// @import{getSubElements} +// @import{registerDomNodeMutatedUnique} +// @import{registerLocationChange} +// @import{RegistrationManager} + +const main = async () => { + const registrationManager = new RegistrationManager() + registerLocationChange((location) => { + registrationManager.cleanupAll() + + if (location.pathname === '/taverne') { + registrationManager.onRegistration(registerDomNodeMutatedUnique( + () => getElements('.flex-1.overflow-hidden'), + (node) => { + const userMessages = getSubElements(node, '[href="/profile/Musardine"]') + if (userMessages.length > 0) { + console.log({ userMessages }) + const nameElement = getSubElements(userMessages[0], 'span')[0] + if (nameElement) { + nameElement.style.color = 'rgb(223,42,132)' + const nextSibling = userMessages[0]?.nextElementSibling?.nextElementSibling + if (nextSibling) { + nextSibling.style.color = 'rgb(223,42,132)' + nextSibling.style.borderColor = 'rgb(223,42,132,0.45)' + nextSibling.style.backgroundColor = 'rgb(223,42,132,0.12)' + } + } + } + } + )) + } + }) +} + +main() \ No newline at end of file diff --git a/src/la-cale/la-cale-no-animated-message.user.css b/src/la-cale/la-cale-no-animated-message.user.css new file mode 100644 index 0000000..cf92cad --- /dev/null +++ b/src/la-cale/la-cale-no-animated-message.user.css @@ -0,0 +1,5 @@ +@-moz-document regexp("^https://(www.)?la-cale.space.*") { + body>div.animate-in { + display: none; + } +} diff --git a/src/website.css b/src/website.css new file mode 100644 index 0000000..58b7b7d --- /dev/null +++ b/src/website.css @@ -0,0 +1 @@ +/* Put here the custom css content you want for your generated user scripts/style website */ diff --git a/userscripts-manager b/userscripts-manager index 250e760..a227a8f 160000 --- a/userscripts-manager +++ b/userscripts-manager @@ -1 +1 @@ -Subproject commit 250e760e2f0dce4b83f340f132ad44f875beae37 +Subproject commit a227a8f72ec4e996379026e728bde388bbdf4f73