1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-02 00:58:32 +00:00
* 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:
SirStendec 2020-12-01 18:19:17 -05:00
parent 7adee6556c
commit ef4ff0c13a
15 changed files with 394 additions and 27 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.20.53", "version": "4.20.54",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {

View file

@ -493,8 +493,6 @@ export default class Badges extends Module {
} }
} }
this.log.info('badge-click', event.target);
if ( url ) { if ( url ) {
const link = createElement('a', { const link = createElement('a', {
target: '_blank', target: '_blank',
@ -586,8 +584,15 @@ export default class Badges extends Module {
}; };
} }
const handled_ids = new Set;
for(const badge of badges) for(const badge of badges)
if ( badge && badge.id != null ) { if ( badge && badge.id != null ) {
if ( handled_ids.has(badge.id) )
continue;
handled_ids.add(badge.id);
const full_badge = this.badges[badge.id] || {}, const full_badge = this.badges[badge.id] || {},
is_hidden = hidden_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 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; let style;
if ( old_badge ) { if ( old_badge ) {
@ -821,7 +832,7 @@ export default class Badges extends Module {
data.replaces = true; 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'; data.click_url = 'https://www.frankerfacez.com/donate';
if ( generate_css ) if ( generate_css )

View file

@ -276,6 +276,13 @@ export default class Room {
return false; 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, const d = data.room,
id = `${d.twitch_id}`; id = `${d.twitch_id}`;
@ -306,14 +313,12 @@ export default class Room {
if ( has(data.sets, set_id) ) if ( has(data.sets, set_id) )
this.manager.emotes.loadSetData(set_id, data.sets[set_id]); this.manager.emotes.loadSetData(set_id, data.sets[set_id]);
const badges = d.user_badge_ids;
const badges = d.user_badges;
if ( badges ) if ( badges )
for(const badge_id in badges) for(const badge_id in badges)
if ( has(badges, badge_id) ) if ( has(badges, badge_id) )
for(const user of 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 ) if ( d.css )
this.style.set('css', d.css); this.style.set('css', d.css);

View 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>

View file

@ -60,6 +60,12 @@ export default class MainMenu extends Module {
getFFZ: () => this.resolve('core') 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', { this.settings.addUI('home', {
path: 'Home @{"sort": -1000, "profile_warning": false}', path: 'Home @{"sort": -1000, "profile_warning": false}',
component: 'home-page' component: 'home-page'

View file

@ -191,7 +191,6 @@ export default class RavenLogger extends Module {
return true; return true;
}, },
shouldSendCallback: data => { shouldSendCallback: data => {
debugger;
if ( this.settings && ! this.settings.get('reports.error.enable') ) { if ( this.settings && ! this.settings.get('reports.error.enable') ) {
if ( data.tags && data.tags.example && this.__example_waiter ) { if ( data.tags && data.tags.example && this.__example_waiter ) {
this.__example_waiter(null); this.__example_waiter(null);

View 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();
}
};

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; 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 {IndexedDBProvider, LocalStorageProvider} from './providers';
import SettingsProfile from './profile'; import SettingsProfile from './profile';
@ -13,6 +13,7 @@ import SettingsContext from './context';
import MigrationManager from './migration'; import MigrationManager from './migration';
import * as FILTERS from './filters'; 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.ui_structures = new Map;
this.definitions = 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 // Filters
this.filters = {}; this.filters = {};
@ -607,6 +615,22 @@ export default class SettingsManager extends Module {
this.ui_structures.set(key, definition); this.ui_structures.set(key, definition);
this.emit(':added-definition', 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);
}
} }

View file

@ -300,9 +300,10 @@ export class IndexedDBProvider extends SettingsProvider {
const {type, key} = event.data; const {type, key} = event.data;
if ( type === 'set' ) { if ( type === 'set' ) {
const val = JSON.parse(localStorage.getItem(this.prefix + key)); this._get(key).then(val => {
this._cached.set(key, val); this._cached.set(key, val);
this.emit('changed', key, val, false); this.emit('changed', key, val, false);
}).catch(err => this.manager.log.error(`Error getting setting "${key}" from database`, err));
} else if ( type === 'delete' ) { } else if ( type === 'delete' ) {
this._cached.delete(key); this._cached.delete(key);
@ -343,8 +344,8 @@ export class IndexedDBProvider extends SettingsProvider {
this._cached.set(key, value); this._cached.set(key, value);
this._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); 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) { async _set(key, value) {
const db = await this.getDB(), const db = await this.getDB(),
trx = db.transaction(['settings'], 'readwrite'), trx = db.transaction(['settings'], 'readwrite'),
@ -480,9 +498,10 @@ export class IndexedDBProvider extends SettingsProvider {
return new Promise((s,f) => { return new Promise((s,f) => {
store.onerror = 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) => { return new Promise((s,f) => {
store.onerror = 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) => { return new Promise((s,f) => {
store.onerror = f; store.onerror = f;
store.onsuccess = s;
store.clear(); const req = store.clear();
req.onerror = f;
req.onsuccess = s;
}); });
} }

View file

@ -762,7 +762,7 @@ export default class EmoteMenu extends Module {
return (<button return (<button
key={emote.id} 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-tooltip-type="emote"
data-provider={emote.provider} data-provider={emote.provider}
data-id={emote.id} data-id={emote.id}

View file

@ -606,7 +606,7 @@ export default class ChatHook extends Module {
); );
if ( chat_color ) if ( chat_color )
chat_dark = chat_color.luminance() < 0.5; chat_dark = chat_color.toHSLA().l < 0.5;
chat_color = chat_dark ? '#191919' : '#E0E0E0'; chat_color = chat_dark ? '#191919' : '#E0E0E0';
@ -617,7 +617,7 @@ export default class ChatHook extends Module {
); );
if ( chat_text ) if ( chat_text )
text_dark = chat_text.luminance() < 0.5; text_dark = chat_text.toHSLA().l < 0.5;
chat_text = text_dark ? '#19171c' : '#dad8de'; chat_text = text_dark ? '#19171c' : '#dad8de';

View file

@ -338,7 +338,7 @@ export default class ChatLine extends Module {
override_mode = t.chat.context.get('chat.filtering.display-deleted'), override_mode = t.chat.context.get('chat.filtering.display-deleted'),
msg = t.chat.standardizeMessage(this.props.message), 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, is_action = msg.messageType === types.Action,
user = msg.user, user = msg.user,
@ -864,7 +864,7 @@ other {# messages were deleted by a moderator.}
e('div', { e('div', {
className: 'chat-line__message-container' className: 'chat-line__message-container'
}, [ }, [
this.props.repliesAppearancePreference === 'expanded' ? this.renderReplyLine() : null, this.props.repliesAppearancePreference && this.props.repliesAppearancePreference === 'expanded' ? this.renderReplyLine() : null,
out out
]), ]),
e('div', { e('div', {

View file

@ -371,7 +371,7 @@
} }
&.locked, &.locked,
&.hidden { &.emote-hidden {
img { img {
opacity: 0.5; opacity: 0.5;
} }

View file

@ -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 { export class SourcedSet {
constructor() { constructor() {
this._cache = []; this._cache = [];

View file

@ -289,6 +289,7 @@ textarea.tw-input {
} }
} }
.ffz--clear-settings code,
.ffz--experiments code { .ffz--experiments code {
user-select: none; user-select: none;
padding: 2px 5px; padding: 2px 5px;
@ -302,6 +303,10 @@ textarea.tw-input {
} }
} }
.ffz--clear-settings code {
font-size: 1.2em;
}
.ffz--changelog, .ffz--changelog,
.ffz--widget { .ffz--widget {
code { code {