diff --git a/package.json b/package.json
index 9f16cc9e..44bd8259 100755
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/i18n.js b/src/i18n.js
index a4a4086c..1041ee86 100644
--- a/src/i18n.js
+++ b/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;
diff --git a/src/modules/chat/actions/components/edit-chat.vue b/src/modules/chat/actions/components/edit-chat.vue
index b14f7c64..dae26f97 100644
--- a/src/modules/chat/actions/components/edit-chat.vue
+++ b/src/modules/chat/actions/components/edit-chat.vue
@@ -17,6 +17,10 @@
{{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
+
+ {{ t('setting.actions.formats', 'Available Formatters: {fmts}', {fmts}) }}
+
+
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];
+ }
+ }
);
}
diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx
index a008a7f4..c4ab7f19 100644
--- a/src/modules/chat/badges.jsx
+++ b/src/modules/chat/badges.jsx
@@ -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',
diff --git a/src/modules/main_menu/components/action-editor.vue b/src/modules/main_menu/components/action-editor.vue
index 0c611fbd..8f80ab24 100644
--- a/src/modules/main_menu/components/action-editor.vue
+++ b/src/modules/main_menu/components/action-editor.vue
@@ -379,6 +379,7 @@
:value="edit_data.options"
:defaults="action_def.defaults"
:vars="vars"
+ :fmts="fmts"
@input="onChangeAction($event)"
/>
@@ -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 || [];
diff --git a/src/settings/providers.js b/src/settings/providers.js
index d52c93b8..758e3934 100644
--- a/src/settings/providers.js
+++ b/src/settings/providers.js
@@ -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';
diff --git a/src/utilities/events.js b/src/utilities/events.js
index e7e00ef1..3ac3d5d6 100644
--- a/src/utilities/events.js
+++ b/src/utilities/events.js
@@ -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, '')