mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-31 06:58:30 +00:00
The In-Line Actions Update
* Add extensible actions system. * Add extensive UI for configuring the actions system. * Add setting to disable channel hosting. * Fix the stupid Rooms thing popping up every time you open a channel. * Fix how we grab chat types from React. * Refactor how we handle incoming chat messages. * Add a hook for outgoing chat messages. * Fix emoji appearing squished with baseline emote alignment. * Display arrows on balloons. * Fix an issue generating emoji URLs. * Do not use the default values for settings with merge strategies if profiles have those settings, just empty. * Display a message in the chat settings menu if we tried opening FFZ's settings and failed. * Wait a bit for webpack's loader if it's not immediately there for some reason. * Probably other stuff. * Not mod cards. Yet.
This commit is contained in:
parent
e9214bb46a
commit
fdde05030f
67 changed files with 7689 additions and 226 deletions
382
src/modules/main_menu/components/action-editor.vue
Normal file
382
src/modules/main_menu/components/action-editor.vue
Normal file
|
@ -0,0 +1,382 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--action tw-elevation-1 tw-c-background tw-border tw-pd-y-05 tw-pd-r-1 tw-mg-y-05 tw-flex tw-flex-nowrap">
|
||||
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start handle tw-pd-x-05 tw-pd-t-1 tw-pd-b-05">
|
||||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div class="tw-flex-grow-1">
|
||||
<template v-if="! editing">
|
||||
<h4>{{ title }}</h4>
|
||||
<div class="description">{{ description }}</div>
|
||||
<div v-if="canEdit" class="visibility tw-c-text-alt">
|
||||
{{ t('setting.actions.visible', 'visible: %{list}', {list: visibility}) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<section>
|
||||
<h5>{{ t('setting.actions.appearance', 'Appearance') }}</h5>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="tooltip">
|
||||
{{ t('setting.actions.tooltip', 'Custom Tooltip') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
v-model="edit_data.appearance.tooltip"
|
||||
class="tw-mg-y-05 tw-input tw-display-inline"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="renderer_type">
|
||||
{{ t('setting.actions.type', 'Type') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="renderer_type"
|
||||
ref="renderer_type"
|
||||
v-model="edit_data.appearance.type"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option
|
||||
v-for="(r, key) in data.renderers"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ r.title_i18n ? t(r.title_i18n, r.title, r) : r.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="renderer && renderer.colored" class="tw-flex tw-align-items-start">
|
||||
<label for="color" class="tw-mg-y-05">
|
||||
{{ t('setting.actions.color', 'Color') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<color-picker v-model="edit_data.appearance.color" :nullable="true" />
|
||||
|
||||
<div class="tw-c-text-alt-2 tw-mg-b-1">
|
||||
{{ t('setting.actions.color.warn', 'This value will be automatically adjusted for visibility based on your chat color settings.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
v-if="renderer"
|
||||
:is="renderer.editor"
|
||||
v-model="edit_data.appearance"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
||||
<h5>{{ t('setting.actions.visibility', 'Visibility') }}</h5>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="vis_mod">
|
||||
{{ t('setting.actions.edit-visible.mod', 'Moderator') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="vis_mod"
|
||||
v-model="edit_data.display.mod"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option :value="undefined" selected>{{ t('setting.unset', 'Unset') }}</option>
|
||||
<option :value="true">{{ t('setting.true', 'True') }}</option>
|
||||
<option :value="false">{{ t('setting.false', 'False') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="vis_mod_icons">
|
||||
{{ t('setting.actions.edit-visible.mod-icons', 'Mod Icons') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="vis_mod_icons"
|
||||
v-model="edit_data.display.mod_icons"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option :value="undefined" selected>{{ t('setting.unset', 'Unset') }}</option>
|
||||
<option :value="true">{{ t('setting.visible', 'Visible') }}</option>
|
||||
<option :value="false">{{ t('setting.hidden', 'Hidden') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="vis_deleted">
|
||||
{{ t('setting.actions.edit-visible.deleted', 'Message Deleted') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="vis_deleted"
|
||||
v-model="edit_data.display.deleted"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option :value="undefined" selected>{{ t('setting.unset', 'Unset') }}</option>
|
||||
<option :value="true">{{ t('setting.true', 'True') }}</option>
|
||||
<option :value="false">{{ t('setting.false', 'False') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
||||
<h5>{{ t('setting.actions.action', 'Action') }}</h5>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<label for="action_type">
|
||||
{{ t('setting.actions.type', 'Type') }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="action_type"
|
||||
v-model="edit_data.action"
|
||||
class="tw-mg-y-05 tw-select tw-display-inline"
|
||||
>
|
||||
<option
|
||||
v-for="(a, key) in data.actions"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ a.title_i18n ? t(a.title_i18n, a.title, a) : a.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<component
|
||||
v-if="action_def && action_def.editor"
|
||||
:is="action_def.editor"
|
||||
:value="edit_data.options"
|
||||
:defaults="action_def.defaults"
|
||||
:vars="vars"
|
||||
@input="onChangeAction($event)"
|
||||
/>
|
||||
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="canPreview" class="tw-mg-l-1 tw-border-l tw-pd-l-1 tw-pd-y-05 tw-flex tw-flex-shrink-0 tw-align-items-start">
|
||||
<action-preview
|
||||
:act="display"
|
||||
:color="display.appearance && data.color(display.appearance.color)"
|
||||
:renderers="data.renderers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-l-1 tw-border-l tw-pd-l-1 tw-flex tw-flex-wrap tw-flex-column tw-justify-content-start tw-align-items-start">
|
||||
<template v-if="editing">
|
||||
<button class="tw-button tw-button--text" @click="save">
|
||||
<span class="tw-button__text ffz-i-floppy">
|
||||
{{ t('setting.save', 'Save') }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text" @click="cancel">
|
||||
<span class="tw-button__text ffz-i-cancel">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="tw-button tw-button--text"
|
||||
@click="edit"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.edit', 'Edit') }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text" @click="$emit('remove', action)">
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['action', 'data', 'inline'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
edit_data: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
display() {
|
||||
if ( this.editing )
|
||||
return this.edit_data;
|
||||
|
||||
return this.action.v;
|
||||
},
|
||||
|
||||
vars() {
|
||||
const out = ['user.login', 'user.displayName', 'user.id', 'user.type'];
|
||||
|
||||
out.push('room.login')
|
||||
out.push('room.id');
|
||||
|
||||
if ( this.inline )
|
||||
out.push('message_id');
|
||||
|
||||
return out.map(x => `{{${x}}}`).join(', ');
|
||||
},
|
||||
|
||||
renderer() {
|
||||
return this.canPreview && this.data.renderers[this.display.appearance.type];
|
||||
},
|
||||
|
||||
action_def() {
|
||||
return this.display && this.data.actions[this.display.action];
|
||||
},
|
||||
|
||||
canEdit() {
|
||||
return this.action.v != null;
|
||||
},
|
||||
|
||||
canPreview() {
|
||||
return this.display && this.display.appearance;
|
||||
},
|
||||
|
||||
title() {
|
||||
if ( this.action.t === 'inherit' )
|
||||
return this.t('setting.inheritance', 'Inheritance Point');
|
||||
|
||||
else if ( ! this.display )
|
||||
return this.t('setting.unknown', 'Unknown Value');
|
||||
|
||||
const def = this.data.actions[this.display.action];
|
||||
if ( ! def )
|
||||
return this.t('setting.actions.unknown', 'Unknown Action Type: %{action}', this.display);
|
||||
|
||||
if ( def.title ) {
|
||||
const data = this.getData(),
|
||||
out = maybe_call(def.title, this, data, def),
|
||||
i18n = def.title_i18n || `chat.actions.${this.display.action}`;
|
||||
|
||||
if ( out )
|
||||
return this.t(i18n, out, data);
|
||||
}
|
||||
|
||||
return this.t('setting.actions.untitled', 'Action: %{action}', this.display);
|
||||
|
||||
},
|
||||
|
||||
description() {
|
||||
if ( this.action.t === 'inherit' )
|
||||
return this.t('setting.inheritance.desc', 'Inherit values from lower priority profiles at this position.');
|
||||
|
||||
const def = this.display && this.data.actions[this.display.action];
|
||||
if ( ! def || ! def.description )
|
||||
return;
|
||||
|
||||
const data = this.getData(),
|
||||
out = maybe_call(def.description, this, data, def),
|
||||
i18n = def.description_i18n || `chat.actions.${this.display.action}.desc`;
|
||||
|
||||
if ( out )
|
||||
return this.t(i18n, out, data);
|
||||
},
|
||||
|
||||
visibility() {
|
||||
if ( ! this.display || ! this.display.appearance )
|
||||
return;
|
||||
|
||||
const disp = this.display.display, out = [];
|
||||
if ( ! disp )
|
||||
return this.t('setting.actions.visible.always', 'always');
|
||||
|
||||
if ( disp.disable )
|
||||
return this.t('setting.actions.visible.never', 'never');
|
||||
|
||||
if ( disp.mod === true )
|
||||
out.push(this.t('setting.actions.visible.mod', 'when moderator'));
|
||||
|
||||
else if ( disp.mod === false )
|
||||
out.push(this.t('setting.actions.visible.unmod', 'when not moderator'));
|
||||
|
||||
if ( disp.mod_icons === true )
|
||||
out.push(this.t('setting.actions.visible.mod_icons', 'when mod icons are shown'));
|
||||
|
||||
else if ( disp.mod_icons === false )
|
||||
out.push(this.t('setting.actions.visible.mod_icons_off', 'when mod icons are hidden'));
|
||||
|
||||
if ( disp.staff === true )
|
||||
out.push(this.t('setting.actions.visible.staff', 'when staff'));
|
||||
|
||||
else if ( disp.staff === false )
|
||||
out.push(this.t('setting.actions.visible.unstaff', 'when not staff'));
|
||||
|
||||
if ( disp.deleted === true )
|
||||
out.push(this.t('setting.actions.visible.deleted', 'if message deleted'));
|
||||
|
||||
else if ( disp.deleted === false )
|
||||
out.push(this.t('setting.actions.visible.undeleted', 'if message not deleted'));
|
||||
|
||||
return out.join(', ');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChangeAction(val) {
|
||||
for(const key in val)
|
||||
if ( has(val, key) ) {
|
||||
const v = val[key];
|
||||
if ( typeof v === 'string' && ! v.length )
|
||||
delete val[key];
|
||||
}
|
||||
|
||||
this.edit_data.options = val;
|
||||
},
|
||||
|
||||
edit() {
|
||||
if ( ! this.canEdit )
|
||||
return;
|
||||
|
||||
this.editing = true;
|
||||
this.edit_data = deep_copy(this.action.v);
|
||||
|
||||
if ( ! this.edit_data.options )
|
||||
this.edit_data.options = {};
|
||||
|
||||
if ( ! this.edit_data.display )
|
||||
this.edit_data.display = {};
|
||||
|
||||
if ( ! this.edit_data.appearance )
|
||||
this.edit_data.appearance = {};
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$emit('save', this.edit_data);
|
||||
this.cancel();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.editing = false;
|
||||
this.edit_data = null;
|
||||
},
|
||||
|
||||
getData() {
|
||||
const def = this.display && this.data.actions[this.display.action];
|
||||
if ( ! def )
|
||||
return;
|
||||
|
||||
return Object.assign({}, this.display, {
|
||||
options: Object.assign({}, def && def.defaults, this.display.options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
31
src/modules/main_menu/components/action-preview.vue
Normal file
31
src/modules/main_menu/components/action-preview.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template lang="html">
|
||||
<div
|
||||
:data-action="act.action"
|
||||
:data-options="act.options ? JSON.stringify(act.options) : null"
|
||||
:data-tip="act.appearance.tooltip"
|
||||
:class="{'ffz-tooltip': tooltip, 'tw-pd-05': pad, 'colored': color && color.length > 0}"
|
||||
data-tooltip-type="action"
|
||||
class="ffz-mod-icon mod-icon tw-c-text-alt-2 tw-font-size-4"
|
||||
>
|
||||
<component
|
||||
v-if="renderer && renderer.preview"
|
||||
:is="renderer.preview"
|
||||
:data="act.appearance"
|
||||
:color="color"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['act', 'color', 'tooltip', 'pad', 'renderers'],
|
||||
|
||||
computed: {
|
||||
renderer() {
|
||||
return this.renderers[this.act.appearance.type]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -5,7 +5,7 @@
|
|||
class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"
|
||||
>
|
||||
<span class="ffz-i-info" />
|
||||
{{ t('setting.badge-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-b-2 tw-align-right">
|
||||
|
|
428
src/modules/main_menu/components/chat-actions.vue
Normal file
428
src/modules/main_menu/components/chat-actions.vue
Normal file
|
@ -0,0 +1,428 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--chat-actions tw-border-t tw-pd-y-1">
|
||||
<div
|
||||
v-if="source && source !== profile"
|
||||
class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"
|
||||
>
|
||||
<span class="ffz-i-info" />
|
||||
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-b-1 tw-border-b tw-mg-b-1">
|
||||
<div class="tw-flex tw-align-items-center ffz--inline">
|
||||
{{ t('setting.actions.preview', 'Preview:') }}
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="as_mod"
|
||||
ref="as_mod"
|
||||
:checked="is_moderator"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="as_mod" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.mod', 'As Moderator') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="is_deleted"
|
||||
ref="is_deleted"
|
||||
:checked="is_deleted"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="is_deleted" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.deleted', 'Deleted Message') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="with_mod_icons"
|
||||
ref="with_mod_icons"
|
||||
:checked="with_mod_icons"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="with_mod_icons" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.mod_icons', 'With Mod Icons') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1">
|
||||
<input
|
||||
id="show_all"
|
||||
ref="show_all"
|
||||
:checked="show_all"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onPreview"
|
||||
>
|
||||
|
||||
<label for="show_all" class="tw-checkbox__label">
|
||||
{{ t('setting.actions.preview.all', 'Show All') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:data-user="JSON.stringify(sample_user)"
|
||||
:data-room="JSON.stringify(sample_room)"
|
||||
class="tw-flex tw-align-items-center tw-justify-content-center tw-pd-t-1"
|
||||
data-msg-id="1234-5678"
|
||||
>
|
||||
<div
|
||||
v-if="! display.length"
|
||||
class="tw-c-text-alt-2 tw-pd-05 tw-font-size-4"
|
||||
>
|
||||
{{ t('setting.actions.no-visible', 'no visible actions') }}
|
||||
</div>
|
||||
|
||||
<action-preview
|
||||
v-for="act in display"
|
||||
:key="act.id"
|
||||
:act="act.v"
|
||||
:color="color(act.v.appearance.color)"
|
||||
:renderers="data.renderers"
|
||||
tooltip="true"
|
||||
pad="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center tw-pd-b-05">
|
||||
<div class="tw-flex-grow-1">
|
||||
{{ t('setting.actions.drag', 'Drag actions to re-order them.') }}
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="closeAdd"
|
||||
class="tw-relative"
|
||||
>
|
||||
<button
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="toggleAdd"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('setting.actions.new', 'New...') }}
|
||||
</span>
|
||||
<span class="tw-button__icon tw-button__icon--right">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>
|
||||
</button>
|
||||
<balloon
|
||||
v-if="add_open"
|
||||
color="background-alt-2"
|
||||
dir="down-right"
|
||||
size="sm"
|
||||
>
|
||||
<simplebar classes="language-select-menu__balloon">
|
||||
<div class="tw-pd-y-1">
|
||||
<template v-for="(preset, idx) in presets">
|
||||
<div
|
||||
v-if="preset.divider"
|
||||
:key="idx"
|
||||
class="tw-mg-1 tw-border-b"
|
||||
/>
|
||||
<button
|
||||
v-else
|
||||
:key="idx"
|
||||
:disabled="preset.disabled"
|
||||
class="tw-interactable"
|
||||
@click="add(preset.value)"
|
||||
>
|
||||
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
|
||||
<div class="tw-flex-grow-1 tw-mg-r-1">
|
||||
{{ t(preset.title_i18n, preset.title, preset) }}
|
||||
</div>
|
||||
<action-preview v-if="preset.appearance" :act="preset" :renderers="data.renderers" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</simplebar>
|
||||
</balloon>
|
||||
</div>
|
||||
<button
|
||||
v-if="val.length"
|
||||
class="tw-mg-l-1 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="clear"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete-all', 'Delete All') }}
|
||||
</span>
|
||||
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.actions.delete-all', "Delete all of this profile's actions.") }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="! val.length"
|
||||
class="tw-mg-l-1 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="populate"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.actions.add-default', 'Add Defaults') }}
|
||||
</span>
|
||||
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.actions.add-default-tip', 'Add all of the default actions to this profile.') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="list" class="ffz--action-list">
|
||||
<div v-if="! val.length" class="tw-c-text-alt-2 tw-font-size-4 tw-align-center tw-c-text-alt-2 tw-pd-1">
|
||||
{{ t('setting.actions.no-actions', 'no actions are defined in this profile') }}
|
||||
</div>
|
||||
<section v-for="act in val" :key="act.id">
|
||||
<action-editor
|
||||
:action="act"
|
||||
:data="data"
|
||||
:inline="item.inline"
|
||||
@remove="remove(act)"
|
||||
@save="save(act, $event)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
import Sortable from 'sortablejs';
|
||||
import {deep_copy} from 'utilities/object';
|
||||
import {mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
|
||||
export default {
|
||||
mixins: [clickaway, SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
is_moderator: true,
|
||||
with_mod_icons: true,
|
||||
is_staff: false,
|
||||
is_deleted: false,
|
||||
show_all: false,
|
||||
|
||||
add_open: false,
|
||||
|
||||
sample_user: {
|
||||
displayName: 'SirStendec',
|
||||
login: 'sirstendec',
|
||||
id: 49399878
|
||||
},
|
||||
|
||||
sample_room: {
|
||||
displayName: 'FrankerFaceZ',
|
||||
login: 'frankerfacez',
|
||||
id: 46622312
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasInheritance() {
|
||||
for(const val of this.val)
|
||||
if ( val.t === 'inherit' )
|
||||
return true;
|
||||
},
|
||||
|
||||
presets() {
|
||||
const out = [];
|
||||
|
||||
out.push({
|
||||
disabled: this.hasInheritance,
|
||||
|
||||
title: 'Inheritance Point',
|
||||
title_i18n: 'setting.inheritance',
|
||||
value: {t: 'inherit'}
|
||||
});
|
||||
|
||||
if ( ! this.item.inline ) {
|
||||
out.push({
|
||||
title: 'New Line',
|
||||
value: {
|
||||
v: {type: 'new-line'}
|
||||
}
|
||||
});
|
||||
|
||||
out.push({
|
||||
title: 'Space (Small)',
|
||||
value: {
|
||||
v: {type: 'space-small'}
|
||||
}
|
||||
});
|
||||
|
||||
out.push({
|
||||
title: 'Space (Expanding)',
|
||||
value: {
|
||||
v: {type: 'space'}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
out.push({divider: true});
|
||||
|
||||
for(const key in this.data.actions) { // eslint-disable-line guard-for-in
|
||||
const act = this.data.actions[key];
|
||||
if ( act && act.presets )
|
||||
for(const preset of act.presets) {
|
||||
if ( typeof act.title !== 'string' && ! preset.title )
|
||||
continue;
|
||||
|
||||
out.push(Object.assign({
|
||||
action: key,
|
||||
title: act.title,
|
||||
title_i18n: act.title_i18n || `chat.actions.${key}`,
|
||||
value: {
|
||||
v: Object.assign({
|
||||
action: key
|
||||
}, preset)
|
||||
}
|
||||
}, preset));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
display() {
|
||||
const out = [];
|
||||
|
||||
if ( this.val )
|
||||
for(const val of this.val) {
|
||||
if ( val.v && this.displayAction(val.v) )
|
||||
out.push(val);
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
val() {
|
||||
if ( ! this.has_value )
|
||||
return [];
|
||||
|
||||
return this.value.map(x => {
|
||||
x.id = x.id || `${Date.now()}-${Math.random()}-${last_id++}`;
|
||||
return x;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this._sortable = Sortable.create(this.$refs.list, {
|
||||
draggable: 'section',
|
||||
filter: 'button',
|
||||
|
||||
onUpdate: event => {
|
||||
if ( event.newIndex === event.oldIndex )
|
||||
return;
|
||||
|
||||
const new_val = Array.from(this.val);
|
||||
new_val.splice(event.newIndex, 0, ...new_val.splice(event.oldIndex, 1));
|
||||
|
||||
this.set(new_val);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if ( this._sortable )
|
||||
this._sortable.destroy();
|
||||
|
||||
this._sortable = null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeAdd() {
|
||||
this.add_open = false;
|
||||
},
|
||||
|
||||
toggleAdd() {
|
||||
this.add_open = ! this.add_open;
|
||||
},
|
||||
|
||||
populate() {
|
||||
this.set(deep_copy(this.default_value));
|
||||
},
|
||||
|
||||
add(val) {
|
||||
const vals = Array.from(this.val);
|
||||
vals.push(val);
|
||||
this.set(deep_copy(vals));
|
||||
this.add_open = false;
|
||||
},
|
||||
|
||||
remove(val) {
|
||||
const vals = Array.from(this.val),
|
||||
idx = vals.indexOf(val);
|
||||
|
||||
if ( idx !== -1 ) {
|
||||
vals.splice(idx, 1);
|
||||
if ( vals.length )
|
||||
this.set(vals);
|
||||
else
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
|
||||
save(val, new_val) {
|
||||
val.v = new_val;
|
||||
this.set(deep_copy(this.val));
|
||||
},
|
||||
|
||||
onPreview() {
|
||||
this.show_all = this.$refs.show_all.checked;
|
||||
this.is_moderator = this.$refs.as_mod.checked;
|
||||
this.is_staff = false; //this.$refs.as_staff.checked;
|
||||
this.with_mod_icons = this.$refs.with_mod_icons.checked;
|
||||
this.is_deleted = this.$refs.is_deleted.checked;
|
||||
},
|
||||
|
||||
displayAction(action) {
|
||||
if ( ! action.appearance )
|
||||
return false;
|
||||
|
||||
const disp = action.display;
|
||||
if ( ! disp || this.show_all )
|
||||
return true;
|
||||
|
||||
if ( disp.disable )
|
||||
return false;
|
||||
|
||||
if ( disp.mod != null && disp.mod !== this.is_moderator )
|
||||
return false;
|
||||
|
||||
if ( disp.mod_icons != null && disp.mod !== this.with_mod_icons )
|
||||
return false;
|
||||
|
||||
if ( disp.staff != null && disp.mod !== this.is_staff )
|
||||
return false;
|
||||
|
||||
if ( disp.deleted != null && disp.deleted !== this.is_deleted )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
color(input) {
|
||||
if ( ! input )
|
||||
return input;
|
||||
|
||||
return this.data.color(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
106
src/modules/main_menu/components/color-picker.vue
Normal file
106
src/modules/main_menu/components/color-picker.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--color-widget tw-relative tw-full-width tw-mg-y-05">
|
||||
<input
|
||||
ref="input"
|
||||
v-bind="$attrs"
|
||||
v-model="color"
|
||||
type="text"
|
||||
class="tw-input tw-pd-r-3"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
@input="onChange"
|
||||
>
|
||||
|
||||
<button
|
||||
class="ffz-color-preview tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-border-l tw-z-default"
|
||||
@click="togglePicker"
|
||||
>
|
||||
<figure v-if="color" :style="`background-color: ${color}`" />
|
||||
<figure v-else class="ffz-i-eyedropper" />
|
||||
</button>
|
||||
<div v-on-clickaway="closePicker" v-if="open" class="tw-absolute tw-z-default tw-right-0">
|
||||
<chrome-picker :value="colors" @input="onPick" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {Color} from 'utilities/color';
|
||||
|
||||
import {Chrome} from 'vue-color';
|
||||
import {mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'chrome-picker': Chrome
|
||||
},
|
||||
|
||||
mixins: [clickaway],
|
||||
|
||||
props: {
|
||||
value: String,
|
||||
default: {
|
||||
type: String,
|
||||
default: '#000'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
color: '',
|
||||
open: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
colors() {
|
||||
return Color.RGBA.fromCSS(this.color || this.default)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val) {
|
||||
this.color = val;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.color = this.value;
|
||||
},
|
||||
|
||||
methods: {
|
||||
openPicker() {
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
closePicker() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
togglePicker() {
|
||||
this.open = ! this.open;
|
||||
},
|
||||
|
||||
onPick(color) {
|
||||
const old_val = this.color;
|
||||
|
||||
if ( color.rgba.a == 1 )
|
||||
this.color = color.hex;
|
||||
else {
|
||||
const c = color.rgba;
|
||||
this.color = `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
|
||||
}
|
||||
|
||||
if ( old_val !== this.color )
|
||||
this.$emit('input', this.color);
|
||||
},
|
||||
|
||||
onChange() {
|
||||
this.$emit('input', this.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,25 +1,25 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--experiments tw-pd-t-05">
|
||||
<div class="tw-pd-b-1 tw-mg-b-1 tw-border-b">
|
||||
{{ t('settings.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
|
||||
{{ t('setting.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
|
||||
<div class="tw-flex-grow-1">
|
||||
{{ t('settings.experiments.unique-id', 'Unique ID: %{id}', {id: unique_id}) }}
|
||||
{{ t('setting.experiments.unique-id', 'Unique ID: %{id}', {id: unique_id}) }}
|
||||
</div>
|
||||
<select
|
||||
ref="sort_select"
|
||||
class="tw-mg-x-05 tw-select tw-display-line tw-width-auto"
|
||||
@change="onSort"
|
||||
>
|
||||
<option :selected="sort_by === 0">{{ t('settings.experiments.sort-name', 'Sort By: Name') }}</option>
|
||||
<option :selected="sort_by === 1">{{ t('settings.experiments.sort-rarity', 'Sort By: Rarity') }}</option>
|
||||
<option :selected="sort_by === 0">{{ t('setting.experiments.sort-name', 'Sort By: Name') }}</option>
|
||||
<option :selected="sort_by === 1">{{ t('setting.experiments.sort-rarity', 'Sort By: Rarity') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3 class="tw-mg-b-1">
|
||||
{{ t('settings.experiments.ffz', 'FrankerFaceZ Experiments') }}
|
||||
{{ t('setting.experiments.ffz', 'FrankerFaceZ Experiments') }}
|
||||
</h3>
|
||||
|
||||
<div class="ffz--experiment-list">
|
||||
|
@ -48,7 +48,7 @@
|
|||
:key="idx"
|
||||
:selected="i.value === exp.value"
|
||||
>
|
||||
{{ t('settings.experiments.entry', '%{value} (weight: %{weight})', i) }}
|
||||
{{ t('setting.experiments.entry', '%{value} (weight: %{weight})', i) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
|
@ -67,12 +67,12 @@
|
|||
</div>
|
||||
</section>
|
||||
<div v-if="! Object.keys(ffz_data).length">
|
||||
{{ t('settings.experiments.none', 'There are no current experiments.') }}
|
||||
{{ t('setting.experiments.none', 'There are no current experiments.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="tw-mg-t-5 tw-mg-b-1">
|
||||
{{ t('settings.experiments.twitch', 'Twitch Experiments') }}
|
||||
{{ t('setting.experiments.twitch', 'Twitch Experiments') }}
|
||||
</h3>
|
||||
|
||||
<div class="ffz--experiment-list">
|
||||
|
@ -115,7 +115,7 @@
|
|||
v-if="exp.in_use === false"
|
||||
:selected="exp.default"
|
||||
>
|
||||
{{ t('settings.experiments.unset', 'unset') }}
|
||||
{{ t('setting.experiments.unset', 'unset') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(i, idx) in exp.groups"
|
||||
|
@ -141,7 +141,7 @@
|
|||
</div>
|
||||
</section>
|
||||
<div v-if="! Object.keys(twitch_data).length">
|
||||
{{ t('settings.experiments.none', 'There are no current experiments.') }}
|
||||
{{ t('setting.experiments.none', 'There are no current experiments.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -38,18 +38,14 @@
|
|||
/>
|
||||
</header>
|
||||
<div class="tw-full-width tw-full-height tw-overflow-hidden tw-flex tw-flex-nowrap tw-relative">
|
||||
<div class="ffz-vertical-nav__items tw-full-width tw-flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-tree
|
||||
:current-item="currentItem"
|
||||
:modal="nav"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<simplebar classes="ffz-vertical-nav__items tw-full-width tw-flex-grow-1">
|
||||
<menu-tree
|
||||
:current-item="currentItem"
|
||||
:modal="nav"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</simplebar>
|
||||
</div>
|
||||
<footer class="tw-c-text-alt tw-border-t tw-pd-1">
|
||||
<div>
|
||||
|
@ -60,20 +56,16 @@
|
|||
</div>
|
||||
</footer>
|
||||
</nav>
|
||||
<main class="tw-flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-page
|
||||
v-if="currentItem"
|
||||
ref="page"
|
||||
:context="context"
|
||||
:item="currentItem"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<simplebar classes="tw-flex-grow-1">
|
||||
<menu-page
|
||||
v-if="currentItem"
|
||||
ref="page"
|
||||
:context="context"
|
||||
:item="currentItem"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</simplebar>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@click="save"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-floppy">
|
||||
{{ t('settings.profiles.save', 'Save') }}
|
||||
{{ t('setting.save', 'Save') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
@ -17,24 +17,24 @@
|
|||
@click="del"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.profiles.delete', 'Delete') }}
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
<!--button class="tw-mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-download">
|
||||
{{ t('setting.profiles.export', 'Export') }}
|
||||
{{ t('setting.export', 'Export') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.general', 'General') }}
|
||||
{{ t('setting.data_management.profiles.edit.general', 'General') }}
|
||||
</header>
|
||||
|
||||
<div class="ffz--widget tw-flex tw-flex-nowrap">
|
||||
<label for="ffz:editor:name">
|
||||
{{ t('settings.data_management.profiles.edit.name', 'Name') }}
|
||||
{{ t('setting.data_management.profiles.edit.name', 'Name') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
<div class="ffz--widget tw-flex tw-flex-nowrap">
|
||||
<label for="ffz:editor:description">
|
||||
{{ t('settings.data_management.profiles.edit.desc', 'Description') }}
|
||||
{{ t('setting.data_management.profiles.edit.desc', 'Description') }}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
|
@ -61,10 +61,10 @@
|
|||
|
||||
<div class="ffz--menu-container tw-border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.rules', 'Rules') }}
|
||||
{{ t('setting.data_management.profiles.edit.rules', 'Rules') }}
|
||||
</header>
|
||||
<section class="tw-pd-b-1">
|
||||
{{ t('settings.data_management.profiles.edit.rules.description',
|
||||
{{ t('setting.data_management.profiles.edit.rules.description',
|
||||
'Rules allows you to define a series of conditions under which this profile will be active.')
|
||||
}}
|
||||
</section>
|
||||
|
@ -144,7 +144,7 @@ export default {
|
|||
del() {
|
||||
if ( this.item.profile || this.unsaved ) {
|
||||
if ( ! confirm(this.t( // eslint-disable-line no-alert
|
||||
'settings.profiles.warn-delete',
|
||||
'setting.profiles.warn-delete',
|
||||
'Are you sure you wish to delete this profile? It cannot be undone.'
|
||||
)) )
|
||||
return
|
||||
|
@ -192,7 +192,7 @@ export default {
|
|||
if ( this.unsaved )
|
||||
return confirm( // eslint-disable-line no-alert
|
||||
this.t(
|
||||
'settings.warn-unsaved',
|
||||
'setting.warn-unsaved',
|
||||
'You have unsaved changes. Are you sure you want to leave the editor?'
|
||||
));
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</button>
|
||||
<!--button class="tw-mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-upload">
|
||||
{{ t('setting.profiles.import', 'Import…') }}
|
||||
{{ t('setting.import', 'Import…') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
@ -48,7 +48,7 @@
|
|||
<div class="tw-flex tw-flex-shrink-0 tw-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') }}
|
||||
{{ t('setting.configure', 'Configure') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue