mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-07 14:50:56 +00:00
4.20.60
* Added: Backup and Restore now supports backing up any binary data stored in FFZ settings, creating a `zip` file rather than a `json` file. * Fixed: Remove dead code from the `clear-settings` menu component. * Changed: Update the theme mapping to include missing elements. * Changed: Data Management > Storage >> Provider now indicates if a provider supports storing binary data. * Changed: Update the link-parsing regex to match Twitch. Currently under limited roll-out while ensuring the implementation is bug-free. * API Added: `setting-hotkey` now functions as would be expected and can be used. * API Changed: A setting's `onUIChange` method now has the Vue component as its second argument, for getting any necessary state from the settings UI. * API Changed: Providers now sanity check the format of Blobs before storing them.
This commit is contained in:
parent
5412a928a1
commit
2c5937c8af
20 changed files with 574 additions and 138 deletions
35
package-lock.json
generated
35
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"version": "4.20.58",
|
"version": "4.20.59",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -4695,6 +4695,12 @@
|
||||||
"integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
|
"integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
|
||||||
"dev": true
|
"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": {
|
"import-fresh": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
|
||||||
|
@ -5284,6 +5290,18 @@
|
||||||
"object.assign": "^4.1.0"
|
"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": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
@ -5306,6 +5324,15 @@
|
||||||
"type-check": "~0.4.0"
|
"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": {
|
"linkify-it": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||||
|
@ -7753,6 +7780,12 @@
|
||||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
||||||
"dev": true
|
"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": {
|
"set-value": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.20.59",
|
"version": "4.20.60",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -44,6 +44,7 @@
|
||||||
"extract-loader": "^2.0.1",
|
"extract-loader": "^2.0.1",
|
||||||
"file-loader": "^4.1.0",
|
"file-loader": "^4.1.0",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
|
"jszip": "^3.6.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"raw-loader": "^3.1.0",
|
"raw-loader": "^3.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|
|
@ -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": {
|
"api_load": {
|
||||||
"name": "New API Stress Testing",
|
"name": "New API Stress Testing",
|
||||||
"description": "Send duplicate requests to the new API server for load testing.",
|
"description": "Send duplicate requests to the new API server for load testing.",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {CATEGORIES} from './emoji';
|
||||||
|
|
||||||
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
||||||
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
|
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 = /([^\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
|
MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
@ -146,6 +147,8 @@ export const Links = {
|
||||||
if ( ! tokens || ! tokens.length )
|
if ( ! tokens || ! tokens.length )
|
||||||
return tokens;
|
return tokens;
|
||||||
|
|
||||||
|
const use_new = this.experiments.getAssignment('new_links');
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
for(const token of tokens) {
|
for(const token of tokens) {
|
||||||
if ( token.type !== 'text' ) {
|
if ( token.type !== 'text' ) {
|
||||||
|
@ -154,24 +157,43 @@ export const Links = {
|
||||||
}
|
}
|
||||||
|
|
||||||
LINK_REGEX.lastIndex = 0;
|
LINK_REGEX.lastIndex = 0;
|
||||||
|
NEW_LINK_REGEX.lastIndex = 0;
|
||||||
const text = token.text;
|
const text = token.text;
|
||||||
let idx = 0, match;
|
let idx = 0, match;
|
||||||
|
|
||||||
while((match = LINK_REGEX.exec(text))) {
|
if ( use_new ) {
|
||||||
const nix = match.index + (match[1] ? match[1].length : 0);
|
while((match = NEW_LINK_REGEX.exec(text))) {
|
||||||
if ( idx !== nix )
|
const nix = match.index;
|
||||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
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({
|
idx = nix + match[0].length;
|
||||||
type: 'link',
|
}
|
||||||
url: (match[3] ? '' : is_mail ? 'mailto:' : 'https://') + match[2],
|
|
||||||
is_mail,
|
|
||||||
text: match[2]
|
|
||||||
});
|
|
||||||
|
|
||||||
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 )
|
if ( idx < text.length )
|
||||||
|
|
|
@ -66,11 +66,11 @@ export default {
|
||||||
this.error = false;
|
this.error = false;
|
||||||
this.message = null;
|
this.message = null;
|
||||||
|
|
||||||
let blob;
|
let file;
|
||||||
try {
|
try {
|
||||||
const settings = this.item.getFFZ().resolve('settings'),
|
const settings = this.item.getFFZ().resolve('settings');
|
||||||
data = await settings.getFullBackup();
|
file = await settings.generateBackupFile();
|
||||||
blob = new Blob([JSON.stringify(data)], {type: 'application/json;charset=utf-8'});
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.error_desc = this.t('setting.backup-restore.dump-error', 'Unable to export settings data to JSON.');
|
this.error_desc = this.t('setting.backup-restore.dump-error', 'Unable to export settings data to JSON.');
|
||||||
this.error = true;
|
this.error = true;
|
||||||
|
@ -78,7 +78,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saveAs(blob, 'ffz-settings.json');
|
saveAs(file, file.name);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.error_desc = this.t('setting.backup-restore.save-error', 'Unable to save.');
|
this.error_desc = this.t('setting.backup-restore.save-error', 'Unable to save.');
|
||||||
}
|
}
|
||||||
|
@ -88,9 +88,22 @@ export default {
|
||||||
this.error = false;
|
this.error = false;
|
||||||
this.message = null;
|
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;
|
let contents;
|
||||||
try {
|
try {
|
||||||
contents = await readFile(await openFile('application/json'));
|
contents = await readFile(file);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.error_desc = this.t('setting.backup-restore.read-error', 'Unable to read file.');
|
this.error_desc = this.t('setting.backup-restore.read-error', 'Unable to read file.');
|
||||||
this.error = true;
|
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.', {
|
this.message = this.t('setting.backup-restore.restored', '{count,number} items have been restored. Please refresh this page.', {
|
||||||
count: i
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,81 +134,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.running = false;
|
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,9 +276,20 @@ export default {
|
||||||
async doImport() {
|
async doImport() {
|
||||||
this.resetImport();
|
this.resetImport();
|
||||||
|
|
||||||
let contents;
|
let file, contents;
|
||||||
try {
|
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) {
|
} catch(err) {
|
||||||
this.import_error = true;
|
this.import_error = true;
|
||||||
this.import_error_message = this.t('setting.backup-restore.read-error', 'Unable to read file.');
|
this.import_error_message = this.t('setting.backup-restore.read-error', 'Unable to read file.');
|
||||||
|
|
|
@ -38,6 +38,9 @@
|
||||||
<span v-if="val.has_data" class="tw-mg-l-1 tw-c-text-alt">
|
<span v-if="val.has_data" class="tw-mg-l-1 tw-c-text-alt">
|
||||||
{{ t('setting.provider.has-data', '(Has Data)') }}
|
{{ t('setting.provider.has-data', '(Has Data)') }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="val.has_blobs" class="tw-mg-l-1 tw-c-text-alt">
|
||||||
|
{{ t('setting.provider.has-blobs', '(Supports Binary Data)') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<section v-if="val.description" class="tw-c-text-alt-2">
|
<section v-if="val.description" class="tw-c-text-alt-2">
|
||||||
<markdown :source="t(val.desc_i18n_key, val.description)" />
|
<markdown :source="t(val.desc_i18n_key, val.description)" />
|
||||||
|
@ -100,6 +103,7 @@ export default {
|
||||||
const prov = {
|
const prov = {
|
||||||
key,
|
key,
|
||||||
has_data: null,
|
has_data: null,
|
||||||
|
has_blobs: val.supportsBlobs,
|
||||||
i18n_key: `setting.provider.${key}.title`,
|
i18n_key: `setting.provider.${key}.title`,
|
||||||
title: val.title || key,
|
title: val.title || key,
|
||||||
desc_i18n_key: val.description ? `setting.provider.${key}.desc` : null,
|
desc_i18n_key: val.description ? `setting.provider.${key}.desc` : null,
|
||||||
|
|
|
@ -1,25 +1,47 @@
|
||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="ffz--widget ffz--hotkey-input">
|
<div
|
||||||
<label :for="item.full_key">
|
:class="{inherits: isInherited, default: isDefault}"
|
||||||
{{ t(item.i18n_key, item.title) }}
|
class="ffz--widget ffz--hotkey-input"
|
||||||
</label>
|
>
|
||||||
<div class="tw-relative">
|
<div class="tw-flex tw-align-items-center">
|
||||||
<div class="tw-input__icon-group tw-input__icon-group--right">
|
<label :for="item.full_key">
|
||||||
<div class="tw-input__icon">
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<figure class="ffz-i-keyboard" />
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
|
||||||
<div
|
<key-picker
|
||||||
:id="item.full_key"
|
:id="item.full_key"
|
||||||
ref="display"
|
ref="control"
|
||||||
type="text"
|
:value="value"
|
||||||
class="tw-mg-05 tw-input tw-input--icon-right"
|
@input="onInput"
|
||||||
tabindex="0"
|
/>
|
||||||
@keyup="onKey"
|
|
||||||
|
<component
|
||||||
|
:is="item.buttons"
|
||||||
|
v-if="item.buttons"
|
||||||
|
:context="context"
|
||||||
|
:item="item"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="source && source !== profile"
|
||||||
|
class="tw-mg-l-05 tw-button tw-button--text"
|
||||||
|
@click="context.currentProfile = source"
|
||||||
>
|
>
|
||||||
|
<span class="tw-button__text ffz-i-right-dir">
|
||||||
</div>
|
{{ sourceDisplay }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="has_value" class="tw-mg-l-05 tw-button tw-button--text tw-tooltip__container" @click="clear">
|
||||||
|
<span class="tw-button__text ffz-i-cancel" />
|
||||||
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||||
|
{{ t('setting.reset', 'Reset to Default') }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
|
@ -34,13 +56,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import SettingMixin from '../setting-mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [SettingMixin],
|
||||||
props: ['item', 'context'],
|
props: ['item', 'context'],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onKey(e) {
|
onInput(value) {
|
||||||
const name = `${e.ctrlKey ? 'Ctrl-' : ''}${e.shiftKey ? 'Shift-' : ''}${e.altKey ? 'Alt-' : ''}${e.code}`;
|
this.set(value);
|
||||||
this.$refs.display.innerText = name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ export default {
|
||||||
this.value = deep_copy(value);
|
this.value = deep_copy(value);
|
||||||
|
|
||||||
if ( this.item.onUIChange )
|
if ( this.item.onUIChange )
|
||||||
this.item.onUIChange(this.value);
|
this.item.onUIChange(this.value, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
@ -102,7 +102,7 @@ export default {
|
||||||
this.has_value = false;
|
this.has_value = false;
|
||||||
|
|
||||||
if ( this.item.onUIChange )
|
if ( this.item.onUIChange )
|
||||||
this.item.onUIChange(this.value);
|
this.item.onUIChange(this.value, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -92,6 +92,13 @@ export default {
|
||||||
return this.source && this.sourceOrder < this.profileOrder;
|
return this.source && this.sourceOrder < this.profileOrder;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
if ( typeof this.item.validator === 'function' )
|
||||||
|
return this.item.validator(this.value, this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
sourceOrder() {
|
sourceOrder() {
|
||||||
return this.source ? this.source.order : Infinity
|
return this.source ? this.source.order : Infinity
|
||||||
},
|
},
|
||||||
|
@ -186,14 +193,14 @@ export default {
|
||||||
this.profile.set(this.item.setting, value);
|
this.profile.set(this.item.setting, value);
|
||||||
|
|
||||||
if ( this.item.onUIChange )
|
if ( this.item.onUIChange )
|
||||||
this.item.onUIChange(value);
|
this.item.onUIChange(value, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.profile.delete(this.item.setting);
|
this.profile.delete(this.item.setting);
|
||||||
|
|
||||||
if ( this.item.onUIChange )
|
if ( this.item.onUIChange )
|
||||||
this.item.onUIChange(this.value);
|
this.item.onUIChange(this.value, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -215,8 +215,86 @@ export default class SettingsManager extends Module {
|
||||||
// Backup and Restore
|
// 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.
|
// 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();
|
await this.provider.awaitReady();
|
||||||
|
|
||||||
const out = {
|
const out = {
|
||||||
|
|
|
@ -10,6 +10,11 @@ import {has} from 'utilities/object';
|
||||||
const DB_VERSION = 1;
|
const DB_VERSION = 1;
|
||||||
|
|
||||||
|
|
||||||
|
export function isValidBlob(blob) {
|
||||||
|
return blob instanceof Blob || blob instanceof File || blob instanceof ArrayBuffer || blob instanceof Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SettingsProvider
|
// SettingsProvider
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
@ -37,6 +42,8 @@ export class SettingsProvider extends EventEmitter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static supportsBlobs = false;
|
||||||
|
|
||||||
awaitReady() {
|
awaitReady() {
|
||||||
if ( this.ready )
|
if ( this.ready )
|
||||||
return Promise.resolve();
|
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
|
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 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 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
|
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 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.';
|
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() {
|
destroy() {
|
||||||
this.disable();
|
this.disable();
|
||||||
|
@ -812,6 +823,9 @@ export class IndexedDBProvider extends SettingsProvider {
|
||||||
if ( this.disabled )
|
if ( this.disabled )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if ( ! this.isValidBlob(value) )
|
||||||
|
throw new Error('Invalid blob type');
|
||||||
|
|
||||||
const db = await this.getDB(),
|
const db = await this.getDB(),
|
||||||
trx = db.transaction(['blobs'], 'readwrite'),
|
trx = db.transaction(['blobs'], 'readwrite'),
|
||||||
store = trx.objectStore('blobs');
|
store = trx.objectStore('blobs');
|
||||||
|
|
|
@ -238,6 +238,7 @@ export default class Scroller extends Module {
|
||||||
inst.ffz_oldScrollEvent = inst.handleScrollEvent;
|
inst.ffz_oldScrollEvent = inst.handleScrollEvent;
|
||||||
inst.ffz_oldScroll = inst.scrollToBottom;
|
inst.ffz_oldScroll = inst.scrollToBottom;
|
||||||
|
|
||||||
|
inst.ffz_acting = false;
|
||||||
inst.ffz_outside = true;
|
inst.ffz_outside = true;
|
||||||
inst._ffz_accessor = `_ffz_contains_${last_id++}`;
|
inst._ffz_accessor = `_ffz_contains_${last_id++}`;
|
||||||
|
|
||||||
|
@ -441,6 +442,20 @@ export default class Scroller extends Module {
|
||||||
|
|
||||||
// Keyboard Stuff
|
// 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() {
|
cls.prototype.ffzUpdateKeyTags = function() {
|
||||||
if ( ! this._ffz_key_frame )
|
if ( ! this._ffz_key_frame )
|
||||||
this._ffz_key_frame = requestAnimationFrame(() => this.ffz_updateKeyTags());
|
this._ffz_key_frame = requestAnimationFrame(() => this.ffz_updateKeyTags());
|
||||||
|
@ -488,6 +503,7 @@ export default class Scroller extends Module {
|
||||||
require_hover = t.pause_hover;
|
require_hover = t.pause_hover;
|
||||||
|
|
||||||
return (! require_hover || ! this.ffz_outside) && (
|
return (! require_hover || ! this.ffz_outside) && (
|
||||||
|
(this.ffz_acting) ||
|
||||||
(this.ffz_ctrl && (mode === 2 || mode === 6)) ||
|
(this.ffz_ctrl && (mode === 2 || mode === 6)) ||
|
||||||
(this.ffz_meta && (mode === 3 || mode === 7)) ||
|
(this.ffz_meta && (mode === 3 || mode === 7)) ||
|
||||||
(this.ffz_alt && (mode === 4 || mode === 8)) ||
|
(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');
|
msg = t.i18n.t('chat.messages-below', 'Chat Paused Due to Scroll');
|
||||||
else if ( this.state.isPaused ) {
|
else if ( this.state.isPaused ) {
|
||||||
const f = this.ffzGetMode(),
|
const f = this.ffzGetMode(),
|
||||||
reason = f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') :
|
reason = this.ffz_acting ? t.i18n.t('chat.acting', 'Taking Action') :
|
||||||
f === 3 ? t.i18n.t('key.meta', 'Meta Key') :
|
f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') :
|
||||||
f === 4 ? t.i18n.t('key.alt', 'Alt Key') :
|
f === 3 ? t.i18n.t('key.meta', 'Meta Key') :
|
||||||
f === 5 ? t.i18n.t('key.shift', 'Shift Key') :
|
f === 4 ? t.i18n.t('key.alt', 'Alt Key') :
|
||||||
f === 6 ? t.i18n.t('key.ctrl_mouse', 'Ctrl or Mouse') :
|
f === 5 ? t.i18n.t('key.shift', 'Shift Key') :
|
||||||
f === 7 ? t.i18n.t('key.meta_mouse', 'Meta or Mouse') :
|
f === 6 ? t.i18n.t('key.ctrl_mouse', 'Ctrl or Mouse') :
|
||||||
f === 8 ? t.i18n.t('key.alt_mouse', 'Alt or Mouse') :
|
f === 7 ? t.i18n.t('key.meta_mouse', 'Meta or Mouse') :
|
||||||
f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') :
|
f === 8 ? t.i18n.t('key.alt_mouse', 'Alt or Mouse') :
|
||||||
t.i18n.t('key.mouse', 'Mouse Movement');
|
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});
|
msg = t.i18n.t('chat.paused', 'Chat Paused Due to {reason}', {reason});
|
||||||
cls = 'ffz--freeze-indicator';
|
cls = 'ffz--freeze-indicator';
|
||||||
|
|
|
@ -23,8 +23,10 @@ const COLORS = [
|
||||||
const ACCENT_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','']}},
|
//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','']}},
|
//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','']}},
|
//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','']}},
|
//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_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':{}}
|
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':{}}
|
||||||
};
|
};
|
||||||
|
|
171
src/std-components/key-picker.vue
Normal file
171
src/std-components/key-picker.vue
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<template lang="html">
|
||||||
|
<div class="ffz--key-widget">
|
||||||
|
<div class="tw-relative tw-full-width tw-mg-05">
|
||||||
|
<div
|
||||||
|
ref="input"
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="default-dimmable tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
|
||||||
|
tabindex="0"
|
||||||
|
@click="startRecording"
|
||||||
|
@keydown="onKey"
|
||||||
|
@keyup="onKeyUp"
|
||||||
|
@blur="cancelRecording"
|
||||||
|
>
|
||||||
|
<template v-if="active">
|
||||||
|
{{ t('setting.record-key', 'Press a Key') }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="value == null">
|
||||||
|
{{ t('setting.unset', 'Unset') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ value }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ffz-button--hollow ffz-clear-key tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-border-l tw-z-default tw-pd-x-1 tw-tooltip__container"
|
||||||
|
@click="clear"
|
||||||
|
>
|
||||||
|
<figure class="ffz-i-trash" />
|
||||||
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||||
|
{{ t('setting.clear', 'Clear') }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {KEYS} from 'utilities/constants';
|
||||||
|
|
||||||
|
const IGNORE_KEYS = [
|
||||||
|
KEYS.Shift,
|
||||||
|
KEYS.Control,
|
||||||
|
KEYS.Alt,
|
||||||
|
KEYS.Meta
|
||||||
|
];
|
||||||
|
|
||||||
|
const BAD_KEYS = [
|
||||||
|
KEYS.Escape,
|
||||||
|
KEYS.Tab
|
||||||
|
];
|
||||||
|
|
||||||
|
const KEYS_MAP = {
|
||||||
|
Backquote: '`',
|
||||||
|
Comma: ',',
|
||||||
|
Period: '.',
|
||||||
|
Slash: '/',
|
||||||
|
Semicolon: ';',
|
||||||
|
Quote: "'",
|
||||||
|
BracketLeft: '[',
|
||||||
|
BracketRight: ']',
|
||||||
|
Backslash: '\\',
|
||||||
|
Minus: '-',
|
||||||
|
Equal: '=',
|
||||||
|
ArrowLeft: 'left',
|
||||||
|
ArrowRight: 'right',
|
||||||
|
ArrowUp: 'up',
|
||||||
|
ArrowDown: 'down'
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const num of [1,2,3,4,5,6,7,8,9,0])
|
||||||
|
KEYS_MAP[`Digit${num}`] = `${num}`;
|
||||||
|
|
||||||
|
for(const letter of 'abcdefghijklmnopqrstuvwxyz')
|
||||||
|
KEYS_MAP[`Key${letter.toUpperCase()}`] = letter;
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: String
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKey(e) {
|
||||||
|
if ( this.active )
|
||||||
|
this.record(e);
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyUp(e) {
|
||||||
|
if ( this.active )
|
||||||
|
this.finishRecording(e);
|
||||||
|
else {
|
||||||
|
const key = e.keyCode || e.which;
|
||||||
|
if ( key === KEYS.Enter || key === KEYS.Space )
|
||||||
|
this.startRecording(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
startRecording(e) {
|
||||||
|
if ( e )
|
||||||
|
this.stop(e);
|
||||||
|
|
||||||
|
if ( this.active )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.active = true;
|
||||||
|
this.key = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
record(e) {
|
||||||
|
const key = e.keyCode || e.which;
|
||||||
|
if ( BAD_KEYS.includes(key) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.stop(e);
|
||||||
|
|
||||||
|
if ( IGNORE_KEYS.includes(key) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.key = e;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelRecording() {
|
||||||
|
this.active = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
finishRecording(e) {
|
||||||
|
const k = e.keyCode || e.which;
|
||||||
|
if ( BAD_KEYS.includes(k) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.stop(e);
|
||||||
|
|
||||||
|
if ( IGNORE_KEYS.includes(k) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.active = false;
|
||||||
|
const key = this.key;
|
||||||
|
this.key = null;
|
||||||
|
if ( ! key )
|
||||||
|
return;
|
||||||
|
|
||||||
|
let code = KEYS_MAP[key.code];
|
||||||
|
if ( ! code )
|
||||||
|
code = key.key;
|
||||||
|
|
||||||
|
const val = `${key.ctrlKey ? 'ctrl+' : ''}${key.altKey ? 'alt+' : ''}${key.shiftKey ? 'shift+' : ''}${code}`;
|
||||||
|
this.$emit('input', val);
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.$emit('input', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -20,6 +20,7 @@ export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/';
|
||||||
|
|
||||||
|
|
||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
|
Tab: 9,
|
||||||
Enter: 13,
|
Enter: 13,
|
||||||
Shift: 16,
|
Shift: 16,
|
||||||
Control: 17,
|
Control: 17,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
@import "./widgets/menu-container.scss";
|
@import "./widgets/menu-container.scss";
|
||||||
@import "./widgets/tab-container.scss";
|
@import "./widgets/tab-container.scss";
|
||||||
|
|
||||||
|
@import "./widgets/key-picker.scss";
|
||||||
@import "./widgets/menu-tree.scss";
|
@import "./widgets/menu-tree.scss";
|
||||||
@import "./widgets/profile-selector.scss";
|
@import "./widgets/profile-selector.scss";
|
||||||
@import "./widgets/badge-visibility.scss";
|
@import "./widgets/badge-visibility.scss";
|
||||||
|
@ -359,13 +360,18 @@ textarea.tw-input {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
color: #000 !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 & {
|
.tw-root--theme-dark & {
|
||||||
border-color: #424242 !important;
|
border-color: #424242 !important;
|
||||||
color: #dadada !important;
|
color: #dadada !important;
|
||||||
|
|
||||||
@include button-colors(#7d5bbe, #9a7fcc, #eeeeee, #7d5bbe)
|
//@include button-colors(#7d5bbe, #9a7fcc, #eeeeee, #7d5bbe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ $bg-dark: #17141f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.default-dimmable,
|
||||||
input, textarea, select {
|
input, textarea, select {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
|
|
3
styles/widgets/key-picker.scss
Normal file
3
styles/widgets/key-picker.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.ffz--key-widget {
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue