1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-31 15:08:31 +00:00

4.0.0 Beta 1

This commit is contained in:
SirStendec 2017-11-13 01:23:39 -05:00
parent c2688646af
commit 262757a20d
187 changed files with 22878 additions and 38882 deletions

View file

@ -0,0 +1,50 @@
<template lang="html">
<div class="ffz--changelog border-t pd-t-1">
<div class="align-center">
<h2>{{ t('home.changelog', 'Changelog') }}</h2>
</div>
<div ref="changes" />
</div>
</template>
<script>
import {SERVER} from 'utilities/constants';
export default {
props: ['item', 'context'],
methods: {
fetch(url, container) {
const done = data => {
if ( ! data )
data = 'There was an error loading this page from the server.';
container.innerHTML = data;
const btn = container.querySelector('#ffz-old-news-button');
if ( btn )
btn.addEventListener('click', () => {
btn.parentElement.removeChild(btn);
const old_news = container.querySelector('#ffz-old-news');
if ( old_news )
this.fetch(`${SERVER}/script/old_changes.html`, old_news);
});
}
fetch(url)
.then(resp => resp.ok ? resp.text() : null)
.then(done)
.catch(err => done(null));
}
},
mounted() {
this.fetch(`${SERVER}/script/changelog.html`, this.$refs.changes);
}
}
</script>

View file

@ -0,0 +1,35 @@
<template lang="html">
<div class="ffz--home border-t pd-t-1">
<h2>Feedback</h2>
<div class="mg-y-1 c-background-accent c-text-overlay pd-1">
<h3 class="ffz-i-attention">
Please keep in mind that FrankerFaceZ v4 is under heavy development.
</h3>
</div>
<p>
Okay, still here? Great! You can provide feedback and bug reports by
<a href="https://github.com/FrankerFaceZ/FrankerFaceZ/issues" target="_blank" rel="noopener">
opening an issue at our GitHub repository</a>.
You can also <a href="https://twitter.com/FrankerFaceZ" target="_blank" rel="noopener">
tweet at us</a>.
</p>
<p>
When creating a GitHub issue, please check that someone else hasn't
already created one for what you'd like to discuss or report.
</p>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
}
</script>

View file

@ -0,0 +1,67 @@
<template lang="html">
<div class="ffz--widget ffz--filter-editor">
<div ref="list" class="ffz--rule-list">
<section v-for="(rule, idx) in rules">
<div
class="ffz--rule elevation-1 c-background border mg-b-05 pd-y-05 pd-r-1 flex flex--nowrap align-items-start"
tabindex="0"
>
<div class="flex flex-shrink-0 align-items-center handle pd-x-05 pd-y-1">
<span class="ffz-i-ellipsis-vert" />
</div>
<div class="flex-shrink-0 pd-y-05">
Channel
</div>
<div class="mg-x-1 flex flex-grow-1">
<div class="flex-shrink-0 mg-r-1">
<select class="tw-select">
<option>is one of</option>
<option>is not one of</option>
</select>
</div>
<div class="flex-grow-1">
<input
type="text"
class="tw-input"
value="SirStendec"
/>
</div>
</div>
<div class="flex flex-shrink-0 align-items-center">
<button class="tw-button tw-button--text" @click="del(idx)">
<span class="tw-button__text ffz-i-trash">
{{ t('setting.filters.delete', 'Delete') }}
</span>
</button>
</div>
</div>
</section>
</div>
<button
class="tw-button tw-button--hollow mg-y-1 full-width"
@click="newRule"
>
<span class="tw-button__text ffz-i-plus">
{{ t('', 'Add New Rule') }}
</span>
</button>
</div>
</template>
<script>
export default {
props: ['filters', 'rules', 'context'],
methods: {
newRule() {
}
}
}
</script>

View file

@ -0,0 +1,100 @@
<template lang="html">
<div class="ffz--home flex flex--nowrap">
<div class="flex-grow-1">
<div class="align-center">
<h1 class="ffz-i-zreknarf ffz-i-pd-1">FrankerFaceZ</h1>
<span class="c-text-alt">
{{ t('home.tag-line', 'The Twitch Enhancement Suite') }}
</span>
</div>
<section class="pd-t-1 border-t mg-t-1">
<h2>Welcome to the v4.0 Beta</h2>
<p>
This is the initial, beta release of FrankerFaceZ v4.0 with support
for the Twitch website rewrite.
As you'll notice, this release is <strong>not</strong> complete.
There are missing features. There are bugs. If you are a moderator,
you will want to just keep opening a Legacy Chat Popout for now.
</p>
<p>
FrankerFaceZ v4.0 is still under heavy development and there will
be significant changes and improvements in the coming weeks. For
now, here are some of the bigger issues:
</p>
<ul class="mg-b-2">
<li>Settings from the old version are not being imported.</li>
<li>Settings cannot be searched.</li>
<li>FFZ badges do not display.</li>
<li>Oh god everything is missing.</li>
<li>FFZ:AP is broken.</li>
<li>Uptime breaks occasionally.</li>
</ul>
<p>And the biggest features still under development:</p>
<ul class="mg-b-2">
<li>Dark Theme (Pls No Purple)</li>
<li>Chat Pause on Hover</li>
<li>Badge Customization</li>
<li>Emoji Rendering</li>
<li>Emotes Menu</li>
<li>Chat Filtering (Highlighted Words, etc.)</li>
<li>Room Status Indicators</li>
<li>Custom Mod Cards</li>
<li>Custom Mod Actions</li>
<li>Chat Room Tabs</li>
<li>Recent Highlights</li>
<li>More Channel Metadata</li>
<li>Disable Hosting</li>
<li>Portrait Mode</li>
<li>Hiding stuff in the directory</li>
<li>Directory Host Stacking</li>
<li>Basically anything to do with the directory</li>
<li>Importing and exporting settings</li>
<li>User Aliases</li>
<li>Rich Content in Chat (aka Clip Embeds)</li>
</ul>
<p>
For a possibly more up-to-date list of what I'm working on,
please consult <a href="https://trello.com/b/LGcYPFwi/frankerfacez-v4" target="_blank">this Trello board</a>.
</p>
</section>
</div>
<div class="mg-l-1 flex-shrink-0 tweet-column">
<a class="twitter-timeline" data-width="300" data-theme="dark" href="https://twitter.com/FrankerFaceZ?ref_src=twsrc%5Etfw">
Tweets by FrankerFaceZ
</a>
</div>
</div>
</template>
<script>
import {createElement as e} from 'utilities/dom';
export default {
props: ['item', 'context'],
mounted() {
let el;
document.head.appendChild(el = e('script', {
id: 'ffz--twitter-widget-script',
async: true,
charset: 'utf-8',
src: 'https://platform.twitter.com/widgets.js',
onLoad: () => el.parentElement.removeChild(el)
}));
}
}
</script>

View file

@ -0,0 +1,180 @@
<template lang="html">
<div class="ffz-main-menu elevation-3 c-background-alt border flex flex--nowrap flex-column" :class="{ maximized }">
<header class="c-background pd-1 pd-l-2 full-width align-items-center flex flex-nowrap" @dblclick="resize">
<h3 class="ffz-i-zreknarf ffz-i-pd-1">FrankerFaceZ</h3>
<div class="flex-grow-1 pd-x-2">
<!--div class="tw-search-input">
<label for="ffz-main-menu.search" class="hide-accessible">{{ t('main-menu.search', 'Search Settings') }}</label>
<div class="relative">
<div class="tw-input__icon-group">
<div class="tw-input__icon">
<figure class="ffz-i-search" />
</div>
</div>
<input type="search" class="tw-input tw-input--icon-left" :placeholder="t('main-menu.search', 'Search Settings')" autocapitalize="off" autocorrect="off" autocomplete="off" id="ffz-main-menu.search">
</div>
</div-->
</div>
<button class="tw-button-icon mg-x-05" @click="resize">
<span class="tw-button-icon__icon">
<figure :class="{'ffz-i-window-maximize': !maximized, 'ffz-i-window-restore': maximized}" />
</span>
</button>
<button class="tw-button-icon mg-x-05" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-window-close" />
</span>
</button>
</header>
<section class="border-t full-height full-width flex flex--nowrap">
<nav class="ffz-vertical-nav c-background-alt-2 border-r full-height flex flex-column flex-shrink-0 flex-nowrap">
<header class="border-b pd-1">
<profile-selector
:context="context"
@navigate="navigate"
/>
</header>
<div class="full-width full-height overflow-hidden flex flex-nowrap relative">
<div class="ffz-vertical-nav__items full-width flex-grow-1 scrollable-area" data-simplebar>
<div class="simplebar-scroll-content">
<div class="simplebar-content">
<menu-tree
:currentItem="currentItem"
:modal="nav"
@change-item="changeItem"
@navigate="navigate"
/>
</div>
</div>
</div>
</div>
<footer class="c-text-alt border-t pd-1">
<div>
{{ t('main-menu.version', 'Version %{version}', {version: version.toString()}) }}
</div>
<div class="c-text-alt-2">
{{version.build}}
</div>
</footer>
</nav>
<main class="flex-grow-1 scrollable-area" data-simplebar>
<div class="simplebar-scroll-content">
<div class="simplebar-content">
<menu-page
ref="page"
:context="context"
:item="currentItem"
@change-item="changeItem"
@navigate="navigate"
v-if="currentItem"
/>
</div>
</div>
</main>
</section>
</div>
</template>
<script>
import displace from 'displacejs';
export default {
data() {
return this.$vnode.data;
},
created() {
this.context.context._add_user();
},
destroyed() {
this.context.context._remove_user();
},
methods: {
changeProfile() {
const new_id = this.$refs.profiles.value,
new_profile = this.context.profiles[new_id];
if ( new_profile )
this.context.currentProfile = new_profile;
},
changeItem(item) {
if ( this.$refs.page && this.$refs.page.onBeforeChange ) {
if ( this.$refs.page.onBeforeChange(this.currentItem, item) === false )
return;
}
this.currentItem = item;
let current = item;
while(current = current.parent)
current.expanded = true;
},
updateDrag() {
if ( this.maximized )
this.destroyDrag();
else
this.createDrag();
},
destroyDrag() {
if ( this.displace ) {
this.displace.destroy();
this.displace = null;
}
},
createDrag() {
this.$nextTick(() => {
if ( ! this.maximized )
this.displace = displace(this.$el, {
handle: this.$el.querySelector('header'),
highlightInputs: true,
constrain: true
});
})
},
handleResize() {
if ( this.displace )
this.displace.reinit();
},
navigate(key) {
let item = this.nav_keys[key];
while(item && item.page)
item = item.parent;
if ( ! item )
return;
this.changeItem(item);
}
},
watch: {
maximized() {
this.updateDrag();
}
},
mounted() {
this.updateDrag();
this._on_resize = this.handleResize.bind(this);
window.addEventListener('resize', this._on_resize);
},
beforeDestroy() {
this.destroyDrag();
if ( this._on_resize ) {
window.removeEventListener('resize', this._on_resize);
this._on_resize = null;
}
}
}
</script>

View file

@ -0,0 +1,34 @@
<template lang="html">
<div v-bind:class="classes" v-if="item.contents">
<header v-if="! item.no_header">
{{ t(item.i18n_key, item.title, item) }}
</header>
<section
v-if="item.description"
v-html="t(item.desc_i18n_key, item.description, item)"
class="pd-b-1"
/>
<component
v-for="i in item.contents"
v-bind:is="i.component"
:context="context"
:item="i"
:key="i.full_key"
/>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
computed: {
classes() {
return [
'ffz--menu-container',
this.item.full_box ? 'border' : 'border-t'
]
}
}
}
</script>

View file

@ -0,0 +1,89 @@
<template lang="html">
<div class="ffz--menu-page">
<header class="mg-b-1">
<template v-for="i in breadcrumbs">
<a v-if="i !== item" href="#" @click="$emit('change-item', i, false)">{{ t(i.i18n_key, i.title, i) }}</a>
<strong v-if="i === item">{{ t(i.i18n_key, i.title, i) }}</strong>
<template v-if="i !== item">&raquo; </template>
</template>
</header>
<section v-if="! context.currentProfile.live && item.profile_warning !== false" class="border-t pd-t-1 pd-b-2">
<div class="c-background-accent c-text-overlay pd-1">
<h3 class="ffz-i-attention">
{{ t('setting.profiles.inactive', "This profile isn't 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."
) }}
</div>
</section>
<section
v-if="item.description"
class="border-t pd-y-1"
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
/>
</section>
<template v-if="! item.contents">
<ul class="border-t pd-y-1">
<li class="pd-x-1" v-for="i in item.items">
<a href="#" @click="$emit('change-item', i, false)">
{{ t(i.i18n_key, i.title, i) }}
</a>
</li>
</ul>
</template>
<component
v-for="i in item.contents"
v-bind:is="i.component"
ref="children"
:context="context"
:item="i"
:key="i.full_key"
@change-item="changeItem"
@navigate="navigate"
/>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
computed: {
breadcrumbs() {
const out = [];
let current = this.item;
while(current) {
out.unshift(current);
current = current.parent;
}
return out;
}
},
methods: {
changeItem(item) {
this.$emit('change-item', item);
},
navigate(...args) {
this.$emit('navigate', ...args);
},
onBeforeChange(current, new_item) {
for(const child of this.$refs.children)
if ( child && child.onBeforeChange ) {
const res = child.onBeforeChange(current, new_item);
if ( res !== undefined )
return res;
}
}
}
}
</script>

