mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-25 03:58:30 +00:00
4.20.54
* Added: New UI for clearing FrankerFaceZ settings (Data Management > Storage). * Fixed: Do not display duplicate bot badges in tool-tips when a bot has both a global bot badge and a channel-specific bot badge. * Fixed: Do not add click URLs for FFZ badges to add-on badges. * Fixed: Remove `debugger;` from automatic error reporting method. * Fixed: IndexedDBProvider not synchronizing settings correctly. * Fixed: Change a CSS class name used when changing emote visibility to avoid bad UX due to misbehaving third-party extensions. * Fixed: Color calculations for chat messages, including highlight colors. (Closes #947) * Changed: Use rounded images from the CDN for FFZ badges in tool-tips. * Changed: Use Twitch IDs rather than usernames for assigning channel-specific badges to users. * API Added: `settings.addClearable(key, definition)` for adding new UI to `Data Management > Storage >> Clear`.
This commit is contained in:
parent
7adee6556c
commit
ef4ff0c13a
15 changed files with 394 additions and 27 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.53",
|
||||
"version": "4.20.54",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -493,8 +493,6 @@ export default class Badges extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
this.log.info('badge-click', event.target);
|
||||
|
||||
if ( url ) {
|
||||
const link = createElement('a', {
|
||||
target: '_blank',
|
||||
|
@ -586,8 +584,15 @@ export default class Badges extends Module {
|
|||
};
|
||||
}
|
||||
|
||||
const handled_ids = new Set;
|
||||
|
||||
for(const badge of badges)
|
||||
if ( badge && badge.id != null ) {
|
||||
if ( handled_ids.has(badge.id) )
|
||||
continue;
|
||||
|
||||
handled_ids.add(badge.id);
|
||||
|
||||
const full_badge = this.badges[badge.id] || {},
|
||||
is_hidden = hidden_badges[badge.id];
|
||||
|
||||
|
@ -609,6 +614,12 @@ export default class Badges extends Module {
|
|||
title: badge.title || full_badge.title
|
||||
};
|
||||
|
||||
// Hacky nonsense.
|
||||
if ( ! full_badge.addon ) {
|
||||
bd.image = `//cdn.frankerfacez.com/badge/${badge.id}/4/rounded`;
|
||||
bd.color = null;
|
||||
}
|
||||
|
||||
let style;
|
||||
|
||||
if ( old_badge ) {
|
||||
|
@ -821,7 +832,7 @@ export default class Badges extends Module {
|
|||
data.replaces = true;
|
||||
}
|
||||
|
||||
if ( data.name === 'developer' || data.name === 'supporter' )
|
||||
if ( ! data.addon && (data.name === 'developer' || data.name === 'supporter') )
|
||||
data.click_url = 'https://www.frankerfacez.com/donate';
|
||||
|
||||
if ( generate_css )
|
||||
|
|
|
@ -276,6 +276,13 @@ export default class Room {
|
|||
return false;
|
||||
}
|
||||
|
||||
const old_badges = this.data?.user_badge_ids;
|
||||
if ( old_badges )
|
||||
for(const badge_id in old_badges )
|
||||
if ( has(old_badges, badge_id) )
|
||||
for(const user of old_badges[badge_id])
|
||||
this.getUser(user, undefined).removeBadge('ffz', badge_id);
|
||||
|
||||
const d = data.room,
|
||||
id = `${d.twitch_id}`;
|
||||
|
||||
|
@ -306,14 +313,12 @@ export default class Room {
|
|||
if ( has(data.sets, set_id) )
|
||||
this.manager.emotes.loadSetData(set_id, data.sets[set_id]);
|
||||
|
||||
|
||||
const badges = d.user_badges;
|
||||
const badges = d.user_badge_ids;
|
||||
if ( badges )
|
||||
for(const badge_id in badges)
|
||||
if ( has(badges, badge_id) )
|
||||
for(const user of badges[badge_id])
|
||||
this.getUser(undefined, user).addBadge('ffz', badge_id);
|
||||
|
||||
this.getUser(user, undefined).addBadge('ffz', badge_id);
|
||||
|
||||
if ( d.css )
|
||||
this.style.set('css', d.css);
|
||||
|
|
216
src/modules/main_menu/components/clear-settings.vue
Normal file
216
src/modules/main_menu/components/clear-settings.vue
Normal file
|
@ -0,0 +1,216 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--clear-settings tw-pd-t-05">
|
||||
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-2">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('setting.clear.warning', 'Be careful! This is permanent.') }}
|
||||
</h3>
|
||||
<markdown :source="t('setting.clear.warning-explain', 'Deleting your data with this tool cannot be reversed. Make sure you have a backup!')" />
|
||||
</div>
|
||||
|
||||
<div v-if="! started">
|
||||
<div class="tw-mg-b-1">
|
||||
{{ t('setting.clear.step-1', 'Please select which types of data you wish to clear:') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-l-5">
|
||||
<div
|
||||
v-for="(type, key) in types"
|
||||
:key="key"
|
||||
class="tw-checkbox tw-relative tw-mg-y-05"
|
||||
>
|
||||
<input
|
||||
:id="key"
|
||||
:ref="key"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
>
|
||||
|
||||
<label :for="key" class="tw-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t(`setting.clear.opt.${key}`, type.label || key) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-t-1 tw-border-t tw-pd-t-1 tw-mg-b-1">
|
||||
<markdown :source="t('setting.clear.step-2', 'Are you really sure? Please enter `{code}` in the text box below to confirm.', {code})" />
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-l-5">
|
||||
<input
|
||||
v-model="entered"
|
||||
type="text"
|
||||
class="tw-block tw-border-radius-medium tw-font-size-6 tw-input tw-pd-x-1 tw-pd-y-05"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-t-1 tw-border-t tw-pd-t-1 tw-mg-b-1">
|
||||
<div class="tw-mg-l-5">
|
||||
<button
|
||||
class="tw-button"
|
||||
:class="{'tw-button--disabled': ! enabled}"
|
||||
@click="clear"
|
||||
>
|
||||
<span class="tw-button__icon tw-button__icon--left">
|
||||
<figure class="ffz-i-trash" />
|
||||
</span>
|
||||
<span class="tw-button__text">
|
||||
{{ t('setting.clear.start', 'Clear My Data') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="started && running">
|
||||
{{ t('setting.clear.running', 'Clearing settings. Please wait...') }}
|
||||
</div>
|
||||
<div v-if="started && ! running">
|
||||
{{ t('setting.clear.done', 'Your settings have been cleared. Please refresh any applicable Twitch pages to ensure no cached data remains.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {generateHex} from 'utilities/object';
|
||||
import { maybe_call } from 'src/utilities/object';
|
||||
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
const ffz = this.context.getFFZ(),
|
||||
settings = ffz.resolve('settings');
|
||||
|
||||
return {
|
||||
types: settings.getClearables(),
|
||||
entered: '',
|
||||
code: generateHex(8),
|
||||
|
||||
started: false,
|
||||
running: false,
|
||||
|
||||
message: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
enabled() {
|
||||
return this.code === this.entered
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async clear() {
|
||||
if ( ! this.enabled )
|
||||
return;
|
||||
|
||||
this.started = true;
|
||||
this.running = true;
|
||||
|
||||
const ffz = this.context.getFFZ(),
|
||||
settings = ffz.resolve('settings'),
|
||||
provider = settings.provider;
|
||||
|
||||
for(const [key, type] of Object.entries(this.types)) {
|
||||
if ( ! this.$refs[key]?.[0]?.checked )
|
||||
continue;
|
||||
|
||||
if ( type.clear )
|
||||
await type.clear.call(this, provider, settings); // eslint-disable-line no-await-in-loop
|
||||
else {
|
||||
let keys = maybe_call(type.keys, this, provider, settings);
|
||||
if ( keys instanceof Promise )
|
||||
keys = await keys; // eslint-disable-line no-await-in-loop
|
||||
|
||||
if ( Array.isArray(keys) )
|
||||
for(const key of keys)
|
||||
provider.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -60,6 +60,12 @@ export default class MainMenu extends Module {
|
|||
getFFZ: () => this.resolve('core')
|
||||
});
|
||||
|
||||
this.settings.addUI('clear', {
|
||||
path: 'Data Management > Storage @{"profile_warning": false} >> tabs ~> Clear',
|
||||
component: 'clear-settings',
|
||||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.addUI('home', {
|
||||
path: 'Home @{"sort": -1000, "profile_warning": false}',
|
||||
component: 'home-page'
|
||||
|
|
|
@ -191,7 +191,6 @@ export default class RavenLogger extends Module {
|
|||
return true;
|
||||
},
|
||||
shouldSendCallback: data => {
|
||||
debugger;
|
||||
if ( this.settings && ! this.settings.get('reports.error.enable') ) {
|
||||
if ( data.tags && data.tags.example && this.__example_waiter ) {
|
||||
this.__example_waiter(null);
|
||||
|
|
68
src/settings/clearables.js
Normal file
68
src/settings/clearables.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Clearable Settings
|
||||
// ============================================================================
|
||||
|
||||
export const Experiments = {
|
||||
label: 'Experiment Overrides',
|
||||
keys: [
|
||||
'exp-lock',
|
||||
'experiment-overrides'
|
||||
]
|
||||
};
|
||||
|
||||
export const HiddenEmotes = {
|
||||
label: 'Hidden Emotes',
|
||||
keys(provider) {
|
||||
const keys = ['emote-menu.hidden-sets'];
|
||||
for(const key of provider.keys())
|
||||
if ( key.startsWith('hidden-emotes.') )
|
||||
keys.push(key);
|
||||
|
||||
return keys;
|
||||
}
|
||||
};
|
||||
|
||||
export const FavoriteEmotes = {
|
||||
label: 'Favorited Emotes',
|
||||
keys(provider) {
|
||||
const keys = [];
|
||||
for(const key of provider.keys())
|
||||
if ( key.startsWith('favorite-emotes.') )
|
||||
keys.push(key);
|
||||
|
||||
return keys;
|
||||
}
|
||||
};
|
||||
|
||||
export const Overrides = {
|
||||
label: 'Name and Color Overrides',
|
||||
keys: [
|
||||
'overrides.colors',
|
||||
'overrides.names'
|
||||
]
|
||||
};
|
||||
|
||||
export const Profiles = {
|
||||
label: 'Profiles',
|
||||
clear(provider, settings) {
|
||||
const keys = ['profiles'];
|
||||
for(const key of provider.keys())
|
||||
if ( /^p:\d+:/.test(key) )
|
||||
keys.push(key);
|
||||
|
||||
for(const key of keys)
|
||||
provider.delete(key);
|
||||
|
||||
settings.loadProfiles();
|
||||
}
|
||||
};
|
||||
|
||||
export const Everything = {
|
||||
label: 'Absolutely Everything',
|
||||
clear(provider, settings) {
|
||||
provider.clear();
|
||||
settings.loadProfiles();
|
||||
}
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {deep_equals, has, debounce} from 'utilities/object';
|
||||
import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
|
||||
|
||||
import {IndexedDBProvider, LocalStorageProvider} from './providers';
|
||||
import SettingsProfile from './profile';
|
||||
|
@ -13,6 +13,7 @@ import SettingsContext from './context';
|
|||
import MigrationManager from './migration';
|
||||
|
||||
import * as FILTERS from './filters';
|
||||
import * as CLEARABLES from './clearables';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
|
@ -51,6 +52,13 @@ export default class SettingsManager extends Module {
|
|||
this.ui_structures = new Map;
|
||||
this.definitions = new Map;
|
||||
|
||||
// Clearable Data Rules
|
||||
this.clearables = {};
|
||||
|
||||
for(const key in CLEARABLES)
|
||||
if ( has(CLEARABLES, key) )
|
||||
this.clearables[key] = CLEARABLES[key];
|
||||
|
||||
// Filters
|
||||
this.filters = {};
|
||||
|
||||
|
@ -607,6 +615,22 @@ export default class SettingsManager extends Module {
|
|||
this.ui_structures.set(key, definition);
|
||||
this.emit(':added-definition', key, definition);
|
||||
}
|
||||
|
||||
|
||||
addClearable(key, definition) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.addClearable(k, key[k]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearables[key] = definition;
|
||||
}
|
||||
|
||||
getClearables() {
|
||||
return deep_copy(this.clearables);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -300,9 +300,10 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
const {type, key} = event.data;
|
||||
|
||||
if ( type === 'set' ) {
|
||||
const val = JSON.parse(localStorage.getItem(this.prefix + key));
|
||||
this._cached.set(key, val);
|
||||
this.emit('changed', key, val, false);
|
||||
this._get(key).then(val => {
|
||||
this._cached.set(key, val);
|
||||
this.emit('changed', key, val, false);
|
||||
}).catch(err => this.manager.log.error(`Error getting setting "${key}" from database`, err));
|
||||
|
||||
} else if ( type === 'delete' ) {
|
||||
this._cached.delete(key);
|
||||
|
@ -343,8 +344,8 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
|
||||
this._cached.set(key, value);
|
||||
this._set(key, value)
|
||||
.catch(err => this.manager.log.error(`Error saving setting "${key}" to database`, err))
|
||||
.then(() => this.broadcast({type: 'set', key}));
|
||||
.then(() => this.broadcast({type: 'set', key}))
|
||||
.catch(err => this.manager.log.error(`Error saving setting "${key}" to database`, err));
|
||||
|
||||
this.emit('set', key, value, false);
|
||||
}
|
||||
|
@ -473,6 +474,23 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
|
||||
async _get(key) {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readonly'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
|
||||
const req = store.get(key);
|
||||
req.onerror = f;
|
||||
req.onsuccess = () => {
|
||||
s(req.result.v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async _set(key, value) {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
|
@ -480,9 +498,10 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
store.onsuccess = s;
|
||||
|
||||
store.put({k: key, v: value});
|
||||
const req = store.put({k: key, v: value});
|
||||
req.onerror = f;
|
||||
req.onsuccess = s;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -494,9 +513,10 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
store.onsuccess = s;
|
||||
|
||||
store.delete(key);
|
||||
const req = store.delete(key);
|
||||
req.onerror = f;
|
||||
req.onsuccess = s;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -508,9 +528,10 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
store.onsuccess = s;
|
||||
|
||||
store.clear();
|
||||
const req = store.clear();
|
||||
req.onerror = f;
|
||||
req.onsuccess = s;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -762,7 +762,7 @@ export default class EmoteMenu extends Module {
|
|||
|
||||
return (<button
|
||||
key={emote.id}
|
||||
class={`ffz-tooltip emote-picker__emote-link${!visibility && locked ? ' locked' : ''}${hidden ? ' hidden' : ''}`}
|
||||
class={`ffz-tooltip emote-picker__emote-link${!visibility && locked ? ' locked' : ''}${hidden ? ' emote-hidden' : ''}`}
|
||||
data-tooltip-type="emote"
|
||||
data-provider={emote.provider}
|
||||
data-id={emote.id}
|
||||
|
|
|
@ -606,7 +606,7 @@ export default class ChatHook extends Module {
|
|||
);
|
||||
|
||||
if ( chat_color )
|
||||
chat_dark = chat_color.luminance() < 0.5;
|
||||
chat_dark = chat_color.toHSLA().l < 0.5;
|
||||
|
||||
chat_color = chat_dark ? '#191919' : '#E0E0E0';
|
||||
|
||||
|
@ -617,7 +617,7 @@ export default class ChatHook extends Module {
|
|||
);
|
||||
|
||||
if ( chat_text )
|
||||
text_dark = chat_text.luminance() < 0.5;
|
||||
text_dark = chat_text.toHSLA().l < 0.5;
|
||||
|
||||
chat_text = text_dark ? '#19171c' : '#dad8de';
|
||||
|
||||
|
|
|
@ -338,7 +338,7 @@ export default class ChatLine extends Module {
|
|||
override_mode = t.chat.context.get('chat.filtering.display-deleted'),
|
||||
|
||||
msg = t.chat.standardizeMessage(this.props.message),
|
||||
reply_tokens = (reply_mode === 2 || (reply_mode === 1 && this.props.repliesAppearancePreference !== 'expanded')) ? ( msg.ffz_reply = msg.ffz_reply || t.chat.tokenizeReply(this.props.reply) ) : null,
|
||||
reply_tokens = (reply_mode === 2 || (reply_mode === 1 && this.props.repliesAppearancePreference && this.props.repliesAppearancePreference !== 'expanded')) ? ( msg.ffz_reply = msg.ffz_reply || t.chat.tokenizeReply(this.props.reply) ) : null,
|
||||
is_action = msg.messageType === types.Action,
|
||||
|
||||
user = msg.user,
|
||||
|
@ -864,7 +864,7 @@ other {# messages were deleted by a moderator.}
|
|||
e('div', {
|
||||
className: 'chat-line__message-container'
|
||||
}, [
|
||||
this.props.repliesAppearancePreference === 'expanded' ? this.renderReplyLine() : null,
|
||||
this.props.repliesAppearancePreference && this.props.repliesAppearancePreference === 'expanded' ? this.renderReplyLine() : null,
|
||||
out
|
||||
]),
|
||||
e('div', {
|
||||
|
|
|
@ -371,7 +371,7 @@
|
|||
}
|
||||
|
||||
&.locked,
|
||||
&.hidden {
|
||||
&.emote-hidden {
|
||||
img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -585,6 +585,18 @@ export function truncate(str, target = 100, overage = 15, ellipsis = '…', brea
|
|||
|
||||
|
||||
|
||||
function decimalToHex(number) {
|
||||
return number.toString(16).padStart(2, '0')
|
||||
}
|
||||
|
||||
|
||||
export function generateHex(length = 40) {
|
||||
const arr = new Uint8Array(length / 2);
|
||||
window.crypto.getRandomValues(arr);
|
||||
return Array.from(arr, decimalToHex).join('')
|
||||
}
|
||||
|
||||
|
||||
export class SourcedSet {
|
||||
constructor() {
|
||||
this._cache = [];
|
||||
|
|
|
@ -289,6 +289,7 @@ textarea.tw-input {
|
|||
}
|
||||
}
|
||||
|
||||
.ffz--clear-settings code,
|
||||
.ffz--experiments code {
|
||||
user-select: none;
|
||||
padding: 2px 5px;
|
||||
|
@ -302,6 +303,10 @@ textarea.tw-input {
|
|||
}
|
||||
}
|
||||
|
||||
.ffz--clear-settings code {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.ffz--changelog,
|
||||
.ffz--widget {
|
||||
code {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue