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, '')