View file

@ -0,0 +1,165 @@
<template lang="html">
<ul
v-if="modal"
class="ffz--menu-tree"
:role="[root ? 'group' : 'tree']"
:tabindex="tabIndex"
@keyup.up="prevItem"
@keyup.down="nextItem"
@keyup.left="prevLevel"
@keyup.right="nextLevel"
@keyup.*="expandAll"
>
<li
v-for="item in modal"
:key="item.full_key"
:class="[currentItem === item ? 'active' : '']"
role="presentation"
>
<div
class="flex__item flex flex--nowrap align-items-center pd-y-05 pd-r-05"
role="treeitem"
:aria-expanded="item.expanded"
:aria-selected="currentItem === item"
@click="clickItem(item)"
>
<span
role="presentation"
class="arrow"
:class="[
item.items ? '' : 'ffz--invisible',
item.expanded ? 'ffz-i-down-dir' : 'ffz-i-right-dir'
]"
/>
<span class="flex-grow-1">
{{ t(item.i18n_key, item.title, item) }}
</span>
<span v-if="item.pill" class="pill">
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill, item) : item.pill }}
</span>
</div>
<menu-tree
:root="item"
:currentItem="currentItem"
:modal="item.items"
v-if="item.items && item.expanded"
@change-item="i => $emit('change-item', i)"
/>
</li>
</ul>
</template>
<script>
function findLastVisible(node) {
if ( node.expanded && node.items )
return findLastVisible(node.items[node.items.length - 1]);
return node;
}
function findNextVisible(node, modal) {
const items = node.parent ? node.parent.items : modal,
idx = items.indexOf(node);
if ( items[idx + 1] )
return items[idx+1];
if ( node.parent )
return findNextVisible(node.parent, modal);
return null;
}
function recursiveExpand(node) {
node.expanded = true;
if ( node.items )
for(const item of node.items)
recursiveExpand(item);
}
export default {
props: ['root', 'modal', 'currentItem'],
computed: {
tabIndex() {
return this.root ? undefined : 0;
}
},
methods: {
clickItem(item) {
if ( ! item.expanded )
item.expanded = true;
else if ( this.currentItem === item )
item.expanded = false;
this.$emit('change-item', item);
},
expandAll() {
for(const item of this.modal)
recursiveExpand(item);
},
prevItem() {
if ( this.root ) return;
const i = this.currentItem,
items = i.parent ? i.parent.items : this.modal,
idx = items.indexOf(i);
if ( idx > 0 )
this.$emit('change-item', findLastVisible(items[idx-1]));
else if ( i.parent )
this.$emit('change-item', i.parent);
},
nextItem(e) {
if ( this.root ) return;
const i = this.currentItem;
let target;
if ( i.expanded && i.items )
target = i.items[0];
else
target = findNextVisible(i, this.modal);
if ( target )
this.$emit('change-item', target);
},
prevLevel() {
if ( this.root ) return;
const i = this.currentItem;
if ( i.expanded && i.items )
i.expanded = false;
else if ( i.parent )
this.$emit('change-item', i.parent);
},
nextLevel() {
if ( this.root ) return;
const i = this.currentItem;
if ( i.expanded && i.items )
this.$emit('change-item', i.items[0]);
else
i.expanded = true;
if ( event.ctrlKey )
recursiveExpand(this.currentItem);
}
}
}
</script>

View file

@ -0,0 +1,205 @@
<template lang="html">
<div class="ffz--profile-editor">
<div class="flex align-items-center border-t pd-1">
<div class="flex-grow-1"></div>
<button
class="tw-button tw-button--text"
@click="save"
>
<span class="tw-button__text ffz-i-floppy">
{{ t('settings.profiles.save', 'Save') }}
</span>
</button>
<button
class="mg-l-1 tw-button tw-button--text"
:disabled="item.profile && context.profiles.length < 2"
@click="del"
>
<span class="tw-button__text ffz-i-trash">
{{ t('setting.profiles.delete', 'Delete') }}
</span>
</button>
<!--button class="mg-l-1 tw-button tw-button--text">
<span class="tw-button__text ffz-i-download">
{{ t('setting.profiles.export', 'Export') }}
</span>
</button-->
</div>
<div class="ffz--menu-container border-t">
<header>
{{ t('settings.data_management.profiles.edit.general', 'General') }}
</header>
<div class="ffz--widget flex flex--nowrap">
<label for="ffz:editor:name">
{{ t('settings.data_management.profiles.edit.name', 'Name') }}
</label>
<input
class="tw-input"
ref="name"
id="ffz:editor:name"
v-model="name"
/>
</div>
<div class="ffz--widget flex flex--nowrap">
<label for="ffz:editor:description">
{{ t('settings.data_management.profiles.edit.desc', 'Description') }}
</label>
<textarea
class="tw-input"
ref="desc"
id="ffz:editor:description"
v-model="desc"
/>
</div>
</div>
<div class="ffz--menu-container border-t">
<header>
{{ t('settings.data_management.profiles.edit.rules', 'Rules') }}
</header>
<section class="pd-b-1">
{{ t(
'settings.data_management.profiles.edit.rules.description',
'Rules allows you to define a series of conditions under which this profile will be active.'
) }}
</section>
<filter-editor
:filters="filters"
:rules="rules"
:context="test_context"
@change="unsaved = true"
/>
</div>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
data() {
return {
old_name: null,
old_desc: null,
name: null,
desc: null,
unsaved: false,
filters: null,
rules: null,
test_context: null
}
},
created() {
this.context.context.on('context_changed', this.updateContext, this);
this.updateContext();
this.revert();
},
beforeDestroy() {
this.context.context.off('context_changed', this.updateContext, this);
},
watch: {
name() {
if ( this.name !== this.old_name )
this.unsaved = true;
},
desc() {
if ( this.desc !== this.old_desc )
this.unsaved = true;
}
},
methods: {
revert() {
const profile = this.item.profile;
this.old_name = this.name = profile ?
profile.i18n_key ?
this.t(profile.i18n_key, profile.title, profile) :
profile.title :
'Unnamed Profile',
this.old_desc = this.desc = profile ?
profile.desc_i18n_key ?
this.t(profile.desc_i18n_key, profile.description, profile) :
profile.description :
'';
this.rules = profile ? profile.context : {};
this.unsaved = ! profile;
},
del() {
if ( this.item.profile || this.unsaved ) {
if ( ! confirm(this.t(
'settings.profiles.warn-delete',
'Are you sure you wish to delete this profile? It cannot be undone.'
)) )
return
if ( this.item.profile )
this.context.deleteProfile(this.item.profile);
}
this.unsaved = false;
this.$emit('navigate', 'data_management.profiles');
},
save() {
if ( ! this.item.profile ) {
this.item.profile = this.context.createProfile({
name: this.name,
description: this.desc
});
} else if ( this.unsaved ) {
const changes = {
name: this.name,
description: this.desc
};
// Disable i18n if required.
if ( this.name !== this.old_name )
changes.i18n_key = undefined;
if ( this.desc !== this.old_desc )
changes.desc_i18n_key = undefined;
this.item.profile.update(changes);
}
this.unsaved = false;
this.$emit('navigate', 'data_management.profiles');
},
updateContext() {
this.test_context = this.context.context.context;
},
onBeforeChange() {
if ( this.unsaved )
return confirm(
this.t(
'settings.warn-unsaved',
'You have unsaved changes. Are you sure you want to leave the editor?'
));
}
}
}
</script>

View file

@ -0,0 +1,129 @@
<template lang="html">
<div class="ffz--widget ffz--profile-manager border-t pd-y-1">
<div class="c-background-accent c-text-overlay pd-1 mg-b-1">
<h3 class="ffz-i-attention">
This feature is not yet finished.
</h3>
Creating and editing profiles is disabled until the rule editor is finished.
</div>
<div class="flex align-items-center pd-b-05">
<div class="flex-grow-1">
{{ t('setting.profiles.drag', 'Drag profiles to change their priority.') }}
</div>
<button class="mg-l-1 tw-button tw-button--text" disabled @notclick="edit()">
<span class="tw-button__text ffz-i-plus">
{{ t('setting.profiles.new', 'New Profile') }}
</span>
</button>
<!--button class="mg-l-1 tw-button tw-button--text">
<span class="tw-button__text ffz-i-upload">
{{ t('setting.profiles.import', 'Import…') }}
</span>
</button-->
</div>
<div ref="list" class="ffz--profile-list">
<section
v-for="p in context.profiles"
:key="p.id"
:data-profile="p.id"
>
<div
class="ffz--profile elevation-1 c-background border pd-y-05 pd-r-1 mg-y-05 flex flex--nowrap"
:class="{live: p.live}"
tabindex="0"
>
<div class="flex flex-shrink-0 align-items-center handle pd-x-05 pd-t-1 pd-b-05">
<span class="ffz-i-ellipsis-vert" />
</div>
<div class="flex-grow-1">
<h4>{{ t(p.i18n_key, p.title, p) }}</h4>
<div v-if="p.description" class="description">
{{ t(p.desc_i18n_key, p.description, p) }}
</div>
</div>
<div class="flex flex-shrink-0 align-items-center">
<button class="tw-button tw-button--text" disabled @notclick="edit(p)">
<span class="tw-button__text ffz-i-cog">
{{ t('setting.profiles.configure', 'Configure') }}
</span>
</button>
</div>
<div class="flex flex-shrink-0 align-items-center border-l mg-l-1 pd-l-1">
<div v-if="p.live" class="ffz--profile__icon ffz-i-ok tw-tooltip-wrapper">
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.profiles.active', 'This profile is active.') }}
</div>
</div>
<div v-if="! p.live" class="ffz--profile__icon ffz-i-cancel 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>
</section>
</div>
</div>
</template>
<script>
import Sortable from 'sortablejs';
export default {
props: ['item', 'context'],
methods: {
edit(profile) {
const item = {
full_key: 'data_management.profiles.edit_profile',
key: 'edit_profile',
profile_warning: false,
title: `Edit Profile`,
i18n_key: 'setting.data_management.profiles.edit_profile',
parent: this.item.parent,
contents: [{
page: true,
profile,
component: 'profile-editor'
}]
};
item.contents[0].parent = item;
this.$emit('change-item', item);
}
},
mounted() {
this._sortable = Sortable.create(this.$refs.list, {
draggable: 'section',
filter: 'button',
onUpdate: (event) => {
const id = event.item.dataset.profile,
profile = this.context.profile_keys[id];
if ( profile )
profile.move(event.newIndex);
}
});
},
beforeDestroy() {
if ( this._sortable )
this._sortable.destroy();
this._sortable = null;
}
}
</script>

View file

@ -0,0 +1,218 @@
<template lang="html">
<div class="ffz--widget ffz--profile-selector">
<div
tabindex="0"
class="tw-select"
:class="{active: opened}"
ref="button"
@keyup.up.stop.prevent="focusShow"
@keyup.left.stop.prevent="focusShow"
@keyup.down.stop.prevent="focusShow"
@keyup.right.stop.prevent="focusShow"
@keyup.enter="focusShow"
@keyup.space="focusShow"
@click="togglePopup"
>
{{ t(context.currentProfile.i18n_key, context.currentProfile.title, context.currentProfile) }}
</div>
<div v-if="opened" v-on-clickaway="hide" class="tw-balloon block tw-balloon--lg tw-balloon--down tw-balloon--left">
<div
class="ffz--profile-list elevation-2 c-background-alt"
@keyup.escape="focusHide"
@focusin="focus"
@focusout="blur"
>
<div class="scrollable-area border-b" data-simplebar>
<div class="simplebar-scroll-content">
<div class="simplebar-content" ref="popup">
<div
v-for="(p, idx) in context.profiles"
tabindex="0"
class="ffz--profile-row relative border-b pd-y-05 pd-r-3 pd-l-1"
:class="{
live: p.live,
current: p === context.currentProfile
}"
@keydown.up.stop.prevent=""
@keydown.down.stop.prevent=""
@keydown.page-up.stop.prevent=""
@keydown.page-down.stop.prevent=""
@keyup.up.stop="prevItem"
@keyup.down.stop="nextItem"
@keyup.home="firstItem"
@keyup.end="lastItem"
@keyup.page-up.stop="prevPage"
@keyup.page-down.stop="nextPage"
@keyup.enter="changeProfile(p)"
@click="changeProfile(p)"
>
<div
v-if="p.live"
class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-ok absolute"
>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.profiles.active', 'This profile is active.') }}
</div>
</div>
<h4>{{ t(p.i18n_key, p.title, p) }}</h4>
<div v-if="p.description" class="description">
{{ t(p.desc_i18n_key, p.description, p) }}
</div>
</div>
</div>
</div>
</div>
<div class="pd-y-05 pd-x-05 align-right">
<button class="tw-button tw-button--text" @click="openConfigure">
<span class="tw-button__text ffz-i-cog">
{{ t('setting.profiles.configure', 'Configure') }}
</span>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mixin as clickaway} from 'vue-clickaway';
const indexOf = Array.prototype.indexOf;
export default {
mixins: [clickaway],
props: ['context'],
data() {
return {
opened: false
}
},
methods: {
openConfigure() {
this.hide();
this.$emit('navigate', 'data_management.profiles');
},
focus() {
this._focused = true;
},
blur() {
this._focused = false;
if ( ! this._blur_timer )
this._blur_timer = setTimeout(() => {
this._blur_timer = null;
if ( ! this._focused && document.hasFocus() )
this.hide();
}, 10);
},
hide() {
this.opened = false;
},
show() {
if ( ! this.opened )
this.opened = true;
},
togglePopup() {
if ( this.opened )
this.hide();
else
this.show();
},
focusHide() {
this.hide();
this.$refs.button.focus();
},
focusShow() {
this.show();
this.$nextTick(() => this.$refs.popup.querySelector('.current').focus());
},
prevItem(e) {
const el = e.target.previousSibling;
if ( el ) {
this.scroll(el);
el.focus();
}
},
nextItem(e) {
const el = e.target.nextSibling;
if ( el ) {
this.scroll(el);
el.focus();
}
},
firstItem() {
const el = this.$refs.popup.firstElementChild;
if ( el ) {
this.scroll(el);
el.focus();
}
},
prevPage(e) {
this.select(indexOf.call(this.$refs.popup.children, e.target) - 5);
},
nextPage(e) {
this.select(indexOf.call(this.$refs.popup.children, e.target) + 5);
},
select(idx) {
const kids = this.$refs.popup.children,
el = kids[idx <= 0 ? 0 : Math.min(idx, kids.length - 1)];
if ( el ) {
this.scroll(el);
el.focus();
}
},
lastItem() {
const el = this.$refs.popup.lastElementChild;
if ( el ) {
this.scroll(el);
el.focus();
}
},
scroll(el) {
const scroller = this.$refs.popup.parentElement,
top = el.offsetTop,
bottom = el.offsetHeight + top,
// We need to use the margin-bottom because of the scrollbar library.
// In fact, the scrollbar library is why any of this function exists.
scroll_top = scroller.scrollTop,
scroll_bottom = scroller.offsetHeight + parseInt(scroller.style.marginBottom || 0, 10) + scroll_top;
if ( top < scroll_top )
scroller.scrollBy(0, top - scroll_top);
else if ( bottom > scroll_bottom )
scroller.scrollBy(0, bottom - scroll_bottom);
},
changeProfile(profile) {
this.context.currentProfile = profile;
this.focusHide();
}
}
}
</script>

View file

@ -0,0 +1,58 @@
<template lang="html">
<div class="ffz--widget ffz--checkbox" :class="{inherits: isInherited, default: isDefault}">
<div class="flex align-items-center">
<input
type="checkbox"
class="tw-checkbox__input"
ref="control"
:id="item.full_key"
:checked="value"
@change="onChange"
>
<label class="tw-checkbox__label" :for="item.full_key">
{{ t(item.i18n_key, item.title, item) }}
</label>
<button
v-if="source && source !== profile"
class="mg-l-05 tw-button tw-button--text"
@click="context.currentProfile = source"
>
<span class="tw-button__text ffz-i-right-dir">
{{ sourceDisplay }}
</span>
</button>
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
<span class="tw-button__text ffz-i-cancel"></span>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</div>
</button>
</div>
<section
v-if="item.description"
class="c-text-alt-2"
style="padding-left:2.2rem"
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
/>
</div>
</template>
<script>
import SettingMixin from '../setting-mixin';
export default {
mixins: [SettingMixin],
props: ['item', 'context'],
methods: {
onChange() {
this.set(this.$refs.control.checked);
}
}
}
</script>

