1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 00:18:31 +00:00
* Fixed: Mentions not appearing in bold font.
* Fixed: Highlighting messages in chat when opening a viewer card by clicking a mention that wasn't all lower-case.
* Fixed: Detection of External add-ons when there is a name collision.
* Changed: Do not report name collision errors.
* Changed: Allow for multi-line add-on descriptions.
This commit is contained in:
SirStendec 2019-06-01 13:58:12 -04:00
parent cdedf29d04
commit 535192d931
11 changed files with 382 additions and 243 deletions

View file

@ -113,6 +113,7 @@ module.exports = {
"warn",
"tab"
],
"vue/valid-template-root": "off",
"vue/max-attributes-per-line": "off",
"vue/require-prop-types": "off",
"vue/require-default-prop": "off",

View file

@ -5,7 +5,7 @@
// ============================================================================
import Module from 'utilities/module';
import { DEBUG, SERVER } from 'utilities/constants';
import { SERVER } from 'utilities/constants';
import { createElement } from 'utilities/dom';
import { timeout, has } from 'utilities/object';
@ -38,7 +38,7 @@ export default class AddonManager extends Module {
this.settings.addUI('add-ons', {
path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch."}',
component: 'add-ons',
component: 'addon-list',
title: 'Add-Ons',
no_filter: true,
@ -129,7 +129,6 @@ export default class AddonManager extends Module {
addon.name_i18n = addon.name_i18n || `addon.${addon.id}.name`;
addon.short_name_i18n = addon.short_name_i18n || `addon.${addon.id}.short_name`;
addon.description_i18n = addon.description_i18n || `addon.${addon.id}.description`;
addon.author_i18n = addon.author_i18n || `addon.${addon.id}.author`;
addon.dev = is_dev;
@ -173,6 +172,9 @@ export default class AddonManager extends Module {
}
isAddonEnabled(id) {
if ( this.isAddonExternal(id) )
return true;
return this.enabled_addons.includes(id);
}

View file

@ -151,7 +151,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 1, revision: 1,
major: 4, minor: 1, revision: 2,
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>

View file

@ -209,13 +209,13 @@ export const Mentions = {
},
render(token, createElement) {
return (<span
return (<strong
class={`chat-line__message-mention${token.me ? ' ffz--mention-me' : ''}`}
data-login={token.recipient}
onClick={this.handleMentionClick}
>
{token.text}
</span>)
</strong>)
},
process(tokens, msg, user) {
@ -274,7 +274,7 @@ export const Mentions = {
type: 'mention',
text: `${at}${recipient}`,
me: mentioned,
recipient
recipient: recipient ? recipient.toLowerCase() : ''
});
if ( mentioned )

View file

@ -1,231 +0,0 @@
<template lang="html">
<div class="ffz--addons tw-border-t tw-pd-y-1">
<div v-if="reload" class="tw-mg-y-1 tw-c-background-accent tw-c-text-overlay tw-pd-1">
<h4 class="ffz-i-attention">
{{ t('addon.refresh-needed', 'You must refresh your Twitch pages for some changes to take effect.') }}
</h4>
<button
class="tw-button tw-button--hollow tw-mg-t-05"
@click="item.refresh()"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-arrows-cw" />
</span>
<span class="tw-button__text">
{{ t('addon.refresh', 'Refresh') }}
</span>
</button>
</div>
<div v-if="! ready" class="tw-align-center tw-pd-1">
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
</div>
<div v-else>
<div
v-for="addon in sorted_addons"
v-if="shouldShow(addon)"
:key="addon.id"
class="ffz--addon-info tw-elevation-1 tw-c-background-base tw-border tw-pd-1 tw-mg-b-1 tw-flex tw-flex-nowrap"
>
<div class="tw-flex tw-flex-column tw-align-center tw-flex-shrink-0 tw-mg-r-1">
<div class="tw-card-img--size-6 tw-overflow-hidden tw-mg-b-1">
<img :src="addon.icon" class="tw-image">
</div>
<div v-if="enabled[addon.id]" class="tw-mg-b-05 tw-pill ffz--pill-enabled">
{{ t('addon.enabled', 'Enabled') }}
</div>
<div v-if="addon.dev" class="tw-mg-b-05 tw-pill">
{{ t('addon.dev', 'Developer') }}
</div>
<div v-if="item.isAddonExternal(addon.id)" class="tw-mg-b-05 tw-pill">
{{ t('addon.external', 'External') }}
</div>
</div>
<div class="tw-flex-grow-1">
<div class="tw-border-b tw-mg-b-05">
<h4>{{ t(addon.name_i18n, addon.name) }} <span class="tw-c-text-alt-2 tw-font-size-6">({{ addon.id }})</span></h4>
<span class="tw-c-text-alt tw-mg-r-1">
{{ t('addon.author', 'By: {author}', {
author: t(addon.author_i18n, addon.author)
}) }}
</span>
<span v-if="item.getVersion(addon.id)" class="tw-c-text-alt">
{{ t('addon.version', 'Version {version}', {version: item.getVersion(addon.id)}) }}
</span>
</div>
<markdown :source="t(addon.description_i18n, addon.description)" />
<div class="tw-mg-t-1 tw-pd-t-1 tw-border-t">
<template v-if="enabled[addon.id]">
<button
v-if="item.isAddonExternal(addon.id)"
disabled
class="tw-button tw-button--hollow tw-button--disabled tw-tooltip-wrapper tw-mg-r-1"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-trash" />
</span>
<span class="tw-button__text">
{{ t('addon.disable', 'Disable') }}
</span>
<div class="tw-tooltip tw-tooltip--up tw-tooltip--align-left">
{{ t('addon.external.description', 'This add-on has been loaded by an external script and cannot be disabled here.') }}
</div>
</button>
<button
v-else
class="tw-button tw-button--hollow ffz--button-disable tw-mg-r-1"
@click="item.disableAddon(addon.id)"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-trash" />
</span>
<span class="tw-button__text">
{{ t('addon.disable', 'Disable') }}
</span>
</button>
<button
v-if="addon.settings"
class="tw-button tw-button--hollow tw-mg-r-1"
@click="openSettings(addon)"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-cog" />
</span>
<span class="tw-button__text">
{{ t('addon.settings', 'Settings') }}
</span>
</button>
</template>
<template v-else>
<button
class="tw-button tw-button--hollow ffz--button-enable tw-mg-r-1"
@click="item.enableAddon(addon.id)"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-download" />
</span>
<span class="tw-button__text">
{{ t('addon.enable', 'Enable') }}
</span>
</button>
</template>
<a
v-if="addon.website"
:href="addon.website"
:title="addon.website"
class="tw-button tw-button--hollow tw-mg-r-1"
target="_blank"
rel="noopener"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-link-ext" />
</span>
<span class="tw-button__text">
{{ t('addon.website', 'Website') }}
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['item', 'context', 'filter'],
data() {
const enabled = {};
for(const addon of this.item.getAddons())
enabled[addon.id] = this.item.isAddonEnabled(addon.id);
return {
ready: this.item.isReady(),
reload: this.item.isReloadRequired(),
enabled
}
},
computed: {
sorted_addons() {
const addons = this.item.getAddons();
addons.sort((a, b) => {
if ( a.sort < b.sort ) return -1;
if ( b.sort < a.sort ) return 1;
const a_n = a.name.toLowerCase(),
b_n = b.name.toLowerCase();
if ( a_n < b_n ) return -1;
if ( b_n < a_n ) return 1;
return 0;
});
return addons;
}
},
created() {
this.item.on(':ready', this.onReady, this);
this.item.on(':addon-enabled', this.onEnabled, this);
this.item.on(':addon-disabled', this.onDisabled, this);
this.item.on(':reload-required', this.onReload, this);
},
destroyed() {
this.item.off(':ready', this.onReady, this);
this.item.off(':addon-enabled', this.onEnabled, this);
this.item.off(':addon-disabled', this.onDisabled, this);
this.item.off(':reload-required', this.onReload, this);
},
methods: {
shouldShow(addon) {
if ( ! this.filter || ! this.filter.length )
return true;
return addon.search_terms.includes(this.filter)
},
onReady() {
this.ready = true;
// Refresh the enabled cache.
for(const addon of this.item.getAddons())
this.enabled[addon.id] = this.item.isAddonEnabled(addon.id);
},
onEnabled(id) {
this.enabled[id] = true;
},
onDisabled(id) {
this.enabled[id] = false;
},
onReload() {
this.reload = true;
},
openSettings(addon) {
let key;
if ( typeof addon.settings === 'string' )
key = addon.settings;
else
key = `add_ons.${addon.id}`;
this.$emit('navigate', key);
}
}
}
</script>

View file

@ -0,0 +1,107 @@
<template lang="html">
<div class="ffz--addons tw-border-t tw-pd-y-1">
<div v-if="reload" class="tw-mg-y-1 tw-c-background-accent tw-c-text-overlay tw-pd-1">
<h4 class="ffz-i-attention">
{{ t('addon.refresh-needed', 'You must refresh your Twitch pages for some changes to take effect.') }}
</h4>
<button
class="tw-button tw-button--hollow tw-mg-t-05"
@click="item.refresh()"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-arrows-cw" />
</span>
<span class="tw-button__text">
{{ t('addon.refresh', 'Refresh') }}
</span>
</button>
</div>
<div v-if="! ready" class="tw-align-center tw-pd-1">
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
</div>
<div v-else>
<addon
v-for="addon in sorted_addons"
v-if="shouldShow(addon)"
:key="addon.id"
:id="addon.id"
:addon="addon"
:item="item"
@navigate="navigate"
/>
</div>
</div>
</template>
<script>
export default {
props: ['item', 'context', 'filter'],
data() {
return {
ready: this.item.isReady(),
reload: this.item.isReloadRequired()
}
},
computed: {
sorted_addons() {
const addons = this.item.getAddons();
addons.sort((a, b) => {
if ( a.sort < b.sort ) return -1;
if ( b.sort < a.sort ) return 1;
const a_n = a.name.toLowerCase(),
b_n = b.name.toLowerCase();
if ( a_n < b_n ) return -1;
if ( b_n < a_n ) return 1;
return 0;
});
return addons;
}
},
created() {
this.item.on(':ready', this.onReady, this);
this.item.on(':added', this.onAdded, this);
this.item.on(':reload-required', this.onReload, this);
},
destroyed() {
this.item.off(':ready', this.onReady, this);
this.item.off(':added', this.onAdded, this);
this.item.off(':reload-required', this.onReload, this);
},
methods: {
shouldShow(addon) {
if ( ! this.filter || ! this.filter.length )
return true;
return addon.search_terms.includes(this.filter)
},
onAdded() {
this.$forceUpdate();
},
onReady() {
this.ready = true;
},
onReload() {
this.reload = true;
},
navigate(...args) {
this.$emit('navigate', ...args);
}
}
}
</script>

View file

@ -0,0 +1,224 @@
<template lang="html">
<div class="ffz--addon-info tw-elevation-1 tw-c-background-base tw-border tw-pd-1 tw-mg-b-1 tw-flex tw-flex-nowrap">
<div class="tw-flex tw-flex-column tw-align-center tw-flex-shrink-0 tw-mg-r-1">
<div class="tw-card-img--size-6 tw-overflow-hidden tw-mg-b-1">
<img :src="icon" class="tw-image">
</div>
<div v-if="external" class="tw-mg-b-05 tw-pill">
{{ t('addon.external', 'External') }}
</div>
<div v-else-if="enabled" class="tw-mg-b-05 tw-pill ffz--pill-enabled">
{{ t('addon.enabled', 'Enabled') }}
</div>
<div v-if="addon.dev" class="tw-mg-b-05 tw-pill">
{{ t('addon.dev', 'Developer') }}
</div>
</div>
<div class="tw-flex-grow-1">
<div class="tw-border-b tw-mg-b-05">
<h4>{{ t(addon.name_i18n, addon.name) }} <span class="tw-c-text-alt-2 tw-font-size-6">({{ addon.id }})</span></h4>
<span class="tw-c-text-alt tw-mg-r-1">
{{ t('addon.author', 'By: {author}', {
author: t(addon.author_i18n, addon.author)
}) }}
</span>
<span v-if="version" class="tw-c-text-alt">
{{ t('addon.version', 'Version {version}', {version}) }}
</span>
</div>
<markdown :source="show_description" />
<a
v-if="multi_line"
href="#"
class="tw-c-text-alt-2"
@click.prevent="toggle"
>
<template v-if="expanded">{{ t('addon.show-less', '(Show Less)') }}</template>
<template v-else>{{ t('addon.show-more', '(Show More)') }}</template>
</a>
<div class="tw-mg-t-1 tw-pd-t-1 tw-border-t">
<template v-if="enabled">
<button
v-if="external"
disabled
class="tw-button tw-button--hollow tw-button--disabled tw-tooltip-wrapper tw-mg-r-1"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-trash" />
</span>
<span class="tw-button__text">
{{ t('addon.disable', 'Disable') }}
</span>
<div class="tw-tooltip tw-tooltip--up tw-tooltip--align-left">
{{ t('addon.external.description', 'This add-on has been loaded by an external script and cannot be disabled here.') }}
</div>
</button>
<button
v-else
class="tw-button tw-button--hollow ffz--button-disable tw-mg-r-1"
@click="item.disableAddon(id)"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-trash" />
</span>
<span class="tw-button__text">
{{ t('addon.disable', 'Disable') }}
</span>
</button>
<button
v-if="addon.settings"
class="tw-button tw-button--hollow tw-mg-r-1"
@click="openSettings()"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-cog" />
</span>
<span class="tw-button__text">
{{ t('addon.settings', 'Settings') }}
</span>
</button>
</template>
<template v-else>
<button
class="tw-button tw-button--hollow ffz--button-enable tw-mg-r-1"
@click="item.enableAddon(id)"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-download" />
</span>
<span class="tw-button__text">
{{ t('addon.enable', 'Enable') }}
</span>
</button>
</template>
<a
v-if="addon.website"
:href="addon.website"
:title="addon.website"
class="tw-button tw-button--hollow tw-mg-r-1"
target="_blank"
rel="noopener"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-link-ext" />
</span>
<span class="tw-button__text">
{{ t('addon.website', 'Website') }}
</span>
</a>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['id', 'addon', 'item'],
data() {
let description;
if ( this.addon.description_i18n )
description = this.t(this.addon.description_i18n, this.addon.description);
else
description = this.addon.description;
const lines = description.split(/\n/);
return {
description,
multi_line: lines.length > 1,
first_line: lines[0],
enabled: this.item.isAddonEnabled(this.id),
external: this.item.isAddonExternal(this.id),
version: this.item.getVersion(this.id),
expanded: false
}
},
computed: {
icon() {
return this.addon.icon || 'https://cdn.frankerfacez.com/badge/2/4/solid'
},
show_description() {
if ( this.expanded )
return this.description;
return this.first_line;
}
},
created() {
this.item.on('i18n:update', this.updateDescription, this);
this.item.on(':added', this.refreshExternal, this);
this.item.on(':ready', this.refreshExternal, this);
this.item.on(':addon-enabled', this.onEnabled, this);
this.item.on(':addon-disabled', this.onDisabled, this);
},
destroyed() {
this.item.off('i18n:update', this.updateDescription, this);
this.item.off(':added', this.refreshExternal, this);
this.item.off(':ready', this.refreshExternal, this);
this.item.off(':addon-enabled', this.onEnabled, this);
this.item.off(':addon-disabled', this.onDisabled, this);
},
methods: {
refreshExternal() {
this.external = this.item.isAddonExternal(this.id);
this.version = this.item.getVersion(this.id);
},
updateDescription() {
if ( this.addon.description_i18n )
this.description = this.t(this.addon.description_i18n, this.addon.description);
else
this.description = this.addon.description;
const lines = this.description.split(/\n/);
this.multi_line = lines.length > 1;
this.first_line = lines[0];
},
onEnabled(id) {
if ( id === this.id )
this.enabled = true;
this.version = this.item.getVersion(this.id);
},
onDisabled(id) {
if ( id === this.id )
this.enabled = false;
},
toggle() {
this.expanded = ! this.expanded
},
openSettings() {
if ( typeof this.addon.settings === 'string' ) {
this.$emit('navigate', this.addon.settings);
return;
}
const list = [`add_ons.${this.id}`];
if ( this.addon.short_name )
list.push(`add_ons.${this.addon.short_name.toSnakeCase()}`);
if ( this.addon.name )
list.push(`add_ons.${this.addon.name.toSnakeCase()}`);
this.$emit('navigate', ...list);
}
}
}
</script>

View file

@ -227,8 +227,14 @@ export default {
this.displace.reinit();
},
navigate(key) {
let item = this.nav_keys[key];
navigate(...keys) {
let item;
for(const key of keys) {
item = this.nav_keys[key];
if ( item )
break;
}
while(item && item.page)
item = item.parent;

View file

@ -141,7 +141,8 @@ export default class RavenLogger extends Module {
'Zugriff verweigert',
'freed script',
'ffzenhancing',
'dead object'
'dead object',
'Name Collision for Module'
],
sanitizeKeys: [
/Token$/

View file

@ -64,6 +64,10 @@ export default class ViewerCards extends Module {
}
updateStyle(login) {
// Make sure we're dealing with lower-case logins.
if ( typeof login === 'string' )
login = login.toLowerCase();
this.last_login = login;
if ( login && this.chat.context.get('chat.viewer-cards.highlight-chat') ) {
let color = this.chat.context.get('chat.viewer-cards.color');

View file

@ -9,12 +9,37 @@ export class Addon extends Module {
}
static register(id, info) {
const ffz = FrankerFaceZ.get();
ffz.register(`addon.${id}`, this);
if ( typeof id === 'object' ) {
info = id;
id = info.id || undefined;
}
if ( ! id ) {
if ( this.name )
id = this.name.toSnakeCase();
else
throw new Error(`Unable to register module without ID.`);
}
if ( ! info && this.info )
info = this.info;
const ffz = FrankerFaceZ.get();
if ( info ) {
info.id = id;
ffz.addons.addAddon(info);
}
try {
ffz.register(`addon.${id}`, this);
} catch(err) {
if ( err.message && err.message.includes('Name Collision for Module') ) {
const module = ffz.resolve(`addon.${id}`);
if ( module )
module.external = true;
}
throw err;
}
}
}