diff --git a/package-lock.json b/package-lock.json index a8193f4a..7c8deaf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "frankerfacez", - "version": "4.20.58", + "version": "4.20.59", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4695,6 +4695,12 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -5284,6 +5290,18 @@ "object.assign": "^4.1.0" } }, + "jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -5306,6 +5324,15 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", @@ -7753,6 +7780,12 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/package.json b/package.json index 2d7ff406..147194f6 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.59", + "version": "4.20.60", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { @@ -44,6 +44,7 @@ "extract-loader": "^2.0.1", "file-loader": "^4.1.0", "json-loader": "^0.5.7", + "jszip": "^3.6.0", "node-sass": "^4.14.1", "raw-loader": "^3.1.0", "rimraf": "^3.0.2", diff --git a/src/experiments.json b/src/experiments.json index 4b20e7e3..a2d5be28 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -1,4 +1,12 @@ { + "new_links": { + "name": "New Link Tokenization", + "description": "Update to Twitch's latest link regex. Experiment while this is checked for bugs.", + "groups": [ + {"value": true, "weight": 50}, + {"value": false, "weight": 50} + ] + }, "api_load": { "name": "New API Stress Testing", "description": "Send duplicate requests to the new API server for load testing.", diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 4b51e8f2..4d1e8ab0 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -13,6 +13,7 @@ import {CATEGORIES} from './emoji'; const EMOTE_CLASS = 'chat-image chat-line__message--emote', LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g, + NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g, //MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex @@ -146,6 +147,8 @@ export const Links = { if ( ! tokens || ! tokens.length ) return tokens; + const use_new = this.experiments.getAssignment('new_links'); + const out = []; for(const token of tokens) { if ( token.type !== 'text' ) { @@ -154,24 +157,43 @@ export const Links = { } LINK_REGEX.lastIndex = 0; + NEW_LINK_REGEX.lastIndex = 0; const text = token.text; let idx = 0, match; - while((match = LINK_REGEX.exec(text))) { - const nix = match.index + (match[1] ? match[1].length : 0); - if ( idx !== nix ) - out.push({type: 'text', text: text.slice(idx, nix)}); + if ( use_new ) { + while((match = NEW_LINK_REGEX.exec(text))) { + const nix = match.index; + if ( idx !== nix ) + out.push({type: 'text', text: text.slice(idx, nix)}); - const is_mail = ! match[3] && match[2].indexOf('/') === -1 && match[2].indexOf('@') !== -1; + out.push({ + type: 'link', + url: `${match[1] ? '' : 'https://'}${match[0]}`, + is_mail: false, + text: match[0] + }); - out.push({ - type: 'link', - url: (match[3] ? '' : is_mail ? 'mailto:' : 'https://') + match[2], - is_mail, - text: match[2] - }); + idx = nix + match[0].length; + } - idx = nix + match[2].length; + } else { + while((match = LINK_REGEX.exec(text))) { + const nix = match.index + (match[1] ? match[1].length : 0); + if ( idx !== nix ) + out.push({type: 'text', text: text.slice(idx, nix)}); + + const is_mail = ! match[3] && match[2].indexOf('/') === -1 && match[2].indexOf('@') !== -1; + + out.push({ + type: 'link', + url: (match[3] ? '' : is_mail ? 'mailto:' : 'https://') + match[2], + is_mail, + text: match[2] + }); + + idx = nix + match[2].length; + } } if ( idx < text.length ) diff --git a/src/modules/main_menu/components/backup-restore.vue b/src/modules/main_menu/components/backup-restore.vue index 203a0f99..fc52c023 100644 --- a/src/modules/main_menu/components/backup-restore.vue +++ b/src/modules/main_menu/components/backup-restore.vue @@ -66,11 +66,11 @@ export default { this.error = false; this.message = null; - let blob; + let file; try { - const settings = this.item.getFFZ().resolve('settings'), - data = await settings.getFullBackup(); - blob = new Blob([JSON.stringify(data)], {type: 'application/json;charset=utf-8'}); + const settings = this.item.getFFZ().resolve('settings'); + file = await settings.generateBackupFile(); + } catch(err) { this.error_desc = this.t('setting.backup-restore.dump-error', 'Unable to export settings data to JSON.'); this.error = true; @@ -78,7 +78,7 @@ export default { } try { - saveAs(blob, 'ffz-settings.json'); + saveAs(file, file.name); } catch(err) { this.error_desc = this.t('setting.backup-restore.save-error', 'Unable to save.'); } @@ -88,9 +88,22 @@ export default { this.error = false; this.message = null; + let file; + try { + file = await openFile('application/json,application/zip'); + } catch(err) { + this.error_desc = this.t('setting.backup-restore.read-error', 'Unable to read file.'); + this.error = true; + return; + } + + // We might get a different MIME than expected, roll with it. + if ( file.type.toLowerCase().includes('zip') ) + return this.restoreZip(file); + let contents; try { - contents = await readFile(await openFile('application/json')); + contents = await readFile(file); } catch(err) { this.error_desc = this.t('setting.backup-restore.read-error', 'Unable to read file.'); this.error = true; @@ -135,6 +148,101 @@ export default { this.message = this.t('setting.backup-restore.restored', '{count,number} items have been restored. Please refresh this page.', { count: i }); + }, + + async restoreZip(file) { + const JSZip = (await import(/* webpackChunkName: "zip" */ 'jszip')).default; + let input, blobs, data; + + try { + input = await (new JSZip().loadAsync(file)); + + blobs = await input.file('blobs.json').async('text'); + data = await input.file('settings.json').async('text'); + + } catch(err) { + this.error_desc = this.t('setting.backup-restore.zip-error', 'Unable to parse ZIP archive.'); + this.error = true; + } + + try { + blobs = JSON.parse(blobs); + data = JSON.parse(data); + } catch(err) { + this.error_desc = this.t('setting.backup-restore.json-error', 'Unable to parse file as JSON.'); + this.error = true; + return; + } + + if ( ! data || data.version !== 2 ) { + this.error_desc = this.t('setting.backup-restore.old-file', 'This file is invalid or was created in another version of FrankerFaceZ and cannot be loaded.'); + this.error = true; + return; + } + + if ( data.type !== 'full' ) { + this.error_desc = this.t('setting.backup-restore.non-full', 'This file is not a full backup and cannot be restored with this tool.'); + this.error = true; + return; + } + + const settings = this.item.getFFZ().resolve('settings'); + await settings.awaitProvider(); + const provider = settings.provider; + await provider.awaitReady(); + + if ( Object.keys(blobs).length && ! provider.supportsBlobs ) { + this.error_desc = this.t('setting.backup-restore.blob-error', 'This backup contains binary data not supported by the current storage provider. Please change your storage provider in Data Management > Storage >> Provider.'); + this.error = true; + return; + } + + // Attempt to load all the blobs, to make sure they're all valid. + const loaded_blobs = {}; + + for(const [safe_key, data] of Object.entries(blobs)) { + let blob; + if ( data.type === 'file' ) { + blob = await input.file(`blobs/${safe_key}`).async('blob'); // eslint-disable-line no-await-in-loop + blob = new File([blob], data.name, {lastModified: data.modified, type: data.mime}); + } else if ( data.type === 'blob' ) + blob = await input.file(`blobs/${safe_key}`).async('blob'); // eslint-disable-line no-await-in-loop + else if ( data.type === 'ab' ) + blob = await input.file(`blobs/${safe_key}`).async('arraybuffer'); // eslint-disable-line no-await-in-loop + else if ( data.type === 'ui8' ) + blob = await input.file(`blobs/${safe_key}`).async('uint8array'); // eslint-disable-line no-await-in-loop + else { + this.error_desc = this.t('setting.backup-restore.invalid-blob', 'This file contains a binary blob with an invalid type: {type}', data); + this.error = true; + } + + loaded_blobs[data.key] = blob; + } + + // We've loaded all data, let's get this installed. + // Blobs first. + let b = 0; + await provider.clearBlobs(); + + for(const [key, blob] of Object.entries(loaded_blobs)) { + await provider.setBlob(key, blob); // eslint-disable-line no-await-in-loop + b++; + } + + // Settings second. + provider.clear(); + let i = 0; + for(const key of Object.keys(data.values)) { + const val = data.values[key]; + provider.set(key, val); + provider.emit('changed', key, val, false); + i++; + } + + this.message = this.t('setting.backup-restore.zip-restored', '{count,number} items and {blobs,number} binary blobs have been restored. Please refresh this page.', { + count: i, + blobs: b + }); } } } diff --git a/src/modules/main_menu/components/clear-settings.vue b/src/modules/main_menu/components/clear-settings.vue index 953389cc..de529b42 100644 --- a/src/modules/main_menu/components/clear-settings.vue +++ b/src/modules/main_menu/components/clear-settings.vue @@ -134,81 +134,6 @@ export default { } this.running = false; - }, - - async backup() { - this.error = false; - this.message = null; - - let blob; - try { - const settings = this.item.getFFZ().resolve('settings'), - data = await settings.getFullBackup(); - blob = new Blob([JSON.stringify(data)], {type: 'application/json;charset=utf-8'}); - } catch(err) { - this.error_desc = this.t('setting.backup-restore.dump-error', 'Unable to export settings data to JSON.'); - this.error = true; - return; - } - - try { - saveAs(blob, 'ffz-settings.json'); - } catch(err) { - this.error_desc = this.t('setting.backup-restore.save-error', 'Unable to save.'); - } - }, - - async restore() { - this.error = false; - this.message = null; - - let contents; - try { - contents = await readFile(await openFile('application/json')); - } catch(err) { - this.error_desc = this.t('setting.backup-restore.read-error', 'Unable to read file.'); - this.error = true; - return; - } - - let data; - try { - data = JSON.parse(contents); - } catch(err) { - this.error_desc = this.t('setting.backup-restore.json-error', 'Unable to parse file as JSON.'); - this.error = true; - return; - } - - if ( ! data || data.version !== 2 ) { - this.error_desc = this.t('setting.backup-restore.old-file', 'This file is invalid or was created in another version of FrankerFaceZ and cannot be loaded.'); - this.error = true; - return; - } - - if ( data.type !== 'full' ) { - this.error_desc = this.t('setting.backup-restore.non-full', 'This file is not a full backup and cannot be restored with this tool.'); - this.error = true; - return; - } - - const settings = this.item.getFFZ().resolve('settings'), - provider = settings.provider; - - await provider.awaitReady(); - - provider.clear(); - let i = 0; - for(const key of Object.keys(data.values)) { - const val = data.values[key]; - provider.set(key, val); - provider.emit('changed', key, val, false); - i++; - } - - this.message = this.t('setting.backup-restore.restored', '{count,number} items have been restored. Please refresh this page.', { - count: i - }); } } } diff --git a/src/modules/main_menu/components/profile-manager.vue b/src/modules/main_menu/components/profile-manager.vue index c6a6b86d..296a1913 100644 --- a/src/modules/main_menu/components/profile-manager.vue +++ b/src/modules/main_menu/components/profile-manager.vue @@ -276,9 +276,20 @@ export default { async doImport() { this.resetImport(); - let contents; + let file, contents; try { - contents = await readFile(await openFile('application/json')); + file = await openFile('application/json,application/zip'); + + // We might get a different MIME than expected, roll with it. + if ( file.type.toLowerCase().includes('zip') ) { + const JSZip = (await import(/* webpackChunkName: "zip" */ 'jszip')).default, + zip = await (new JSZip().loadAsync(file)); + + contents = await zip.file('settings.json').async('text'); + + } else + contents = await readFile(file); + } catch(err) { this.import_error = true; this.import_error_message = this.t('setting.backup-restore.read-error', 'Unable to read file.'); diff --git a/src/modules/main_menu/components/provider.vue b/src/modules/main_menu/components/provider.vue index e0ca5f2f..43a0b037 100644 --- a/src/modules/main_menu/components/provider.vue +++ b/src/modules/main_menu/components/provider.vue @@ -38,6 +38,9 @@ {{ t('setting.provider.has-data', '(Has Data)') }} + + {{ t('setting.provider.has-blobs', '(Supports Binary Data)') }} + @@ -100,6 +103,7 @@ export default { const prov = { key, has_data: null, + has_blobs: val.supportsBlobs, i18n_key: `setting.provider.${key}.title`, title: val.title || key, desc_i18n_key: val.description ? `setting.provider.${key}.desc` : null, diff --git a/src/modules/main_menu/components/setting-hotkey.vue b/src/modules/main_menu/components/setting-hotkey.vue index 40f61fef..4e86b271 100644 --- a/src/modules/main_menu/components/setting-hotkey.vue +++ b/src/modules/main_menu/components/setting-hotkey.vue @@ -1,25 +1,47 @@ - - - {{ t(item.i18n_key, item.title) }} - - - - - - - - + + + {{ t(item.i18n_key, item.title) }} + {{ t('setting.new', 'New') }} + + + + + + + - - + + {{ sourceDisplay }} + + + + + + + {{ t('setting.reset', 'Reset to Default') }} + + + +import SettingMixin from '../setting-mixin'; + export default { + mixins: [SettingMixin], props: ['item', 'context'], methods: { - onKey(e) { - const name = `${e.ctrlKey ? 'Ctrl-' : ''}${e.shiftKey ? 'Shift-' : ''}${e.altKey ? 'Alt-' : ''}${e.code}`; - this.$refs.display.innerText = name; + onInput(value) { + this.set(value); } } } diff --git a/src/modules/main_menu/provider-mixin.js b/src/modules/main_menu/provider-mixin.js index 3b6e0107..28709616 100644 --- a/src/modules/main_menu/provider-mixin.js +++ b/src/modules/main_menu/provider-mixin.js @@ -90,7 +90,7 @@ export default { this.value = deep_copy(value); if ( this.item.onUIChange ) - this.item.onUIChange(this.value); + this.item.onUIChange(this.value, this); }, clear() { @@ -102,7 +102,7 @@ export default { this.has_value = false; if ( this.item.onUIChange ) - this.item.onUIChange(this.value); + this.item.onUIChange(this.value, this); } } } \ No newline at end of file diff --git a/src/modules/main_menu/setting-mixin.js b/src/modules/main_menu/setting-mixin.js index 494ba339..5dddc055 100644 --- a/src/modules/main_menu/setting-mixin.js +++ b/src/modules/main_menu/setting-mixin.js @@ -92,6 +92,13 @@ export default { return this.source && this.sourceOrder < this.profileOrder; }, + isValid() { + if ( typeof this.item.validator === 'function' ) + return this.item.validator(this.value, this); + + return true; + }, + sourceOrder() { return this.source ? this.source.order : Infinity }, @@ -186,14 +193,14 @@ export default { this.profile.set(this.item.setting, value); if ( this.item.onUIChange ) - this.item.onUIChange(value); + this.item.onUIChange(value, this); }, clear() { this.profile.delete(this.item.setting); if ( this.item.onUIChange ) - this.item.onUIChange(this.value); + this.item.onUIChange(this.value, this); } } } \ No newline at end of file diff --git a/src/settings/index.js b/src/settings/index.js index 711581aa..d5289a43 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -215,8 +215,86 @@ export default class SettingsManager extends Module { // Backup and Restore // ======================================================================== - async getFullBackup() { + async generateBackupFile() { + if ( await this._needsZipBackup() ) { + const blob = await this._getZipBackup(); + return new File([blob], 'ffz-settings.zip', {type: 'application/zip'}); + } + + const settings = await this.getSettingsDump(); + return new File([JSON.stringify(settings)], 'ffz-settings.json', {type: 'application/json;charset=utf-8'}); + } + + + async _needsZipBackup() { // Before we do anything else, make sure the provider is ready. + await this.awaitProvider(); + await this.provider.awaitReady(); + + if ( ! this.provider.supportsBlobs ) + return false; + + const keys = await this.provider.blobKeys(); + return Array.isArray(keys) ? keys.length > 0 : false; + } + + + async _getZipBackup() { + // Before we do anything else, make sure the provider is ready. + await this.awaitProvider(); + await this.provider.awaitReady(); + + // Create our ZIP file. + const JSZip = (await import(/* webpackChunkName: "zip" */ 'jszip')).default, + out = new JSZip(); + + // Normal Settings + const settings = await this.getSettingsDump(); + out.file('settings.json', JSON.stringify(settings)); + + // Blob Settings + const metadata = {}; + + if ( this.provider.supportsBlobs ) { + const keys = await this.provider.blobKeys(); + for(const key of keys) { + const safe_key = encodeURIComponent(key), + blob = await this.provider.getBlob(key); // eslint-disable-line no-await-in-loop + if ( ! blob ) + continue; + + const md = {key}; + + if ( blob instanceof File ) { + md.type = 'file'; + md.name = blob.name; + md.modified = blob.lastModified; + md.mime = blob.type; + + } else if ( blob instanceof Blob ) { + md.type = 'blob'; + + } else if ( blob instanceof ArrayBuffer ) { + md.type = 'ab'; + } else if ( blob instanceof Uint8Array ) { + md.type = 'ui8'; + } else + continue; + + metadata[safe_key] = md; + out.file(`blobs/${safe_key}`, blob); + } + } + + out.file('blobs.json', JSON.stringify(metadata)); + + return out.generateAsync({type: 'blob'}); + } + + + async getSettingsDump() { + // Before we do anything else, make sure the provider is ready. + await this.awaitProvider(); await this.provider.awaitReady(); const out = { diff --git a/src/settings/providers.js b/src/settings/providers.js index f816c9d7..796072ba 100644 --- a/src/settings/providers.js +++ b/src/settings/providers.js @@ -10,6 +10,11 @@ import {has} from 'utilities/object'; const DB_VERSION = 1; +export function isValidBlob(blob) { + return blob instanceof Blob || blob instanceof File || blob instanceof ArrayBuffer || blob instanceof Uint8Array; +} + + // ============================================================================ // SettingsProvider // ============================================================================ @@ -37,6 +42,8 @@ export class SettingsProvider extends EventEmitter { return false; } + static supportsBlobs = false; + awaitReady() { if ( this.ready ) return Promise.resolve(); @@ -60,7 +67,9 @@ export class SettingsProvider extends EventEmitter { entries() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this get size() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this - get supportsBlobs() { return false; } // eslint-disable-line class-methods-use-this + get supportsBlobs() { return this.constructor.supportsBlobs; } // eslint-disable-line class-methods-use-this + + isValidBlob(blob) { return this.supportsBlobs && isValidBlob(blob) } async getBlob(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await async setBlob(key, value) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await @@ -384,7 +393,9 @@ export class IndexedDBProvider extends SettingsProvider { static title = 'IndexedDB'; static description = '[IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) is available on most platforms, and has a slightly slower initialization time than Local Storage. IndexedDB has a higher storage capacity and is less likely to be cleared unexpectedly.'; - get supportsBlobs() { return true; } // eslint-disable-line class-methods-use-this + static supportsBlobs = true; + + //get supportsBlobs() { return true; } // eslint-disable-line class-methods-use-this destroy() { this.disable(); @@ -812,6 +823,9 @@ export class IndexedDBProvider extends SettingsProvider { if ( this.disabled ) return; + if ( ! this.isValidBlob(value) ) + throw new Error('Invalid blob type'); + const db = await this.getDB(), trx = db.transaction(['blobs'], 'readwrite'), store = trx.objectStore('blobs'); diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js index 9272acad..19d27a26 100644 --- a/src/sites/twitch-twilight/modules/chat/scroller.js +++ b/src/sites/twitch-twilight/modules/chat/scroller.js @@ -238,6 +238,7 @@ export default class Scroller extends Module { inst.ffz_oldScrollEvent = inst.handleScrollEvent; inst.ffz_oldScroll = inst.scrollToBottom; + inst.ffz_acting = false; inst.ffz_outside = true; inst._ffz_accessor = `_ffz_contains_${last_id++}`; @@ -441,6 +442,20 @@ export default class Scroller extends Module { // Keyboard Stuff + cls.prototype.ffzUpdateActing = function() { + if ( ! this._ffz_key_frame_acting ) + this._ffz_key_frame_acting = requestAnimationFrame(() => this.ffz_updateActing()); + } + + cls.prototype.ffz_updateActing = function() { + this._ffz_key_frame_acting = null; + + if ( ! this.scrollRef?.root ) + return; + + this.scrollRef.root.dataset.acting = this.ffz_acting; + } + cls.prototype.ffzUpdateKeyTags = function() { if ( ! this._ffz_key_frame ) this._ffz_key_frame = requestAnimationFrame(() => this.ffz_updateKeyTags()); @@ -488,6 +503,7 @@ export default class Scroller extends Module { require_hover = t.pause_hover; return (! require_hover || ! this.ffz_outside) && ( + (this.ffz_acting) || (this.ffz_ctrl && (mode === 2 || mode === 6)) || (this.ffz_meta && (mode === 3 || mode === 7)) || (this.ffz_alt && (mode === 4 || mode === 8)) || @@ -526,15 +542,16 @@ export default class Scroller extends Module { msg = t.i18n.t('chat.messages-below', 'Chat Paused Due to Scroll'); else if ( this.state.isPaused ) { const f = this.ffzGetMode(), - reason = f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') : - f === 3 ? t.i18n.t('key.meta', 'Meta Key') : - f === 4 ? t.i18n.t('key.alt', 'Alt Key') : - f === 5 ? t.i18n.t('key.shift', 'Shift Key') : - f === 6 ? t.i18n.t('key.ctrl_mouse', 'Ctrl or Mouse') : - f === 7 ? t.i18n.t('key.meta_mouse', 'Meta or Mouse') : - f === 8 ? t.i18n.t('key.alt_mouse', 'Alt or Mouse') : - f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') : - t.i18n.t('key.mouse', 'Mouse Movement'); + reason = this.ffz_acting ? t.i18n.t('chat.acting', 'Taking Action') : + f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') : + f === 3 ? t.i18n.t('key.meta', 'Meta Key') : + f === 4 ? t.i18n.t('key.alt', 'Alt Key') : + f === 5 ? t.i18n.t('key.shift', 'Shift Key') : + f === 6 ? t.i18n.t('key.ctrl_mouse', 'Ctrl or Mouse') : + f === 7 ? t.i18n.t('key.meta_mouse', 'Meta or Mouse') : + f === 8 ? t.i18n.t('key.alt_mouse', 'Alt or Mouse') : + f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') : + t.i18n.t('key.mouse', 'Mouse Movement'); msg = t.i18n.t('chat.paused', 'Chat Paused Due to {reason}', {reason}); cls = 'ffz--freeze-indicator'; diff --git a/src/sites/twitch-twilight/modules/theme/index.js b/src/sites/twitch-twilight/modules/theme/index.js index 38e0d0bb..2b3c5f48 100644 --- a/src/sites/twitch-twilight/modules/theme/index.js +++ b/src/sites/twitch-twilight/modules/theme/index.js @@ -23,8 +23,10 @@ const COLORS = [ const ACCENT_COLORS = { //dark: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-selected':9,'background-interactable-hover':8,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'/*,'text-tooltip':1*/},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}}, //light: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-selected':9,'background-interactable-hover':8,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}}, - dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}}, - light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}}, + //dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}}, + //light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}}, + dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':10,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':10,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':10,'border-input-checkbox-focus':10,'border-input-focus':10,'border-interactable-selected':10,'border-range-handle':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-checked':10,'border-toggle-focus':10,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'text-toggle-checked-icon':10,'text-tooltip':1,'text-button-text':10},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[8,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 0',''],'tab-focus':[11,' 0 4px 6px -4px','']}}, + light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-range-handle':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-checked':9,'border-toggle-focus':9,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8,'text-toggle-checked-icon':9,'text-tooltip':1,'text-button-text':8,'background-tooltip':1,'text-button-text-focus':'o1','text-button-text-hover':'o1'},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[10,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 1px',''],'tab-focus':[8,' 0 4px 6px -4px','']}}, accent_dark: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}}, accent_light: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}} }; diff --git a/src/std-components/key-picker.vue b/src/std-components/key-picker.vue new file mode 100644 index 00000000..a3db0e61 --- /dev/null +++ b/src/std-components/key-picker.vue @@ -0,0 +1,171 @@ + + + + + + {{ t('setting.record-key', 'Press a Key') }} + + + {{ t('setting.unset', 'Unset') }} + + + {{ value }} + + + + + + + {{ t('setting.clear', 'Clear') }} + + + + + + + \ No newline at end of file diff --git a/src/utilities/constants.js b/src/utilities/constants.js index ff5f55a0..1e5db5a9 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -20,6 +20,7 @@ export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/'; export const KEYS = { + Tab: 9, Enter: 13, Shift: 16, Control: 17, diff --git a/styles/widgets.scss b/styles/widgets.scss index 4bea6c30..a59bd404 100644 --- a/styles/widgets.scss +++ b/styles/widgets.scss @@ -4,6 +4,7 @@ @import "./widgets/menu-container.scss"; @import "./widgets/tab-container.scss"; +@import "./widgets/key-picker.scss"; @import "./widgets/menu-tree.scss"; @import "./widgets/profile-selector.scss"; @import "./widgets/badge-visibility.scss"; @@ -359,13 +360,18 @@ textarea.tw-input { background-color: transparent !important; color: #000 !important; - @include button-colors(#6441a4, #9a7fcc, #fff, #7d5bbc); + @include button-colors( + var(--color-background-button-hover), + var(--color-border-button-hover), + var(--color-text-button-hover), + var(--color-border-button-hover) + ); .tw-root--theme-dark & { border-color: #424242 !important; color: #dadada !important; - @include button-colors(#7d5bbe, #9a7fcc, #eeeeee, #7d5bbe) + //@include button-colors(#7d5bbe, #9a7fcc, #eeeeee, #7d5bbe) } } diff --git a/styles/widgets/container.scss b/styles/widgets/container.scss index 781a2824..ca677f29 100644 --- a/styles/widgets/container.scss +++ b/styles/widgets/container.scss @@ -37,6 +37,7 @@ $bg-dark: #17141f; } } + .default-dimmable, input, textarea, select { opacity: 0.5; diff --git a/styles/widgets/key-picker.scss b/styles/widgets/key-picker.scss new file mode 100644 index 00000000..99b765c4 --- /dev/null +++ b/styles/widgets/key-picker.scss @@ -0,0 +1,3 @@ +.ffz--key-widget { + min-width: 20rem; +} \ No newline at end of file