1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-02 16:08:31 +00:00
* Added: Profiles can now be toggled on and off, rather than relying on rules to control them.
* Added: Right-clicking the FFZ menu button in the top right of the page will open a Profiles dialog, letting you quickly toggle a profile on or off.
* Added: Profiles can now be enabled or disabled based upon the time of day.
* Added: Polish and Serbian pluralization rules.
* Fixed: Ensure tool-tips work on the new dashboard.
* Fixed: Ensure the FFZ Control Center works on the new dashboard.
This commit is contained in:
SirStendec 2019-10-09 16:02:25 -04:00
parent 02efd61f00
commit 62bb6440f3
30 changed files with 503 additions and 52 deletions

View file

@ -561,6 +561,12 @@
"css": "youtube-play", "css": "youtube-play",
"code": 61802, "code": 61802,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "861ab06e455e2de3232ebef67d60d708",
"css": "minus",
"code": 59445,
"src": "fontawesome"
} }
] ]
} }

View file

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

Binary file not shown.

View file

@ -112,6 +112,8 @@
<glyph glyph-name="clip" unicode="&#xe834;" d="M900 608l-14 99a50 50 0 0 1-57 43l-123-18 95-138 99 14z m-251-35l-96 138-148-21 95-138 149 21z m-287-42l-95 138-124-17a50 50 0 0 1-42-57l14-99 247 35z m538-131v-404c0-28-22-46-50-46h-700c-28 0-50 18-50 46v404h250l-100-150h150l100 150h150l-100-150h150l100 150h100z" horiz-adv-x="1000" /> <glyph glyph-name="clip" unicode="&#xe834;" d="M900 608l-14 99a50 50 0 0 1-57 43l-123-18 95-138 99 14z m-251-35l-96 138-148-21 95-138 149 21z m-287-42l-95 138-124-17a50 50 0 0 1-42-57l14-99 247 35z m538-131v-404c0-28-22-46-50-46h-700c-28 0-50 18-50 46v404h250l-100-150h150l100 150h150l-100-150h150l100 150h100z" horiz-adv-x="1000" />
<glyph glyph-name="minus" unicode="&#xe835;" d="M786 439v-107q0-22-16-38t-38-15h-678q-23 0-38 15t-16 38v107q0 23 16 38t38 16h678q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" /> <glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
<glyph glyph-name="twitter" unicode="&#xf099;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" /> <glyph glyph-name="twitter" unicode="&#xf099;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -486,7 +486,7 @@ export class TranslationManager extends Module {
out = 'FFZ Control Center'; out = 'FFZ Control Center';
else { else {
let label = match[1]; let label = match[1];
if ( label === 'Proxy.render' && location[2].includes('.vue') ) if ( (label === 'Proxy.render' || label.startsWith('Proxy.push')) && location[2].includes('.vue') )
label = 'Vue Component'; label = 'Vue Component';
out = `${label} (${location[2]}:${location[3]})`; out = `${label} (${location[2]}:${location[3]})`;

View file

@ -7,16 +7,24 @@
<template v-if="i !== item">&raquo; </template> <template v-if="i !== item">&raquo; </template>
</span> </span>
</header> </header>
<section v-if="! context.currentProfile.live && item.profile_warning !== false" class="tw-border-t tw-pd-t-1 tw-pd-b-2"> <section v-if="(! context.currentProfile.live || ! context.currentProfile.toggled) && item.profile_warning !== false" class="tw-border-t tw-pd-t-1 tw-pd-b-2">
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1"> <div class="tw-c-background-accent tw-c-text-overlay tw-pd-1">
<h3 class="ffz-i-attention"> <h3 class="ffz-i-attention">
{{ t('setting.profiles.inactive', "This profile isn't active.") }} {{ t('setting.profiles.inactive', "This profile is not active.") }}
</h3> </h3>
{{ t('setting.profiles.inactive.description', <span v-if="! context.currentProfile.toggled">
"This profile's rules don't match the current context and it therefore isn't currently active, so you " + {{ t('setting.profiles.disabled.description',
"won't see changes you make here reflected on Twitch.") "This profile has been disabled, so you won't see changes you make here reflected on Twitch.")
}} }}
</span>
<span v-else>
{{ t('setting.profiles.inactive.description',
"This profile's rules don't match the current context and it therefore isn't currently active, so you " +
"won't see changes you make here reflected on Twitch.")
}}
</span>
</div> </div>
</section> </section>
<section v-if="context.has_update" class="tw-border-t tw-pd-t-1 tw-pd-b-2"> <section v-if="context.has_update" class="tw-border-t tw-pd-t-1 tw-pd-b-2">

View file

@ -164,16 +164,28 @@
</div> </div>
<div class="tw-flex tw-flex-shrink-0 tw-align-items-center tw-border-l tw-mg-l-1 tw-pd-l-1"> <div class="tw-flex tw-flex-shrink-0 tw-align-items-center tw-border-l tw-mg-l-1 tw-pd-l-1">
<div v-if="p.live" class="ffz--profile__icon ffz-i-ok tw-relative tw-tooltip-wrapper"> <button class="tw-button tw-button--text" @click="toggle(p)">
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right"> <div
{{ t('setting.profiles.active', 'This profile is active.') }} :class="{
'ffz-i-ok': p.live,
'ffz-i-cancel': ! p.toggled,
'ffz-i-minus': p.toggled && ! p.live
}"
class="ffz--profile__icon tw-relative tw-tooltip-wrapper"
>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
<span v-if="p.live">
{{ t('setting.profiles.active', 'This profile is enabled and active.') }}
</span>
<span v-if="! p.toggled">
{{ t('setting.profiles.disabled', 'This profile is disabled.') }}
</span>
<span v-if="p.toggled && ! p.live">
{{ t('setting.profiles.disabled.rules', 'This profile is enabled, but inactive due to its rules.') }}
</span>
</div>
</div> </div>
</div> </button>
<div v-if="! p.live" class="ffz--profile__icon ffz-i-cancel tw-relative tw-tooltip-wrapper">
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.profiles.inactive', 'This profile is not active.') }}
</div>
</div>
</div> </div>
</div> </div>
</section> </section>
@ -247,6 +259,10 @@ export default {
this.$emit('change-item', item); this.$emit('change-item', item);
}, },
toggle(profile) {
profile.toggle();
},
resetImport() { resetImport() {
this.import_error = false; this.import_error = false;
this.import_error_message = null; this.import_error_message = null;

View file

@ -13,7 +13,7 @@
@keyup.space="focusShow" @keyup.space="focusShow"
@click="togglePopup" @click="togglePopup"
> >
{{ t(context.currentProfile.i18n_key, context.currentProfile.title, context.currentProfile) }} {{ context.currentProfile.i18n_key ? t(context.currentProfile.i18n_key, context.currentProfile.title, context.currentProfile) : context.currentProfile.title }}
</div> </div>
<div <div
v-if="opened" v-if="opened"
@ -51,6 +51,14 @@
@keyup.enter="changeProfile(p)" @keyup.enter="changeProfile(p)"
@click="changeProfile(p)" @click="changeProfile(p)"
> >
<div
v-if="! p.toggled"
class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-cancel tw-absolute"
>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.profiles.disabled', 'This profile is disabled.') }}
</div>
</div>
<div <div
v-if="p.live" v-if="p.live"
class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-ok tw-absolute" class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-ok tw-absolute"

View file

@ -526,6 +526,8 @@ export default class MainMenu extends Module {
order: context.manager.__profiles.indexOf(profile), order: context.manager.__profiles.indexOf(profile),
live: context.__profiles.includes(profile), live: context.__profiles.includes(profile),
toggled: profile.toggled,
title: profile.name, title: profile.name,
i18n_key: profile.i18n_key, i18n_key: profile.i18n_key,
@ -541,6 +543,7 @@ export default class MainMenu extends Module {
profile.save() profile.save()
}, },
toggle: () => profile.toggled = ! profile.toggled,
getBackup: () => deep_copy(profile.getBackup()), getBackup: () => deep_copy(profile.getBackup()),
context: deep_copy(profile.context), context: deep_copy(profile.context),
@ -617,6 +620,11 @@ export default class MainMenu extends Module {
this._update_profiles(profile); this._update_profiles(profile);
}, },
_profile_toggled(profile, val) {
Vue.set(profile_keys[profile.id], 'toggled', val);
this._update_profiles(profile);
},
_profile_deleted(profile) { _profile_deleted(profile) {
Vue.delete(profile_keys, profile.id); Vue.delete(profile_keys, profile.id);
this._update_profiles(); this._update_profiles();
@ -641,6 +649,7 @@ export default class MainMenu extends Module {
_add_user() { _add_user() {
this._users++; this._users++;
if ( this._users === 1 ) { if ( this._users === 1 ) {
settings.on(':profile-toggled', this._profile_toggled, this);
settings.on(':profile-created', this._profile_created, this); settings.on(':profile-created', this._profile_created, this);
settings.on(':profile-changed', this._profile_changed, this); settings.on(':profile-changed', this._profile_changed, this);
settings.on(':profile-deleted', this._profile_deleted, this); settings.on(':profile-deleted', this._profile_deleted, this);
@ -654,6 +663,7 @@ export default class MainMenu extends Module {
_remove_user() { _remove_user() {
this._users--; this._users--;
if ( this._users === 0 ) { if ( this._users === 0 ) {
settings.off(':profile-toggled', this._profile_toggled, this);
settings.off(':profile-created', this._profile_created, this); settings.off(':profile-created', this._profile_created, this);
settings.off(':profile-changed', this._profile_changed, this); settings.off(':profile-changed', this._profile_changed, this);
settings.off(':profile-deleted', this._profile_deleted, this); settings.off(':profile-deleted', this._profile_deleted, this);

View file

@ -60,7 +60,8 @@ export default class TooltipProvider extends Module {
} }
onEnable() { onEnable() {
const container = document.querySelector('#root>div') || document.querySelector('#root') || document.querySelector('.clips-root') || document.body; const container = document.querySelector('.sunlight-root') || document.querySelector('#root>.tw-absolute:not(.tw-flex)') || document.querySelector('#root') || document.querySelector('.clips-root') || document.body;
// is_minimal = false; //container && container.classList.contains('twilight-minimal-root'); // is_minimal = false; //container && container.classList.contains('twilight-minimal-root');
this.tips = new Tooltip(container, 'ffz-tooltip', { this.tips = new Tooltip(container, 'ffz-tooltip', {

View file

@ -0,0 +1,47 @@
<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">
<div class="tw-mg-r-1">
{{ t(type.i18n, type.title) }}
</div>
<label :for="'start-time$' + id" class="tw-mg-l-1">
{{ t('settings.filter.time.start', 'Start:') }}
</label>
<input
: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"
>
<label :for="'end-time$' + id" class="tw-mg-l-1">
{{ t('settings.filter.time.end', 'End:') }}
</label>
<input
: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"
>
</div>
</section>
</template>
<script>
let last_id = 0;
export default {
props: ['value', 'type', 'filters', 'context'],
data() {
return {
id: last_id++
}
}
}
</script>

View file

@ -118,8 +118,9 @@ export default class SettingsContext extends EventEmitter {
selectProfiles() { selectProfiles() {
const new_profiles = [], const new_profiles = [],
order = this.order = []; order = this.order = [];
for(const profile of this.manager.__profiles) for(const profile of this.manager.__profiles)
if ( profile.matches(this.__context) ) { if ( profile.toggled && profile.matches(this.__context) ) {
new_profiles.push(profile); new_profiles.push(profile);
order.push(profile.id); order.push(profile.id);
} }
@ -389,6 +390,16 @@ export default class SettingsContext extends EventEmitter {
} }
hasProfile(profile) {
if ( typeof profile === 'number' )
for(const prof of this.__profiles)
if ( prof.id === profile )
return true;
return this.__profiles.includes(profile);
}
_getRaw(key, type) { _getRaw(key, type) {
if ( ! type ) if ( ! type )
throw new Error(`non-existent type for ${key}`) throw new Error(`non-existent type for ${key}`)

View file

@ -42,6 +42,74 @@ export const Or = {
// Context Stuff // Context Stuff
function parseTime(time) {
if ( typeof time !== 'string' || ! time.length )
return null;
const idx = time.indexOf(':');
if ( idx === -1 )
return null;
let hours, minutes;
try {
hours = parseInt(time.slice(0, idx), 10);
minutes = parseInt(time.slice(idx + 1), 10);
} catch(err) {
return null;
}
return hours * 60 + minutes;
}
export const Time = {
_captured: new Set,
createTest(config) {
const start = parseTime(config[0]),
end = parseTime(config[1]);
if ( start == null || end == null )
return () => false;
if ( start <= end )
return () => {
Time._captured.add(start);
Time._captured.add(end + 1);
const d = new Date,
v = d.getHours() * 60 + d.getMinutes();
return v >= start && v <= end;
}
return () => {
Time._captured.add(start + 1);
Time._captured.add(end);
const d = new Date,
v = d.getHours() * 60 + d.getMinutes();
return v <= start || v >= end;
}
},
captured: () => {
const out = Array.from(Time._captured);
Time._captured = new Set;
out.sort((a, b) => a - b);
return out;
},
title: 'Time of Day',
i18n: 'settings.filter.time',
default: ['08:00', '18:00'],
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/time.vue')
}
export const TheaterMode = { export const TheaterMode = {
createTest(config) { createTest(config) {
return ctx => ctx.ui && ctx.ui.theatreModeEnabled === config; return ctx => ctx.ui && ctx.ui.theatreModeEnabled === config;

View file

@ -46,7 +46,6 @@ export default class SettingsManager extends Module {
this.migrations = new MigrationManager(this); this.migrations = new MigrationManager(this);
// Also create the main context as early as possible. // Also create the main context as early as possible.
this.main_context = new SettingsContext(this); this.main_context = new SettingsContext(this);
@ -98,6 +97,42 @@ export default class SettingsManager extends Module {
this.log.info(`Initialization complete after ${duration.toFixed(5)}ms -- Values: ${this.provider.size} -- Profiles: ${this.__profiles.length}`) this.log.info(`Initialization complete after ${duration.toFixed(5)}ms -- Values: ${this.provider.size} -- Profiles: ${this.__profiles.length}`)
this.scheduleUpdates(); this.scheduleUpdates();
this.updateClock();
}
updateClock() {
const captured = require('./filters').Time.captured();
if ( ! captured?.length )
return;
if ( this._time_timer )
clearTimeout(this._time_timer);
const d = new Date,
now = d.getHours() * 60 + d.getMinutes();
let next = this._time_next != null ? this._time_next : null;
for(const value of captured) {
if ( value <= now )
continue;
if ( next == null || value < next )
next = value;
}
// There's no time waiting for today. Skip to the next day.
if ( next == null )
next = captured[0] + 1440;
// Determine how long it'll take to reach the next time period.
const delta = (next - now) * 60 * 1000 - 59750 + (60000 - Date.now() % 60000);
this._time_timer = setTimeout(() => {
for(const context of this.__contexts)
context.selectProfiles();
this.updateClock();
}, delta);
} }
@ -186,6 +221,8 @@ export default class SettingsManager extends Module {
// Look up the profile it belongs to and emit a changed event from // Look up the profile it belongs to and emit a changed event from
// that profile, thus notifying any contexts or UI instances. // that profile, thus notifying any contexts or UI instances.
key = key.substr(2); key = key.substr(2);
// Is it a value?
const idx = key.indexOf(':'); const idx = key.indexOf(':');
if ( idx === -1 ) if ( idx === -1 )
return; return;
@ -193,8 +230,12 @@ export default class SettingsManager extends Module {
const profile = this.__profile_ids[key.slice(0, idx)], const profile = this.__profile_ids[key.slice(0, idx)],
s_key = key.slice(idx + 1); s_key = key.slice(idx + 1);
if ( profile ) if ( profile ) {
profile.emit('changed', s_key, new_value, deleted); if ( s_key === ':enabled' )
profile.emit('toggled', profile, deleted ? true : new_value);
else
profile.emit('changed', s_key, new_value, deleted);
}
} }
@ -210,6 +251,17 @@ export default class SettingsManager extends Module {
// And then re-select the active profiles. // And then re-select the active profiles.
for(const context of this.__contexts) for(const context of this.__contexts)
context.selectProfiles(); context.selectProfiles();
this.updateClock();
}
_onProfileToggled(profile, val) {
for(const context of this.__contexts)
context.selectProfiles();
this.updateClock();
this.emit(':profile-toggled', profile, val);
} }
@ -250,6 +302,9 @@ export default class SettingsManager extends Module {
let reordered = false, let reordered = false,
changed = false; changed = false;
for(const profile of old_profiles)
profile.off('toggled', this._onProfileToggled, this);
for(const profile_data of raw_profiles) { for(const profile_data of raw_profiles) {
const id = profile_data.id, const id = profile_data.id,
slot_id = profiles.length, slot_id = profiles.length,
@ -293,12 +348,17 @@ export default class SettingsManager extends Module {
changed = true; changed = true;
} }
for(const profile of profiles)
profile.on('toggled', this._onProfileToggled, this);
if ( ! changed && ! old_ids.size || suppress_events ) if ( ! changed && ! old_ids.size || suppress_events )
return; return;
for(const context of this.__contexts) for(const context of this.__contexts)
context.selectProfiles(); context.selectProfiles();
this.updateClock();
for(const id of new_ids) for(const id of new_ids)
this.emit(':profile-created', profile_ids[id]); this.emit(':profile-created', profile_ids[id]);
@ -327,9 +387,10 @@ export default class SettingsManager extends Module {
options.name = `Unnamed Profile ${i}`; options.name = `Unnamed Profile ${i}`;
const profile = this.__profile_ids[i] = new SettingsProfile(this, options); const profile = this.__profile_ids[i] = new SettingsProfile(this, options);
this.__profiles.unshift(profile); this.__profiles.unshift(profile);
profile.on('toggled', this._onProfileToggled, this);
this._saveProfiles(); this._saveProfiles();
this.emit(':profile-created', profile); this.emit(':profile-created', profile);
return profile; return profile;
@ -351,6 +412,7 @@ export default class SettingsManager extends Module {
if ( profile.id === 0 ) if ( profile.id === 0 )
throw new Error('cannot delete default profile'); throw new Error('cannot delete default profile');
profile.off('toggled', this._onProfileToggled, this);
profile.clear(); profile.clear();
this.__profile_ids[id] = null; this.__profile_ids[id] = null;
@ -400,6 +462,8 @@ export default class SettingsManager extends Module {
this.provider.set('profiles', this.__profiles.map(prof => prof.data)); this.provider.set('profiles', this.__profiles.map(prof => prof.data));
for(const context of this.__contexts) for(const context of this.__contexts)
context.selectProfiles(); context.selectProfiles();
this.updateClock();
} }

View file

@ -24,6 +24,7 @@ export default class SettingsProfile extends EventEmitter {
this.data = data; this.data = data;
this.prefix = `p:${this.id}:`; this.prefix = `p:${this.id}:`;
this.enabled_key = `${this.prefix}:enabled`;
} }
get data() { get data() {
@ -38,6 +39,7 @@ export default class SettingsProfile extends EventEmitter {
desc_i18n_key: this.desc_i18n_key, desc_i18n_key: this.desc_i18n_key,
url: this.url, url: this.url,
show_toggle: this.show_toggle,
context: this.context context: this.context
} }
@ -72,6 +74,7 @@ export default class SettingsProfile extends EventEmitter {
version: 2, version: 2,
type: 'profile', type: 'profile',
profile: this.data, profile: this.data,
toggled: this.toggled,
values: {} values: {}
}; };
@ -107,6 +110,23 @@ export default class SettingsProfile extends EventEmitter {
} }
// ========================================================================
// Toggled
// ========================================================================
get toggled() {
return this.provider.get(this.enabled_key, true);
}
set toggled(val) {
if ( val === this.toggleState )
return;
this.provider.set(this.enabled_key, val);
this.emit('toggled', this, val);
}
// ======================================================================== // ========================================================================
// Context // Context
// ======================================================================== // ========================================================================
@ -158,7 +178,7 @@ export default class SettingsProfile extends EventEmitter {
len = p.length; len = p.length;
for(const key of this.provider.keys()) for(const key of this.provider.keys())
if ( key.startsWith(p) ) if ( key.startsWith(p) && key !== this.enabled_key )
out.push(key.slice(len)); out.push(key.slice(len));
return out; return out;
@ -168,7 +188,7 @@ export default class SettingsProfile extends EventEmitter {
const p = this.prefix, const p = this.prefix,
len = p.length; len = p.length;
for(const key of this.provider.keys()) for(const key of this.provider.keys())
if ( key.startsWith(p) ) { if ( key.startsWith(p) && key !== this.enabled_key ) {
this.provider.delete(key); this.provider.delete(key);
this.emit('changed', key.slice(len), undefined, true); this.emit('changed', key.slice(len), undefined, true);
} }
@ -179,7 +199,7 @@ export default class SettingsProfile extends EventEmitter {
len = p.length; len = p.length;
for(const key of this.provider.keys()) for(const key of this.provider.keys())
if ( key.startsWith(p) ) if ( key.startsWith(p) && key !== this.enabled_key )
yield [key.slice(len), this.provider.get(key)]; yield [key.slice(len), this.provider.get(key)];
} }
@ -188,7 +208,7 @@ export default class SettingsProfile extends EventEmitter {
let count = 0; let count = 0;
for(const key of this.provider.keys()) for(const key of this.provider.keys())
if ( key.startsWith(p) ) if ( key.startsWith(p) && key !== this.enabled_key )
count++; count++;
return count; return count;

View file

@ -236,6 +236,6 @@ Twilight.ROUTES = {
}; };
Twilight.DIALOG_EXCLUSIVE = '.twilight-main,.twilight-minimal-root>div,#root>div>.tw-full-height,.clips-root'; Twilight.DIALOG_EXCLUSIVE = '.sunlight-root,.twilight-main,.twilight-minimal-root>div,#root>div>.tw-full-height,.clips-root';
Twilight.DIALOG_MAXIMIZED = '.twilight-main,.twilight-minimal-root,#root .dashboard-side-nav+.tw-full-height,.clips-root>.tw-full-height .scrollable-area'; Twilight.DIALOG_MAXIMIZED = '.sunlight-page,.twilight-main,.twilight-minimal-root,#root .dashboard-side-nav+.tw-full-height,.clips-root>.tw-full-height .scrollable-area';
Twilight.DIALOG_SELECTOR = '#root>div>.tw-full-height,.twilight-minimal-root>.tw-full-height,.clips-root>.tw-full-height .scrollable-area'; Twilight.DIALOG_SELECTOR = '.sunlight-root,#root,.twilight-minimal-root>.tw-full-height,.clips-root>.tw-full-height .scrollable-area';

View file

@ -6,7 +6,7 @@
import {DEBUG} from 'utilities/constants'; import {DEBUG} from 'utilities/constants';
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {createElement} from 'utilities/dom'; import {createElement, ClickOutside, setChildren} from 'utilities/dom';
export default class MenuButton extends SiteModule { export default class MenuButton extends SiteModule {
constructor(...args) { constructor(...args) {
@ -192,6 +192,7 @@ export default class MenuButton extends SiteModule {
{btn = (<button {btn = (<button
class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative" class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
onClick={e => this.handleClick(e, btn)} // eslint-disable-line react/jsx-no-bind onClick={e => this.handleClick(e, btn)} // eslint-disable-line react/jsx-no-bind
onContextMenu={e => this.renderContext(e, btn)} // eslint-disable-line react/jsx-no-bind
> >
<div class="tw-align-items-center tw-flex tw-flex-grow-0"> <div class="tw-align-items-center tw-flex tw-flex-grow-0">
<span class="tw-button-icon__icon"> <span class="tw-button-icon__icon">
@ -256,6 +257,9 @@ export default class MenuButton extends SiteModule {
</div>); </div>);
container.insertBefore(el, container.lastElementChild); container.insertBefore(el, container.lastElementChild);
if ( this._ctx_open )
this.renderContext(null, btn);
} }
handleClick(event, btn) { handleClick(event, btn) {
@ -269,13 +273,160 @@ export default class MenuButton extends SiteModule {
this.emit(':clicked', event, btn); this.emit(':clicked', event, btn);
} }
renderButtonIcon(profile) {
const live = this.settings.main_context.hasProfile(profile);
return (<figure class={`ffz--profile__icon ${live ? 'ffz-i-ok' : profile.toggled ? 'ffz-i-minus' : 'ffz-i-cancel'}`} />);
}
renderButtonTip(profile) {
if ( ! profile.toggled )
return this.i18n.t('setting.profiles.disabled', 'This profile is disabled.');
if ( this.settings.main_context.hasProfile(profile) )
return this.i18n.t('setting.profiles.active', 'This profile is enabled and active.');
return this.i18n.t('setting.profiles.disabled.rules', 'This profile is enabled, but inactive due to its rules.');
}
renderContext(event, btn) {
if ( event ) {
if ( event.shiftKey || event.ctrlKey )
return;
event.preventDefault();
}
const container = btn.parentElement.parentElement;
let ctx = container.querySelector('.ffz--menu-context');
if ( ctx ) {
if ( ctx._ffz_destroy )
ctx._ffz_destroy();
else
return;
}
const destroy = () => {
this._ctx_open = false;
if ( ctx._ffz_outside )
ctx._ffz_outside.destroy();
ctx.remove();
}
const profiles = [];
for(const profile of this.settings.__profiles) {
const toggle = (<button
class="tw-flex-shrink-0 tw-mg-r-1 tw-align-items-center tw-align-middle tw-border-radius-medium tw-button-icon tw-button-icon--secondary tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-relative ffz-tooltip ffz-tooltip--no-mouse"
data-title={this.renderButtonTip(profile)}
onClick={e => { // eslint-disable-line react/jsx-no-bind
profile.toggled = ! profile.toggled;
setChildren(toggle, this.renderButtonIcon(profile));
toggle.dataset.title = this.renderButtonTip(profile);
if ( toggle['_ffz_tooltip$0']?.rerender )
toggle['_ffz_tooltip$0'].rerender();
this.emit('tooltips:cleanup');
e.preventDefault();
e.stopPropagation();
}}
>
{ this.renderButtonIcon(profile) }
</button>)
profiles.push(<div class="tw-relative tw-border-b tw-pd-y-05 tw-pd-l-1 tw-flex">
{toggle}
<div>
<h4>{ profile.i18n_key ? this.i18n.t(profile.i18n_key, profile.name, profile) : profile.name }</h4>
{profile.description && (<div class="description">
{ profile.desc_i18n_key ? this.i18n.t(profile.desc_i18n_key, profile.description, profile) : profile.description }
</div>)}
</div>
</div>);
}
ctx = (<div class="tw-absolute tw-balloon tw-balloon--down tw-balloon--lg tw-balloon--right 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" />
<div class="tw-align-items-center tw-flex tw-flex-column tw-flex-grow-1 tw-justify-content-center">
<h5 class="tw-align-center tw-c-text-alt tw-semibold">
{ this.i18n.t('site.menu_button.profiles', 'Profiles') }
</h5>
</div>
<div class="tw-flex tw-flex-column tw-justify-content-center tw-mg-l-05 tw-popover-header__icon-slot--right">
<div class="tw-inline-flex tw-relative tw-tooltip-wrapper">
<button
class="tw-align-items-center tw-align-middle tw-border-radius-medium tw-button-icon tw-button-icon--secondary tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden"
onClick={e => this.openSettings(e)} // eslint-disable-line react/jsx-no-bind
>
<span class="tw-button-icon__icon">
<figure class="ffz-i-cog" />
</span>
</button>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center">
{ this.i18n.t('setting.profiles.configure', 'Configure') }
</div>
</div>
</div>
<div class="tw-flex tw-flex-column tw-justify-content-center tw-mg-l-05 tw-popover-header__icon-slot--right">
<button
class="tw-align-items-center tw-align-middle tw-border-radius-medium tw-button-icon tw-button-icon--secondary tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
onClick={destroy} // eslint-disable-line react/jsx-no-bind
>
<span class="tw-button-icon__icon">
<figure class="ffz-i-cancel" />
</span>
</button>
</div>
</div>
<div class="center-window__long-scrollable-area scrollable-area scrollable-area--suppress-scroll-x" data-simplebar>
{profiles}
</div>
</div>
</div>);
ctx._ffz_destroy = destroy;
ctx._ffz_outside = new ClickOutside(ctx, destroy);
container.appendChild(ctx);
this._ctx_open = true;
}
openSettings() {
const menu = this.resolve('main_menu');
if ( ! menu )
return;
menu.requestPage('data_management.profiles');
if ( menu.showing )
return;
this.emit(':clicked');
}
loadMenu(event, btn, page) {
const menu = this.resolve('main_menu');
if ( ! menu )
return;
loadMenu(event, btn) {
const cl = btn && btn.classList; const cl = btn && btn.classList;
if ( cl ) if ( cl )
cl.add('loading'); cl.add('loading');
this.resolve('main_menu').enable(event).then(() => { if ( page )
menu.requestPage(page);
if ( menu.showing )
return;
menu.enable(event).then(() => {
if ( cl ) if ( cl )
cl.remove('loading'); cl.remove('loading');

View file

@ -80,5 +80,6 @@ export default [
"sort-alt-down", "sort-alt-down",
"user", "user",
"clip", "clip",
"youtube-play" "youtube-play",
"minus"
]; ];

View file

@ -487,6 +487,8 @@ const CARDINAL_TO_LANG = {
german: ['de', 'el', 'en', 'es', 'fi', 'hu', 'it', 'nl', 'no', 'nb', 'tr', 'sv'], german: ['de', 'el', 'en', 'es', 'fi', 'hu', 'it', 'nl', 'no', 'nb', 'tr', 'sv'],
hebrew: ['he'], hebrew: ['he'],
persian: ['fa'], persian: ['fa'],
polish: ['pl'],
serbian: ['sr'],
french: ['fr', 'pt'], french: ['fr', 'pt'],
russian: ['ru','uk'] russian: ['ru','uk']
} }
@ -522,6 +524,28 @@ const CARDINAL_TYPES = {
persian: (n, i) => (i === 0 || n === 1) ? 1 : 5, persian: (n, i) => (i === 0 || n === 1) ? 1 : 5,
serbian(n, i, v, t) {
if ( v !== 0 ) return 5;
const i1 = i % 10, i2 = i % 100;
const t1 = t % 10, t2 = t % 100;
if ( i1 === 1 && i2 !== 11 ) return 1;
if ( t1 === 1 && t2 !== 11 ) return 1;
if ( i1 >= 2 && i1 <= 4 && !(i2 >= 12 && i2 <= 14) ) return 3;
if ( t1 >= 2 && t1 <= 4 && !(t2 >= 12 && t2 <= 14) ) return 3;
return 5;
},
polish(n, i, v) {
if ( v !== 0 ) return 5;
if ( n === 1 ) return 1;
const n1 = n % 10, n2 = n % 100;
if ( n1 >= 2 && n1 <= 4 && !(n2 >= 12 && n2 <= 14) ) return 3;
if ( i !== 1 && (n1 === 0 || n1 === 1) ) return 4;
if ( n1 >= 5 && n1 <= 9 ) return 4;
if ( n2 >= 12 && n2 <= 14 ) return 4;
return 5;
},
russian(n,i,v) { russian(n,i,v) {
const n1 = n % 10, n2 = n % 100; const n1 = n % 10, n2 = n % 100;
if ( n1 === 1 && n2 !== 11 ) return 1; if ( n1 === 1 && n2 !== 11 ) return 1;

View file

@ -61,7 +61,10 @@
} }
} }
.ffz-has-dialog {
position: relative;
.ffz-has-dialog > :not(.ffz-dialog) { & > :not(.ffz-dialog) {
visibility: hidden; visibility: hidden;
}
} }

View file

@ -52,6 +52,7 @@
.ffz-i-play:before { content: '\e832'; } /* '' */ .ffz-i-play:before { content: '\e832'; } /* '' */
.ffz-i-user:before { content: '\e833'; } /* '' */ .ffz-i-user:before { content: '\e833'; } /* '' */
.ffz-i-clip:before { content: '\e834'; } /* '' */ .ffz-i-clip:before { content: '\e834'; } /* '' */
.ffz-i-minus:before { content: '\e835'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */ .ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */ .ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-github:before { content: '\f09b'; } /* '' */ .ffz-i-github:before { content: '\f09b'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -52,6 +52,7 @@
.ffz-i-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); } .ffz-i-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.ffz-i-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); } .ffz-i-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); }
.ffz-i-clip { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); } .ffz-i-clip { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.ffz-i-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe835;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); } .ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); } .ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); } .ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }

View file

@ -63,6 +63,7 @@
.ffz-i-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); } .ffz-i-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.ffz-i-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); } .ffz-i-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); }
.ffz-i-clip { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); } .ffz-i-clip { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.ffz-i-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe835;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); } .ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); } .ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); } .ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'ffz-fontello'; font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.eot?84652283'); src: url('../font/ffz-fontello.eot?15513462');
src: url('../font/ffz-fontello.eot?84652283#iefix') format('embedded-opentype'), src: url('../font/ffz-fontello.eot?15513462#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?84652283') format('woff2'), url('../font/ffz-fontello.woff2?15513462') format('woff2'),
url('../font/ffz-fontello.woff?84652283') format('woff'), url('../font/ffz-fontello.woff?15513462') format('woff'),
url('../font/ffz-fontello.ttf?84652283') format('truetype'), url('../font/ffz-fontello.ttf?15513462') format('truetype'),
url('../font/ffz-fontello.svg?84652283#ffz-fontello') format('svg'); url('../font/ffz-fontello.svg?15513462#ffz-fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'ffz-fontello'; font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.svg?84652283#ffz-fontello') format('svg'); src: url('../font/ffz-fontello.svg?15513462#ffz-fontello') format('svg');
} }
} }
*/ */
@ -108,6 +108,7 @@
.ffz-i-play:before { content: '\e832'; } /* '' */ .ffz-i-play:before { content: '\e832'; } /* '' */
.ffz-i-user:before { content: '\e833'; } /* '' */ .ffz-i-user:before { content: '\e833'; } /* '' */
.ffz-i-clip:before { content: '\e834'; } /* '' */ .ffz-i-clip:before { content: '\e834'; } /* '' */
.ffz-i-minus:before { content: '\e835'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */ .ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */ .ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-github:before { content: '\f09b'; } /* '' */ .ffz-i-github:before { content: '\f09b'; } /* '' */

View file

@ -149,12 +149,18 @@ textarea.tw-input {
} }
} }
.ffz--profile-row__icon,
.ffz--profile__icon { .ffz--profile__icon {
&.ffz-i-ok, &.ffz-i-ok,
.ffz-i-ok { .ffz-i-ok {
color: var(--color-green-darker); color: var(--color-green-darker);
} }
&.ffz-i-minus,
.ffz-i-minus {
color: var(--color-text-alt-2);
}
&.ffz-i-cancel, &.ffz-i-cancel,
.ffz-i-cancel { .ffz-i-cancel {
color: var(--color-red); color: var(--color-red);

View file

@ -18,9 +18,9 @@
border-left: 4px solid; border-left: 4px solid;
border-left-color: transparent; border-left-color: transparent;
&.live .ffz--profile-row__icon { /*&.live .ffz--profile-row__icon {
color: var(--color-green-darker); color: var(--color-green-darker);
} }*/
&:not(.live):not(:hover):not(:focus) { &:not(.live):not(:hover):not(:focus) {
opacity: .5; opacity: .5;