View file

@ -0,0 +1,44 @@
<template lang="html">
<div class="ffz--widget ffz--hotkey-input">
<label
:for="item.full_key"
v-html="t(item.i18n_key, item.title, item)"
/>
<div class="relative">
<div class="tw-input__icon-group tw-input__icon-group--right">
<div class="tw-input__icon">
<figure class="ffz-i-keyboard" />
</div>
</div>
<div
type="text"
class="mg-05 tw-input tw-input--icon-right"
ref="display"
:id="item.full_key"
tabindex="0"
@keyup="onKey"
>
&nbsp;
</div>
</div>
<section
v-if="item.description"
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
/>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
methods: {
onKey(e) {
const name = `${e.ctrlKey ? 'Ctrl-' : ''}${e.shiftKey ? 'Shift-' : ''}${e.altKey ? 'Alt-' : ''}${e.code}`;
this.$refs.display.innerText = name;
}
}
}
</script>

View file

@ -0,0 +1,27 @@
<template lang="html">
<div class="atw-input">
<header>
{{ t(item.i18n_key, item.title, item) }}
</header>
<section
v-if="item.description"
class="c-text-alt-2"
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
/>
<div v-for="(i, idx) in data" class="mg-l-1">
<input type="radio" :name="item.full_key" :id="item.full_key + idx" :value="i.value" class="tw-radio__input">
<label :for="item.full_key + idx" class="pd-y-05 tw-radio__label">{{ t(i.i18n_key, i.title, i) }}</label>
</div>
</div>
</template>
<script>
import SettingMixin from '../setting-mixin';
export default {
mixins: [SettingMixin],
props: ['item', 'context']
}
</script>

View file

@ -0,0 +1,64 @@
<template lang="html">
<div class="ffz--widget ffz--select-box" :class="{inherits: isInherited, default: isDefault}">
<div class="flex align-items-center">
<label :for="item.full_key">
{{ t(item.i18n_key, item.title, item) }}
</label>
<select
class="mg-05 tw-select display-inline width-auto"
ref="control"
:id="item.full_key"
@change="onChange"
>
<option v-for="i in data" :selected="i.value === value">
{{ i.i18n_key ? t(i.i18n_key, i.title, i) : i.title }}
</option>
</select>
<button
v-if="source && source !== profile"
class="mg-l-05 tw-button tw-button--text"
@click="context.currentProfile = source"
>
<span class="tw-button__text ffz-i-right-dir">
{{ sourceDisplay }}
</span>
</button>
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
<span class="tw-button__text ffz-i-cancel"></span>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</div>
</button>
</div>
<section
v-if="item.description"
class="c-text-alt-2"
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
/>
</div>
</template>
<script>
import SettingMixin from '../setting-mixin';
export default {
mixins: [SettingMixin],
props: ['item', 'context'],
methods: {
onChange() {
const idx = this.$refs.control.selectedIndex,
raw_value = this.data[idx];
if ( raw_value )
this.set(raw_value.value);
}
}
}
</script>

View file

@ -0,0 +1,59 @@
<template lang="html">
<div class="ffz--widget ffz--text-box" :class="{inherits: isInherited, default: isDefault}">
<div class="flex align-items-center">
<label :for="item.full_key">
{{ t(item.i18n_key, item.title, item) }}
</label>
<input
class="mg-05 tw-input display-inline width-auto"
ref="control"
:id="item.full_key"
@change="onChange"
:value="value"
/>
<button
v-if="source && source !== profile"
class="mg-l-05 tw-button tw-button--text"
@click="context.currentProfile = source"
>
<span class="tw-button__text ffz-i-right-dir">
{{ sourceDisplay }}
</span>
</button>
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
<span class="tw-button__text ffz-i-cancel"></span>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</div>
</button>
</div>
<section
v-if="item.description"
class="c-text-alt-2"
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
/>
</div>
</template>
<script>
import SettingMixin from '../setting-mixin';
export default {
mixins: [SettingMixin],
props: ['item', 'context'],
methods: {
onChange() {
const value = this.$refs.control.value;
if ( value != null )
this.set(value);
}
}
}
</script>