mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.33.0
* Added: Formatters for chat action variables. (Closes #1199) * Changed: By default, open the user card to a badge when clicking a badge in chat. (Closes #1195) * Fixed: The settings bridge functioning incorrectly for users without a set storage provider, causing pages that rely on the settings bridge including the Stream Dashboard to never correctly load FFZ.
This commit is contained in:
parent
084a3ee5e0
commit
2af7d5618b
8 changed files with 186 additions and 21 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.32.5",
|
||||
"version": "4.33.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
43
src/i18n.js
43
src/i18n.js
|
@ -191,7 +191,7 @@ export class TranslationManager extends Module {
|
|||
},
|
||||
data: () => {
|
||||
const out = [], now = new Date;
|
||||
for (const [key,fmt] of Object.entries(this._.formats.date)) {
|
||||
for (const [key, fmt] of Object.entries(this._.formats.date)) {
|
||||
out.push({
|
||||
value: key, title: `${this.formatDate(now, key)} (${key})`
|
||||
})
|
||||
|
@ -815,6 +815,28 @@ export class TranslationManager extends Module {
|
|||
const DOLLAR_REGEX = /\$/g;
|
||||
const REPLACE = String.prototype.replace;
|
||||
|
||||
const FORMAT_REGEX = /^\s*([^(]+?)\s*(?:\(\s*([^)]+?)\s*\))?\s*$/;
|
||||
|
||||
export function parseFormatters(fmt) {
|
||||
if (!fmt || ! fmt.length)
|
||||
return;
|
||||
|
||||
const result = [];
|
||||
|
||||
for(const token of fmt.split(/\|/g)) {
|
||||
const match = FORMAT_REGEX.exec(token);
|
||||
if (!match)
|
||||
continue;
|
||||
|
||||
result.push({
|
||||
fmt: match[1],
|
||||
extra: match[2]
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function transformPhrase(phrase, substitutions, locale, token_regex, formatters) {
|
||||
const is_array = Array.isArray(phrase);
|
||||
if ( substitutions == null )
|
||||
|
@ -828,14 +850,23 @@ export function transformPhrase(phrase, substitutions, locale, token_regex, form
|
|||
|
||||
if ( typeof result === 'string' )
|
||||
result = REPLACE.call(result, token_regex, (expr, arg, fmt) => {
|
||||
let val = get(arg, options);
|
||||
let val = get(arg.trim(), options);
|
||||
if ( val == null )
|
||||
return '';
|
||||
|
||||
const formatter = formatters[fmt];
|
||||
if ( typeof formatter === 'function' )
|
||||
val = formatter(val, locale, options);
|
||||
else if ( typeof val === 'string' )
|
||||
const fmts = parseFormatters(fmt);
|
||||
let formatted = false;
|
||||
if (fmts) {
|
||||
for(const format of fmts) {
|
||||
const formatter = formatters[format.fmt];
|
||||
if (typeof formatter === 'function') {
|
||||
val = formatter(val, locale, options, format.extra);
|
||||
formatted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! formatted && typeof val === 'string' )
|
||||
val = REPLACE.call(val, DOLLAR_REGEX, '$$');
|
||||
|
||||
return val;
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
{{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
|
||||
</div>
|
||||
|
||||
<div class="tw-c-text-alt-2 tw-mg-b-1">
|
||||
{{ t('setting.actions.formats', 'Available Formatters: {fmts}', {fmts}) }}
|
||||
</div>
|
||||
|
||||
<div class="ffz-checkbox">
|
||||
<input
|
||||
:id="'chat-paste$' + id"
|
||||
|
@ -41,7 +45,7 @@
|
|||
let last_id = 0;
|
||||
|
||||
export default {
|
||||
props: ['value', 'defaults', 'vars'],
|
||||
props: ['value', 'defaults', 'vars', 'fmts'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -245,7 +245,79 @@ export default class Actions extends Module {
|
|||
data,
|
||||
this.i18n.locale,
|
||||
VAR_REPLACE,
|
||||
{}
|
||||
{
|
||||
upper(val) {
|
||||
return val.toString().toUpperCase();
|
||||
},
|
||||
uppercase(val) {
|
||||
return val.toString().toUpperCase();
|
||||
},
|
||||
lower(val) {
|
||||
return val.toString().toLowerCase();
|
||||
},
|
||||
lowercase(val) {
|
||||
return val.toString().toLowerCase();
|
||||
},
|
||||
snakecase(val) {
|
||||
return val.toString().toSnakeCase();
|
||||
},
|
||||
slugify(val, locale, options, extra) {
|
||||
return val.toString().toSlug(extra && extra.length ? extra : '-');
|
||||
},
|
||||
word(val, locale, options, extra) {
|
||||
if (! extra || ! extra.length)
|
||||
return val;
|
||||
|
||||
let start, end;
|
||||
const bits = extra.split(',');
|
||||
|
||||
try {
|
||||
start = parseInt(bits[0], 10);
|
||||
if (isNaN(start) || !isFinite(start))
|
||||
return val;
|
||||
} catch (err) {
|
||||
this.log.warn('Invalid value for word(start)', bits[0]);
|
||||
return val;
|
||||
}
|
||||
|
||||
if (bits.length > 1) {
|
||||
const bit = bits[1].trim();
|
||||
if (! bit.length )
|
||||
end = -1;
|
||||
else
|
||||
try {
|
||||
end = parseInt(bits[1], 10);
|
||||
if (isNaN(end) || !isFinite(end))
|
||||
return val;
|
||||
} catch(err) {
|
||||
this.log.warn('Invalid value for word(end)', bits[1]);
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
const words = val.split(/\s+/);
|
||||
|
||||
if (start < 0)
|
||||
start = words.length + start;
|
||||
if (start < 0)
|
||||
start = 0;
|
||||
if (start >= words.length)
|
||||
start = words.length - 1;
|
||||
|
||||
if (end != null) {
|
||||
if (end < 0)
|
||||
end = words.length + end;
|
||||
if (end < start)
|
||||
end = start;
|
||||
if (end > words.length)
|
||||
end = words.length;
|
||||
|
||||
return words.slice(start, end + 1).join(' ');
|
||||
}
|
||||
|
||||
return words[start];
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -223,12 +223,24 @@ export default class Badges extends Module {
|
|||
});
|
||||
|
||||
this.settings.add('chat.badges.clickable', {
|
||||
default: true,
|
||||
default: 2,
|
||||
process(ctx, val) {
|
||||
if (val === true)
|
||||
return 2;
|
||||
else if (val === false)
|
||||
return 0;
|
||||
return val;
|
||||
},
|
||||
ui: {
|
||||
path: 'Chat > Badges >> Behavior',
|
||||
title: 'Allow clicking badges.',
|
||||
description: 'Certain badges, such as Prime Gaming, act as links when this is enabled.',
|
||||
component: 'setting-check-box'
|
||||
component: 'setting-select-box',
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
{value: 1, title: 'Legacy (Open URLs)'},
|
||||
{value: 2, title: 'Open Badge Card'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -534,7 +546,8 @@ export default class Badges extends Module {
|
|||
|
||||
|
||||
handleClick(event) {
|
||||
if ( ! this.parent.context.get('chat.badges.clickable') )
|
||||
const mode = this.parent.context.get('chat.badges.clickable');
|
||||
if ( ! mode )
|
||||
return;
|
||||
|
||||
const target = event.target;
|
||||
|
@ -544,6 +557,7 @@ export default class Badges extends Module {
|
|||
return;
|
||||
|
||||
let url = null;
|
||||
let click_badge = null;
|
||||
|
||||
for(const d of ds.data) {
|
||||
const p = d.provider;
|
||||
|
@ -553,14 +567,14 @@ export default class Badges extends Module {
|
|||
if ( ! bd )
|
||||
continue;
|
||||
|
||||
if ( bd.click_url )
|
||||
if ( mode == 1 && bd.click_url )
|
||||
url = bd.click_url;
|
||||
else if ( global_badge.click_url )
|
||||
else if ( mode == 1 && global_badge.click_url )
|
||||
url = global_badge.click_url;
|
||||
else if ( (bd.click_action === 'sub' || global_badge.click_action === 'sub') && ds.room_login )
|
||||
else if ( mode == 1 && (bd.click_action === 'sub' || global_badge.click_action === 'sub') && ds.room_login )
|
||||
url = `https://www.twitch.tv/subs/${ds.room_login}`;
|
||||
else
|
||||
continue;
|
||||
click_badge = bd;
|
||||
|
||||
break;
|
||||
|
||||
|
@ -578,6 +592,17 @@ export default class Badges extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
if (click_badge) {
|
||||
const fine = this.resolve('site.fine');
|
||||
if (fine) {
|
||||
const line = fine.searchParent(target, n => n.openBadgeDetails && n.props?.message);
|
||||
if (line) {
|
||||
line.openBadgeDetails(click_badge, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( url ) {
|
||||
const link = createElement('a', {
|
||||
target: '_blank',
|
||||
|
|
|
@ -379,6 +379,7 @@
|
|||
:value="edit_data.options"
|
||||
:defaults="action_def.defaults"
|
||||
:vars="vars"
|
||||
:fmts="fmts"
|
||||
@input="onChangeAction($event)"
|
||||
/>
|
||||
</section>
|
||||
|
@ -493,6 +494,20 @@ export default {
|
|||
return this.modifiers
|
||||
},
|
||||
|
||||
fmts() {
|
||||
const out = [];
|
||||
|
||||
out.push('word(start)');
|
||||
out.push('word(start,end)');
|
||||
out.push('upper');
|
||||
out.push('lower');
|
||||
out.push('snakecase');
|
||||
out.push('slugify');
|
||||
out.push('slugify(separator)');
|
||||
|
||||
return out.join(', ');
|
||||
},
|
||||
|
||||
vars() {
|
||||
const out = [],
|
||||
ctx = this.context || [];
|
||||
|
|
|
@ -1027,9 +1027,9 @@ export class CrossOriginStorageBridge extends SettingsProvider {
|
|||
this._last_id = 0;
|
||||
|
||||
const frame = this.frame = document.createElement('iframe');
|
||||
frame.src = this.manager.root.host === 'twitch' ?
|
||||
'//www.twitch.tv/p/ffz_bridge/' :
|
||||
'//www.youtube.com/__ffz_bridge/';
|
||||
frame.src = this.manager.root.host === 'youtube' ?
|
||||
'//www.youtube.com/__ffz_bridge/' :
|
||||
'//www.twitch.tv/p/ffz_bridge/';
|
||||
frame.id = 'ffz-settings-bridge';
|
||||
frame.style.width = 0;
|
||||
frame.style.height = 0;
|
||||
|
@ -1041,7 +1041,7 @@ export class CrossOriginStorageBridge extends SettingsProvider {
|
|||
|
||||
// Static Properties
|
||||
|
||||
static supported(manager) { return manager.root.host === 'twitch' ? NOT_WWW_TWITCH : NOT_WWW_YT; }
|
||||
static supported() { return NOT_WWW_TWITCH && NOT_WWW_YT; }
|
||||
static hasContent(manager) { return CrossOriginStorageBridge.supported(manager); }
|
||||
|
||||
static key = 'cosb';
|
||||
|
|
|
@ -12,9 +12,27 @@ const SNAKE_CAPS = /([a-z])([A-Z])/g,
|
|||
SNAKE_SPACE = /[ \t\W]/g,
|
||||
SNAKE_TRIM = /^_+|_+$/g;
|
||||
|
||||
String.prototype.toSlug = function(separator = '-') {
|
||||
let result = this;
|
||||
if (result.normalize)
|
||||
result = result.normalize('NFD');
|
||||
|
||||
return result
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9 ]/g, '')
|
||||
.replace(/\s+/g, separator);
|
||||
}
|
||||
|
||||
String.prototype.toSnakeCase = function() {
|
||||
return this
|
||||
let result = this;
|
||||
if (result.normalize)
|
||||
result = result.normalize('NFD');
|
||||
|
||||
return result
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
.replace(SNAKE_CAPS, '$1_$2')
|
||||
.replace(SNAKE_SPACE, '_')
|
||||
.replace(SNAKE_TRIM, '')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue