1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-03 17:48:30 +00:00
This was a small update until Twitch ripped out half their CSS.

* Added: Cross-Origin Storage Bridge for settings, better synchronizing settings on sub-domains with support for binary blobs at the cost of slightly increased start-up time.
* Fixed: Rendering issues caused by missing CSS.
* Fixed: FFZ Control Center button not appearing on dashboard pages, and appearing in the incorrect place.
* Changed: Work towards splitting modules into their own JS files for a faster, more asynchronous startup.
* API Added: Methods for serializing and deserializing Blobs for transmission across postMessage.
This commit is contained in:
SirStendec 2021-02-11 19:40:12 -05:00
parent 2c5937c8af
commit 264c375f13
88 changed files with 1685 additions and 500 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.20.60", "version": "4.20.61",
"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

@ -6,6 +6,7 @@ import Logger from 'utilities/logging';
import Module from 'utilities/module'; import Module from 'utilities/module';
import {DEBUG} from 'utilities/constants'; import {DEBUG} from 'utilities/constants';
import {serializeBlob, deserializeBlob} from 'utilities/blobs';
import SettingsManager from './settings/index'; import SettingsManager from './settings/index';
@ -28,6 +29,7 @@ class FFZBridge extends Module {
this.inject('raven', RavenLogger); this.inject('raven', RavenLogger);
this.log = new Logger(null, null, null, this.raven); this.log = new Logger(null, null, null, this.raven);
this.log.label = 'FFZBridge';
this.log.init = true; this.log.init = true;
this.core_log = this.log.get('core'); this.core_log = this.log.get('core');
@ -60,15 +62,21 @@ class FFZBridge extends Module {
return FFZBridge.instance; return FFZBridge.instance;
} }
onEnable() { async onEnable() {
window.addEventListener('message', this.onMessage.bind(this)); window.addEventListener('message', this.onMessage.bind(this));
this.settings.provider.on('changed', this.onProviderChange, this); this.settings.provider.on('changed', this.onProviderChange, this);
this.settings.provider.on('changed-blob', this.onProviderBlobChange, this);
this.settings.provider.on('clear-blobs', this.onProviderClearBlobs, this);
await this.settings.awaitProvider();
await this.settings.provider.awaitReady();
this.send({ this.send({
ffz_type: 'ready' ffz_type: 'ready'
}); });
} }
onMessage(event) { async onMessage(event) {
const msg = event.data; const msg = event.data;
if ( ! msg || ! msg.ffz_type ) if ( ! msg || ! msg.ffz_type )
return; return;
@ -83,13 +91,86 @@ class FFZBridge extends Module {
data: out data: out
}); });
} else if ( msg.ffz_type === 'change' ) return;
} else if ( msg.ffz_type === 'change' ) {
this.onChange(msg); this.onChange(msg);
return;
} }
send(msg) { // eslint-disable-line class-methods-use-this if ( ! msg.id )
return this.log.warn('Received command with no reply ID');
let reply, transfer;
try { try {
window.parent.postMessage(msg, '*') if ( msg.ffz_type === 'init-load' ) {
reply = {
blobs: this.settings.provider.supportsBlobs,
values: {}
};
for(const [key,value] of this.settings.provider.entries())
reply.values[key] = value;
} else if ( msg.ffz_type === 'set' )
this.settings.provider.set(msg.key, msg.value);
else if ( msg.ffz_type === 'delete' )
this.settings.provider.delete(msg.key);
else if ( msg.ffz_type === 'clear' )
this.settings.provider.clear();
else if ( msg.ffz_type === 'get-blob' ) {
reply = await serializeBlob(await this.settings.provider.getBlob(msg.key));
if ( reply )
transfer = reply.buffer;
} else if ( msg.ffz_type === 'set-blob' ) {
const blob = deserializeBlob(msg.value);
await this.settings.provider.setBlob(msg.key, blob);
} else if ( msg.ffz_type === 'delete-blob' )
await this.settings.provider.deleteBlob(msg.key);
else if ( msg.ffz_type === 'has-blob' )
reply = await this.settings.provider.hasBlob(msg.key);
else if ( msg.ffz_type === 'clear-blobs' )
await this.settings.provider.clearBlobs();
else if ( msg.ffz_type === 'blob-keys' )
reply = await this.settings.provider.blobKeys();
else if ( msg.ffz_type === 'flush' )
await this.settings.provider.flush();
else
return this.send({
ffz_type: 'reply-error',
id: msg.id,
error: 'bad-command'
});
this.send({
ffz_type: 'reply',
id: msg.id,
reply
}, transfer);
} catch(err) {
this.log.error('Error handling command.', err);
this.send({
ffz_type: 'reply-error',
id: msg.id
});
}
}
send(msg, blob) { // eslint-disable-line class-methods-use-this
try {
window.parent.postMessage(msg, '*', blob ? [blob] : undefined)
} catch(err) { this.log.error('send error', err); /* no-op */ } } catch(err) { this.log.error('send error', err); /* no-op */ }
} }
@ -112,6 +193,20 @@ class FFZBridge extends Module {
deleted deleted
}); });
} }
onProviderBlobChange(key, deleted) {
this.send({
ffz_type: 'change-blob',
key,
deleted
});
}
onProviderClearBlobs() {
this.send({
ffz_type: 'clear-blobs'
});
}
} }
FFZBridge.Logger = Logger; FFZBridge.Logger = Logger;

View file

@ -68,9 +68,9 @@ class FrankerFaceZ extends Module {
// Startup // Startup
// ======================================================================== // ========================================================================
this.discoverModules(); this.discoverModules()
.then(() => this.enable())
this.enable().then(() => this.enableInitialModules()).then(() => { .then(() => this.enableInitialModules()).then(() => {
const duration = performance.now() - start_time; const duration = performance.now() - start_time;
this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`); this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
this.log.init = false; this.log.init = false;
@ -132,9 +132,10 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n'
// Modules // Modules
// ======================================================================== // ========================================================================
discoverModules() { async discoverModules() {
const ctx = require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/), // TODO: Actually do async modules.
modules = this.populate(ctx, this.core_log); const ctx = await require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/ /*, 'lazy-once' */);
const modules = this.populate(ctx, this.core_log);
this.core_log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`); this.core_log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
} }

View file

@ -8,7 +8,7 @@
<input <input
id="edit_reason" id="edit_reason"
v-model.trim="value.reason" v-model.trim="value.reason"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >
</div> </div>

View file

@ -9,7 +9,7 @@
:id="'edit_chat$' + id" :id="'edit_chat$' + id"
v-model="value.command" v-model="value.command"
:placeholder="defaults.command" :placeholder="defaults.command"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >
@ -17,16 +17,16 @@
{{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }} {{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
</div> </div>
<div class="tw-checkbox"> <div class="ffz-checkbox">
<input <input
:id="'chat-paste$' + id" :id="'chat-paste$' + id"
v-model="value.paste" v-model="value.paste"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="$emit('input', value)" @change="$emit('input', value)"
> >
<label :for="'chat-paste$' + id" class="tw-checkbox__label"> <label :for="'chat-paste$' + id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.set-chat', 'Paste this message into chat rather than sending it directly.') }} {{ t('setting.actions.set-chat', 'Paste this message into chat rather than sending it directly.') }}
</span> </span>

View file

@ -19,7 +19,7 @@
<input <input
id="edit_text" id="edit_text"
v-model="value.text" v-model="value.text"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >
</div> </div>

View file

@ -7,7 +7,7 @@
<input <input
id="edit_image" id="edit_image"
v-model="value.image" v-model="value.image"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >
</div> </div>

View file

@ -7,7 +7,7 @@
<input <input
id="edit_text" id="edit_text"
v-model="value.text" v-model="value.text"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >
</div> </div>

View file

@ -9,7 +9,7 @@
id="edit_duration" id="edit_duration"
v-model="value.duration_rich" v-model="value.duration_rich"
:placeholder="defaults.duration" :placeholder="defaults.duration"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
type="text" type="text"
@input="update()" @input="update()"
> >
@ -23,7 +23,7 @@
<input <input
id="edit_reason" id="edit_reason"
v-model.trim="value.reason" v-model.trim="value.reason"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >
</div> </div>

View file

@ -9,7 +9,7 @@
id="edit_url" id="edit_url"
v-model="value.url" v-model="value.url"
:placeholder="defaults.url" :placeholder="defaults.url"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)" @input="$emit('input', value)"
> >

View file

@ -263,7 +263,7 @@ export default class Actions extends Module {
reason_elements.push(<li class="tw-full-width tw-relative"> reason_elements.push(<li class="tw-full-width tw-relative">
<a <a
href="#" href="#"
class="tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive tw-pd-05" class="tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-pd-05"
onClick={click_fn(text)} onClick={click_fn(text)}
> >
{text} {text}
@ -280,7 +280,7 @@ export default class Actions extends Module {
reason_elements.push(<li class="tw-full-width tw-relative"> reason_elements.push(<li class="tw-full-width tw-relative">
<a <a
href="#" href="#"
class="tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive tw-pd-05" class="tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-pd-05"
onClick={click_fn(rule)} onClick={click_fn(rule)}
> >
{rule} {rule}
@ -350,9 +350,9 @@ export default class Actions extends Module {
hover_events: true, hover_events: true,
no_update: true, no_update: true,
tooltipClass: 'ffz-action-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base', tooltipClass: 'ffz-action-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute', arrowClass: 'ffz-balloon__tail tw-overflow-hidden tw-absolute',
arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute', arrowInner: 'ffz-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
innerClass: '', innerClass: '',
popper: { popper: {

View file

@ -281,8 +281,8 @@ export default {
class: [ class: [
tooltip && 'ffz-tooltip', tooltip && 'ffz-tooltip',
this.accent && 'ffz-accent-card', this.accent && 'ffz-accent-card',
!this.error && 'tw-interactable--hover-enabled', !this.error && 'ffz-interactable--hover-enabled',
'tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--default tw-interactive' 'tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--default tw-interactive'
], ],
attrs: { attrs: {
'data-tooltip-type': 'link', 'data-tooltip-type': 'link',

View file

@ -20,7 +20,7 @@
<input <input
id="user-name" id="user-name"
ref="name" ref="name"
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input tw-flex-grow-1" class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input tw-flex-grow-1"
:value="name" :value="name"
@change="updateName" @change="updateName"
> >

View file

@ -67,9 +67,9 @@ export default class Overrides extends Module {
no_update: true, no_update: true,
no_auto_remove: true, no_auto_remove: true,
tooltipClass: 'ffz-action-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base', tooltipClass: 'ffz-action-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
arrowClass: '', //tw-balloon__tail tw-overflow-hidden tw-absolute', arrowClass: '', //ffz-balloon__tail tw-overflow-hidden tw-absolute',
arrowInner: '', //tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute', arrowInner: '', //ffz-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
innerClass: '', innerClass: '',
popper: { popper: {

View file

@ -19,7 +19,7 @@
ref="json" ref="json"
v-model="json" v-model="json"
readonly readonly
class="tw-full-width tw-full-height tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-full-width tw-full-height tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
@focus="$event.target.select()" @focus="$event.target.select()"
/> />
</template> </template>
@ -34,7 +34,7 @@
<input <input
v-model="edit_data.appearance.tooltip" v-model="edit_data.appearance.tooltip"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
> >
</div> </div>
@ -47,7 +47,7 @@
id="renderer_type" id="renderer_type"
ref="renderer_type" ref="renderer_type"
v-model="edit_data.appearance.type" v-model="edit_data.appearance.type"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option <option
v-for="(r, key) in data.renderers" v-for="(r, key) in data.renderers"
@ -91,7 +91,7 @@
<select <select
id="vis_mod" id="vis_mod"
v-model="edit_data.display.mod" v-model="edit_data.display.mod"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -113,7 +113,7 @@
<select <select
id="vis_mod_icons" id="vis_mod_icons"
v-model="edit_data.display.mod_icons" v-model="edit_data.display.mod_icons"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -135,7 +135,7 @@
<select <select
id="vis_deleted" id="vis_deleted"
v-model="edit_data.display.deleted" v-model="edit_data.display.deleted"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -157,7 +157,7 @@
<select <select
id="vis_emote" id="vis_emote"
v-model="edit_data.display.emoteOnly" v-model="edit_data.display.emoteOnly"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -179,7 +179,7 @@
<select <select
id="vis_slow" id="vis_slow"
v-model="edit_data.display.slowMode" v-model="edit_data.display.slowMode"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -201,7 +201,7 @@
<select <select
id="vis_subs" id="vis_subs"
v-model="edit_data.display.followersOnly" v-model="edit_data.display.followersOnly"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -223,7 +223,7 @@
<select <select
id="vis_subs" id="vis_subs"
v-model="edit_data.display.subsMode" v-model="edit_data.display.subsMode"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -245,7 +245,7 @@
<select <select
id="vis_r9k" id="vis_r9k"
v-model="edit_data.display.r9kMode" v-model="edit_data.display.r9kMode"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option :value="undefined" selected> <option :value="undefined" selected>
{{ t('setting.unset', 'Unset') }} {{ t('setting.unset', 'Unset') }}
@ -266,80 +266,80 @@
<div> <div>
<div class="ffz--inline tw-flex"> <div class="ffz--inline tw-flex">
<div class="tw-pd-r-1 tw-checkbox"> <div class="tw-pd-r-1 ffz-checkbox">
<input <input
:id="'key_ctrl$' + id" :id="'key_ctrl$' + id"
ref="key_ctrl" ref="key_ctrl"
:checked="edit_data.display.keys & 1" :checked="edit_data.display.keys & 1"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onChangeKeys" @change="onChangeKeys"
> >
<label :for="'key_ctrl$' + id" class="tw-checkbox__label"> <label :for="'key_ctrl$' + id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.key.ctrl', 'Ctrl') }} {{ t('setting.key.ctrl', 'Ctrl') }}
</span> </span>
</label> </label>
</div> </div>
<div class="tw-pd-r-1 tw-checkbox"> <div class="tw-pd-r-1 ffz-checkbox">
<input <input
:id="'key_shift$' + id" :id="'key_shift$' + id"
ref="key_shift" ref="key_shift"
:checked="edit_data.display.keys & 2" :checked="edit_data.display.keys & 2"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onChangeKeys" @change="onChangeKeys"
> >
<label :for="'key_shift$' + id" class="tw-checkbox__label"> <label :for="'key_shift$' + id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.key.shift', 'Shift') }} {{ t('setting.key.shift', 'Shift') }}
</span> </span>
</label> </label>
</div> </div>
<div class="tw-pd-r-1 tw-checkbox"> <div class="tw-pd-r-1 ffz-checkbox">
<input <input
:id="'key_alt$' + id" :id="'key_alt$' + id"
ref="key_alt" ref="key_alt"
:checked="edit_data.display.keys & 4" :checked="edit_data.display.keys & 4"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onChangeKeys" @change="onChangeKeys"
> >
<label :for="'key_alt$' + id" class="tw-checkbox__label"> <label :for="'key_alt$' + id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.key.alt', 'Alt') }} {{ t('setting.key.alt', 'Alt') }}
</span> </span>
</label> </label>
</div> </div>
<div class="tw-pd-r-1 tw-checkbox"> <div class="tw-pd-r-1 ffz-checkbox">
<input <input
:id="'key_meta$' + id" :id="'key_meta$' + id"
ref="key_meta" ref="key_meta"
:checked="edit_data.display.keys & 8" :checked="edit_data.display.keys & 8"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onChangeKeys" @change="onChangeKeys"
> >
<label :for="'key_meta$' + id" class="tw-checkbox__label"> <label :for="'key_meta$' + id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.key.meta', 'Meta') }} {{ t('setting.key.meta', 'Meta') }}
</span> </span>
</label> </label>
</div> </div>
<div class="tw-pd-r-1 tw-checkbox"> <div class="tw-pd-r-1 ffz-checkbox">
<input <input
:id="'key_hover$' + id" :id="'key_hover$' + id"
ref="key_hover" ref="key_hover"
:checked="edit_data.display.hover" :checked="edit_data.display.hover"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onChangeKeys" @change="onChangeKeys"
> >
<label :for="'key_hover$' + id" class="tw-checkbox__label"> <label :for="'key_hover$' + id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.key.hover', 'Hover') }} {{ t('setting.key.hover', 'Hover') }}
</span> </span>
@ -361,7 +361,7 @@
<select <select
id="action_type" id="action_type"
v-model="edit_data.action" v-model="edit_data.action"
class="tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05" class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
> >
<option <option
v-for="(a, key) in data.actions" v-for="(a, key) in data.actions"

View file

@ -61,7 +61,7 @@
<input <input
ref="unlisted" ref="unlisted"
:placeholder="t('addon.unlisted.id', 'add-on id')" :placeholder="t('addon.unlisted.id', 'add-on id')"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
@keydown.enter="addUnlisted" @keydown.enter="addUnlisted"
> >
<button <button

View file

@ -18,7 +18,7 @@
<input <input
ref="url_box" ref="url_box"
:value="url" :value="url"
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
type="text" type="text"
readonly readonly
@focusin="selectURL" @focusin="selectURL"

View file

@ -18,7 +18,7 @@
<select <select
v-if="editing" v-if="editing"
v-model="edit_data.v" v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
> >
<optgroup <optgroup
v-for="section in badges" v-for="section in badges"

View file

@ -32,20 +32,20 @@
<header <header
v-if="sec.id" v-if="sec.id"
:class="{default: badgeDefault(sec.id)}" :class="{default: badgeDefault(sec.id)}"
class="tw-flex tw-checkbox" class="tw-flex ffz-checkbox"
> >
<input <input
:id="sec.id" :id="sec.id"
:checked="badgeChecked(sec.id)" :checked="badgeChecked(sec.id)"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@click="onChange(sec.id, $event)" @click="onChange(sec.id, $event)"
@contextmenu.prevent="reset(sec.id)" @contextmenu.prevent="reset(sec.id)"
> >
<label <label
:for="sec.id" :for="sec.id"
:title="t('setting.right-click-reset', 'Right-Click to Reset')" :title="t('setting.right-click-reset', 'Right-Click to Reset')"
class="tw-checkbox__label" class="ffz-checkbox__label"
@contextmenu.prevent="reset(sec.id)" @contextmenu.prevent="reset(sec.id)"
> >
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
@ -61,17 +61,17 @@
v-for="i in sort(sec.badges)" v-for="i in sort(sec.badges)"
:key="i.id" :key="i.id"
:class="{default: badgeDefault(i.id)}" :class="{default: badgeDefault(i.id)}"
class="ffz--badge-info tw-pd-y-1 tw-pd-r-1 tw-flex tw-checkbox" class="ffz--badge-info tw-pd-y-1 tw-pd-r-1 tw-flex ffz-checkbox"
> >
<input <input
:id="i.id" :id="i.id"
:checked="badgeChecked(i.id)" :checked="badgeChecked(i.id)"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@click="onChange(i.id, $event)" @click="onChange(i.id, $event)"
> >
<label :for="i.id" class="tw-checkbox__label"> <label :for="i.id" class="ffz-checkbox__label">
<div class="tw-mg-l-1 tw-flex"> <div class="tw-mg-l-1 tw-flex">
<div <div
:style="{backgroundColor: i.color, backgroundImage: i.styleImage }" :style="{backgroundColor: i.color, backgroundImage: i.styleImage }"

View file

@ -8,7 +8,7 @@
<select <select
v-if="editing" v-if="editing"
v-model="edit_data.v" v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
> >
<option <option
v-for="type in types" v-for="type in types"

View file

@ -11,22 +11,22 @@
<div v-if=" ! addons" class="tw-mg-b-1 tw-flex tw-align-items-center"> <div v-if=" ! addons" class="tw-mg-b-1 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1" /> <div class="tw-flex-grow-1" />
<div class="tw-checkbox tw-relative tw-tooltip__container"> <div class="ffz-checkbox tw-relative tw-tooltip__container">
<input <input
id="nonversioned" id="nonversioned"
ref="nonversioned" ref="nonversioned"
v-model="nonversioned" v-model="nonversioned"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
> >
<label for="nonversioned" class="tw-checkbox__label"> <label for="nonversioned" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('home.changelog.show-nonversioned', 'Include non-versioned commits.') }} {{ t('home.changelog.show-nonversioned', 'Include non-versioned commits.') }}
</span> </span>
</label> </label>
<div class="tw-tooltip tw-balloon--md tw-tooltip--wrap tw-tooltip--down tw-tooltip--align-right"> <div class="tw-tooltip ffz-balloon--md tw-tooltip--wrap tw-tooltip--down tw-tooltip--align-right">
{{ t('home.changelog.about-nonversioned', 'Non-versioned commits are commits to the FrankerFaceZ repository not associated with a release build. They typically represent maintenance or contributions from the community that will be included in a subsequent release.') }} {{ t('home.changelog.about-nonversioned', 'Non-versioned commits are commits to the FrankerFaceZ repository not associated with a release build. They typically represent maintenance or contributions from the community that will be included in a subsequent release.') }}
</div> </div>
</div> </div>
@ -58,7 +58,7 @@
> >
<figure <figure
v-if="commit.author.avatar_url" v-if="commit.author.avatar_url"
class="tw-avatar tw-avatar--size-20 tw-mg-r-05" class="ffz-avatar ffz-avatar--size-20 tw-mg-r-05"
> >
<img <img
:src="commit.author.avatar_url" :src="commit.author.avatar_url"

View file

@ -12,153 +12,153 @@
<div class="tw-flex tw-flex-wrap tw-align-items-center ffz--inline"> <div class="tw-flex tw-flex-wrap tw-align-items-center ffz--inline">
{{ t('setting.actions.preview', 'Preview:') }} {{ t('setting.actions.preview', 'Preview:') }}
<div class="tw-pd-x-1 tw-checkbox"> <div class="tw-pd-x-1 ffz-checkbox">
<input <input
id="as_mod" id="as_mod"
ref="as_mod" ref="as_mod"
:checked="is_moderator" :checked="is_moderator"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="as_mod" class="tw-checkbox__label"> <label for="as_mod" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.mod', 'As Moderator') }} {{ t('setting.actions.preview.mod', 'As Moderator') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_msg" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_msg" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="is_deleted" id="is_deleted"
ref="is_deleted" ref="is_deleted"
:checked="is_deleted" :checked="is_deleted"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="is_deleted" class="tw-checkbox__label"> <label for="is_deleted" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.deleted', 'Deleted Message') }} {{ t('setting.actions.preview.deleted', 'Deleted Message') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_icons" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_icons" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="with_mod_icons" id="with_mod_icons"
ref="with_mod_icons" ref="with_mod_icons"
:checked="with_mod_icons" :checked="with_mod_icons"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="with_mod_icons" class="tw-checkbox__label"> <label for="with_mod_icons" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.mod_icons', 'With Mod Icons') }} {{ t('setting.actions.preview.mod_icons', 'With Mod Icons') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_mode" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_mode" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="with_slow" id="with_slow"
ref="with_slow" ref="with_slow"
:checked="with_slow" :checked="with_slow"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="with_slow" class="tw-checkbox__label"> <label for="with_slow" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.slow', 'Slow Mode') }} {{ t('setting.actions.preview.slow', 'Slow Mode') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_mode" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_mode" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="with_emote" id="with_emote"
ref="with_emote" ref="with_emote"
:checked="with_emote" :checked="with_emote"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="with_emote" class="tw-checkbox__label"> <label for="with_emote" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.emote-only', 'Emote-Only') }} {{ t('setting.actions.preview.emote-only', 'Emote-Only') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_mode" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_mode" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="with_subs" id="with_subs"
ref="with_subs" ref="with_subs"
:checked="with_subs" :checked="with_subs"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="with_subs" class="tw-checkbox__label"> <label for="with_subs" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.subs', 'Subs Only') }} {{ t('setting.actions.preview.subs', 'Subs Only') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_mode" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_mode" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="with_followers" id="with_followers"
ref="with_followers" ref="with_followers"
:checked="with_followers" :checked="with_followers"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="with_followers" class="tw-checkbox__label"> <label for="with_followers" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.followers', 'Followers-Only') }} {{ t('setting.actions.preview.followers', 'Followers-Only') }}
</span> </span>
</label> </label>
</div> </div>
<div v-if="has_mode" class="tw-pd-x-1 tw-checkbox"> <div v-if="has_mode" class="tw-pd-x-1 ffz-checkbox">
<input <input
id="with_r9k" id="with_r9k"
ref="with_r9k" ref="with_r9k"
:checked="with_r9k" :checked="with_r9k"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="with_r9k" class="tw-checkbox__label"> <label for="with_r9k" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.r9k', 'R9k Mode') }} {{ t('setting.actions.preview.r9k', 'R9k Mode') }}
</span> </span>
</label> </label>
</div> </div>
<div class="tw-pd-x-1 tw-checkbox"> <div class="tw-pd-x-1 ffz-checkbox">
<input <input
id="show_all" id="show_all"
ref="show_all" ref="show_all"
:checked="show_all" :checked="show_all"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onPreview" @change="onPreview"
> >
<label for="show_all" class="tw-checkbox__label"> <label for="show_all" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.actions.preview.all', 'Show All') }} {{ t('setting.actions.preview.all', 'Show All') }}
</span> </span>
@ -230,7 +230,7 @@
<input <input
ref="paste" ref="paste"
:placeholder="t('setting.paste-json.json', '[json]')" :placeholder="t('setting.paste-json.json', '[json]')"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
@keydown.enter="addFromJSON" @keydown.enter="addFromJSON"
> >
<button <button
@ -245,7 +245,7 @@
</div> </div>
<div v-else class="tw-pd-y-1"> <div v-else class="tw-pd-y-1">
<button <button
class="tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive tw-full-width" class="ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-full-width"
@click="add_pasting = true" @click="add_pasting = true"
> >
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1"> <div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
@ -264,7 +264,7 @@
v-else v-else
:key="idx" :key="idx"
:disabled="preset.disabled" :disabled="preset.disabled"
class="tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive tw-full-width" class="ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-full-width"
@click="add(preset.value)" @click="add(preset.value)"
> >
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1"> <div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">

View file

@ -16,16 +16,16 @@
<div <div
v-for="(type, key) in types" v-for="(type, key) in types"
:key="key" :key="key"
class="tw-checkbox tw-relative tw-mg-y-05" class="ffz-checkbox tw-relative tw-mg-y-05"
> >
<input <input
:id="key" :id="key"
:ref="key" :ref="key"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
> >
<label :for="key" class="tw-checkbox__label"> <label :for="key" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t(`setting.clear.opt.${key}`, type.label || key) }} {{ t(`setting.clear.opt.${key}`, type.label || key) }}
</span> </span>
@ -41,7 +41,7 @@
<input <input
v-model="entered" v-model="entered"
type="text" type="text"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-input tw-pd-x-1 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 ffz-input tw-pd-x-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
> >

View file

@ -16,7 +16,7 @@
<input <input
ref="code" ref="code"
type="text" type="text"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
@keydown.enter="enterCode" @keydown.enter="enterCode"
@ -31,7 +31,7 @@
</div> </div>
<select <select
ref="sort_select" ref="sort_select"
class="tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05" class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onSort" @change="onSort"
> >
<option :selected="sort_by === 0"> <option :selected="sort_by === 0">
@ -44,16 +44,16 @@
</div> </div>
<div class="tw-mg-b-2 tw-flex tw-align-items-center"> <div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1" /> <div class="tw-flex-grow-1" />
<div class="tw-checkbox tw-relative"> <div class="ffz-checkbox tw-relative">
<input <input
id="unused" id="unused"
ref="unused" ref="unused"
v-model="unused" v-model="unused"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
> >
<label for="unused" class="tw-checkbox__label"> <label for="unused" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('setting.experiments.show-unused', 'Display unused experiments.') }} {{ t('setting.experiments.show-unused', 'Display unused experiments.') }}
</span> </span>
@ -90,7 +90,7 @@
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start"> <div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
<select <select
:data-key="key" :data-key="key"
class="tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05" class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onChange($event)" @change="onChange($event)"
> >
<option <option
@ -169,7 +169,7 @@
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start"> <div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
<select <select
:data-key="key" :data-key="key"
class="tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05" class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onTwitchChange($event)" @change="onTwitchChange($event)"
> >
<option <option

View file

@ -21,7 +21,7 @@
<select <select
v-once v-once
ref="add_box" ref="add_box"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-select" class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-select"
> >
<option <option
v-for="(filter, key) in filters" v-for="(filter, key) in filters"

View file

@ -10,7 +10,7 @@
<select <select
id="selector" id="selector"
ref="selector" ref="selector"
class="tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05" class="tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
@change="onSelectChange" @change="onSelectChange"
> >
<option <option
@ -27,7 +27,7 @@
<input <input
ref="text" ref="text"
:disabled="! isCustomURL" :disabled="! isCustomURL"
class="ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
@blur="updateText" @blur="updateText"
@input="onTextChange" @input="onTextChange"
> >
@ -37,34 +37,34 @@
<div class="tw-flex tw-mg-b-1"> <div class="tw-flex tw-mg-b-1">
<div class="tw-flex-grow-1" /> <div class="tw-flex-grow-1" />
<div class="tw-pd-x-1 tw-checkbox"> <div class="tw-pd-x-1 ffz-checkbox">
<input <input
id="force_media" id="force_media"
ref="force_media" ref="force_media"
:checked="force_media" :checked="force_media"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onCheck" @change="onCheck"
> >
<label for="force_media" class="tw-checkbox__label"> <label for="force_media" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('debug.link-provider.allow.media', 'Allow Media') }} {{ t('debug.link-provider.allow.media', 'Allow Media') }}
</span> </span>
</label> </label>
</div> </div>
<div class="tw-pd-x-1 tw-checkbox"> <div class="tw-pd-x-1 ffz-checkbox">
<input <input
id="force_unsafe" id="force_unsafe"
ref="force_unsafe" ref="force_unsafe"
:checked="force_unsafe" :checked="force_unsafe"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onCheck" @change="onCheck"
> >
<label for="force_unsafe" class="tw-checkbox__label"> <label for="force_unsafe" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('debug.link-provider.allow.unsafe', 'Allow NSFW') }} {{ t('debug.link-provider.allow.unsafe', 'Allow NSFW') }}
</span> </span>
@ -104,17 +104,17 @@
</a> </a>
</div> </div>
<div class="tw-pd-x-1 tw-checkbox"> <div class="tw-pd-x-1 ffz-checkbox">
<input <input
id="force_tooltip" id="force_tooltip"
ref="force_tooltip" ref="force_tooltip"
:checked="force_tooltip" :checked="force_tooltip"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onTooltip" @change="onTooltip"
> >
<label for="force_tooltip" class="tw-checkbox__label"> <label for="force_tooltip" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('debug.link-provider.force-tooltip', 'Force Tooltip') }} {{ t('debug.link-provider.force-tooltip', 'Force Tooltip') }}
</span> </span>

View file

@ -11,7 +11,7 @@
<div class="tw-search-input"> <div class="tw-search-input">
<label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('main-menu.search', 'Search Settings') }}</label> <label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('main-menu.search', 'Search Settings') }}</label>
<div class="tw-relative"> <div class="tw-relative">
<div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height tw-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default"> <div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height ffz-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
<figure class="ffz-i-search" /> <figure class="ffz-i-search" />
</div> </div>
<input <input
@ -19,7 +19,7 @@
v-model="query" v-model="query"
:placeholder="t('main-menu.search', 'Search Settings')" :placeholder="t('main-menu.search', 'Search Settings')"
type="search" type="search"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-3 tw-pd-r-1 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-3 tw-pd-r-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
autocomplete="off" autocomplete="off"

View file

@ -98,7 +98,7 @@
id="ffz:editor:name" id="ffz:editor:name"
ref="name" ref="name"
v-model="name" v-model="name"
class="tw-full-width tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-full-width tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
> >
</div> </div>
@ -111,7 +111,7 @@
id="ffz:editor:description" id="ffz:editor:description"
ref="desc" ref="desc"
v-model="desc" v-model="desc"
class="tw-full-width tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-full-width tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
/> />
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@
ref="button" ref="button"
:class="{active: opened}" :class="{active: opened}"
tabindex="0" tabindex="0"
class="tw-c-background-alt tw-block tw-border tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05" class="tw-c-background-alt tw-block tw-border tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
@keyup.up.stop.prevent="focusShow" @keyup.up.stop.prevent="focusShow"
@keyup.left.stop.prevent="focusShow" @keyup.left.stop.prevent="focusShow"
@keyup.down.stop.prevent="focusShow" @keyup.down.stop.prevent="focusShow"
@ -18,7 +18,7 @@
<div <div
v-if="opened" v-if="opened"
v-on-clickaway="hide" v-on-clickaway="hide"
class="tw-balloon tw-block tw-balloon--lg tw-balloon--down tw-balloon--left tw-z-above" class="ffz-balloon tw-block ffz-balloon--lg ffz-balloon--down ffz-balloon--left tw-z-above"
> >
<div <div
class="ffz--profile-list tw-elevation-2 tw-c-background-alt" class="ffz--profile-list tw-elevation-2 tw-c-background-alt"

View file

@ -1,5 +1,14 @@
<template lang="html"> <template lang="html">
<div class="ffz--provider tw-pd-t-05"> <div class="ffz--provider tw-pd-t-05">
<div v-if="not_www" class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention">
{{ t('setting.provider.warn-domain.title', 'You\'re far from home!') }}
</h3>
<div>
<markdown :source="t('setting.provider.warn-domain.desc', 'You are currently changing settings for a sub-domain of Twitch. Any changes here will only affect this sub-domain. You probably want to change this from Twitch\'s main website, and not here.')" />
</div>
</div>
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"> <div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention"> <h3 class="ffz-i-attention">
{{ t('setting.provider.warn.title', 'Be careful!') }} {{ t('setting.provider.warn.title', 'Be careful!') }}
@ -14,18 +23,18 @@
</section> </section>
<div class="ffz-options"> <div class="ffz-options">
<div v-for="val of providers" :key="val.key" class="tw-pd-b-1 tw-radio ffz-radio-top"> <div v-for="val of providers" :key="val.key" class="tw-pd-b-1 ffz-radio ffz-radio-top">
<input <input
:id="'ffz--provider-opt-' + val.key" :id="'ffz--provider-opt-' + val.key"
v-model="selected" v-model="selected"
:value="val.key" :value="val.key"
name="ffz--provider-opt" name="ffz--provider-opt"
type="radio" type="radio"
class="tw-radio__input" class="ffz-radio__input"
> >
<label <label
:for="'ffz--provider-opt-' + val.key" :for="'ffz--provider-opt-' + val.key"
class="tw-block tw-radio__label" class="tw-block ffz-radio__label"
> >
<div class="tw-mg-l-1"> <div class="tw-mg-l-1">
<div> <div>
@ -51,20 +60,23 @@
</div> </div>
<div class="tw-border-t tw-pd-t-1"> <div class="tw-border-t tw-pd-t-1">
<div class="tw-flex tw-align-items-center tw-checkbox"> <div v-if="canTransfer" class="tw-flex tw-align-items-center ffz-checkbox">
<input id="transfer" ref="transfer" checked type="checkbox" class="tw-checkbox__input"> <input id="transfer" ref="transfer" checked type="checkbox" class="ffz-checkbox__input">
<label for="transfer" class="tw-checkbox__label"> <label for="transfer" class="ffz-checkbox__label">
<div class="tw-mg-l-1"> <div class="tw-mg-l-1">
{{ t('setting.provider.transfer', 'Transfer my settings when switching provider.') }} {{ t('setting.provider.transfer', 'Transfer my settings when switching provider.') }}
</div> </div>
</label> </label>
</div> </div>
<section class="tw-c-text-alt-2" style="padding-left:2.5rem"> <section v-if="canTransfer" class="tw-c-text-alt-2 tw-pd-b-05" style="padding-left:2.5rem">
<markdown :source="t('setting.provider.transfer.desc', '**Note:** It is recommended to leave this enabled unless you know what you\'re doing.')" /> <markdown :source="t('setting.provider.transfer.desc', '**Note:** It is recommended to leave this enabled unless you know what you\'re doing.')" />
</section> </section>
<div class="tw-mg-t-1 tw-flex tw-align-items-center tw-checkbox"> <div v-else class="tw-flex tw-align-items-center" style="padding-left:2.5rem">
<input id="backup" ref="backup" v-model="backup" type="checkbox" class="tw-checkbox__input"> {{ t('setting.provider.no-transfer', 'Automatically transfering settings from your current provider to the selected provider is not allowed. Please use Backup and Restore.') }}
<label for="backup" class="tw-checkbox__label"> </div>
<div class="tw-mg-t-1 tw-flex tw-align-items-center ffz-checkbox">
<input id="backup" ref="backup" v-model="backup" type="checkbox" class="ffz-checkbox__input">
<label for="backup" class="ffz-checkbox__label">
<div class="tw-mg-l-1"> <div class="tw-mg-l-1">
{{ t('setting.provider.backup', 'Yes, I made a backup.') }} {{ t('setting.provider.backup', 'Yes, I made a backup.') }}
</div> </div>
@ -97,33 +109,41 @@ export default {
data() { data() {
const ffz = this.context.getFFZ(), const ffz = this.context.getFFZ(),
settings = ffz.resolve('settings'), settings = ffz.resolve('settings'),
providers = []; providers = [],
transfers = {};
for(const [key, val] of Object.entries(settings.getProviders())) { for(const [key, val] of Object.entries(settings.getProviders())) {
const prov = { const prov = {
key, key,
priority: val.priority || 0,
has_data: null, has_data: null,
has_blobs: val.supportsBlobs, has_blobs: val.supportsBlobs,
has_trans: val.allowTransfer,
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,
description: val.description description: val.description
}; };
transfers[key] = val.allowTransfer;
if ( val.supported() ) if ( val.supported() )
Promise.resolve(val.hasContent()).then(v => { Promise.resolve(val.hasContent()).then(v => {
prov.has_data = v; prov.has_data = v;
}); });
providers.push(prov); providers.push(prov);
} }
providers.sort((a,b) => b.priority - a.priority);
const current = settings.getActiveProvider(); const current = settings.getActiveProvider();
return { return {
backup: false, backup: false,
not_www: window.location.host !== 'www.twitch.tv',
providers, providers,
transfers,
current, current,
selected: current selected: current
} }
@ -132,6 +152,10 @@ export default {
computed: { computed: {
enabled() { enabled() {
return this.selected !== this.current && this.backup return this.selected !== this.current && this.backup
},
canTransfer() {
return this.transfers[this.selected] && this.transfers[this.current]
} }
}, },

View file

@ -10,7 +10,7 @@
v-model="edit_data.text" v-model="edit_data.text"
:placeholder="adding ? t('setting.reasons.add-placeholder', 'Add a new reason') : edit_data.text" :placeholder="adding ? t('setting.reasons.add-placeholder', 'Add a new reason') : edit_data.text"
type="text" type="text"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
> >

View file

@ -3,17 +3,17 @@
:class="{inherits: isInherited, default: isDefault}" :class="{inherits: isInherited, default: isDefault}"
class="ffz--widget ffz--checkbox" class="ffz--widget ffz--checkbox"
> >
<div class="tw-flex tw-align-items-center tw-checkbox"> <div class="tw-flex tw-align-items-center ffz-checkbox">
<input <input
:id="item.full_key" :id="item.full_key"
ref="control" ref="control"
:checked="value" :checked="value"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
@change="onChange" @change="onChange"
> >
<label :for="item.full_key" class="tw-checkbox__label"> <label :for="item.full_key" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t(item.i18n_key, item.title) }} {{ t(item.i18n_key, item.title) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>

View file

@ -13,7 +13,7 @@
<select <select
:id="item.full_key" :id="item.full_key"
ref="control" ref="control"
class="tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05" class="tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
@change="onChange" @change="onChange"
> >
<option <option
@ -31,7 +31,7 @@
ref="text" ref="text"
:value="value" :value="value"
:disabled="! isCustom" :disabled="! isCustom"
class="ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
@change="onTextChange" @change="onTextChange"
> >
</div> </div>

View file

@ -1,5 +1,5 @@
<template lang="html"> <template lang="html">
<div class="tw-input"> <div class="ffz-input">
<header> <header>
{{ t(item.i18n_key, item.title) }} {{ t(item.i18n_key, item.title) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
@ -19,11 +19,11 @@
:name="item.full_key" :name="item.full_key"
:value="i.value" :value="i.value"
type="radio" type="radio"
class="tw-radio__input" class="ffz-radio__input"
> >
<label <label
:for="item.full_key + idx" :for="item.full_key + idx"
class="tw-pd-y-05 tw-radio__label" class="tw-pd-y-05 ffz-radio__label"
> >
{{ t(i.i18n_key, i.title, i) }} {{ t(i.i18n_key, i.title, i) }}
</label> </label>

View file

@ -12,7 +12,7 @@
<select <select
:id="item.full_key" :id="item.full_key"
ref="control" ref="control"
class="tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-05" class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-05"
@change="onChange" @change="onChange"
> >
<template v-for="i in nested_data"> <template v-for="i in nested_data">

View file

@ -15,7 +15,7 @@
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:value="value" :value="value"
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-mg-05 tw-input" class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-mg-05 ffz-input"
@change="onChange" @change="onChange"
> >

View file

@ -22,7 +22,7 @@
v-model="edit_data.v" v-model="edit_data.v"
:placeholder="adding ? t('setting.terms.add-placeholder', 'Add a new term') : edit_data.v" :placeholder="adding ? t('setting.terms.add-placeholder', 'Add a new term') : edit_data.v"
type="text" type="text"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
> >
@ -40,7 +40,7 @@
<select <select
v-else v-else
v-model="edit_data.t" v-model="edit_data.t"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 ffz-min-width-unset" class="tw-block tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 ffz-min-width-unset"
> >
<option value="text"> <option value="text">
{{ t('setting.terms.type.text', 'Text') }} {{ t('setting.terms.type.text', 'Text') }}

View file

@ -210,7 +210,7 @@ export default class Metadata extends Module {
return false; return false;
} : null; } : null;
tip.element.classList.add('tw-balloon--lg'); tip.element.classList.add('ffz-balloon--lg');
return (<div> return (<div>
<div class="tw-pd-b-1 tw-mg-b-1 tw-border-b tw-semibold"> <div class="tw-pd-b-1 tw-mg-b-1 tw-border-b tw-semibold">
@ -220,7 +220,7 @@ export default class Metadata extends Module {
</div> </div>
<div class="tw-flex tw-align-items-center"> <div class="tw-flex tw-align-items-center">
<input <input
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input tw-full-width" class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input tw-full-width"
type="text" type="text"
value={url} value={url}
onFocus={e => e.target.select()} onFocus={e => e.target.select()}
@ -737,10 +737,10 @@ export default class Metadata extends Module {
live: false, live: false,
html: true, html: true,
tooltipClass: 'ffz-metadata-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base', tooltipClass: 'ffz-metadata-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
// Hide the arrow for now, until we re-do our CSS to make it render correctly. // Hide the arrow for now, until we re-do our CSS to make it render correctly.
arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute', arrowClass: 'ffz-balloon__tail tw-overflow-hidden tw-absolute',
arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute', arrowInner: 'ffz-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
innerClass: 'tw-pd-1', innerClass: 'tw-pd-1',
popper: { popper: {

View file

@ -21,8 +21,8 @@
<textarea <textarea
ref="editor" ref="editor"
v-model="value" v-model="value"
:class="{'tw-textarea--error': ! valid}" :class="{'ffz-textarea--error': ! valid}"
class="tw-block tw-font-size-6 tw-full-width tw-textarea" class="tw-block tw-font-size-6 tw-full-width ffz-textarea"
@input="onInput" @input="onInput"
@blur="onBlur" @blur="onBlur"
@focus="open = true" @focus="open = true"
@ -86,7 +86,7 @@
:id="`ui-ctx:${entry.key}:${val.key}`" :id="`ui-ctx:${entry.key}:${val.key}`"
:type="val.is_number ? 'number' : 'text'" :type="val.is_number ? 'number' : 'text'"
:value="val.value" :value="val.value"
class="tw-full-width tw-block tw-border-radius-medium tw-font-size-6 tw-input tw-pd-x-1 tw-pd-y-05" class="tw-full-width tw-block tw-border-radius-medium tw-font-size-6 ffz-input tw-pd-x-1 tw-pd-y-05"
@input="updateContext(val.key, $event)" @input="updateContext(val.key, $event)"
> >
</div> </div>

View file

@ -9,7 +9,7 @@
<div class="tw-search-input"> <div class="tw-search-input">
<label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('i18n.ui.search', 'Search Strings') }}</label> <label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('i18n.ui.search', 'Search Strings') }}</label>
<div class="tw-relative"> <div class="tw-relative">
<div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height tw-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default"> <div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height ffz-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
<figure class="ffz-i-search" /> <figure class="ffz-i-search" />
</div> </div>
<input <input
@ -17,7 +17,7 @@
v-model="query" v-model="query"
:placeholder="t('i18n.ui.search', 'Search Strings')" :placeholder="t('i18n.ui.search', 'Search Strings')"
type="search" type="search"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-3 tw-pd-r-1 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-3 tw-pd-r-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
autocomplete="off" autocomplete="off"
@ -99,7 +99,7 @@
ref="pager" ref="pager"
:value="page" :value="page"
:max="pages" :max="pages"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-input tw-pd-x-1 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 ffz-input tw-pd-x-1 tw-pd-y-05"
type="number" type="number"
min="1" min="1"
@keydown.enter="closePage" @keydown.enter="closePage"

View file

@ -13,7 +13,7 @@
<div class="tw-flex tw-flex-column tw-full-height tw-full-width viewer-card__overlay"> <div class="tw-flex tw-flex-column tw-full-height tw-full-width viewer-card__overlay">
<div class="tw-align-center tw-align-items-start tw-c-background-overlay tw-c-text-overlay tw-flex tw-flex-grow-1 tw-flex-row tw-full-width tw-justify-content-start tw-pd-05 tw-relative viewer-card__banner"> <div class="tw-align-center tw-align-items-start tw-c-background-overlay tw-c-text-overlay tw-flex tw-flex-grow-1 tw-flex-row tw-full-width tw-justify-content-start tw-pd-05 tw-relative viewer-card__banner">
<div class="tw-mg-l-05 tw-mg-y-05 tw-inline-flex viewer-card-drag-cancel"> <div class="tw-mg-l-05 tw-mg-y-05 tw-inline-flex viewer-card-drag-cancel">
<figure class="tw-avatar tw-avatar--size-50"> <figure class="ffz-avatar ffz-avatar--size-50">
<img <img
v-if="loaded" v-if="loaded"
:src="user.profileImageURL" :src="user.profileImageURL"

151
src/player.js Normal file
View file

@ -0,0 +1,151 @@
'use strict';
import RavenLogger from './raven';
import Logger from 'utilities/logging';
import Module from 'utilities/module';
import { timeout } from 'utilities/object';
import {DEBUG} from 'utilities/constants';
import SettingsManager from './settings/index';
import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
class FFZPlayer extends Module {
constructor() {
super();
const start_time = performance.now(),
VER = FFZPlayer.version_info;
FFZPlayer.instance = this;
this.name = 'ffz_player';
this.__state = 0;
this.__modules.core = this;
// ========================================================================
// Error Reporting and Logging
// ========================================================================
this.inject('raven', RavenLogger);
this.log = new Logger(null, null, null, this.raven);
this.log.label = 'FFZPlayer';
this.log.init = true;
this.core_log = this.log.get('core');
this.log.info(`FrankerFaceZ Standalone Player v${VER} (build ${VER.build}${VER.commit ? ` - commit ${VER.commit}` : ''})`);
// ========================================================================
// Core Systems
// ========================================================================
this.inject('settings', SettingsManager);
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
// ========================================================================
// Startup
// ========================================================================
this.enable().then(() => {
const duration = performance.now() - start_time;
this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
this.log.init = false;
}).catch(err => {
this.core_log.error(`An error occurred during initialization.`, err);
this.log.init = false;
});
}
static get() {
return FFZPlayer.instance;
}
// ========================================================================
// Generate Log
// ========================================================================
async generateLog() {
const promises = [];
for(const key in this.__modules) { // eslint-disable-line guard-for-in
const module = this.__modules[key];
if ( module instanceof Module && module.generateLog && module != this )
promises.push((async () => {
try {
return [
key,
await timeout(Promise.resolve(module.generateLog()), 5000)
];
} catch(err) {
return [
key,
`Error: ${err}`
]
}
})());
}
const out = await Promise.all(promises);
if ( this.log.captured_init && this.log.captured_init.length > 0 ) {
const logs = [];
for(const msg of this.log.captured_init) {
const time = dayjs(msg.time).locale('en').format('H:mm:ss');
logs.push(`[${time}] ${msg.level} | ${msg.category || 'core'}: ${msg.message}`);
}
out.unshift(['initialization', logs.join('\n')]);
}
return out.map(x => `${x[0]}
-------------------------------------------------------------------------------
${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n');
}
async onEnable() {
}
}
FFZPlayer.Logger = Logger;
const VER = FFZPlayer.version_info = {
major: __version_major__,
minor: __version_minor__,
revision: __version_patch__,
extra: __version_prerelease__?.length && __version_prerelease__[0],
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
}
FFZPlayer.utilities = {
addon: require('utilities/addon'),
color: require('utilities/color'),
constants: require('utilities/constants'),
dialog: require('utilities/dialog'),
dom: require('utilities/dom'),
events: require('utilities/events'),
fontAwesome: require('utilities/font-awesome'),
graphql: require('utilities/graphql'),
logging: require('utilities/logging'),
module: require('utilities/module'),
object: require('utilities/object'),
time: require('utilities/time'),
tooltip: require('utilities/tooltip'),
i18n: require('utilities/translation-core'),
dayjs: require('dayjs'),
popper: require('popper.js').default
}
window.FFZPlayer = FFZPlayer;
window.ffz_player = new FFZPlayer();

View file

@ -61,10 +61,10 @@ export const Profiles = {
export const Everything = { export const Everything = {
label: 'Absolutely Everything', label: 'Absolutely Everything',
clear(provider, settings) { async clear(provider, settings) {
provider.clear(); provider.clear();
if ( provider.supportsBlobs() ) if ( provider.supportsBlobs )
provider.clearBlobs(); await provider.clearBlobs();
settings.loadProfiles(); settings.loadProfiles();
} }

View file

@ -1,13 +1,13 @@
<template> <template>
<section class="tw-flex-grow-1 tw-align-self-start tw-flex tw-align-items-center"> <section class="tw-flex-grow-1 tw-align-self-start tw-flex tw-align-items-center">
<div class="tw-flex tw-align-items-center tw-checkbox tw-mg-y-05"> <div class="tw-flex tw-align-items-center ffz-checkbox tw-mg-y-05">
<input <input
:id="'enabled$' + value.id" :id="'enabled$' + value.id"
v-model="value.data" v-model="value.data"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
> >
<label :for="'enabled$' + value.id" class="tw-checkbox__label"> <label :for="'enabled$' + value.id" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t(type.i18n, type.title) }} {{ t(type.i18n, type.title) }}
</span> </span>

View file

@ -11,7 +11,7 @@
v-if="current" v-if="current"
:alt="current.displayName || current.name" :alt="current.displayName || current.name"
:src="current.boxArtURL" :src="current.boxArtURL"
class="tw-avatar__img tw-image" class="ffz-avatar__img tw-image"
> >
</aspect> </aspect>
</div> </div>

View file

@ -6,13 +6,13 @@
</label> </label>
<div class="ffz--search-avatar tw-mg-x-05"> <div class="ffz--search-avatar tw-mg-x-05">
<figure class="tw-avatar tw-avatar--size-30"> <figure class="ffz-avatar ffz-avatar--size-30">
<div class="tw-border-radius-rounded tw-overflow-hidden"> <div class="tw-border-radius-rounded tw-overflow-hidden">
<img <img
v-if="current" v-if="current"
:alt="current.displayName" :alt="current.displayName"
:src="current.profileImageURL" :src="current.profileImageURL"
class="tw-avatar__img tw-image" class="ffz-avatar__img tw-image"
> >
</div> </div>
</figure> </figure>

View file

@ -8,7 +8,7 @@
<select <select
:id="'page$' + id" :id="'page$' + id"
v-model="value.data.route" v-model="value.data.route"
class="tw-flex-grow-1 tw-mg-l-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-select" class="tw-flex-grow-1 tw-mg-l-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-select"
> >
<option <option
v-for="(_, key) in routes" v-for="(_, key) in routes"
@ -45,7 +45,7 @@
<input <input
:id="'page$' + id + '$part-' + part.key" :id="'page$' + id + '$part-' + part.key"
v-model="value.data.values[part.key]" v-model="value.data.values[part.key]"
class="tw-mg-l-1 tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-mg-l-1 tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-input"
> >
</div> </div>
</div> </div>

View file

@ -13,7 +13,7 @@
:id="'start-time$' + id" :id="'start-time$' + id"
v-model="value.data[0]" v-model="value.data[0]"
type="time" type="time"
class="ffz-min-width-unset tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-mg-x-1 tw-pd-x-1 tw-pd-y-05 tw-input" class="ffz-min-width-unset tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-mg-x-1 tw-pd-x-1 tw-pd-y-05 ffz-input"
> >
<label :for="'end-time$' + id" class="tw-mg-l-1"> <label :for="'end-time$' + id" class="tw-mg-l-1">
@ -24,7 +24,7 @@
:id="'end-time$' + id" :id="'end-time$' + id"
v-model="value.data[1]" v-model="value.data[1]"
type="time" type="time"
class="ffz-min-width-unset tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-mg-x-1 tw-input" class="ffz-min-width-unset tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-mg-x-1 ffz-input"
> >
</div> </div>
</section> </section>

View file

@ -22,7 +22,7 @@
:id="'title$' + id" :id="'title$' + id"
v-model="value.data.title" v-model="value.data.title"
type="text" type="text"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-mg-x-1 tw-pd-x-1 tw-pd-y-05 tw-input" class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-mg-x-1 tw-pd-x-1 tw-pd-y-05 ffz-input"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
> >
@ -30,7 +30,7 @@
<select <select
:id="'mode$' + id" :id="'mode$' + id"
v-model="value.data.mode" v-model="value.data.mode"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 ffz-min-width-unset" class="tw-block tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 ffz-min-width-unset"
> >
<option value="text"> <option value="text">
{{ t('setting.terms.type.text', 'Text') }} {{ t('setting.terms.type.text', 'Text') }}
@ -43,14 +43,14 @@
</option> </option>
</select> </select>
<div class="tw-flex tw-align-items-center tw-checkbox tw-mg-l-1 tw-mg-y-05"> <div class="tw-flex tw-align-items-center ffz-checkbox tw-mg-l-1 tw-mg-y-05">
<input <input
:id="'sensitive$' + id" :id="'sensitive$' + id"
v-model="value.data.sensitive" v-model="value.data.sensitive"
type="checkbox" type="checkbox"
class="ffz-min-width-unset tw-checkbox__input" class="ffz-min-width-unset ffz-checkbox__input"
> >
<label :for="'sensitive$' + id" class="tw-checkbox__label tw-relative tw-tooltip__container"> <label :for="'sensitive$' + id" class="ffz-checkbox__label tw-relative tw-tooltip__container">
<span class="tw-mg-l-05"> <span class="tw-mg-l-05">
Aa Aa
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right"> <div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">

View file

@ -366,9 +366,13 @@ export default class SettingsManager extends Module {
* @returns {SettingsProvider} The provider to store everything. * @returns {SettingsProvider} The provider to store everything.
*/ */
async _createProvider() { async _createProvider() {
let wanted = localStorage.ffzProvider; // If we should be using Cross-Origin Storage Bridge, do so.
//if ( this.providers.cosb && this.providers.cosb.supported() )
// return new this.providers.cosb(this);
let wanted = localStorage.ffzProviderv2;
if ( wanted == null ) if ( wanted == null )
wanted = localStorage.ffzProvider = await this.sniffProvider(); wanted = localStorage.ffzProviderv2 = await this.sniffProvider();
if ( this.providers[wanted] ) { if ( this.providers[wanted] ) {
const provider = new this.providers[wanted](this); const provider = new this.providers[wanted](this);
@ -434,7 +438,9 @@ export default class SettingsManager extends Module {
// Are we transfering settings? // Are we transfering settings?
if ( transfer ) { if ( transfer ) {
const new_provider = new this.providers[key](this); const new_provider = new this.providers[key](this);
await new_provider.awaitReady();
if ( new_provider.allowTransfer && old_provider.allowTransfer ) {
old_provider.disableEvents(); old_provider.disableEvents();
// When transfering, we clear all existing settings. // When transfering, we clear all existing settings.
@ -460,6 +466,7 @@ export default class SettingsManager extends Module {
await old_provider.flush(); await old_provider.flush();
await new_provider.flush(); await new_provider.flush();
} }
}
// Change over. // Change over.
localStorage.ffzProvider = key; localStorage.ffzProvider = key;

View file

@ -1,5 +1,6 @@
'use strict'; 'use strict';
import { isValidBlob, deserializeBlob, serializeBlob } from 'src/utilities/blobs';
// ============================================================================ // ============================================================================
// Settings Providers // Settings Providers
// ============================================================================ // ============================================================================
@ -7,12 +8,8 @@
import {EventEmitter} from 'utilities/events'; import {EventEmitter} from 'utilities/events';
import {has} from 'utilities/object'; import {has} from 'utilities/object';
const DB_VERSION = 1; const DB_VERSION = 1,
NOT_WWW = window.location.host !== 'www.twitch.tv';
export function isValidBlob(blob) {
return blob instanceof Blob || blob instanceof File || blob instanceof ArrayBuffer || blob instanceof Uint8Array;
}
// ============================================================================ // ============================================================================
@ -43,6 +40,7 @@ export class SettingsProvider extends EventEmitter {
} }
static supportsBlobs = false; static supportsBlobs = false;
static allowTransfer = true;
awaitReady() { awaitReady() {
if ( this.ready ) if ( this.ready )
@ -51,6 +49,8 @@ export class SettingsProvider extends EventEmitter {
return Promise.reject(new Error('Not Implemented')); return Promise.reject(new Error('Not Implemented'));
} }
get allowTransfer() { return this.constructor.allowTransfer; }
broadcastTransfer() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this broadcastTransfer() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
disableEvents() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this disableEvents() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
@ -933,3 +933,251 @@ export class IndexedDBProvider extends SettingsProvider {
} }
} }
// ============================================================================
// CrossOriginStorageBridge
// ============================================================================
export class CrossOriginStorageBridge extends SettingsProvider {
constructor(manager) {
super(manager);
this._start_time = performance.now();
this._rpc = new Map;
this._cached = new Map;
this.resolved_ready = false;
this.ready = false;
this._ready_wait = null;
this._blobs = null;
this._last_id = 0;
const frame = this.frame = document.createElement('iframe');
frame.src = '//www.twitch.tv/p/ffz_bridge/';
frame.id = 'ffz-settings-bridge';
frame.style.width = 0;
frame.style.height = 0;
window.addEventListener('message', this.onMessage.bind(this));
document.body.appendChild(frame);
}
// Static Properties
static supported() { return NOT_WWW; }
static hasContent() { return NOT_WWW; }
static key = 'cosb';
static priority = 100;
static title = 'Cross-Origin Storage Bridge';
static description = 'This provider uses an `<iframe>` to synchronize storage across subdomains. Due to the `<iframe>`, this provider takes longer than others to load, but should perform roughly the same once loaded. You should be using this on non-www subdomains of Twitch unless you don\'t want your settings to automatically synchronize for some reason.';
static supportsBlobs = true;
static allowTransfer = false;
get supportsBlobs() {
return this._blobs;
}
// Initialization
_resolveReady(success, data) {
if ( this.manager )
this.manager.log.info(`COSB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
this.resolved_ready = true;
this.ready = success;
const waiters = this._ready_wait;
this._ready_wait = null;
if ( waiters )
for(const pair of waiters)
pair[success ? 0 : 1](data);
}
awaitReady() {
if ( this.resolved_ready ) {
if ( this.ready )
return Promise.resolve();
return Promise.reject();
}
return new Promise((s,f) => {
const waiters = this._ready_wait = this._ready_wait || [];
waiters.push([s,f]);
})
}
// Provider Methods
get(key, default_value) {
return this._cached.has(key) ? this._cached.get(key) : default_value;
}
set(key, value) {
if ( value === undefined ) {
if ( this.has(key) )
this.delete(key);
return;
}
this._cached.set(key, value);
this.rpc({ffz_type: 'set', key, value}).catch(err => this.manager.log.error('Error setting value', err));
this.emit('set', key, value, false);
}
delete(key) {
this._cached.delete(key);
this.rpc({ffz_type: 'delete', key}).catch(err => this.manager.log.error('Error deleting value', err));
this.emit('set', key, undefined, true);
}
clear() {
const old_cache = this._cached;
this._cached = new Map;
for(const key of old_cache.keys())
this.emit('changed', key, undefined, true);
this.rpc('clear').catch(err => this.manager.log.error('Error clearing storage', err));
}
has(key) { return this._cached.has(key); }
keys() { return this._cached.keys(); }
entries() { return this._cached.entries(); }
get size() { return this._cached.size; }
async flush() {
await this.rpc('flush');
}
// Provider Methods: Blobs
async getBlob(key) {
const msg = await this.rpc({ffz_type: 'get-blob', key});
return msg.reply && deserializeBlob(msg.reply);
}
async setBlob(key, value) {
await this.rpc({
ffz_type: 'set-blob',
key,
value: await serializeBlob(value)
});
}
async deleteBlob(key) {
await this.rpc({
ffz_type: 'delete-blob',
key
});
}
async hasBlob(key) {
const msg = await this.rpc({ffz_type: 'has-blob', key});
return msg.reply;
}
async clearBlobs() {
await this.rpc('clear-blobs');
}
async blobKeys() {
const msg = await this.rpc('blob-keys');
return msg.reply;
}
// CORS Communication
send(msg, transfer) {
if ( typeof msg === 'string' )
msg = {ffz_type: msg};
try {
this.frame.contentWindow.postMessage(
msg,
'*',
transfer ? (Array.isArray(transfer) ? transfer : [transfer]) : undefined
);
} catch(err) {
this.manager.log.error('Error sending message to bridge.', err, msg, transfer);
}
}
rpc(msg, transfer) {
const id = ++this._last_id;
return new Promise((s,f) => {
this._rpc.set(id, [s,f]);
if ( typeof msg === 'string' )
msg = {ffz_type: msg};
msg.id = id;
this.send(msg, transfer);
});
}
onMessage(event) {
const msg = event.data;
if ( ! msg || ! msg.ffz_type )
return;
if ( msg.ffz_type === 'ready' )
this.rpc('init-load').then(msg => {
this._blobs = msg.reply.blobs;
for(const [key, value] of Object.entries(msg.reply.values))
this._cached.set(key, value);
this._resolveReady(true);
}).catch(err => {
this._resolveReady(false, err);
});
else if ( msg.ffz_type === 'change' )
this.onChange(msg);
else if ( msg.ffz_type === 'change-blob' )
this.emit('changed-blob', msg.key, msg.deleted);
else if ( msg.ffz_type === 'clear-blobs' )
this.emit('clear-blobs');
else if ( msg.ffz_type === 'reply' || msg.ffz_type === 'reply-error' )
this.onReply(msg);
else
this.log.warn('Unknown Message', msg.ffz_type, msg);
}
onChange(msg) {
const key = msg.key,
value = msg.value,
deleted = msg.deleted;
if ( deleted ) {
this._cached.delete(key);
this.emit('changed', key, undefined, true);
} else {
this._cached.set(key, value);
this.emit('changed', key, value, false);
}
}
onReply(msg) {
const id = msg.id,
success = msg.ffz_type === 'reply',
cbs = this._rpc.get(id);
if ( ! cbs )
return this.log.warn('Received reply for unknown ID', id);
this._rpc.delete(id);
cbs[success ? 0 : 1](msg);
}
}

View file

@ -15,9 +15,9 @@ export default class BaseSite extends Module {
this.log.info(`Using: ${this.constructor.name}`); this.log.info(`Using: ${this.constructor.name}`);
} }
populateModules() { async populateModules() {
const ctx = require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/); const ctx = await require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
const modules = this.populate(ctx, this.log); const modules = await this.populate(ctx, this.log);
this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`); this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
} }

View file

@ -42,8 +42,8 @@ export default class Twilight extends BaseSite {
this._dom_updates = []; this._dom_updates = [];
} }
onLoad() { async onLoad() {
this.populateModules(); await this.populateModules();
this.web_munch.known(Twilight.KNOWN_MODULES); this.web_munch.known(Twilight.KNOWN_MODULES);

View file

@ -429,7 +429,7 @@ export default class EmoteMenu extends Module {
return (<button return (<button
key={data.code} key={data.code}
data-tone={tone} data-tone={tone}
class="tw-interactive tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive tw-pd-y-05 tw-pd-x-2" class="tw-interactive tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-pd-y-05 tw-pd-x-2"
onClick={this.clickTone} onClick={this.clickTone}
> >
{this.renderEmoji(data)} {this.renderEmoji(data)}
@ -446,7 +446,7 @@ export default class EmoteMenu extends Module {
const tones = Object.entries(emoji.variants).map(([tone, emoji]) => this.renderTone(emoji, tone)); const tones = Object.entries(emoji.variants).map(([tone, emoji]) => this.renderTone(emoji, tone));
return (<div class="tw-absolute tw-balloon tw-balloon--up tw-balloon--right tw-balloon tw-block"> return (<div class="tw-absolute ffz-balloon ffz-balloon--up ffz-balloon--right ffz-balloon tw-block">
<div class="tw-border-b tw-border-l tw-border-r tw-border-t tw-border-radius-medium tw-c-background-base tw-elevation-1"> <div class="tw-border-b tw-border-l tw-border-r tw-border-t tw-border-radius-medium tw-c-background-base tw-elevation-1">
{this.renderTone(emoji, null)} {this.renderTone(emoji, null)}
{tones} {tones}
@ -903,7 +903,7 @@ export default class EmoteMenu extends Module {
const padding = t.chat.context.get('chat.emote-menu.reduced-padding'); const padding = t.chat.context.get('chat.emote-menu.reduced-padding');
return (<div return (<div
class={`tw-balloon tw-balloon--md tw-balloon--up tw-balloon--right tw-block tw-absolute ffz--emote-picker${padding ? ' reduced-padding' : ''}`} class={`ffz-balloon ffz-balloon--md ffz-balloon--up ffz-balloon--right tw-block tw-absolute ffz--emote-picker${padding ? ' reduced-padding' : ''}`}
data-a-target="emote-picker" data-a-target="emote-picker"
> >
<div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base"> <div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base">
@ -2092,7 +2092,7 @@ export default class EmoteMenu extends Module {
return (<div class="tw-block"> return (<div class="tw-block">
<div class="tw-absolute tw-attached tw-attached--right tw-attached--up"> <div class="tw-absolute tw-attached tw-attached--right tw-attached--up">
<div <div
class={`tw-balloon tw-balloon--auto tw-inline-block tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2 ffz--emote-picker${padding ? ' reduced-padding' : ''}`} class={`ffz-balloon ffz-balloon--auto tw-inline-block tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2 ffz--emote-picker${padding ? ' reduced-padding' : ''}`}
data-a-target="emote-picker" data-a-target="emote-picker"
role="dialog" role="dialog"
> >
@ -2128,7 +2128,7 @@ export default class EmoteMenu extends Module {
<div class="tw-flex"> <div class="tw-flex">
<input <input
type="text" type="text"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05"
placeholder={ placeholder={
is_emoji ? is_emoji ?
t.i18n.t('emote-menu.search-emoji', 'Search for Emoji') : t.i18n.t('emote-menu.search-emoji', 'Search for Emoji') :
@ -2170,7 +2170,7 @@ export default class EmoteMenu extends Module {
<div class="emote-picker__tab-nav-container tw-flex tw-border-t tw-c-background-alt"> <div class="emote-picker__tab-nav-container tw-flex tw-border-t tw-c-background-alt">
{! visibility && <div class={`emote-picker-tab-item${tab === 'fav' ? ' emote-picker-tab-item--active' : ''} tw-relative`}> {! visibility && <div class={`emote-picker-tab-item${tab === 'fav' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button <button
class={`ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive${tab === 'fav' ? ' tw-interactable--selected' : ''}`} class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'fav' ? ' ffz-interactable--selected' : ''}`}
id="emote-picker__fav" id="emote-picker__fav"
data-tab="fav" data-tab="fav"
data-tooltip-type="html" data-tooltip-type="html"
@ -2184,7 +2184,7 @@ export default class EmoteMenu extends Module {
</div>} </div>}
{this.state.has_channel_tab && <div class={`emote-picker-tab-item${tab === 'channel' ? ' emote-picker-tab-item--active' : ''} tw-relative`}> {this.state.has_channel_tab && <div class={`emote-picker-tab-item${tab === 'channel' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button <button
class={`ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive${tab === 'channel' ? ' tw-interactable--selected' : ''}`} class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'channel' ? ' ffz-interactable--selected' : ''}`}
id="emote-picker__channel" id="emote-picker__channel"
data-tab="channel" data-tab="channel"
data-tooltip-type="html" data-tooltip-type="html"
@ -2198,7 +2198,7 @@ export default class EmoteMenu extends Module {
</div>} </div>}
<div class={`emote-picker-tab-item${tab === 'all' ? ' emote-picker-tab-item--active' : ''} tw-relative`}> <div class={`emote-picker-tab-item${tab === 'all' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button <button
class={`ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive${tab === 'all' ? ' tw-interactable--selected' : ''}`} class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'all' ? ' ffz-interactable--selected' : ''}`}
id="emote-picker__all" id="emote-picker__all"
data-tab="all" data-tab="all"
data-tooltip-type="html" data-tooltip-type="html"
@ -2212,7 +2212,7 @@ export default class EmoteMenu extends Module {
</div> </div>
{! visibility && this.state.has_emoji_tab && <div class={`emote-picker-tab-item${tab === 'emoji' ? ' emote-picker-tab-item--active' : ''} tw-relative`}> {! visibility && this.state.has_emoji_tab && <div class={`emote-picker-tab-item${tab === 'emoji' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button <button
class={`ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive${tab === 'emoji' ? ' tw-interactable--selected' : ''}`} class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'emoji' ? ' ffz-interactable--selected' : ''}`}
id="emote-picker__emoji" id="emote-picker__emoji"
data-tab="emoji" data-tab="emoji"
data-tooltip-type="html" data-tooltip-type="html"
@ -2227,7 +2227,7 @@ export default class EmoteMenu extends Module {
<div class="tw-flex-grow-1" /> <div class="tw-flex-grow-1" />
<div class="emote-picker-tab-item tw-relative"> <div class="emote-picker-tab-item tw-relative">
<button <button
class="ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" class="ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
data-tooltip-type="html" data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.settings', 'Open Settings')} data-title={t.i18n.t('emote-menu.settings', 'Open Settings')}
onClick={this.clickSettings} onClick={this.clickSettings}

View file

@ -227,7 +227,7 @@ export default class RichContent extends Module {
const tooltip = this.props.card_tooltip && this.state.full && ! this.props.force_full; const tooltip = this.props.card_tooltip && this.state.full && ! this.props.force_full;
if ( this.state.url ) { if ( this.state.url ) {
content = (<a content = (<a
class={`${tooltip ? 'ffz-tooltip ' : ''}${this.state.accent ? 'ffz-accent-card ' : ''}${this.state.error ? '': 'tw-interactable--hover-enabled '}tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--default tw-interactive`} class={`${tooltip ? 'ffz-tooltip ' : ''}${this.state.accent ? 'ffz-accent-card ' : ''}${this.state.error ? '': 'ffz-interactable--hover-enabled '}tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--default tw-interactive`}
data-tooltip-type="link" data-tooltip-type="link"
data-url={this.state.url} data-url={this.state.url}
data-is-mail={false} data-is-mail={false}

View file

@ -57,7 +57,7 @@ export default class SettingsMenu extends Module {
this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu}); this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu});
val.props.children.push(<div class="tw-full-width tw-relative"> val.props.children.push(<div class="tw-full-width tw-relative">
<button class="tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" onClick={this.ffzSettingsClick}> <button class="tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive" onClick={this.ffzSettingsClick}>
<div class="tw-align-items-center tw-flex tw-pd-05 tw-relative"> <div class="tw-align-items-center tw-flex tw-pd-05 tw-relative">
<div class="tw-flex-grow-1"> <div class="tw-flex-grow-1">
{t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')} {t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
@ -85,7 +85,7 @@ export default class SettingsMenu extends Module {
val.props.children.push(<div class="tw-full-width tw-relative"> val.props.children.push(<div class="tw-full-width tw-relative">
<button <button
class="tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" class="tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
onClick={this.ffzPauseClick} onClick={this.ffzPauseClick}
> >
<div class="tw-align-items-center tw-flex tw-pd-05 tw-relative"> <div class="tw-align-items-center tw-flex tw-pd-05 tw-relative">
@ -110,7 +110,7 @@ export default class SettingsMenu extends Module {
if ( ! this.ffzPauseClick ) if ( ! this.ffzPauseClick )
this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu}); this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu});
return (<div class="tw-absolute tw-balloon tw-balloon--auto tw-balloon--right tw-balloon--up tw-block" data-a-target="chat-settings-balloon" style={{marginRight: '-5.3rem'}}> return (<div class="tw-absolute ffz-balloon ffz-balloon--auto ffz-balloon--right ffz-balloon--up tw-block" data-a-target="chat-settings-balloon" style={{marginRight: '-5.3rem'}}>
<div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2"> <div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2">
<div class="chat-settings__popover"> <div class="chat-settings__popover">
<div class="chat-settings__header tw-align-items-center tw-c-background-base tw-flex tw-pd-x-1 tw-relative"> <div class="chat-settings__header tw-align-items-center tw-c-background-base tw-flex tw-pd-x-1 tw-relative">
@ -143,7 +143,7 @@ export default class SettingsMenu extends Module {
</p> </p>
</div> </div>
<button <button
class="tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" class="tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
data-page="chat.behavior" data-page="chat.behavior"
onClick={this.ffzSettingsClick} onClick={this.ffzSettingsClick}
> >

View file

@ -1,5 +1,6 @@
.user-avatar-animated, .user-avatar-animated,
.search-result-card, .search-result-card,
.ffz-avatar,
.tw-avatar { .tw-avatar {
--border-radius-rounded: 0 !important; --border-radius-rounded: 0 !important;
} }

View file

@ -160,7 +160,7 @@ export default class Following extends SiteModule {
// Hosted Channel Content // Hosted Channel Content
simplebarContentChildren.push(<a simplebarContentChildren.push(<a
class="tw-block tw-border-radius-small tw-full-width tw-interactable tw-interactable--default tw-interactable--hover-enabled tw-interactive" class="tw-block tw-border-radius-small tw-full-width ffz-interactable ffz-interactable--default ffz-interactable--hover-enabled tw-interactive"
href={`/${inst.props.channelLogin}`} href={`/${inst.props.channelLogin}`}
onClick={e => this.parent.hijackUserClick(e, inst.props.channelLogin, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind onClick={e => this.parent.hijackUserClick(e, inst.props.channelLogin, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
> >
@ -182,7 +182,7 @@ export default class Following extends SiteModule {
// Hosting Channels Content // Hosting Channels Content
for (const channel of channels) { for (const channel of channels) {
simplebarContentChildren.push(<a simplebarContentChildren.push(<a
class="tw-block tw-border-radius-small tw-full-width tw-interactable tw-interactable--default tw-interactable--hover-enabled tw-interactive" class="tw-block tw-border-radius-small tw-full-width ffz-interactable ffz-interactable--default ffz-interactable--hover-enabled tw-interactive"
href={`/${channel.login}`} href={`/${channel.login}`}
onClick={e => this.parent.hijackUserClick(e, channel.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind onClick={e => this.parent.hijackUserClick(e, channel.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
> >
@ -197,7 +197,7 @@ export default class Following extends SiteModule {
</a>); </a>);
} }
this.hostMenu = (<div class="ffz-host-menu tw-balloon tw-block"> this.hostMenu = (<div class="ffz-host-menu ffz-balloon tw-block">
<div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base tw-pd-05"> <div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base tw-pd-05">
<div class="scrollable-area" data-simplebar> <div class="scrollable-area" data-simplebar>
{simplebarContentChildren} {simplebarContentChildren}

View file

@ -46,6 +46,21 @@ export default class FeaturedFollow extends Module {
} }
}); });
this.follow_data = {};
this.socket.on(':command:follow_buttons', data => {
for(const channel_login in data)
if ( has(data, channel_login) )
this.follow_data[channel_login] = data[channel_login];
if ( this.vueFeaturedFollow )
this.vueFeaturedFollow.data.hasUpdate = true;
this.metadata.updateMetadata('following');
});
}
onEnable() {
this.metadata.definitions.following = { this.metadata.definitions.following = {
order: 150, order: 150,
button: true, button: true,
@ -60,7 +75,7 @@ export default class FeaturedFollow extends Module {
this._featured_follow_tip = tip; this._featured_follow_tip = tip;
tip.element.classList.remove('tw-pd-1'); tip.element.classList.remove('tw-pd-1');
tip.element.classList.add('tw-balloon--lg'); tip.element.classList.add('ffz-balloon--lg');
vue.component('featured-follow', featured_follows_vue.default); vue.component('featured-follow', featured_follows_vue.default);
return this.buildFeaturedFollowMenu(vue, data.channel.login, follows, add_callback); return this.buildFeaturedFollowMenu(vue, data.channel.login, follows, add_callback);
}, },
@ -82,18 +97,7 @@ export default class FeaturedFollow extends Module {
icon: 'ffz-i-heart' icon: 'ffz-i-heart'
}; };
this.follow_data = {};
this.socket.on(':command:follow_buttons', data => {
for(const channel_login in data)
if ( has(data, channel_login) )
this.follow_data[channel_login] = data[channel_login];
if ( this.vueFeaturedFollow )
this.vueFeaturedFollow.data.hasUpdate = true;
this.metadata.updateMetadata('following'); this.metadata.updateMetadata('following');
});
} }
async getFollowsForLogin(login) { async getFollowsForLogin(login) {

View file

@ -26,7 +26,7 @@
:data-id="host.id" :data-id="host.id"
class="tw-border-t ffz--host-user" class="tw-border-t ffz--host-user"
> >
<div class="tw-interactable tw-interactable--default"> <div class="ffz-interactable ffz-interactable--default">
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1"> <div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1">
<figure class="ffz-i-ellipsis-vert handle" /> <figure class="ffz-i-ellipsis-vert handle" />
<div class="ffz-channel-avatar"> <div class="ffz-channel-avatar">
@ -68,16 +68,16 @@
<div class="simplebar-content"> <div class="simplebar-content">
<div class="tw-pd-1"> <div class="tw-pd-1">
<div class="ffz--widget ffz--checkbox"> <div class="ffz--widget ffz--checkbox">
<div class="tw-flex tw-align-items-center tw-checkbox"> <div class="tw-flex tw-align-items-center ffz-checkbox">
<input <input
id="autoHostSettings:enabled" id="autoHostSettings:enabled"
:checked="autoHostSettings.enabled" :checked="autoHostSettings.enabled"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
data-setting="enabled" data-setting="enabled"
@change="updateCheckbox" @change="updateCheckbox"
> >
<label for="autoHostSettings:enabled" class="tw-checkbox__label"> <label for="autoHostSettings:enabled" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('metadata.host.setting.auto-hosting.title', 'Auto Hosting') }} {{ t('metadata.host.setting.auto-hosting.title', 'Auto Hosting') }}
</span> </span>
@ -88,16 +88,16 @@
</section> </section>
</div> </div>
<div class="ffz--widget ffz--checkbox"> <div class="ffz--widget ffz--checkbox">
<div class="tw-flex tw-align-items-center tw-checkbox"> <div class="tw-flex tw-align-items-center ffz-checkbox">
<input <input
id="autoHostSettings:teamHost" id="autoHostSettings:teamHost"
:checked="autoHostSettings.teamHost" :checked="autoHostSettings.teamHost"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
data-setting="teamHost" data-setting="teamHost"
@change="updateCheckbox" @change="updateCheckbox"
> >
<label for="autoHostSettings:teamHost" class="tw-checkbox__label"> <label for="autoHostSettings:teamHost" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('metadata.host.setting.team-hosting.title', 'Team Hosting') }} {{ t('metadata.host.setting.team-hosting.title', 'Team Hosting') }}
</span> </span>
@ -108,16 +108,16 @@
</section> </section>
</div> </div>
<div class="ffz--widget ffz--checkbox"> <div class="ffz--widget ffz--checkbox">
<div class="tw-flex tw-align-items-center tw-checkbox"> <div class="tw-flex tw-align-items-center ffz-checkbox">
<input <input
id="autoHostSettings:deprioritizeVodcast" id="autoHostSettings:deprioritizeVodcast"
:checked="autoHostSettings.deprioritizeVodcast" :checked="autoHostSettings.deprioritizeVodcast"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
data-setting="deprioritizeVodcast" data-setting="deprioritizeVodcast"
@change="updateCheckbox" @change="updateCheckbox"
> >
<label for="autoHostSettings:deprioritizeVodcast" class="tw-checkbox__label"> <label for="autoHostSettings:deprioritizeVodcast" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('metadata.host.setting.vodcast-hosting.title', 'Host pre-recorded videos') }} {{ t('metadata.host.setting.vodcast-hosting.title', 'Host pre-recorded videos') }}
</span> </span>
@ -128,16 +128,16 @@
</section> </section>
</div> </div>
<div class="ffz--widget ffz--checkbox"> <div class="ffz--widget ffz--checkbox">
<div class="tw-flex tw-align-items-center tw-checkbox"> <div class="tw-flex tw-align-items-center ffz-checkbox">
<input <input
id="autoHostSettings:strategy" id="autoHostSettings:strategy"
:checked="autoHostSettings.strategy === 'RANDOM'" :checked="autoHostSettings.strategy === 'RANDOM'"
type="checkbox" type="checkbox"
class="tw-checkbox__input" class="ffz-checkbox__input"
data-setting="strategy" data-setting="strategy"
@change="updateCheckbox" @change="updateCheckbox"
> >
<label for="autoHostSettings:strategy" class="tw-checkbox__label"> <label for="autoHostSettings:strategy" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t('metadata.host.setting.strategy.title', 'Randomize Host Order') }} {{ t('metadata.host.setting.strategy.title', 'Randomize Host Order') }}
</span> </span>

View file

@ -53,67 +53,6 @@ export default class HostButton extends Module {
this.metadata.updateMetadata('host'); this.metadata.updateMetadata('host');
} }
}); });
this.metadata.definitions.host = {
order: 150,
border: true,
button: true,
fade_in: true,
modview: true,
disabled: () => this._host_updating || this._host_error,
click: data => {
if ( data.channel )
this.sendHostUnhostCommand(data.channel.login);
},
popup: async (data, tip) => {
const vue = this.resolve('vue'),
_host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'),
_autoHosts = this.fetchAutoHosts(),
_autoHostSettings = this.fetchAutoHostSettings();
const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]);
this._auto_host_tip = tip;
tip.element.classList.remove('tw-pd-1');
tip.element.classList.add('tw-balloon--lg');
vue.component('host-options', host_options_vue.default);
return this.buildAutoHostMenu(vue, autoHosts, autoHostSettings, data.channel);
},
label: data => {
const ffz_user = this.site.getUser();
if ( ! this.settings.get('metadata.host-button') || ! ffz_user || ! data.channel || data.channel.login === ffz_user.login )
return;
if ( data.channel.video && ! this.isChannelHosted(data.channel.login) )
return;
if ( this._host_updating )
return this.i18n.t('metadata.host-button.updating', 'Updating...');
return (this._last_hosted_channel && this.isChannelHosted(data.channel && data.channel.login))
? this.i18n.t('metadata.host-button.unhost', 'Unhost')
: this.i18n.t('metadata.host-button.host', 'Host');
},
tooltip: () => {
if (this._host_error) {
return this.i18n.t(
`metadata.host-button.tooltip.error.${this._host_error.key}`,
this._host_error.text);
} else {
return this.i18n.t('metadata.host-button.tooltip',
'Currently hosting: {channel}',
{
channel: this._last_hosted_channel || this.i18n.t('metadata.host-button.tooltip.none', 'None')
});
}
}
};
} }
isChannelHosted(channelLogin) { isChannelHosted(channelLogin) {
@ -198,6 +137,67 @@ export default class HostButton extends Module {
onEnable() { onEnable() {
this.on('i18n:update', () => this.metadata.updateMetadata('host')); this.on('i18n:update', () => this.metadata.updateMetadata('host'));
this.metadata.definitions.host = {
order: 150,
border: true,
button: true,
fade_in: true,
modview: true,
disabled: () => this._host_updating || this._host_error,
click: data => {
if ( data.channel )
this.sendHostUnhostCommand(data.channel.login);
},
popup: async (data, tip) => {
const vue = this.resolve('vue'),
_host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'),
_autoHosts = this.fetchAutoHosts(),
_autoHostSettings = this.fetchAutoHostSettings();
const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]);
this._auto_host_tip = tip;
tip.element.classList.remove('tw-pd-1');
tip.element.classList.add('ffz-balloon--lg');
vue.component('host-options', host_options_vue.default);
return this.buildAutoHostMenu(vue, autoHosts, autoHostSettings, data.channel);
},
label: data => {
const ffz_user = this.site.getUser();
if ( ! this.settings.get('metadata.host-button') || ! ffz_user || ! data.channel || data.channel.login === ffz_user.login )
return;
if ( data.channel.video && ! this.isChannelHosted(data.channel.login) )
return;
if ( this._host_updating )
return this.i18n.t('metadata.host-button.updating', 'Updating...');
return (this._last_hosted_channel && this.isChannelHosted(data.channel && data.channel.login))
? this.i18n.t('metadata.host-button.unhost', 'Unhost')
: this.i18n.t('metadata.host-button.host', 'Host');
},
tooltip: () => {
if (this._host_error) {
return this.i18n.t(
`metadata.host-button.tooltip.error.${this._host_error.key}`,
this._host_error.text);
} else {
return this.i18n.t('metadata.host-button.tooltip',
'Currently hosting: {channel}',
{
channel: this._last_hosted_channel || this.i18n.t('metadata.host-button.tooltip.none', 'None')
});
}
}
};
this.metadata.updateMetadata('host'); this.metadata.updateMetadata('host');
this.chat.ChatService.ready((cls, instances) => { this.chat.ChatService.ready((cls, instances) => {

View file

@ -18,6 +18,7 @@ export default class MenuButton extends SiteModule {
this.inject('i18n'); this.inject('i18n');
this.inject('settings'); this.inject('settings');
this.inject('site.fine'); this.inject('site.fine');
this.inject('site.elemental');
//this.inject('addons'); //this.inject('addons');
this.should_enable = true; this.should_enable = true;
@ -44,10 +45,16 @@ export default class MenuButton extends SiteModule {
['mod-view'] ['mod-view']
); );
this.SunlightDash = this.fine.define( /*this.SunlightDash = this.fine.define(
'sunlight-dash', 'sunlight-dash',
n => n.getIsChannelEditor && n.getIsChannelModerator && n.getIsAdsEnabled && n.getIsSquadStreamsEnabled, n => n.getIsChannelEditor && n.getIsChannelModerator && n.getIsAdsEnabled && n.getIsSquadStreamsEnabled,
Twilight.SUNLIGHT_ROUTES Twilight.SUNLIGHT_ROUTES
);*/
this.SunlightNav = this.elemental.define(
'sunlight-nav', '.sunlight-top-nav > .tw-flex > .tw-flex > .tw-justify-content-end > .tw-flex',
Twilight.SUNLIGHT_ROUTES,
{attributes: true}, 1
); );
this.NavBar = this.fine.define( this.NavBar = this.fine.define(
@ -195,8 +202,11 @@ export default class MenuButton extends SiteModule {
for(const inst of this.MultiController.instances) for(const inst of this.MultiController.instances)
this.updateButton(inst); this.updateButton(inst);
for(const inst of this.SunlightDash.instances) //for(const inst of this.SunlightDash.instances)
this.updateButton(inst); // this.updateButton(inst);
for(const el of this.SunlightNav.instances)
this.updateButton(null, el, true);
for(const inst of this.ModBar.instances) for(const inst of this.ModBar.instances)
this.updateButton(inst); this.updateButton(inst);
@ -216,9 +226,13 @@ export default class MenuButton extends SiteModule {
this.MultiController.on('mount', this.updateButton, this); this.MultiController.on('mount', this.updateButton, this);
this.MultiController.on('update', this.updateButton, this); this.MultiController.on('update', this.updateButton, this);
this.SunlightDash.ready(() => this.update()); /*this.SunlightDash.ready(() => this.update());
this.SunlightDash.on('mount', this.updateButton, this); this.SunlightDash.on('mount', this.updateButton, this);
this.SunlightDash.on('update', this.updateButton, this); this.SunlightDash.on('update', this.updateButton, this);*/
this.SunlightNav.on('mount', el => this.updateButton(null, el, true));
this.SunlightNav.on('mutate', el => this.updateButton(null, el, true));
this.SunlightNav.each(el => this.updateButton(null, el, true));
this.ModBar.ready(() => this.update()); this.ModBar.ready(() => this.update());
this.ModBar.on('mount', this.updateButton, this); this.ModBar.on('mount', this.updateButton, this);
@ -241,11 +255,13 @@ export default class MenuButton extends SiteModule {
}); });
} }
updateButton(inst) { updateButton(inst, container, is_sunlight) {
const root = this.fine.getChildNode(inst); const root = this.fine.getChildNode(inst);
let is_squad = false, let is_squad = false,
is_sunlight = false, //is_sunlight = false,
is_mod = false, is_mod = false;
if ( ! container )
container = root && root.querySelector('.top-nav__menu'); container = root && root.querySelector('.top-nav__menu');
if ( ! container ) { if ( ! container ) {
@ -265,7 +281,7 @@ export default class MenuButton extends SiteModule {
} }
if ( ! container && inst.getIsAdsEnabled ) { if ( ! container && inst.getIsAdsEnabled ) {
container = root && root.querySelector('.sunlight-top-nav > .tw-flex'); container = root && root.querySelector('.sunlight-top-nav > .tw-flex > .tw-flex > .tw-justify-content-end > .tw-flex');
if ( container ) if ( container )
is_sunlight = true; is_sunlight = true;
} }
@ -279,7 +295,7 @@ export default class MenuButton extends SiteModule {
if ( ! container ) if ( ! container )
return; return;
if ( ! is_squad && ! is_mod ) { if ( ! is_squad && ! is_mod && ! is_sunlight ) {
let user_stuff = null; let user_stuff = null;
try { try {
user_stuff = container.querySelector(':scope > .tw-justify-content-end:last-child'); user_stuff = container.querySelector(':scope > .tw-justify-content-end:last-child');
@ -314,7 +330,7 @@ export default class MenuButton extends SiteModule {
</span> </span>
</div> </div>
</button>)} </button>)}
{this.has_error && (<div class={`tw-absolute tw-balloon tw-balloon--lg tw-block ${is_mod ? 'tw-tooltip--up tw-tooltip--align-left' : 'tw-tooltip--down tw-tooltip--align-right'}`}> {this.has_error && (<div class={`tw-absolute ffz-balloon ffz-balloon--lg tw-block ${is_mod ? 'tw-tooltip--up tw-tooltip--align-left' : 'tw-tooltip--down tw-tooltip--align-right'}`}>
<div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-4 tw-pd-1"> <div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-4 tw-pd-1">
<div class="tw-flex tw-align-items-center"> <div class="tw-flex tw-align-items-center">
<div class="tw-flex-grow-1"> <div class="tw-flex-grow-1">
@ -377,8 +393,16 @@ export default class MenuButton extends SiteModule {
if ( is_mod ) if ( is_mod )
container.insertBefore(el, container.firstElementChild); container.insertBefore(el, container.firstElementChild);
else {
let before = container.lastElementChild;
if ( before && before.classList.contains('resize-detector') )
before = before.previousElementSibling;
if ( before )
container.insertBefore(el, before);
else else
container.insertBefore(el, container.lastElementChild); container.appendChild(el);
}
if ( this._ctx_open ) if ( this._ctx_open )
this.renderContext(null, btn); this.renderContext(null, btn);
@ -476,7 +500,7 @@ export default class MenuButton extends SiteModule {
ctx = (<div class={`tw-absolute tw-attached ${is_mod ? 'tw-attached--up tw-attached--left' : 'tw-attached--down tw-attached--right'}`}> ctx = (<div class={`tw-absolute tw-attached ${is_mod ? 'tw-attached--up tw-attached--left' : 'tw-attached--down tw-attached--right'}`}>
<div class={`tw-balloon tw-balloon--lg tw-block ffz--menu-context`}> <div class={`ffz-balloon ffz-balloon--lg tw-block ffz--menu-context`}>
<div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-4"> <div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-4">
<div class="tw-c-text-base tw-elevation-1 tw-flex tw-flex-shrink-0 tw-pd-x-1 tw-pd-y-05 tw-popover-header"> <div class="tw-c-text-base tw-elevation-1 tw-flex tw-flex-shrink-0 tw-pd-x-1 tw-pd-y-05 tw-popover-header">
<div class="tw-flex tw-flex-column tw-justify-content-center tw-mg-l-05 tw-popover-header__icon-slot--left"> <div class="tw-flex tw-flex-column tw-justify-content-center tw-mg-l-05 tw-popover-header__icon-slot--left">

View file

@ -1,104 +0,0 @@
'use strict';
// ============================================================================
// Settings Sync
// ============================================================================
import Module from 'utilities/module';
import {createElement} from 'utilities/dom';
export default class SettingsSync extends Module {
constructor(...args) {
super(...args);
this.should_enable = window.location.host !== 'www.twitch.tv';
this.inject('settings');
}
onEnable() {
const frame = this.frame = createElement('iframe');
frame.src = '//www.twitch.tv/p/ffz_bridge/';
frame.id = 'ffz-settings-bridge';
frame.style.width = 0;
frame.style.height = 0;
this.settings.provider.on('set', this.onProviderSet, this);
window.addEventListener('message', this.onMessage.bind(this));
document.body.appendChild(frame);
}
send(msg) {
try {
this.frame.contentWindow.postMessage(msg, '*');
} catch(err) { this.log.error('send error', err); /* no-op */ }
}
onMessage(event) {
const msg = event.data;
if ( ! msg || ! msg.ffz_type )
return;
if ( msg.ffz_type === 'ready' )
this.send({ffz_type: 'load'});
else if ( msg.ffz_type === 'loaded' )
this.onLoad(msg.data);
else if ( msg.ffz_type === 'change' )
this.onChange(msg);
else
this.log.info('Unknown Message', msg.ffz_type, msg);
}
onLoad(data) {
if ( ! data )
return;
const provider = this.settings.provider,
old_keys = new Set(provider.keys());
this.skip = true;
for(const [key, value] of Object.entries(data)) {
old_keys.delete(key);
if ( provider.get(key) === value )
continue;
provider.set(key, value);
provider.emit('changed', key, value, false);
}
for(const key of old_keys) {
provider.delete(key);
provider.emit('changed', key, undefined, true);
}
this.skip = false;
}
onProviderSet(key, value, deleted) {
if ( this.skip )
return;
this.send({
ffz_type: 'change',
key,
value,
deleted
});
}
onChange(msg) {
const key = msg.key,
value = msg.value,
deleted = msg.deleted;
this.skip = true;
if ( deleted )
this.settings.provider.delete(key);
else
this.settings.provider.set(key, value);
this.skip = false;
this.settings.provider.emit('changed', key, value, deleted, true);
}
}

View file

@ -126,19 +126,19 @@ export default class VideoChatHook extends Module {
<figure class="ffz-i-ellipsis-vert" /> <figure class="ffz-i-ellipsis-vert" />
</span> </span>
</button> </button>
<div class={`tw-absolute tw-balloon tw-balloon--down tw-balloon--right tw-balloon--sm ${is_open ? 'tw-block' : 'tw-hide'}`}> <div class={`tw-absolute ffz-balloon ffz-balloon--down ffz-balloon--right ffz-balloon--sm ${is_open ? 'tw-block' : 'tw-hide'}`}>
<div class="tw-absolute tw-balloon__tail tw-overflow-hidden"> <div class="tw-absolute ffz-balloon__tail tw-overflow-hidden">
<div class="tw-absolute tw-balloon__tail-symbol tw-border-b tw-border-l tw-border-r tw-border-t tw-c-background-base" /> <div class="tw-absolute ffz-balloon__tail-symbol tw-border-b tw-border-l tw-border-r tw-border-t tw-c-background-base" />
</div> </div>
<div class="tw-border-b tw-border-l tw-border-r tw-border-radius-medium tw-border-t tw-c-background-base tw-elevation-1 tw-pd-y-1"> <div class="tw-border-b tw-border-l tw-border-r tw-border-radius-medium tw-border-t tw-c-background-base tw-elevation-1 tw-pd-y-1">
<button class="tw-interactable tw-interactable--inverted tw-full-width tw-pd-y-05 tw-pd-x-1">{ <button class="ffz-interactable ffz-interactable--inverted tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.copy-link', 'Copy Link') t.i18n.t('video-chat.copy-link', 'Copy Link')
}</button> }</button>
<button class="tw-interactable tw-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{ <button class="ffz-interactable ffz-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.delete', 'Delete') t.i18n.t('video-chat.delete', 'Delete')
}</button> }</button>
<div class="tw-mg-1 tw-border-b" /> <div class="tw-mg-1 tw-border-b" />
<button class="tw-interactable tw-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{ <button class="ffz-interactable ffz-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.ban', 'Ban User') t.i18n.t('video-chat.ban', 'Ban User')
}</button> }</button>
</div> </div>
@ -302,7 +302,7 @@ export default class VideoChatHook extends Module {
{hide_timestamps || (<div data-test-selector="message-timestamp" class="tw-align-right tw-flex tw-flex-shrink-0 vod-message__header"> {hide_timestamps || (<div data-test-selector="message-timestamp" class="tw-align-right tw-flex tw-flex-shrink-0 vod-message__header">
<div class="tw-mg-r-05"> <div class="tw-mg-r-05">
<div class="tw-inline-flex tw-relative tw-tooltip__container"> <div class="tw-inline-flex tw-relative tw-tooltip__container">
<button class="tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" onClick={this.onTimestampClickHandler}> <button class="tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive" onClick={this.onTimestampClickHandler}>
<div class="tw-pd-x-05"> <div class="tw-pd-x-05">
<p class="tw-font-size-7">{print_duration(context.comment.contentOffset)}</p> <p class="tw-font-size-7">{print_duration(context.comment.contentOffset)}</p>
</div> </div>

View file

@ -195,7 +195,7 @@
} }
.ffz--action &, .ffz--action &,
.tw-interactable:hover &:not(.disabled), .ffz-interactable:hover &:not(.disabled),
&:not(.disabled):hover { &:not(.disabled):hover {
.tw-root--theme-dark &, & { .tw-root--theme-dark &, & {
&.tw-c-text-alt-2 { &.tw-c-text-alt-2 {
@ -335,7 +335,7 @@
} }
.ffz--emoji-tone-picker { .ffz--emoji-tone-picker {
.tw-balloon { .ffz-balloon {
min-width: unset; min-width: unset;
} }

View file

@ -42,7 +42,7 @@
max-height: 25vh; max-height: 25vh;
} }
.tw-interactable { .ffz-interactable {
padding: .1rem 0; padding: .1rem 0;
} }

View file

@ -3,7 +3,7 @@
<div class="tw-search-input" data-a-target="dropdown-search-input"> <div class="tw-search-input" data-a-target="dropdown-search-input">
<label v-if="placeholder" :for="_id" class="tw-hide-accessible">{{ placeholder }}</label> <label v-if="placeholder" :for="_id" class="tw-hide-accessible">{{ placeholder }}</label>
<div class="tw-relative"> <div class="tw-relative">
<div v-if="hasIcon" class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height tw-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default"> <div v-if="hasIcon" class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height ffz-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
<figure :class="icon" /> <figure :class="icon" />
</div> </div>
<input <input
@ -12,7 +12,7 @@
:placeholder="placeholder" :placeholder="placeholder"
:class="[hasIcon ? 'tw-pd-l-3' : 'tw-pd-l-1']" :class="[hasIcon ? 'tw-pd-l-3' : 'tw-pd-l-1']"
type="search" type="search"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-r-1 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-r-1 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
autocomplete="off" autocomplete="off"
@ -45,8 +45,8 @@
v-for="(item, idx) of filteredItems" v-for="(item, idx) of filteredItems"
:id="'ffz-autocomplete-item-' + id + '-' + idx" :id="'ffz-autocomplete-item-' + id + '-' + idx"
:key="has(item, 'id') ? item.id : idx" :key="has(item, 'id') ? item.id : idx"
:class="{'tw-interactable--hover' : idx === index}" :class="{'ffz-interactable--hover' : idx === index}"
class="tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" class="tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
tabindex="-1" tabindex="-1"
data-selectable="true" data-selectable="true"
@mouseenter="index = idx" @mouseenter="index = idx"

View file

@ -1,12 +1,12 @@
<template lang="html"> <template lang="html">
<div <div
:class="classes" :class="classes"
class="tw-balloon tw-block tw-absolute tw-z-above" class="ffz-balloon tw-block tw-absolute tw-z-above"
> >
<div class="tw-balloon__tail tw-overflow-hidden tw-absolute"> <div class="ffz-balloon__tail tw-overflow-hidden tw-absolute">
<div <div
:class="`tw-c-${color}`" :class="`tw-c-${color}`"
class="tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-absolute" class="ffz-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-absolute"
/> />
</div> </div>
<div class="tw-border-t tw-border-r tw-border-b tw-border-l tw-elevation-1 tw-border-radius-small"> <div class="tw-border-t tw-border-r tw-border-b tw-border-l tw-elevation-1 tw-border-radius-small">
@ -41,7 +41,7 @@ export default {
}).join(' '); }).join(' ');
} }
return `tw-c-${this.color} ${this.size ? `tw-balloon--${this.size}` : ''} ${dir}`; return `tw-c-${this.color} ${this.size ? `ffz-balloon--${this.size}` : ''} ${dir}`;
} }
} }
} }

View file

@ -7,7 +7,7 @@
v-model="color" v-model="color"
v-bind="$attrs" v-bind="$attrs"
type="text" type="text"
class="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" class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
autocomplete="off" autocomplete="off"
@ -47,7 +47,7 @@
v-if="open" v-if="open"
v-on-clickaway="closePicker" v-on-clickaway="closePicker"
:class="{'ffz-bottom-100': openUp}" :class="{'ffz-bottom-100': openUp}"
class="tw-absolute tw-z-above tw-balloon--up tw-balloon--right" class="tw-absolute tw-z-above ffz-balloon--up ffz-balloon--right"
> >
<chrome-picker :disable-alpha="! alpha" :value="colors" @input="onPick" /> <chrome-picker :disable-alpha="! alpha" :value="colors" @input="onPick" />
</div> </div>

View file

@ -3,7 +3,7 @@
<div class="tw-search-input tw-full-width"> <div class="tw-search-input tw-full-width">
<label v-if="open" :for="'icon-search$' + id" class="tw-hide-accessible">{{ t('setting.icon.search', 'Search for Icon') }}</label> <label v-if="open" :for="'icon-search$' + id" class="tw-hide-accessible">{{ t('setting.icon.search', 'Search for Icon') }}</label>
<div class="tw-relative"> <div class="tw-relative">
<div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height tw-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default"> <div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height ffz-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
<figure :class="[(isOpen || ! val || ! val.length) ? 'ffz-i-search' : val]" /> <figure :class="[(isOpen || ! val || ! val.length) ? 'ffz-i-search' : val]" />
</div> </div>
<input <input
@ -13,7 +13,7 @@
:value="isOpen ? search : val" :value="isOpen ? search : val"
:class="[clearable ? 'tw-pd-r-5' : 'tw-pd-r-1']" :class="[clearable ? 'tw-pd-r-5' : 'tw-pd-r-1']"
type="text" type="text"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-3 tw-pd-y-05" class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-3 tw-pd-y-05"
autocapitalize="off" autocapitalize="off"
autocorrect="off" autocorrect="off"
autocomplete="off" autocomplete="off"
@ -46,9 +46,9 @@
v-for="i of visible" v-for="i of visible"
:key="i[0]" :key="i[0]"
:aria-checked="val === i[0]" :aria-checked="val === i[0]"
:class="{'tw-interactable--selected': val === i[0]}" :class="{'ffz-interactable--selected': val === i[0]}"
:data-title="i[1]" :data-title="i[1]"
class="ffz-tooltip ffz-icon tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive" class="ffz-tooltip ffz-icon ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
role="radio" role="radio"
tabindex="0" tabindex="0"
@keydown.space.stop.prevent="" @keydown.space.stop.prevent=""
@ -156,7 +156,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if ( this.val ) { if ( this.val ) {
const root = this.$refs.list, const root = this.$refs.list,
el = root && root.querySelector('.tw-interactable--selected'); el = root && root.querySelector('.ffz-interactable--selected');
if ( el ) if ( el )
el.scrollIntoViewIfNeeded(); el.scrollIntoViewIfNeeded();

View file

@ -4,7 +4,7 @@
<div <div
ref="input" ref="input"
v-bind="$attrs" 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" class="default-dimmable tw-block tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
tabindex="0" tabindex="0"
@click="startRecording" @click="startRecording"
@keydown="onKey" @keydown="onKey"

59
src/utilities/blobs.js Normal file
View file

@ -0,0 +1,59 @@
'use strict';
export function isValidBlob(blob) {
return blob instanceof Blob || blob instanceof File || blob instanceof ArrayBuffer || blob instanceof Uint8Array;
}
export async function serializeBlob(blob) {
if ( ! blob )
return null;
if ( blob instanceof Blob )
return {
type: 'blob',
mime: blob.type,
buffer: await blob.arrayBuffer(),
}
if ( blob instanceof File )
return {
type: 'file',
mime: blob.type,
name: blob.name,
modified: blob.lastModified,
buffer: await blob.arrayBuffer()
}
if ( blob instanceof ArrayBuffer )
return {
type: 'ab',
buffer: blob
}
if ( blob instanceof Uint8Array )
return {
type: 'u8',
buffer: blob.buffer
}
throw new TypeError('Invalid type');
}
export function deserializeBlob(data) {
if ( ! data || ! data.type )
return null;
if ( data.type === 'blob' )
return new Blob([data.buffer], {type: data.mime});
if ( data.type === 'file' )
return new File([data.buffer], data.name, {type: data.mime, lastModified: data.modified});
if ( data.type === 'ab' )
return data.buffer;
if ( data.type === 'u8' )
return new Uint8Array(data.buffer);
throw new TypeError('Invalid type');
}

View file

@ -14,8 +14,10 @@ export class Logger {
this.parent = parent; this.parent = parent;
this.name = name; this.name = name;
if ( this.root == this ) if ( this.root == this ) {
this.captured_init = []; this.captured_init = [];
this.label = 'FFZ';
}
this.init = false; this.init = false;
this.enabled = true; this.enabled = true;
@ -92,9 +94,9 @@ export class Logger {
}); });
if ( this.name ) if ( this.name )
message.unshift(`%cFFZ [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', ''); message.unshift(`%c${this.root.label} [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', '');
else else
message.unshift('%cFFZ:%c', 'color:#755000; font-weight:bold', ''); message.unshift(`%c${this.root.label}:%c`, 'color:#755000; font-weight:bold', '');
if ( level === Logger.DEBUG ) if ( level === Logger.DEBUG )
console.debug(...message); console.debug(...message);

View file

@ -613,11 +613,11 @@ export class Module extends EventEmitter {
} }
populate(ctx, log) { async populate(ctx, log) {
log = log || this.log; log = log || this.log;
const added = {}; const added = {};
for(const raw_path of ctx.keys()) { for(const raw_path of ctx.keys()) {
const raw_module = ctx(raw_path), const raw_module = await ctx(raw_path), // eslint-disable-line no-await-in-loop
module = raw_module.module || raw_module.default, module = raw_module.module || raw_module.default,
lix = raw_path.lastIndexOf('.'), lix = raw_path.lastIndexOf('.'),
trimmed = lix > 2 ? raw_path.slice(2, lix) : raw_path, trimmed = lix > 2 ? raw_path.slice(2, lix) : raw_path,

View file

@ -816,7 +816,7 @@ TOKEN_TYPES.link = function(token, createElement, ctx) {
const klass = []; const klass = [];
if ( token.interactive ) if ( token.interactive )
klass.push(`tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive`); klass.push(`ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive`);
if ( token.tooltip !== false ) if ( token.tooltip !== false )
klass.push('ffz-tooltip'); klass.push('ffz-tooltip');

268
styles/input/checkbox.scss Normal file
View file

@ -0,0 +1,268 @@
.ffz-checkbox .ffz-checkbox__label:before,
.ffz-radio__label:before {
background-clip: padding-box;
box-sizing: border-box;
content: "";
height: 1.6rem;
margin-top: -.8rem;
transition: box-shadow .1s ease-in, background .1s ease-in;
width: 1.6rem;
}
.ffz-checkbox__input,
.ffz-radio__input {
clip: rect(0 0 0 0);
height: .1rem;
margin: -.1rem;
overflow: hidden;
width: .1rem;
}
.ffz-checkbox__input {
border: none;
color: var(--color-text-label);
padding: 0;
position: absolute;
&:hover:checked,
&:hover:indeterminate {
& + .ffz-checkbox__label:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox-checked);
}
}
&:checked,
&:indeterminate {
& + .ffz-checkbox__label {
&:before {
background-color: var(--color-background-input-checkbox-checked-background);
border: var(--border-width-input) solid var(--color-border-input-checkbox-checked);
}
&:after {
background-color: var(--color-background-input-checkbox-checked);
content: "";
left: .4rem;
position: absolute;
top: 50%;
width: .8rem;
}
}
}
&:checked + .ffz-checkbox__label:after {
height: .8rem;
transform: translate3d(0, -50%, 0);
}
&:indeterminate + .ffz-checkbox__label:after {
display: block;
height: .2rem;
transform: translateY(-50%);
}
&:disabled + .ffz-checkbox__label {
opacity: 0.5;
pointer-events: none;
}
.js-focus-visible &:focus:not([data-focus-visible-added]) + .ffz-checkbox__label {
outline: none;
}
&[data-focus-visible-added] + .ffz-checkbox__label:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox-focus);
box-shadow: var(--shadow-input-focus);
}
}
.ffz-checkbox {
.ffz-checkbox__label {
border-radius: var(--border-radius-medium);
color: var(--color-text-label);
cursor: pointer;
display: inline-block;
padding: 0 0 0 1.6rem;
position: relative;
&:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox);
border-radius: .2rem;
border-radius: var(--border-radius-small);
left: 0;
position: absolute;
top: 50%;
}
&:after {
content: '';
display: block;
}
&:hover:before {
border-color: var(--color-border-input-checkbox-hover);
}
}
}
.ffz-checkbox--error .ffz-checkbox__label:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox-error);
}
.ffz-checkbox--overlay {
.ffz-checkbox__input {
&:checked + .ffz-checkbox__label {
&:before {
background-color: var(--color-background-input-checkbox-checked-background-overlay);
border: var(--border-width-input) solid var(--color-border-input-checkbox-checked-overlay);
}
&:after {
background-color: var(--color-background-input-checkbox-checked-overlay);
}
}
&:hover:checked,
&:indeterminate {
& + .ffz-checkbox__label:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox-checked-overlay);
}
}
&:indeterminate + .ffz-checkbox__label {
&:before {
background-color: var(--color-background-input-checkbox-checked-background-overlay);
border: var(--border-width-input) solid var(--color-border-input-checkbox-checked-overlay);
}
&:after {
background: var(--color-background-input-checkbox-checked-overlay);
content: "";
display: block;
height: .2rem;
left: .4rem;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: .8rem;
}
}
.js-focus-visible &:focus:not([data-focus-visible-added]) + .ffz-checkbox__label {
outline: none;
}
&[data-focus-visible-added] + .ffz-checkbox__label:before {
border: var(--border-width-input) solid var(--color-border-input-overlay-focus);
box-shadow: 0 0 6px 0 var(--color-border-input-overlay-focus);
}
}
.ffz-checkbox__label {
color: var(--color-text-overlay);
&:before {
background-color: var(--color-background-input-overlay);
border: var(--border-width-input) solid var(--color-border-input-checkbox-overlay);
}
&:hover:before {
border-color: var(--color-border-input-checkbox-hover-overlay);
}
}
}
// Radio
.ffz-radio__input {
border: none;
color: var(--color-text-label);
padding: 0;
position: absolute;
&:checked + .ffz-radio__label:after {
background-color: var(--color-background-input-checkbox-checked);
border-radius: 50%;
height: .8rem;
left: .4rem;
position: absolute;
top: 50%;
transform: translate3d(0, -50%, 0);
width: .8rem;
}
&:disabled + .ffz-radio__label {
opacity: .5;
pointer-events: none;
}
.js-focus-visible &:focus:not([data-focus-visible-added]) + .ffz-radio__label {
outline: 0;
}
&[data-focus-visible-added] + .ffz-radio__label:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox-focus);
box-shadow: var(--shadow-input-focus);
}
}
.ffz-radio__label {
border-radius: var(--border-radius-medium);
color: var(--color-text-label);
cursor: pointer;
display: inline-block;
padding: 0 0 0 1.6rem;
position: relative;
&:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox);
border-radius: 50%;
left: 0;
position: absolute;
top: 50%;
}
&:after {
background-color: var(--color-background-input);
content: "";
display: block;
transition: background .1s ease-in;
}
&:hover:before {
border-color: var(--color-border-input-checkbox-hover);
}
}
.ffz-radio--error .ffz-radio__label:before {
border: var(--border-width-input) solid var(--color-border-input-checkbox-error);
}
// TODO: radio overlay
// Selection Stuff
@media (-webkit-min-device-pixel-ratio: 0) {
.ffz-checkbox--overlay .ffz-checkbox__input:focus+.ffz-checkbox__label:before,
.ffz-checkbox__input:focus+.ffz-checkbox__label,
.ffz-radio--overlay .ffz-radio__input:focus+.ffz-radio__label:before,
.ffz-radio__input:focus+.ffz-radio__label,
.ffz-segmented-button-option__input--checkbox:focus+.ffz-segmented-button-option__label,
.ffz-segmented-button-option__input--radio:focus+.ffz-segmented-button-option__label,
.ffz-toggle__input:focus+.ffz-toggle__button {
outline-color: -webkit-focus-ring-color;
outline-style: auto
}
}
.ffz-checkbox--overlay .ffz-checkbox__input:focus+.ffz-checkbox__label:before,
.ffz-checkbox__input:focus+.ffz-checkbox__label,
.ffz-radio--overlay .ffz-radio__input:focus+.ffz-radio__label:before,
.ffz-radio__input:focus+.ffz-radio__label,
.ffz-segmented-button-option__input--checkbox:focus+.ffz-segmented-button-option__label,
.ffz-segmented-button-option__input--radio:focus+.ffz-segmented-button-option__label,
.ffz-toggle__input:focus+.ffz-toggle__button {
outline: 5px solid Highlight;
}

3
styles/input/index.scss Normal file
View file

@ -0,0 +1,3 @@
@import "checkbox";
@import "text";
@import "interactable";

View file

@ -0,0 +1,115 @@
.ffz-interactable {
color: inherit;
&:hover {
color: inherit;
text-decoration: none;
}
&.ffz-interactable--hover-enabled:hover:active,
&:active {
background-color: var(--color-background-interactable-active);
}
&--selected, &:focus {
position: relative;
}
&--disabled, &:disabled {
opacity: 0.5;
pointer-events: none;
}
}
.ffz-interactable--default {
&.ffz-interactable--hover-forced,
&[data-focus-visible-added],
.tw-root--hover &.ffz-interactable--hover-enabled:hover {
background-color: var(--color-background-interactable-hover);
}
&.ffz-interactable--active,
&.ffz-interactable--hover-enabled:hover:active,
&:active {
background-color: var(--color-background-interactable-active);
}
&.ffz-interactable--selected {
color: var(--color-text-interactable-selected);
&,
&.ffz-interactable--hover-enabled:hover:active,
&.ffz-interactable--hover-forced,
&:active,
&[data-focus-visible-added],
.tw-root &.ffz-interactable--hover-enabled:hover {
background-color: var(--color-background-interactable-selected);
}
}
}
.ffz-interactable--alert {
&, &:hover {
color: var(--color-text-alert);
}
&.ffz-interactable--hover-forced,
&[data-focus-visible-added],
.tw-root--hover &.ffz-interactable--hover-enabled:hover {
background-color: var(--color-background-interactable-destructive-hover);
color: var(--color-text-interactable-inverted);
}
&.ffz-interactable--active,
&.ffz-interactable--hover-enabled:hover:active,
&:active {
background-color: var(--color-background-interactable-destructive-active);
}
}
.ffz-interactable--overlay {
background-color: transparent;
&.ffz-interactable--hover-forced,
&[data-focus-visible-added],
.tw-root--hover &.ffz-interactable--hover-enabled:hover {
background-color: var(--color-background-interactable-overlay-hover);
}
&.ffz-interactable--active,
&.ffz-interactable--hover-enabled:hover:active,
&:active {
background-color: var(--color-background-interactable-overlay-active);
}
&.ffz-interactable--selected {
color: var(--color-text-interactable-overlay-selected);
&,
&.ffz-interactable--hover-enabled:hover:active,
&.ffz-interactable--hover-forced,
&:active,
&[data-focus-visible-added],
.tw-root &.ffz-interactable--hover-enabled:hover {
background: var(--color-background-interactable-overlay-selected);
}
}
&.ffz-interactable--border {
border-color: var(--color-border-overlay);
}
}
.ffz-interactable--border {
border: var(--border-width-default) solid var(--color-border-base);
&.ffz-interactable--selected {
border: var(--border-width-default) solid var(--color-border-interactable-selected);
}
}
.ffz-interactable--selectable-text {
-ms-user-select: text;
-webkit-user-select: text;
user-select: text
}

232
styles/input/text.scss Normal file
View file

@ -0,0 +1,232 @@
/*
* Extracted styles for Twitch-like input elements
* because Twitch is stupid and removed their nice,
* standardized classes with nice standardized names
* in favor of procedural classes.
*/
// Input / Shared
.ffz-input {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
border: var(--border-width-input) solid var(--color-border-input);
color: var(--color-text-input);
line-height: 1.5;
&[type=number] {
-moz-appearance: textfield;
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}
.ffz-form-tag, .ffz-input, .ffz-select, .ffz-select-button, .ffz-textarea {
background-clip: padding-box;
}
.ffz-input, .ffz-select, .ffz-textarea {
font-family: inherit;
transition: box-shadow .1s ease-in, border .1s ease-in, background-color .1s ease-in;
//&::-webkit-input-placeholder,
//&:-ms-input-placeholder,
//&::-ms-input-placeholder,
&::placeholder {
color: var(--color-text-input-placeholder);
}
&::-ms-clear {
display: none;
}
&:-moz-focus-inner {
border: none;
padding: 0;
}
&:disabled {
opacity: 0.5;
pointer-events: none;
}
&, &:hover {
background-color: var(--color-background-input);
}
&:hover {
border-color: var(--color-border-input-hover);
outline: 0;
}
&:focus, &:focus:hover {
background-color: var(--color-background-input-focus);
border: var(--border-width-input) solid var(--color-border-input-focus);
outline: 0;
}
}
.ffz-input--error, .ffz-select--error {
box-shadow: var(--shadow-input-error);
&, &:focus {
border: var(--border-width-input) solid var(--color-border-input-error);
}
}
.ffz-input--error:focus,
.ffz-select--error:focus {
box-shadow: var(--shadow-input-error-focus);
}
.ffz-input--overlay,
.ffz-select--overlay,
.ffz-textarea--overlay {
background-color: var(--color-background-input-overlay);
border: none;
box-shadow: inset 0 0 0 var(--border-width-input) var(--color-border-input-overlay);
color: var(--color-text-overlay);
&:hover {
background-color: var(--color-background-input-overlay);
box-shadow: inset 0 0 0 var(--border-width-input) var(--color-border-input-overlay-hover);
}
&:focus, &:focus:hover {
background-color: var(--color-background-input-overlay-focus);
border: none;
box-shadow: inset 0 0 0 var(--border-width-input) var(--color-border-input-overlay-focus);
}
//&::-webkit-input-placeholder,
//&:-ms-input-placeholder,
//&::-ms-input-placeholder,
&::placeholder {
color: var(--color-text-input-placeholder-overlay);
}
&.ffz-input--error,
&.ffz-select--error {
border: var(--border-width-input) solid var(--color-border-input-error);
box-shadow: var(--shadow-input-error);
&:focus {
border: var(--border-width-input) solid var(--color-border-input-error);
box-shadow: var(--shadow-input-error-focus);
}
}
}
.ffz-input--password {
font-family: sans-serif;
}
.ffz-input__icon {
min-width: 3rem;
}
.ffz-input__icon--overlay {
color: var(--color-text-input-placeholder-overlay);
}
.ffz-input--small,
.ffz-select--small {
height: 2.4rem;
padding: .2rem 0
}
.ffz-input, .ffz-select {
height: 3rem;
}
.ffz-input--large,
.ffz-select--large {
height: 3.6rem
}
// Select
.ffz-select {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%230e0e10' d='M10.5 13.683l2.85-2.442 1.3 1.518-3.337 2.86a1.25 1.25 0 01-1.626 0l-3.338-2.86 1.302-1.518 2.849 2.442zm0-7.366L7.65 8.76l-1.3-1.518 3.337-2.86a1.25 1.25 0 011.627 0l3.337 2.86-1.302 1.518L10.5 6.317z'/%3E%3C/svg%3E");
background-position: right .8rem center;
background-repeat: no-repeat;
background-size: 2rem;
border: var(--border-width-input) solid var(--color-border-input);
color: var(--color-text-input);
cursor: pointer;
line-height: 1.5;
line-height: normal;
.tw-root--theme-dark & {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23efeff1' d='M10.5 13.683l2.85-2.442 1.3 1.518-3.337 2.86a1.25 1.25 0 01-1.626 0l-3.338-2.86 1.302-1.518 2.849 2.442zm0-7.366L7.65 8.76l-1.3-1.518 3.337-2.86a1.25 1.25 0 011.627 0l3.337 2.86-1.302 1.518L10.5 6.317z'/%3E%3C/svg%3E");
}
// option isn't scoped, unlike default Twitch, to avoid flicker
&[data-focus-visible-added], option {
background-color: var(--color-background-input-focus);
border-color: var(--color-border-input-focus)
}
}
.ffz-select--overlay {
&,
.tw-root--theme-dark &,
.tw-root--theme-light & {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M10.5 13.683l2.85-2.442 1.3 1.518-3.337 2.86a1.25 1.25 0 01-1.626 0l-3.338-2.86 1.302-1.518 2.849 2.442zm0-7.366L7.65 8.76l-1.3-1.518 3.337-2.86a1.25 1.25 0 011.627 0l3.337 2.86-1.302 1.518L10.5 6.317z'/%3E%3C/svg%3E");
}
&[data-focus-visible-added] {
&, & option {
background-color: var(--color-background-input-overlay-focus);
box-shadow: inset 0 0 0 var(--border-width-input) var(--color-border-input-overlay-focus);
}
}
}
.ffz-select--small {
background-size: 1.6rem;
}
.ffz-select--large {
background-size: 2.4rem;
}
// Textarea
.ffz-textarea {
-moz-appearance: none;
-ms-overflow-style: none;
-webkit-appearance: none;
appearance: none;
border: var(--border-width-input) solid var(--color-border-input);
color: var(--color-text-input);
padding: .5rem 1rem;
resize: vertical;
&[cols] {
max-width: 100%;
width: auto;
}
}
.ffz-textarea--error:focus {
box-shadow: var(--shadow-input-error-focus);
}
.ffz-textarea--no-resize {
resize: none;
}

View file

@ -3,6 +3,9 @@
@import 'widgets'; @import 'widgets';
@import 'dialog'; @import 'dialog';
@import 'input/index';
@import 'structure/index';
@import 'chat'; @import 'chat';
@keyframes ffz-rotateplane { @keyframes ffz-rotateplane {
@ -12,7 +15,6 @@
100% { transform: rotateY(90deg) rotateX(0deg) } 100% { transform: rotateY(90deg) rotateX(0deg) }
} }
.ffz-i-t-reset.loading, .ffz-i-t-reset.loading,
.ffz-i-t-reset-clicked.loading, .ffz-i-t-reset-clicked.loading,
.ffz-i-zreknarf.loading { .ffz-i-zreknarf.loading {

View file

@ -0,0 +1,23 @@
.ffz-balloon {
max-width: 90vw;
min-width: 16rem;
&--auto {
min-width: 0;
white-space: nowrap;
}
&--xs {
min-width: 10rem;
width: 10rem;
}
&--sm { width: 20rem }
&--md { width: 30rem }
&--lg { width: 40rem }
&--xl { width: 50rem }
}
.ffz-balloon--overlay-border {
border: 2px solid var(--color-border-balloon-overlay);
}

View file

@ -0,0 +1,26 @@
@import "balloon";
.ffz-avatar {
background-color: inherit;
position: relative;
&--size-10 { height: 1rem; width: 1rem }
&--size-15 { width: 1.5rem; height: 1.5rem }
&--size-20 { width: 2rem; height: 2rem }
&--size-24 { width: 2.4rem; height: 2.4rem }
&--size-30 { width: 3rem; height: 3rem }
&--size-36 { width: 3.6rem; height: 3.6rem }
&--size-40 { width: 4rem; height: 4rem }
&--size-50 { width: 5rem; height: 5rem }
&--size-60 { width: 6rem; height: 6rem }
&--size-64 { width: 6.4rem; height: 6.4rem }
&--size-80 { width: 8rem; height: 8rem }
&--size-96 { width: 9.6rem; height: 9.6rem }
&--size-120 { width: 12rem; height: 12rem }
&--size-300 { width: 30rem; height: 30rem }
}
.ffz-image-avatar,
.ffz-avatar__img {
width: 100%;
}

View file

@ -20,41 +20,41 @@ body {
} }
.tw-balloon { .ffz-balloon {
&[x-placement^="bottom"] > .tw-balloon__tail { &[x-placement^="bottom"] > .ffz-balloon__tail {
bottom: 100%; bottom: 100%;
.tw-balloon__tail-symbol { .ffz-balloon__tail-symbol {
top: auto; top: auto;
bottom: -8px; bottom: -8px;
left: 8px; left: 8px;
} }
} }
&[x-placement^="top"] > .tw-balloon__tail { &[x-placement^="top"] > .ffz-balloon__tail {
top: 100%; top: 100%;
.tw-balloon__tail-symbol { .ffz-balloon__tail-symbol {
top: auto; top: auto;
bottom: 8px; bottom: 8px;
left: 8px; left: 8px;
} }
} }
&[x-placement^="right"] > .tw-balloon__tail { &[x-placement^="right"] > .ffz-balloon__tail {
right: 100%; right: 100%;
.tw-balloon__tail-symbol { .ffz-balloon__tail-symbol {
left: auto; left: auto;
right: -8px; right: -8px;
top: 8px; top: 8px;
} }
} }
&[x-placement^="left"] > .tw-balloon__tail { &[x-placement^="left"] > .ffz-balloon__tail {
left: 100%; left: 100%;
.tw-balloon__tail-symbol { .ffz-balloon__tail-symbol {
left: auto; left: auto;
right: 8px; right: 8px;
top: 8px; top: 8px;

View file

@ -29,8 +29,8 @@
} }
.ffz-radio-top { .ffz-radio-top {
.tw-radio__input:checked+.tw-radio__label:after, .ffz-radio__input:checked+.ffz-radio__label:after,
.tw-radio__label:before { .ffz-radio__label:before {
top: 1rem; top: 1rem;
} }
} }
@ -53,7 +53,7 @@
} }
} }
textarea.tw-input { textarea.ffz-input {
height: unset; height: unset;
} }

View file

@ -30,7 +30,7 @@
} }
} }
.tw-checkbox__input:checked+.tw-checkbox__label:after, .ffz-checkbox__input:checked+.ffz-checkbox__label:after,
label:before, label:after { label:before, label:after {
top: 1.05rem !important; top: 1.05rem !important;
} }

View file

@ -1,5 +1,5 @@
.tw-checkbox__input { .ffz-checkbox__input {
&:indeterminate + .tw-checkbox__label { &:indeterminate + .ffz-checkbox__label {
&:before { &:before {
background: var(--ffz-color-accent-8); background: var(--ffz-color-accent-8);
border: 1px solid var(--ffz-color-accent-8); border: 1px solid var(--ffz-color-accent-8);

View file

@ -1,7 +1,7 @@
.ffz--profile-selector { .ffz--profile-selector {
position: relative; position: relative;
.tw-balloon { .ffz-balloon {
position: absolute; position: absolute;
margin-top: 0 !important margin-top: 0 !important
} }

View file

@ -12,6 +12,7 @@ const PRODUCTION = process.env.NODE_ENV === 'production';
module.exports = { module.exports = {
entry: { entry: {
bridge: './src/bridge.js', bridge: './src/bridge.js',
player: './src/player.js',
avalon: './src/main.js' avalon: './src/main.js'
}, },
resolve: { resolve: {
@ -39,6 +40,9 @@ module.exports = {
}, },
optimization: { optimization: {
splitChunks: { splitChunks: {
chunks(chunk) {
return chunk.name !== 'avalon' && chunk.name !== 'bridge' && chunk.name !== 'player'
},
cacheGroups: { cacheGroups: {
vendors: false vendors: false
} }