mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.14.0
* 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:
parent
02efd61f00
commit
62bb6440f3
30 changed files with 503 additions and 52 deletions
|
@ -561,6 +561,12 @@
|
|||
"css": "youtube-play",
|
||||
"code": 61802,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "861ab06e455e2de3232ebef67d60d708",
|
||||
"css": "minus",
|
||||
"code": 59445,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.13.3",
|
||||
"version": "4.14.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
Binary file not shown.
|
@ -112,6 +112,8 @@
|
|||
|
||||
<glyph glyph-name="clip" unicode="" 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="" 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="" 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="" 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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -486,7 +486,7 @@ export class TranslationManager extends Module {
|
|||
out = 'FFZ Control Center';
|
||||
else {
|
||||
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';
|
||||
|
||||
out = `${label} (${location[2]}:${location[3]})`;
|
||||
|
|
|
@ -7,16 +7,24 @@
|
|||
<template v-if="i !== item">» </template>
|
||||
</span>
|
||||
</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">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('setting.profiles.inactive', "This profile isn't active.") }}
|
||||
{{ t('setting.profiles.inactive', "This profile is not active.") }}
|
||||
</h3>
|
||||
|
||||
{{ 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 v-if="! context.currentProfile.toggled">
|
||||
{{ t('setting.profiles.disabled.description',
|
||||
"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>
|
||||
</section>
|
||||
<section v-if="context.has_update" class="tw-border-t tw-pd-t-1 tw-pd-b-2">
|
||||
|
|
|
@ -164,16 +164,28 @@
|
|||
</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 v-if="p.live" class="ffz--profile__icon ffz-i-ok tw-relative tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is active.') }}
|
||||
<button class="tw-button tw-button--text" @click="toggle(p)">
|
||||
<div
|
||||
: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 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -247,6 +259,10 @@ export default {
|
|||
this.$emit('change-item', item);
|
||||
},
|
||||
|
||||
toggle(profile) {
|
||||
profile.toggle();
|
||||
},
|
||||
|
||||
resetImport() {
|
||||
this.import_error = false;
|
||||
this.import_error_message = null;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
@keyup.space="focusShow"
|
||||
@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
|
||||
v-if="opened"
|
||||
|
@ -51,6 +51,14 @@
|
|||
@keyup.enter="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
|
||||
v-if="p.live"
|
||||
class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-ok tw-absolute"
|
||||
|
|
|
@ -526,6 +526,8 @@ export default class MainMenu extends Module {
|
|||
order: context.manager.__profiles.indexOf(profile),
|
||||
live: context.__profiles.includes(profile),
|
||||
|
||||
toggled: profile.toggled,
|
||||
|
||||
title: profile.name,
|
||||
i18n_key: profile.i18n_key,
|
||||
|
||||
|
@ -541,6 +543,7 @@ export default class MainMenu extends Module {
|
|||
profile.save()
|
||||
},
|
||||
|
||||
toggle: () => profile.toggled = ! profile.toggled,
|
||||
getBackup: () => deep_copy(profile.getBackup()),
|
||||
|
||||
context: deep_copy(profile.context),
|
||||
|
@ -617,6 +620,11 @@ export default class MainMenu extends Module {
|
|||
this._update_profiles(profile);
|
||||
},
|
||||
|
||||
_profile_toggled(profile, val) {
|
||||
Vue.set(profile_keys[profile.id], 'toggled', val);
|
||||
this._update_profiles(profile);
|
||||
},
|
||||
|
||||
_profile_deleted(profile) {
|
||||
Vue.delete(profile_keys, profile.id);
|
||||
this._update_profiles();
|
||||
|
@ -641,6 +649,7 @@ export default class MainMenu extends Module {
|
|||
_add_user() {
|
||||
this._users++;
|
||||
if ( this._users === 1 ) {
|
||||
settings.on(':profile-toggled', this._profile_toggled, this);
|
||||
settings.on(':profile-created', this._profile_created, this);
|
||||
settings.on(':profile-changed', this._profile_changed, this);
|
||||
settings.on(':profile-deleted', this._profile_deleted, this);
|
||||
|
@ -654,6 +663,7 @@ export default class MainMenu extends Module {
|
|||
_remove_user() {
|
||||
this._users--;
|
||||
if ( this._users === 0 ) {
|
||||
settings.off(':profile-toggled', this._profile_toggled, this);
|
||||
settings.off(':profile-created', this._profile_created, this);
|
||||
settings.off(':profile-changed', this._profile_changed, this);
|
||||
settings.off(':profile-deleted', this._profile_deleted, this);
|
||||
|
|
|
@ -60,7 +60,8 @@ export default class TooltipProvider extends Module {
|
|||
}
|
||||
|
||||
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');
|
||||
|
||||
this.tips = new Tooltip(container, 'ffz-tooltip', {
|
||||
|
|
47
src/settings/components/time.vue
Normal file
47
src/settings/components/time.vue
Normal 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>
|
|
@ -118,8 +118,9 @@ export default class SettingsContext extends EventEmitter {
|
|||
selectProfiles() {
|
||||
const new_profiles = [],
|
||||
order = this.order = [];
|
||||
|
||||
for(const profile of this.manager.__profiles)
|
||||
if ( profile.matches(this.__context) ) {
|
||||
if ( profile.toggled && profile.matches(this.__context) ) {
|
||||
new_profiles.push(profile);
|
||||
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) {
|
||||
if ( ! type )
|
||||
throw new Error(`non-existent type for ${key}`)
|
||||
|
|
|
@ -42,6 +42,74 @@ export const Or = {
|
|||
|
||||
// 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 = {
|
||||
createTest(config) {
|
||||
return ctx => ctx.ui && ctx.ui.theatreModeEnabled === config;
|
||||
|
|
|
@ -46,7 +46,6 @@ export default class SettingsManager extends Module {
|
|||
|
||||
this.migrations = new MigrationManager(this);
|
||||
|
||||
|
||||
// Also create the main context as early as possible.
|
||||
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.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
|
||||
// that profile, thus notifying any contexts or UI instances.
|
||||
key = key.substr(2);
|
||||
|
||||
// Is it a value?
|
||||
const idx = key.indexOf(':');
|
||||
if ( idx === -1 )
|
||||
return;
|
||||
|
@ -193,8 +230,12 @@ export default class SettingsManager extends Module {
|
|||
const profile = this.__profile_ids[key.slice(0, idx)],
|
||||
s_key = key.slice(idx + 1);
|
||||
|
||||
if ( profile )
|
||||
profile.emit('changed', s_key, new_value, deleted);
|
||||
if ( profile ) {
|
||||
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.
|
||||
for(const context of this.__contexts)
|
||||
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,
|
||||
changed = false;
|
||||
|
||||
for(const profile of old_profiles)
|
||||
profile.off('toggled', this._onProfileToggled, this);
|
||||
|
||||
for(const profile_data of raw_profiles) {
|
||||
const id = profile_data.id,
|
||||
slot_id = profiles.length,
|
||||
|
@ -293,12 +348,17 @@ export default class SettingsManager extends Module {
|
|||
changed = true;
|
||||
}
|
||||
|
||||
for(const profile of profiles)
|
||||
profile.on('toggled', this._onProfileToggled, this);
|
||||
|
||||
if ( ! changed && ! old_ids.size || suppress_events )
|
||||
return;
|
||||
|
||||
for(const context of this.__contexts)
|
||||
context.selectProfiles();
|
||||
|
||||
this.updateClock();
|
||||
|
||||
for(const id of new_ids)
|
||||
this.emit(':profile-created', profile_ids[id]);
|
||||
|
||||
|
@ -327,9 +387,10 @@ export default class SettingsManager extends Module {
|
|||
options.name = `Unnamed Profile ${i}`;
|
||||
|
||||
const profile = this.__profile_ids[i] = new SettingsProfile(this, options);
|
||||
|
||||
this.__profiles.unshift(profile);
|
||||
|
||||
profile.on('toggled', this._onProfileToggled, this);
|
||||
|
||||
this._saveProfiles();
|
||||
this.emit(':profile-created', profile);
|
||||
return profile;
|
||||
|
@ -351,6 +412,7 @@ export default class SettingsManager extends Module {
|
|||
if ( profile.id === 0 )
|
||||
throw new Error('cannot delete default profile');
|
||||
|
||||
profile.off('toggled', this._onProfileToggled, this);
|
||||
profile.clear();
|
||||
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));
|
||||
for(const context of this.__contexts)
|
||||
context.selectProfiles();
|
||||
|
||||
this.updateClock();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
|
||||
this.data = data;
|
||||
this.prefix = `p:${this.id}:`;
|
||||
this.enabled_key = `${this.prefix}:enabled`;
|
||||
}
|
||||
|
||||
get data() {
|
||||
|
@ -38,6 +39,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
desc_i18n_key: this.desc_i18n_key,
|
||||
|
||||
url: this.url,
|
||||
show_toggle: this.show_toggle,
|
||||
|
||||
context: this.context
|
||||
}
|
||||
|
@ -72,6 +74,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
version: 2,
|
||||
type: 'profile',
|
||||
profile: this.data,
|
||||
toggled: this.toggled,
|
||||
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
|
||||
// ========================================================================
|
||||
|
@ -158,7 +178,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
len = p.length;
|
||||
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) )
|
||||
if ( key.startsWith(p) && key !== this.enabled_key )
|
||||
out.push(key.slice(len));
|
||||
|
||||
return out;
|
||||
|
@ -168,7 +188,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
const p = this.prefix,
|
||||
len = p.length;
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) ) {
|
||||
if ( key.startsWith(p) && key !== this.enabled_key ) {
|
||||
this.provider.delete(key);
|
||||
this.emit('changed', key.slice(len), undefined, true);
|
||||
}
|
||||
|
@ -179,7 +199,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
len = p.length;
|
||||
|
||||
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)];
|
||||
}
|
||||
|
||||
|
@ -188,7 +208,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
let count = 0;
|
||||
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) )
|
||||
if ( key.startsWith(p) && key !== this.enabled_key )
|
||||
count++;
|
||||
|
||||
return count;
|
||||
|
|
|
@ -236,6 +236,6 @@ Twilight.ROUTES = {
|
|||
};
|
||||
|
||||
|
||||
Twilight.DIALOG_EXCLUSIVE = '.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_SELECTOR = '#root>div>.tw-full-height,.twilight-minimal-root>.tw-full-height,.clips-root>.tw-full-height .scrollable-area';
|
||||
Twilight.DIALOG_EXCLUSIVE = '.sunlight-root,.twilight-main,.twilight-minimal-root>div,#root>div>.tw-full-height,.clips-root';
|
||||
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 = '.sunlight-root,#root,.twilight-minimal-root>.tw-full-height,.clips-root>.tw-full-height .scrollable-area';
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import {DEBUG} from 'utilities/constants';
|
||||
import {SiteModule} from 'utilities/module';
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
|
||||
|
||||
export default class MenuButton extends SiteModule {
|
||||
constructor(...args) {
|
||||
|
@ -192,6 +192,7 @@ export default class MenuButton extends SiteModule {
|
|||
{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"
|
||||
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">
|
||||
<span class="tw-button-icon__icon">
|
||||
|
@ -256,6 +257,9 @@ export default class MenuButton extends SiteModule {
|
|||
</div>);
|
||||
|
||||
container.insertBefore(el, container.lastElementChild);
|
||||
|
||||
if ( this._ctx_open )
|
||||
this.renderContext(null, btn);
|
||||
}
|
||||
|
||||
handleClick(event, btn) {
|
||||
|
@ -269,13 +273,160 @@ export default class MenuButton extends SiteModule {
|
|||
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;
|
||||
if ( cl )
|
||||
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 )
|
||||
cl.remove('loading');
|
||||
|
||||
|
|
|
@ -80,5 +80,6 @@ export default [
|
|||
"sort-alt-down",
|
||||
"user",
|
||||
"clip",
|
||||
"youtube-play"
|
||||
"youtube-play",
|
||||
"minus"
|
||||
];
|
|
@ -487,6 +487,8 @@ const CARDINAL_TO_LANG = {
|
|||
german: ['de', 'el', 'en', 'es', 'fi', 'hu', 'it', 'nl', 'no', 'nb', 'tr', 'sv'],
|
||||
hebrew: ['he'],
|
||||
persian: ['fa'],
|
||||
polish: ['pl'],
|
||||
serbian: ['sr'],
|
||||
french: ['fr', 'pt'],
|
||||
russian: ['ru','uk']
|
||||
}
|
||||
|
@ -522,6 +524,28 @@ const CARDINAL_TYPES = {
|
|||
|
||||
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) {
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
if ( n1 === 1 && n2 !== 11 ) return 1;
|
||||
|
|
|
@ -61,7 +61,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ffz-has-dialog {
|
||||
position: relative;
|
||||
|
||||
.ffz-has-dialog > :not(.ffz-dialog) {
|
||||
visibility: hidden;
|
||||
& > :not(.ffz-dialog) {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@
|
|||
.ffz-i-play:before { content: '\e832'; } /* '' */
|
||||
.ffz-i-user:before { content: '\e833'; } /* '' */
|
||||
.ffz-i-clip:before { content: '\e834'; } /* '' */
|
||||
.ffz-i-minus:before { content: '\e835'; } /* '' */
|
||||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-github:before { content: '\f09b'; } /* '' */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -52,6 +52,7 @@
|
|||
.ffz-i-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-clip { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
.ffz-i-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-clip { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'ffz-fontello';
|
||||
src: url('../font/ffz-fontello.eot?84652283');
|
||||
src: url('../font/ffz-fontello.eot?84652283#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?84652283') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?84652283') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?84652283') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?84652283#ffz-fontello') format('svg');
|
||||
src: url('../font/ffz-fontello.eot?15513462');
|
||||
src: url('../font/ffz-fontello.eot?15513462#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?15513462') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?15513462') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?15513462') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?15513462#ffz-fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
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-user:before { content: '\e833'; } /* '' */
|
||||
.ffz-i-clip:before { content: '\e834'; } /* '' */
|
||||
.ffz-i-minus:before { content: '\e835'; } /* '' */
|
||||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-github:before { content: '\f09b'; } /* '' */
|
||||
|
|
|
@ -149,12 +149,18 @@ textarea.tw-input {
|
|||
}
|
||||
}
|
||||
|
||||
.ffz--profile-row__icon,
|
||||
.ffz--profile__icon {
|
||||
&.ffz-i-ok,
|
||||
.ffz-i-ok {
|
||||
color: var(--color-green-darker);
|
||||
}
|
||||
|
||||
&.ffz-i-minus,
|
||||
.ffz-i-minus {
|
||||
color: var(--color-text-alt-2);
|
||||
}
|
||||
|
||||
&.ffz-i-cancel,
|
||||
.ffz-i-cancel {
|
||||
color: var(--color-red);
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
border-left: 4px solid;
|
||||
border-left-color: transparent;
|
||||
|
||||
&.live .ffz--profile-row__icon {
|
||||
/*&.live .ffz--profile-row__icon {
|
||||
color: var(--color-green-darker);
|
||||
}
|
||||
}*/
|
||||
|
||||
&:not(.live):not(:hover):not(:focus) {
|
||||
opacity: .5;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue