1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +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",
"author": "Dan Salvato LLC",
"version": "4.20.60",
"version": "4.20.61",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {

View file

@ -6,6 +6,7 @@ import Logger from 'utilities/logging';
import Module from 'utilities/module';
import {DEBUG} from 'utilities/constants';
import {serializeBlob, deserializeBlob} from 'utilities/blobs';
import SettingsManager from './settings/index';
@ -28,6 +29,7 @@ class FFZBridge extends Module {
this.inject('raven', RavenLogger);
this.log = new Logger(null, null, null, this.raven);
this.log.label = 'FFZBridge';
this.log.init = true;
this.core_log = this.log.get('core');
@ -60,15 +62,21 @@ class FFZBridge extends Module {
return FFZBridge.instance;
}
onEnable() {
async onEnable() {
window.addEventListener('message', this.onMessage.bind(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({
ffz_type: 'ready'
});
}
onMessage(event) {
async onMessage(event) {
const msg = event.data;
if ( ! msg || ! msg.ffz_type )
return;
@ -83,13 +91,86 @@ class FFZBridge extends Module {
data: out
});
} else if ( msg.ffz_type === 'change' )
return;
} else if ( msg.ffz_type === 'change' ) {
this.onChange(msg);
return;
}
if ( ! msg.id )
return this.log.warn('Received command with no reply ID');
let reply, transfer;
try {
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) { // eslint-disable-line class-methods-use-this
send(msg, blob) { // eslint-disable-line class-methods-use-this
try {
window.parent.postMessage(msg, '*')
window.parent.postMessage(msg, '*', blob ? [blob] : undefined)
} catch(err) { this.log.error('send error', err); /* no-op */ }
}
@ -112,6 +193,20 @@ class FFZBridge extends Module {
deleted
});
}
onProviderBlobChange(key, deleted) {
this.send({
ffz_type: 'change-blob',
key,
deleted
});
}
onProviderClearBlobs() {
this.send({
ffz_type: 'clear-blobs'
});
}
}
FFZBridge.Logger = Logger;

View file

@ -68,17 +68,17 @@ class FrankerFaceZ extends Module {
// Startup
// ========================================================================
this.discoverModules();
this.discoverModules()
.then(() => this.enable())
.then(() => this.enableInitialModules()).then(() => {
const duration = performance.now() - start_time;
this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
this.log.init = false;
this.enable().then(() => this.enableInitialModules()).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;
});
}).catch(err => {
this.core_log.error('An error occurred during initialization.', err);
this.log.init = false;
});
}
static get() {
@ -132,9 +132,10 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n'
// Modules
// ========================================================================
discoverModules() {
const ctx = require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/),
modules = this.populate(ctx, this.core_log);
async discoverModules() {
// TODO: Actually do async modules.
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.`);
}

View file

@ -8,7 +8,7 @@
<input
id="edit_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)"
>
</div>

View file

@ -9,7 +9,7 @@
:id="'edit_chat$' + id"
v-model="value.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)"
>
@ -17,16 +17,16 @@
{{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
</div>
<div class="tw-checkbox">
<div class="ffz-checkbox">
<input
:id="'chat-paste$' + id"
v-model="value.paste"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.actions.set-chat', 'Paste this message into chat rather than sending it directly.') }}
</span>

View file

@ -19,7 +19,7 @@
<input
id="edit_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)"
>
</div>

View file

@ -7,7 +7,7 @@
<input
id="edit_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)"
>
</div>

View file

@ -7,7 +7,7 @@
<input
id="edit_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)"
>
</div>

View file

@ -9,7 +9,7 @@
id="edit_duration"
v-model="value.duration_rich"
: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"
@input="update()"
>
@ -23,7 +23,7 @@
<input
id="edit_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)"
>
</div>

View file

@ -9,7 +9,7 @@
id="edit_url"
v-model="value.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)"
>

View file

@ -263,7 +263,7 @@ export default class Actions extends Module {
reason_elements.push(<li class="tw-full-width tw-relative">
<a
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)}
>
{text}
@ -280,7 +280,7 @@ export default class Actions extends Module {
reason_elements.push(<li class="tw-full-width tw-relative">
<a
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)}
>
{rule}
@ -350,9 +350,9 @@ export default class Actions extends Module {
hover_events: 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',
arrowClass: 'tw-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',
tooltipClass: 'ffz-action-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
arrowClass: 'ffz-balloon__tail tw-overflow-hidden 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: '',
popper: {

View file

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

View file

@ -20,7 +20,7 @@
<input
id="user-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"
@change="updateName"
>

View file

@ -67,9 +67,9 @@ export default class Overrides extends Module {
no_update: 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',
arrowClass: '', //tw-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',
tooltipClass: 'ffz-action-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
arrowClass: '', //ffz-balloon__tail tw-overflow-hidden 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: '',
popper: {

View file

@ -19,7 +19,7 @@
ref="json"
v-model="json"
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()"
/>
</template>
@ -34,7 +34,7 @@
<input
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>
@ -47,7 +47,7 @@
id="renderer_type"
ref="renderer_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
v-for="(r, key) in data.renderers"
@ -91,7 +91,7 @@
<select
id="vis_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>
{{ t('setting.unset', 'Unset') }}
@ -113,7 +113,7 @@
<select
id="vis_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>
{{ t('setting.unset', 'Unset') }}
@ -135,7 +135,7 @@
<select
id="vis_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>
{{ t('setting.unset', 'Unset') }}
@ -157,7 +157,7 @@
<select
id="vis_emote"
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>
{{ t('setting.unset', 'Unset') }}
@ -179,7 +179,7 @@
<select
id="vis_slow"
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>
{{ t('setting.unset', 'Unset') }}
@ -201,7 +201,7 @@
<select
id="vis_subs"
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>
{{ t('setting.unset', 'Unset') }}
@ -223,7 +223,7 @@
<select
id="vis_subs"
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>
{{ t('setting.unset', 'Unset') }}
@ -245,7 +245,7 @@
<select
id="vis_r9k"
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>
{{ t('setting.unset', 'Unset') }}
@ -266,80 +266,80 @@
<div>
<div class="ffz--inline tw-flex">
<div class="tw-pd-r-1 tw-checkbox">
<div class="tw-pd-r-1 ffz-checkbox">
<input
:id="'key_ctrl$' + id"
ref="key_ctrl"
:checked="edit_data.display.keys & 1"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.key.ctrl', 'Ctrl') }}
</span>
</label>
</div>
<div class="tw-pd-r-1 tw-checkbox">
<div class="tw-pd-r-1 ffz-checkbox">
<input
:id="'key_shift$' + id"
ref="key_shift"
:checked="edit_data.display.keys & 2"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.key.shift', 'Shift') }}
</span>
</label>
</div>
<div class="tw-pd-r-1 tw-checkbox">
<div class="tw-pd-r-1 ffz-checkbox">
<input
:id="'key_alt$' + id"
ref="key_alt"
:checked="edit_data.display.keys & 4"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.key.alt', 'Alt') }}
</span>
</label>
</div>
<div class="tw-pd-r-1 tw-checkbox">
<div class="tw-pd-r-1 ffz-checkbox">
<input
:id="'key_meta$' + id"
ref="key_meta"
:checked="edit_data.display.keys & 8"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.key.meta', 'Meta') }}
</span>
</label>
</div>
<div class="tw-pd-r-1 tw-checkbox">
<div class="tw-pd-r-1 ffz-checkbox">
<input
:id="'key_hover$' + id"
ref="key_hover"
:checked="edit_data.display.hover"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.key.hover', 'Hover') }}
</span>
@ -361,7 +361,7 @@
<select
id="action_type"
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
v-for="(a, key) in data.actions"

View file

@ -61,7 +61,7 @@
<input
ref="unlisted"
: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"
>
<button

View file

@ -18,7 +18,7 @@
<input
ref="url_box"
: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"
readonly
@focusin="selectURL"

View file

@ -18,7 +18,7 @@
<select
v-if="editing"
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
v-for="section in badges"

View file

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

View file

@ -8,7 +8,7 @@
<select
v-if="editing"
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
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 class="tw-flex-grow-1" />
<div class="tw-checkbox tw-relative tw-tooltip__container">
<div class="ffz-checkbox tw-relative tw-tooltip__container">
<input
id="nonversioned"
ref="nonversioned"
v-model="nonversioned"
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">
{{ t('home.changelog.show-nonversioned', 'Include non-versioned commits.') }}
</span>
</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.') }}
</div>
</div>
@ -58,7 +58,7 @@
>
<figure
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
:src="commit.author.avatar_url"

View file

@ -12,153 +12,153 @@
<div class="tw-flex tw-flex-wrap tw-align-items-center ffz--inline">
{{ t('setting.actions.preview', 'Preview:') }}
<div class="tw-pd-x-1 tw-checkbox">
<div class="tw-pd-x-1 ffz-checkbox">
<input
id="as_mod"
ref="as_mod"
:checked="is_moderator"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="as_mod" class="tw-checkbox__label">
<label for="as_mod" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.mod', 'As Moderator') }}
</span>
</label>
</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
id="is_deleted"
ref="is_deleted"
:checked="is_deleted"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="is_deleted" class="tw-checkbox__label">
<label for="is_deleted" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.deleted', 'Deleted Message') }}
</span>
</label>
</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
id="with_mod_icons"
ref="with_mod_icons"
:checked="with_mod_icons"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t('setting.actions.preview.mod_icons', 'With Mod Icons') }}
</span>
</label>
</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
id="with_slow"
ref="with_slow"
:checked="with_slow"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="with_slow" class="tw-checkbox__label">
<label for="with_slow" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.slow', 'Slow Mode') }}
</span>
</label>
</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
id="with_emote"
ref="with_emote"
:checked="with_emote"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="with_emote" class="tw-checkbox__label">
<label for="with_emote" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.emote-only', 'Emote-Only') }}
</span>
</label>
</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
id="with_subs"
ref="with_subs"
:checked="with_subs"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="with_subs" class="tw-checkbox__label">
<label for="with_subs" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.subs', 'Subs Only') }}
</span>
</label>
</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
id="with_followers"
ref="with_followers"
:checked="with_followers"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="with_followers" class="tw-checkbox__label">
<label for="with_followers" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.followers', 'Followers-Only') }}
</span>
</label>
</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
id="with_r9k"
ref="with_r9k"
:checked="with_r9k"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="with_r9k" class="tw-checkbox__label">
<label for="with_r9k" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.r9k', 'R9k Mode') }}
</span>
</label>
</div>
<div class="tw-pd-x-1 tw-checkbox">
<div class="tw-pd-x-1 ffz-checkbox">
<input
id="show_all"
ref="show_all"
:checked="show_all"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onPreview"
>
<label for="show_all" class="tw-checkbox__label">
<label for="show_all" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.actions.preview.all', 'Show All') }}
</span>
@ -230,7 +230,7 @@
<input
ref="paste"
: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"
>
<button
@ -245,7 +245,7 @@
</div>
<div v-else class="tw-pd-y-1">
<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"
>
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
@ -264,7 +264,7 @@
v-else
:key="idx"
: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)"
>
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">

View file

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

View file

@ -16,7 +16,7 @@
<input
ref="code"
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"
autocorrect="off"
@keydown.enter="enterCode"
@ -31,7 +31,7 @@
</div>
<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"
>
<option :selected="sort_by === 0">
@ -44,16 +44,16 @@
</div>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1" />
<div class="tw-checkbox tw-relative">
<div class="ffz-checkbox tw-relative">
<input
id="unused"
ref="unused"
v-model="unused"
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">
{{ t('setting.experiments.show-unused', 'Display unused experiments.') }}
</span>
@ -90,7 +90,7 @@
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
<select
: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)"
>
<option
@ -169,7 +169,7 @@
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
<select
: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)"
>
<option

View file

@ -21,7 +21,7 @@
<select
v-once
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
v-for="(filter, key) in filters"

View file

@ -10,7 +10,7 @@
<select
id="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"
>
<option
@ -27,7 +27,7 @@
<input
ref="text"
: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"
@input="onTextChange"
>
@ -37,34 +37,34 @@
<div class="tw-flex tw-mg-b-1">
<div class="tw-flex-grow-1" />
<div class="tw-pd-x-1 tw-checkbox">
<div class="tw-pd-x-1 ffz-checkbox">
<input
id="force_media"
ref="force_media"
:checked="force_media"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onCheck"
>
<label for="force_media" class="tw-checkbox__label">
<label for="force_media" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('debug.link-provider.allow.media', 'Allow Media') }}
</span>
</label>
</div>
<div class="tw-pd-x-1 tw-checkbox">
<div class="tw-pd-x-1 ffz-checkbox">
<input
id="force_unsafe"
ref="force_unsafe"
:checked="force_unsafe"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onCheck"
>
<label for="force_unsafe" class="tw-checkbox__label">
<label for="force_unsafe" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('debug.link-provider.allow.unsafe', 'Allow NSFW') }}
</span>
@ -104,17 +104,17 @@
</a>
</div>
<div class="tw-pd-x-1 tw-checkbox">
<div class="tw-pd-x-1 ffz-checkbox">
<input
id="force_tooltip"
ref="force_tooltip"
:checked="force_tooltip"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@change="onTooltip"
>
<label for="force_tooltip" class="tw-checkbox__label">
<label for="force_tooltip" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('debug.link-provider.force-tooltip', 'Force Tooltip') }}
</span>

View file

@ -11,7 +11,7 @@
<div class="tw-search-input">
<label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('main-menu.search', 'Search Settings') }}</label>
<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" />
</div>
<input
@ -19,7 +19,7 @@
v-model="query"
:placeholder="t('main-menu.search', 'Search Settings')"
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"
autocorrect="off"
autocomplete="off"

View file

@ -98,7 +98,7 @@
id="ffz:editor:name"
ref="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>
@ -111,7 +111,7 @@
id="ffz:editor:description"
ref="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>

View file

@ -4,7 +4,7 @@
ref="button"
:class="{active: opened}"
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.left.stop.prevent="focusShow"
@keyup.down.stop.prevent="focusShow"
@ -18,7 +18,7 @@
<div
v-if="opened"
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
class="ffz--profile-list tw-elevation-2 tw-c-background-alt"

View file

@ -1,5 +1,14 @@
<template lang="html">
<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">
<h3 class="ffz-i-attention">
{{ t('setting.provider.warn.title', 'Be careful!') }}
@ -14,18 +23,18 @@
</section>
<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
:id="'ffz--provider-opt-' + val.key"
v-model="selected"
:value="val.key"
name="ffz--provider-opt"
type="radio"
class="tw-radio__input"
class="ffz-radio__input"
>
<label
: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>
@ -51,20 +60,23 @@
</div>
<div class="tw-border-t tw-pd-t-1">
<div class="tw-flex tw-align-items-center tw-checkbox">
<input id="transfer" ref="transfer" checked type="checkbox" class="tw-checkbox__input">
<label for="transfer" class="tw-checkbox__label">
<div v-if="canTransfer" class="tw-flex tw-align-items-center ffz-checkbox">
<input id="transfer" ref="transfer" checked type="checkbox" class="ffz-checkbox__input">
<label for="transfer" class="ffz-checkbox__label">
<div class="tw-mg-l-1">
{{ t('setting.provider.transfer', 'Transfer my settings when switching provider.') }}
</div>
</label>
</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.')" />
</section>
<div class="tw-mg-t-1 tw-flex tw-align-items-center tw-checkbox">
<input id="backup" ref="backup" v-model="backup" type="checkbox" class="tw-checkbox__input">
<label for="backup" class="tw-checkbox__label">
<div v-else class="tw-flex tw-align-items-center" style="padding-left:2.5rem">
{{ t('setting.provider.no-transfer', 'Automatically transfering settings from your current provider to the selected provider is not allowed. Please use Backup and Restore.') }}
</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">
{{ t('setting.provider.backup', 'Yes, I made a backup.') }}
</div>
@ -97,33 +109,41 @@ export default {
data() {
const ffz = this.context.getFFZ(),
settings = ffz.resolve('settings'),
providers = [];
providers = [],
transfers = {};
for(const [key, val] of Object.entries(settings.getProviders())) {
const prov = {
key,
priority: val.priority || 0,
has_data: null,
has_blobs: val.supportsBlobs,
has_trans: val.allowTransfer,
i18n_key: `setting.provider.${key}.title`,
title: val.title || key,
desc_i18n_key: val.description ? `setting.provider.${key}.desc` : null,
description: val.description
};
transfers[key] = val.allowTransfer;
if ( val.supported() )
Promise.resolve(val.hasContent()).then(v => {
prov.has_data = v;
});
providers.push(prov);
}
providers.sort((a,b) => b.priority - a.priority);
const current = settings.getActiveProvider();
return {
backup: false,
not_www: window.location.host !== 'www.twitch.tv',
providers,
transfers,
current,
selected: current
}
@ -132,6 +152,10 @@ export default {
computed: {
enabled() {
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"
:placeholder="adding ? t('setting.reasons.add-placeholder', 'Add a new reason') : edit_data.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"
autocorrect="off"
>

View file

@ -3,17 +3,17 @@
:class="{inherits: isInherited, default: isDefault}"
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
:id="item.full_key"
ref="control"
:checked="value"
type="checkbox"
class="tw-checkbox__input"
class="ffz-checkbox__input"
@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">
{{ t(item.i18n_key, item.title) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>

View file

@ -13,7 +13,7 @@
<select
:id="item.full_key"
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"
>
<option
@ -31,7 +31,7 @@
ref="text"
:value="value"
: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"
>
</div>

View file

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

View file

@ -12,7 +12,7 @@
<select
:id="item.full_key"
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"
>
<template v-for="i in nested_data">

View file

@ -15,7 +15,7 @@
:type="type"
:placeholder="placeholder"
: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"
>

View file

@ -22,7 +22,7 @@
v-model="edit_data.v"
:placeholder="adding ? t('setting.terms.add-placeholder', 'Add a new term') : edit_data.v"
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"
autocorrect="off"
>
@ -40,7 +40,7 @@
<select
v-else
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">
{{ t('setting.terms.type.text', 'Text') }}

View file

@ -210,7 +210,7 @@ export default class Metadata extends Module {
return false;
} : null;
tip.element.classList.add('tw-balloon--lg');
tip.element.classList.add('ffz-balloon--lg');
return (<div>
<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 class="tw-flex tw-align-items-center">
<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"
value={url}
onFocus={e => e.target.select()}
@ -737,10 +737,10 @@ export default class Metadata extends Module {
live: false,
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.
arrowClass: 'tw-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',
arrowClass: 'ffz-balloon__tail tw-overflow-hidden 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',
popper: {

View file

@ -21,8 +21,8 @@
<textarea
ref="editor"
v-model="value"
:class="{'tw-textarea--error': ! valid}"
class="tw-block tw-font-size-6 tw-full-width tw-textarea"
:class="{'ffz-textarea--error': ! valid}"
class="tw-block tw-font-size-6 tw-full-width ffz-textarea"
@input="onInput"
@blur="onBlur"
@focus="open = true"
@ -86,7 +86,7 @@
:id="`ui-ctx:${entry.key}:${val.key}`"
:type="val.is_number ? 'number' : 'text'"
: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)"
>
</div>

View file

@ -9,7 +9,7 @@
<div class="tw-search-input">
<label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('i18n.ui.search', 'Search Strings') }}</label>
<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" />
</div>
<input
@ -17,7 +17,7 @@
v-model="query"
:placeholder="t('i18n.ui.search', 'Search Strings')"
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"
autocorrect="off"
autocomplete="off"
@ -99,7 +99,7 @@
ref="pager"
:value="page"
: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"
min="1"
@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-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">
<figure class="tw-avatar tw-avatar--size-50">
<figure class="ffz-avatar ffz-avatar--size-50">
<img
v-if="loaded"
: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 = {
label: 'Absolutely Everything',
clear(provider, settings) {
async clear(provider, settings) {
provider.clear();
if ( provider.supportsBlobs() )
provider.clearBlobs();
if ( provider.supportsBlobs )
await provider.clearBlobs();
settings.loadProfiles();
}

View file

@ -1,13 +1,13 @@
<template>
<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
:id="'enabled$' + value.id"
v-model="value.data"
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">
{{ t(type.i18n, type.title) }}
</span>

View file

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

View file

@ -6,13 +6,13 @@
</label>
<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">
<img
v-if="current"
:alt="current.displayName"
:src="current.profileImageURL"
class="tw-avatar__img tw-image"
class="ffz-avatar__img tw-image"
>
</div>
</figure>

View file

@ -8,7 +8,7 @@
<select
:id="'page$' + id"
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
v-for="(_, key) in routes"
@ -45,7 +45,7 @@
<input
:id="'page$' + id + '$part-' + 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>

View file

@ -13,7 +13,7 @@
:id="'start-time$' + id"
v-model="value.data[0]"
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">
@ -24,7 +24,7 @@
:id="'end-time$' + id"
v-model="value.data[1]"
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>
</section>

View file

@ -22,7 +22,7 @@
:id="'title$' + id"
v-model="value.data.title"
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"
autocorrect="off"
>
@ -30,7 +30,7 @@
<select
:id="'mode$' + id"
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">
{{ t('setting.terms.type.text', 'Text') }}
@ -43,14 +43,14 @@
</option>
</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
:id="'sensitive$' + id"
v-model="value.data.sensitive"
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">
Aa
<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.
*/
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 )
wanted = localStorage.ffzProvider = await this.sniffProvider();
wanted = localStorage.ffzProviderv2 = await this.sniffProvider();
if ( this.providers[wanted] ) {
const provider = new this.providers[wanted](this);
@ -434,31 +438,34 @@ export default class SettingsManager extends Module {
// Are we transfering settings?
if ( transfer ) {
const new_provider = new this.providers[key](this);
await new_provider.awaitReady();
old_provider.disableEvents();
if ( new_provider.allowTransfer && old_provider.allowTransfer ) {
old_provider.disableEvents();
// When transfering, we clear all existing settings.
await new_provider.clear();
if ( new_provider.supportsBlobs )
await new_provider.clearBlobs();
// When transfering, we clear all existing settings.
await new_provider.clear();
if ( new_provider.supportsBlobs )
await new_provider.clearBlobs();
for(const [key,val] of old_provider.entries())
new_provider.set(key, val);
for(const [key,val] of old_provider.entries())
new_provider.set(key, val);
if ( old_provider.supportsBlobs && new_provider.supportsBlobs ) {
for(const key of await old_provider.blobKeys() ) {
const blob = await old_provider.getBlob(key); // eslint-disable-line no-await-in-loop
if ( blob )
await new_provider.setBlob(key, blob); // eslint-disable-line no-await-in-loop
if ( old_provider.supportsBlobs && new_provider.supportsBlobs ) {
for(const key of await old_provider.blobKeys() ) {
const blob = await old_provider.getBlob(key); // eslint-disable-line no-await-in-loop
if ( blob )
await new_provider.setBlob(key, blob); // eslint-disable-line no-await-in-loop
}
await old_provider.clearBlobs();
}
await old_provider.clearBlobs();
old_provider.clear();
await old_provider.flush();
await new_provider.flush();
}
old_provider.clear();
await old_provider.flush();
await new_provider.flush();
}
// Change over.

View file

@ -1,5 +1,6 @@
'use strict';
import { isValidBlob, deserializeBlob, serializeBlob } from 'src/utilities/blobs';
// ============================================================================
// Settings Providers
// ============================================================================
@ -7,12 +8,8 @@
import {EventEmitter} from 'utilities/events';
import {has} from 'utilities/object';
const DB_VERSION = 1;
export function isValidBlob(blob) {
return blob instanceof Blob || blob instanceof File || blob instanceof ArrayBuffer || blob instanceof Uint8Array;
}
const DB_VERSION = 1,
NOT_WWW = window.location.host !== 'www.twitch.tv';
// ============================================================================
@ -43,6 +40,7 @@ export class SettingsProvider extends EventEmitter {
}
static supportsBlobs = false;
static allowTransfer = true;
awaitReady() {
if ( this.ready )
@ -51,6 +49,8 @@ export class SettingsProvider extends EventEmitter {
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
disableEvents() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
@ -932,4 +932,252 @@ 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}`);
}
populateModules() {
const ctx = require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
const modules = this.populate(ctx, this.log);
async populateModules() {
const ctx = await require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
const modules = await this.populate(ctx, this.log);
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 = [];
}
onLoad() {
this.populateModules();
async onLoad() {
await this.populateModules();
this.web_munch.known(Twilight.KNOWN_MODULES);

View file

@ -429,7 +429,7 @@ export default class EmoteMenu extends Module {
return (<button
key={data.code}
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}
>
{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));
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">
{this.renderTone(emoji, null)}
{tones}
@ -903,7 +903,7 @@ export default class EmoteMenu extends Module {
const padding = t.chat.context.get('chat.emote-menu.reduced-padding');
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"
>
<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">
<div class="tw-absolute tw-attached tw-attached--right tw-attached--up">
<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"
role="dialog"
>
@ -2128,7 +2128,7 @@ export default class EmoteMenu extends Module {
<div class="tw-flex">
<input
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={
is_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">
{! visibility && <div class={`emote-picker-tab-item${tab === 'fav' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<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"
data-tab="fav"
data-tooltip-type="html"
@ -2184,7 +2184,7 @@ export default class EmoteMenu extends Module {
</div>}
{this.state.has_channel_tab && <div class={`emote-picker-tab-item${tab === 'channel' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<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"
data-tab="channel"
data-tooltip-type="html"
@ -2198,7 +2198,7 @@ export default class EmoteMenu extends Module {
</div>}
<div class={`emote-picker-tab-item${tab === 'all' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<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"
data-tab="all"
data-tooltip-type="html"
@ -2212,7 +2212,7 @@ export default class EmoteMenu extends Module {
</div>
{! visibility && this.state.has_emoji_tab && <div class={`emote-picker-tab-item${tab === 'emoji' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<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"
data-tab="emoji"
data-tooltip-type="html"
@ -2227,7 +2227,7 @@ export default class EmoteMenu extends Module {
<div class="tw-flex-grow-1" />
<div class="emote-picker-tab-item tw-relative">
<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-title={t.i18n.t('emote-menu.settings', 'Open Settings')}
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;
if ( this.state.url ) {
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-url={this.state.url}
data-is-mail={false}

View file

@ -57,7 +57,7 @@ export default class SettingsMenu extends Module {
this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu});
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-flex-grow-1">
{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">
<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}
>
<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 )
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="chat-settings__popover">
<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>
</div>
<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"
onClick={this.ffzSettingsClick}
>

View file

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

View file

@ -160,7 +160,7 @@ export default class Following extends SiteModule {
// Hosted Channel Content
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}`}
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
for (const channel of channels) {
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}`}
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>);
}
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="scrollable-area" data-simplebar>
{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 = {
order: 150,
button: true,
@ -60,7 +75,7 @@ export default class FeaturedFollow extends Module {
this._featured_follow_tip = tip;
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);
return this.buildFeaturedFollowMenu(vue, data.channel.login, follows, add_callback);
},
@ -82,18 +97,7 @@ export default class FeaturedFollow extends Module {
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) {

View file

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

View file

@ -53,67 +53,6 @@ export default class HostButton extends Module {
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) {
@ -198,6 +137,67 @@ export default class HostButton extends Module {
onEnable() {
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.chat.ChatService.ready((cls, instances) => {

View file

@ -18,6 +18,7 @@ export default class MenuButton extends SiteModule {
this.inject('i18n');
this.inject('settings');
this.inject('site.fine');
this.inject('site.elemental');
//this.inject('addons');
this.should_enable = true;
@ -44,10 +45,16 @@ export default class MenuButton extends SiteModule {
['mod-view']
);
this.SunlightDash = this.fine.define(
/*this.SunlightDash = this.fine.define(
'sunlight-dash',
n => n.getIsChannelEditor && n.getIsChannelModerator && n.getIsAdsEnabled && n.getIsSquadStreamsEnabled,
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(
@ -195,8 +202,11 @@ export default class MenuButton extends SiteModule {
for(const inst of this.MultiController.instances)
this.updateButton(inst);
for(const inst of this.SunlightDash.instances)
this.updateButton(inst);
//for(const inst of this.SunlightDash.instances)
// this.updateButton(inst);
for(const el of this.SunlightNav.instances)
this.updateButton(null, el, true);
for(const inst of this.ModBar.instances)
this.updateButton(inst);
@ -216,9 +226,13 @@ export default class MenuButton extends SiteModule {
this.MultiController.on('mount', 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('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.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);
let is_squad = false,
is_sunlight = false,
is_mod = false,
//is_sunlight = false,
is_mod = false;
if ( ! container )
container = root && root.querySelector('.top-nav__menu');
if ( ! container ) {
@ -265,7 +281,7 @@ export default class MenuButton extends SiteModule {
}
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 )
is_sunlight = true;
}
@ -279,7 +295,7 @@ export default class MenuButton extends SiteModule {
if ( ! container )
return;
if ( ! is_squad && ! is_mod ) {
if ( ! is_squad && ! is_mod && ! is_sunlight ) {
let user_stuff = null;
try {
user_stuff = container.querySelector(':scope > .tw-justify-content-end:last-child');
@ -314,7 +330,7 @@ export default class MenuButton extends SiteModule {
</span>
</div>
</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-flex tw-align-items-center">
<div class="tw-flex-grow-1">
@ -377,8 +393,16 @@ export default class MenuButton extends SiteModule {
if ( is_mod )
container.insertBefore(el, container.firstElementChild);
else
container.insertBefore(el, container.lastElementChild);
else {
let before = container.lastElementChild;
if ( before && before.classList.contains('resize-detector') )
before = before.previousElementSibling;
if ( before )
container.insertBefore(el, before);
else
container.appendChild(el);
}
if ( this._ctx_open )
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'}`}>
<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-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">

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" />
</span>
</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 tw-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 ffz-balloon--down ffz-balloon--right ffz-balloon--sm ${is_open ? 'tw-block' : 'tw-hide'}`}>
<div class="tw-absolute ffz-balloon__tail tw-overflow-hidden">
<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 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')
}</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')
}</button>
<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')
}</button>
</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">
<div class="tw-mg-r-05">
<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">
<p class="tw-font-size-7">{print_duration(context.comment.contentOffset)}</p>
</div>

View file

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

View file

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

View file

@ -3,7 +3,7 @@
<div class="tw-search-input" data-a-target="dropdown-search-input">
<label v-if="placeholder" :for="_id" class="tw-hide-accessible">{{ placeholder }}</label>
<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" />
</div>
<input
@ -12,7 +12,7 @@
:placeholder="placeholder"
:class="[hasIcon ? 'tw-pd-l-3' : 'tw-pd-l-1']"
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"
autocorrect="off"
autocomplete="off"
@ -45,8 +45,8 @@
v-for="(item, idx) of filteredItems"
:id="'ffz-autocomplete-item-' + id + '-' + idx"
:key="has(item, 'id') ? item.id : idx"
:class="{'tw-interactable--hover' : idx === index}"
class="tw-block tw-full-width tw-interactable tw-interactable--hover-enabled tw-interactable--default tw-interactive"
:class="{'ffz-interactable--hover' : idx === index}"
class="tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
tabindex="-1"
data-selectable="true"
@mouseenter="index = idx"

View file

@ -1,12 +1,12 @@
<template lang="html">
<div
: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
: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 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(' ');
}
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-bind="$attrs"
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"
autocorrect="off"
autocomplete="off"
@ -47,7 +47,7 @@
v-if="open"
v-on-clickaway="closePicker"
: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" />
</div>

View file

@ -3,7 +3,7 @@
<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>
<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]" />
</div>
<input
@ -13,7 +13,7 @@
:value="isOpen ? search : val"
:class="[clearable ? 'tw-pd-r-5' : 'tw-pd-r-1']"
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"
autocorrect="off"
autocomplete="off"
@ -46,9 +46,9 @@
v-for="i of visible"
:key="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]"
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"
tabindex="0"
@keydown.space.stop.prevent=""
@ -156,7 +156,7 @@ export default {
this.$nextTick(() => {
if ( this.val ) {
const root = this.$refs.list,
el = root && root.querySelector('.tw-interactable--selected');
el = root && root.querySelector('.ffz-interactable--selected');
if ( el )
el.scrollIntoViewIfNeeded();

View file

@ -4,7 +4,7 @@
<div
ref="input"
v-bind="$attrs"
class="default-dimmable tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
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"
@click="startRecording"
@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.name = name;
if ( this.root == this )
if ( this.root == this ) {
this.captured_init = [];
this.label = 'FFZ';
}
this.init = false;
this.enabled = true;
@ -92,9 +94,9 @@ export class Logger {
});
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
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 )
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;
const added = {};
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,
lix = raw_path.lastIndexOf('.'),
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 = [];
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 )
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 'dialog';
@import 'input/index';
@import 'structure/index';
@import 'chat';
@keyframes ffz-rotateplane {
@ -12,7 +15,6 @@
100% { transform: rotateY(90deg) rotateX(0deg) }
}
.ffz-i-t-reset.loading,
.ffz-i-t-reset-clicked.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 {
&[x-placement^="bottom"] > .tw-balloon__tail {
.ffz-balloon {
&[x-placement^="bottom"] > .ffz-balloon__tail {
bottom: 100%;
.tw-balloon__tail-symbol {
.ffz-balloon__tail-symbol {
top: auto;
bottom: -8px;
left: 8px;
}
}
&[x-placement^="top"] > .tw-balloon__tail {
&[x-placement^="top"] > .ffz-balloon__tail {
top: 100%;
.tw-balloon__tail-symbol {
.ffz-balloon__tail-symbol {
top: auto;
bottom: 8px;
left: 8px;
}
}
&[x-placement^="right"] > .tw-balloon__tail {
&[x-placement^="right"] > .ffz-balloon__tail {
right: 100%;
.tw-balloon__tail-symbol {
.ffz-balloon__tail-symbol {
left: auto;
right: -8px;
top: 8px;
}
}
&[x-placement^="left"] > .tw-balloon__tail {
&[x-placement^="left"] > .ffz-balloon__tail {
left: 100%;
.tw-balloon__tail-symbol {
.ffz-balloon__tail-symbol {
left: auto;
right: 8px;
top: 8px;

View file

@ -29,8 +29,8 @@
}
.ffz-radio-top {
.tw-radio__input:checked+.tw-radio__label:after,
.tw-radio__label:before {
.ffz-radio__input:checked+.ffz-radio__label:after,
.ffz-radio__label:before {
top: 1rem;
}
}
@ -53,7 +53,7 @@
}
}
textarea.tw-input {
textarea.ffz-input {
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 {
top: 1.05rem !important;
}

View file

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

View file

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

View file

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