mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-11 00:20:54 +00:00
4.20.22
This release implements a massive change to how link tool-tips and embeds work. They might act up a bit while we get the new back-end installed and tweaked. * Added: Options to use custom formats for dates and times, under `Appearance > Localization`. * Added: Options to change the colors of tool-tips. * Changed: Completely rewrite how link information is formatted together with a complete rewrite of the link information service. * Changed: The FFZ Control Center now remembers you previously open settings if you reload the page. * Fixed: Update chat lines when i18n data loads. * Fixed: i18n not correctly formatting certain numbers. * Fixed: Theater mode automatically enabling on user home pages. (Closes #866) * Fixed: Theater metadata overlapping chat with Swap Sidebars enabled. (Closes #835) * API Added: New icons: `location`, `link`, and `volume-off` * API Fixed: `createElement` not properly handling `<video>` related attributes.
This commit is contained in:
parent
eec65551fb
commit
6310a2ed49
49 changed files with 2432 additions and 884 deletions
|
@ -3,10 +3,12 @@ query FFZ_GetClipInfo($slug: ID!) {
|
|||
id
|
||||
curator {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
}
|
||||
broadcaster {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
}
|
||||
game {
|
||||
|
|
|
@ -1,27 +1,39 @@
|
|||
<script>
|
||||
|
||||
import {has, timeout} from 'utilities/object';
|
||||
import {ALLOWED_ATTRIBUTES, ALLOWED_TAGS} from 'utilities/constants';
|
||||
|
||||
const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0';
|
||||
|
||||
let tokenizer;
|
||||
|
||||
|
||||
export default {
|
||||
props: ['data', 'url', 'events'],
|
||||
props: ['data', 'url', 'events', 'forceFull', 'forceUnsafe', 'forceMedia'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
has_tokenizer: false,
|
||||
loaded: false,
|
||||
error: null,
|
||||
title: this.t('card.loading', 'Loading...'),
|
||||
title_tokens: null,
|
||||
desc_1: null,
|
||||
desc_1_tokens: null,
|
||||
desc_2: null,
|
||||
desc_2_tokens: null,
|
||||
image: null,
|
||||
image_title: null,
|
||||
image_square: false,
|
||||
accent: null
|
||||
accent: null,
|
||||
short: null,
|
||||
full: null,
|
||||
unsafe: false,
|
||||
urls: null,
|
||||
allow_media: false,
|
||||
allow_unsafe: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
has_full() {
|
||||
if ( this.full == null )
|
||||
return false;
|
||||
|
||||
if ( this.full?.type === 'media' && ! this.allow_media )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -29,26 +41,50 @@ export default {
|
|||
data() {
|
||||
this.reset();
|
||||
this.load();
|
||||
},
|
||||
|
||||
events() {
|
||||
this.listen();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if ( this.events ) {
|
||||
this._events = this.events;
|
||||
this._events.on('chat:update-link-resolver', this.checkRefresh, this);
|
||||
}
|
||||
this.loadTokenizer();
|
||||
|
||||
this.listen();
|
||||
this.load();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if ( this._events ) {
|
||||
this._events.off('chat:update-link-resolver', this.checkRefresh, this);
|
||||
this._events = null;
|
||||
}
|
||||
this.unlisten();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadTokenizer() {
|
||||
if ( tokenizer )
|
||||
this.has_tokenizer = true;
|
||||
else {
|
||||
tokenizer = await import(/* webpack-chunk-name: 'rich_tokens' */ 'utilities/rich_tokens');
|
||||
this.has_tokenizer = true;
|
||||
}
|
||||
},
|
||||
|
||||
listen() {
|
||||
this.unlisten();
|
||||
|
||||
if ( this.events?.on ) {
|
||||
this._es = this.events;
|
||||
this._es.on('chat:update-link-resolver', this.checkRefresh, this);
|
||||
}
|
||||
},
|
||||
|
||||
unlisten() {
|
||||
if ( this._es?.off ) {
|
||||
this._es.off('chat:update-link-resolver', this.checkRefresh, this);
|
||||
this._es = null;
|
||||
}
|
||||
},
|
||||
|
||||
checkRefresh(url) {
|
||||
if ( ! url || (url && url === this.url) ) {
|
||||
this.reset();
|
||||
|
@ -59,16 +95,13 @@ export default {
|
|||
reset() {
|
||||
this.loaded = false;
|
||||
this.error = null;
|
||||
this.title = this.t('card.loading', 'Loading...');
|
||||
this.title_tokens = null;
|
||||
this.desc_1 = null;
|
||||
this.desc_1_tokens = null;
|
||||
this.desc_2 = null;
|
||||
this.desc_2_tokens = null;
|
||||
this.image = null;
|
||||
this.image_title = null;
|
||||
this.image_square = null;
|
||||
this.accent = null;
|
||||
this.short = null;
|
||||
this.full = null;
|
||||
this.unsafe = false;
|
||||
this.urls = null;
|
||||
this.allow_media = false;
|
||||
this.allow_unsafe = false;
|
||||
},
|
||||
|
||||
async load() {
|
||||
|
@ -83,152 +116,130 @@ export default {
|
|||
data = await data;
|
||||
}
|
||||
|
||||
if ( ! data )
|
||||
data = {
|
||||
error: true,
|
||||
title: this.t('card.error', 'An error occured.'),
|
||||
desc_1: this.t('card.empty', 'No data was returned.')
|
||||
}
|
||||
} catch(err) {
|
||||
data = {
|
||||
error: true,
|
||||
title: this.t('card.error', 'An error occured.'),
|
||||
desc_1: String(err)
|
||||
}
|
||||
error: String(err)
|
||||
};
|
||||
}
|
||||
|
||||
if ( ! data )
|
||||
data = {
|
||||
error: {type: 'i18n', key: 'card.empty', phrase: 'No data was returned.'}
|
||||
};
|
||||
|
||||
if ( data.error )
|
||||
data = {
|
||||
short: {
|
||||
type: 'header',
|
||||
logo: {type: 'image', url: ERROR_IMAGE},
|
||||
title: {type: 'i18n', key: 'card.error', phrase: 'An error occurred.'},
|
||||
subtitle: data.error
|
||||
}
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.error = data.error;
|
||||
this.title = data.title;
|
||||
this.title_tokens = data.title_tokens;
|
||||
this.desc_1 = data.desc_1;
|
||||
this.desc_1_tokens = data.desc_1_tokens;
|
||||
this.desc_2 = data.desc_2;
|
||||
this.desc_2_tokens = data.desc_2_tokens;
|
||||
this.image = data.image;
|
||||
this.image_square = data.image_square;
|
||||
this.image_title = data.image_title;
|
||||
this.accent = data.accent;
|
||||
this.short = data.short;
|
||||
this.full = data.full;
|
||||
this.unsafe = data.unsafe;
|
||||
this.urls = data.urls;
|
||||
this.allow_media = data.allow_media;
|
||||
this.allow_unsafe = data.allow_unsafe;
|
||||
},
|
||||
|
||||
// Rendering
|
||||
|
||||
renderCard(h) {
|
||||
if ( this.data.renderBody )
|
||||
return [this.data.renderBody(h)];
|
||||
if ( this.data.renderBody ) {
|
||||
const out = this.data.renderBody(h);
|
||||
return Array.isArray(out) ? out : [out];
|
||||
}
|
||||
|
||||
return [
|
||||
this.renderImage(h),
|
||||
this.renderDescription(h)
|
||||
];
|
||||
this.renderUnsafe(h),
|
||||
//this.forceFull ? null : this.renderImage(h),
|
||||
this.renderBody(h)
|
||||
]
|
||||
},
|
||||
|
||||
renderTokens(tokens, h) {
|
||||
let out = [];
|
||||
if ( ! Array.isArray(tokens) )
|
||||
tokens = [tokens];
|
||||
renderUnsafe(h) {
|
||||
if ( ! this.unsafe )
|
||||
return null;
|
||||
|
||||
for(const token of tokens) {
|
||||
if ( Array.isArray(token) )
|
||||
out = out.concat(this.renderTokens(token, h));
|
||||
const reasons = Array.from(new Set(this.urls.map(url => url.flags).flat())).join(', ');
|
||||
|
||||
else if ( typeof token !== 'object' )
|
||||
out.push(token);
|
||||
|
||||
else if ( token.type === 't') {
|
||||
const content = {};
|
||||
if ( token.content )
|
||||
for(const [key,val] of Object.entries(token.content))
|
||||
content[key] = this.renderTokens(val, h);
|
||||
|
||||
out = out.concat(this.tList(token.key, token.phrase, content));
|
||||
|
||||
} else {
|
||||
const tag = token.tag || 'span';
|
||||
if ( ! ALLOWED_TAGS.includes(tag) ) {
|
||||
console.log('Skipping disallowed tag', tag);
|
||||
continue;
|
||||
}
|
||||
|
||||
const attrs = {};
|
||||
if ( token.attrs ) {
|
||||
for(const [key,val] of Object.entries(token.attrs)) {
|
||||
if ( ! ALLOWED_ATTRIBUTES.includes(key) && ! key.startsWith('data-') )
|
||||
console.log('Skipping disallowed attribute', key);
|
||||
else
|
||||
attrs[key] = val;
|
||||
return h('div', {
|
||||
class: 'ffz--corner-flag ffz--corner-flag__warn ffz-tooltip ffz-tooltip--no-mouse',
|
||||
attrs: {
|
||||
'data-title': this.t(
|
||||
'tooltip.link-unsafe',
|
||||
"Caution: This URL is on Google's Safe Browsing List for: {reasons}",
|
||||
{
|
||||
reasons: reasons.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
const el = h(tag, {
|
||||
class: token.class,
|
||||
attrs
|
||||
}, this.renderTokens(token.content, h));
|
||||
|
||||
out.push(el);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}, [
|
||||
h('figure', {
|
||||
class: 'ffz-i-attention'
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
renderDescription(h) {
|
||||
let title = this.title,
|
||||
title_tokens = this.title_tokens,
|
||||
desc_1 = this.desc_1,
|
||||
desc_1_tokens = this.desc_1_tokens,
|
||||
desc_2 = this.desc_2,
|
||||
desc_2_tokens = this.desc_2_tokens;
|
||||
renderBody(h) {
|
||||
if ( this.has_tokenizer && this.loaded && (this.forceFull ? this.full : this.short) ) {
|
||||
return h('div', {
|
||||
class: 'ffz--card-rich tw-full-width tw-overflow-hidden tw-flex tw-flex-column'
|
||||
}, tokenizer.renderTokens(this.forceFull ? this.full : this.short, h, {
|
||||
vue: true,
|
||||
tList: (...args) => this.tList(...args),
|
||||
i18n: this.getI18n(),
|
||||
|
||||
if ( ! this.loaded ) {
|
||||
desc_1 = this.t('card.loading', 'Loading...');
|
||||
desc_1_tokens = desc_2 = desc_2_tokens = title = title_tokens = null;
|
||||
}
|
||||
|
||||
return h('div', {
|
||||
class: [
|
||||
'ffz--card-text tw-overflow-hidden tw-align-items-center tw-flex',
|
||||
desc_2 && 'ffz--two-line'
|
||||
]
|
||||
}, [h('div', {class: 'tw-full-width tw-pd-l-1'}, [
|
||||
h('div', {class: 'chat-card__title tw-ellipsis'},
|
||||
[h('span', {class: 'tw-strong', attrs: {title}}, title_tokens ? this.renderTokens(title_tokens, h) : title)]),
|
||||
h('div', {class: 'tw-ellipsis'},
|
||||
[h('span', {class: 'tw-c-text-alt-2', attrs: {title: desc_1}}, desc_1_tokens ? this.renderTokens(desc_1_tokens, h) : desc_1)]),
|
||||
desc_2 && h('div', {class: 'tw-ellipsis'},
|
||||
[h('span', {class: 'tw-c-text-alt-2', attrs: {title: desc_2}}, desc_2_tokens ? this.renderTokens(desc_2_tokens, h) : desc_2)])
|
||||
])]);
|
||||
allow_media: this.forceMedia ?? this.allow_media,
|
||||
allow_unsafe: this.forceUnsafe ?? this.allow_unsafe
|
||||
}));
|
||||
} else
|
||||
return this.renderBasic(h);
|
||||
},
|
||||
|
||||
renderImage(h) {
|
||||
let content;
|
||||
if ( this.error )
|
||||
content = h('img', {
|
||||
class: 'chat-card__error-img',
|
||||
attrs: {
|
||||
src: ERROR_IMAGE
|
||||
}
|
||||
});
|
||||
else {
|
||||
content = h('div', {
|
||||
class: 'tw-card-img tw-flex-shrink-0 tw-overflow-hidden'
|
||||
}, [h('aspect', {
|
||||
props: {
|
||||
ratio: 16/9
|
||||
}
|
||||
}, [this.loaded && this.image && h('img', {
|
||||
class: 'tw-image',
|
||||
attrs: {
|
||||
src: this.image,
|
||||
alt: this.image_title ?? this.title
|
||||
}
|
||||
})])]);
|
||||
renderBasic(h) {
|
||||
let title, description;
|
||||
if ( this.loaded && this.forceFull && ! this.full ) {
|
||||
description = 'null';
|
||||
|
||||
} else if ( this.error ) {
|
||||
title = this.t('card.error', 'An error occurred.');
|
||||
description = this.error;
|
||||
|
||||
} else if ( this.loaded && this.has_tokenizer ) {
|
||||
title = this.title;
|
||||
description = this.description;
|
||||
} else {
|
||||
description = this.t('card.loading', 'Loading...');
|
||||
}
|
||||
|
||||
return h('div', {
|
||||
class: [
|
||||
'chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center',
|
||||
this.image_square && 'square'
|
||||
]
|
||||
}, [content])
|
||||
if ( ! title && ! description )
|
||||
description = this.t('card.empty', 'No data was returned.');
|
||||
|
||||
description = description ? description.split(/\n+/).slice(0,2).map(desc =>
|
||||
h('div', {
|
||||
class: 'tw-c-text-alt-2 tw-ellipsis tw-mg-x-05',
|
||||
attrs:{title: desc}
|
||||
}, [desc])
|
||||
) : [];
|
||||
|
||||
return [
|
||||
h('div', {class: 'ffz--header-image'}),
|
||||
h('div', {
|
||||
class: 'ffz--card-text tw-full-width tw-overflow-hidden tw-flex tw-flex-column tw-justify-content-center'
|
||||
}, [
|
||||
title ? h('div', {class: 'chat-card__title tw-ellipsis tw-mg-x-05'}, [
|
||||
h('span', {class: 'tw-strong', attrs:{title}}, [title])
|
||||
]) : null,
|
||||
...description
|
||||
])
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -237,8 +248,9 @@ export default {
|
|||
class: 'tw-flex tw-flex-nowrap tw-pd-05'
|
||||
}, this.renderCard(h));
|
||||
|
||||
if ( this.url ) {
|
||||
const tooltip = this.data.card_tooltip;
|
||||
const tooltip = this.has_full && ! this.forceFull;
|
||||
|
||||
if ( this.url )
|
||||
content = h('a', {
|
||||
class: [
|
||||
tooltip && 'ffz-tooltip',
|
||||
|
@ -255,10 +267,21 @@ export default {
|
|||
href: this.url
|
||||
}
|
||||
}, [content]);
|
||||
}
|
||||
else if ( tooltip )
|
||||
content = h('div', {
|
||||
class: 'ffz-tooltip tw-block tw-border-radius-medium tw-full-width',
|
||||
attrs: {
|
||||
'data-tooltip-type': 'link',
|
||||
'data-url': this.url,
|
||||
'data-is-mail': false,
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return h('div', {
|
||||
class: 'tw-border-radius-medium tw-elevation-1 ffz--chat-card',
|
||||
class: [
|
||||
'tw-border-radius-medium tw-elevation-1 ffz--chat-card tw-relative',
|
||||
this.unsafe ? 'ffz--unsafe' : ''
|
||||
],
|
||||
style: {
|
||||
'--ffz-color-accent': this.accent
|
||||
}
|
||||
|
@ -266,7 +289,6 @@ export default {
|
|||
class: 'tw-border-radius-medium tw-c-background-base tw-flex tw-full-width'
|
||||
}, [content])]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -24,6 +24,7 @@ import Actions from './actions';
|
|||
|
||||
export const SEPARATORS = '[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]';
|
||||
|
||||
const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0';
|
||||
const EMOTE_CHARS = /[ .,!]/;
|
||||
|
||||
export default class Chat extends Module {
|
||||
|
@ -1411,7 +1412,7 @@ export default class Chat extends Module {
|
|||
const tt = tokenizer.tooltip;
|
||||
const tk = this.tooltips.types[type] = tt.bind(this);
|
||||
|
||||
for(const i of ['interactive', 'delayShow', 'delayHide'])
|
||||
for(const i of ['interactive', 'delayShow', 'delayHide', 'onShow', 'onHide'])
|
||||
tk[i] = typeof tt[i] === 'function' ? tt[i].bind(this) : tt[i];
|
||||
}
|
||||
|
||||
|
@ -1579,6 +1580,8 @@ export default class Chat extends Module {
|
|||
info = this._link_info[url] = [false, null, [[resolve, reject]]];
|
||||
|
||||
const handle = (success, data) => {
|
||||
data = this.fixLinkInfo(data);
|
||||
|
||||
const callbacks = ! info[0] && info[2];
|
||||
info[0] = true;
|
||||
info[1] = Date.now() + 120000;
|
||||
|
@ -1608,4 +1611,43 @@ export default class Chat extends Module {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
fixLinkInfo(data) {
|
||||
if ( data.error && data.message )
|
||||
data.error = data.message;
|
||||
|
||||
if ( data.error )
|
||||
data = {
|
||||
v: 5,
|
||||
title: this.i18n.t('card.error', 'An error occured.'),
|
||||
description: data.error,
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: ERROR_IMAGE},
|
||||
title: {type: 'i18n', key: 'card.error', phrase: 'An error occured.'},
|
||||
subtitle: data.error
|
||||
}
|
||||
}
|
||||
|
||||
if ( data.v < 5 && ! data.short && ! data.full && (data.title || data.desc_1 || data.desc_2) ) {
|
||||
const image = data.preview || data.image;
|
||||
|
||||
data = {
|
||||
v: 5,
|
||||
short: {
|
||||
type: 'header',
|
||||
image: image ? {
|
||||
type: 'image',
|
||||
url: image,
|
||||
sfw: data.image_safe ?? false,
|
||||
} : null,
|
||||
title: data.title,
|
||||
subtitle: data.desc_1,
|
||||
extra: data.desc_2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ const BAD_USERS = [
|
|||
import GET_CLIP from './clip_info.gql';
|
||||
import GET_VIDEO from './video_info.gql';
|
||||
|
||||
import {truncate} from 'utilities/object';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// General Links
|
||||
|
@ -47,31 +49,20 @@ export const Links = {
|
|||
} catch(err) {
|
||||
return {
|
||||
url: token.url,
|
||||
title: this.i18n.t('card.error', 'An error occurred.'),
|
||||
desc_1: String(err)
|
||||
error: String(err)
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! data )
|
||||
return {
|
||||
url: token.url,
|
||||
title: this.i18n.t('card.error', 'An error occurred.'),
|
||||
desc_1: this.i18n.t('card.empty', 'No data was returned.')
|
||||
url: token.url
|
||||
}
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: data.accent,
|
||||
image: this.context.get('tooltip.link-images') ? (data.image_safe || this.context.get('tooltip.link-nsfw-images') ) ? data.preview || data.image : null : null,
|
||||
image_title: data.image_title,
|
||||
image_square: data.image_square,
|
||||
title: data.title,
|
||||
title_tokens: data.title_tokens,
|
||||
desc_1: data.desc_1,
|
||||
desc_1_tokens: data.desc_1_tokens,
|
||||
desc_2: data.desc_2,
|
||||
desc_2_tokens: data.desc_2_tokens
|
||||
}
|
||||
...data,
|
||||
allow_media: this.context.get('tooltip.link-images'),
|
||||
allow_unsafe: this.context.get('tooltip.link-nsfw-images')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,66 +99,82 @@ export const Users = {
|
|||
if ( ! user || ! user.id )
|
||||
return null;
|
||||
|
||||
const game = user.broadcastSettings?.game?.displayName;
|
||||
const game = user.broadcastSettings?.game?.displayName,
|
||||
stream_id = user.stream?.id;
|
||||
|
||||
let desc_1 = null, desc_2 = null, desc_1_tokens = null, desc_2_tokens = null;
|
||||
if ( user.stream?.id && game ) {
|
||||
desc_1_tokens = this.i18n.tList('cards.user.streaming', 'streaming {game}', {
|
||||
game: {class: 'tw-semibold', content: [game]}
|
||||
});
|
||||
desc_1 = this.i18n.t('cards.user.streaming', 'streaming {game}', {
|
||||
game
|
||||
});
|
||||
}
|
||||
let subtitle
|
||||
if ( stream_id && game )
|
||||
subtitle = {
|
||||
type: 'i18n',
|
||||
key: 'cards.user.streaming', phrase: 'streaming {game}', content: {
|
||||
game: {type: 'style', weight: 'semibold', content: game}
|
||||
}
|
||||
};
|
||||
|
||||
const bits_tokens = this.i18n.tList('cards.user.stats', 'Views: {views,number} • Followers: {followers,number}', {
|
||||
views: {class: 'tw-semibold', content: [this.i18n.formatNumber(user.profileViewCount || 0)]},
|
||||
followers: {class: 'tw-semibold', content: [this.i18n.formatNumber(user.followers?.totalCount || 0)]}
|
||||
}),
|
||||
bits = this.i18n.t('cards.user.stats', 'Views: {views,number} • Followers: {followers,number}', {
|
||||
views: user.profileViewCount || 0,
|
||||
followers: user.followers?.totalCount || 0
|
||||
const extra = truncate(user.description);
|
||||
const title = [user.displayName];
|
||||
|
||||
if ( user.displayName.trim().toLowerCase() !== user.login )
|
||||
title.push({
|
||||
type: 'style', color: 'alt-2',
|
||||
content: [' (', user.login, ')']
|
||||
});
|
||||
|
||||
if ( desc_1 ) {
|
||||
desc_2 = bits;
|
||||
desc_2_tokens = bits_tokens;
|
||||
} else {
|
||||
desc_1 = bits;
|
||||
desc_1_tokens = bits_tokens;
|
||||
}
|
||||
if ( user.roles?.isPartner )
|
||||
title.push({
|
||||
type: 'style', color: 'link',
|
||||
content: {type: 'icon', name: 'verified'}
|
||||
});
|
||||
|
||||
const has_i18n = user.displayName.trim().toLowerCase() !== user.login;
|
||||
let title = user.displayName, title_tokens = null;
|
||||
if ( has_i18n ) {
|
||||
title = `${user.displayName} (${user.login})`;
|
||||
title_tokens = [
|
||||
user.displayName,
|
||||
{class: 'chat-author__intl-login', content: ` (${user.login})`}
|
||||
];
|
||||
}
|
||||
/*const full = [{
|
||||
type: 'header',
|
||||
image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1},
|
||||
title,
|
||||
subtitle,
|
||||
extra: stream_id ? extra : null
|
||||
}];
|
||||
|
||||
if ( user.roles?.isPartner ) {
|
||||
if ( ! title_tokens )
|
||||
title_tokens = [title];
|
||||
if ( stream_id ) {
|
||||
full.push({type: 'box', 'mg-y': 'small', lines: 1, content: user.broadcastSettings.title});
|
||||
full.push({type: 'conditional', content: {
|
||||
type: 'gallery', items: [{
|
||||
type: 'image', aspect: 16/9, sfw: false, url: user.stream.previewImageURL
|
||||
}]
|
||||
}});
|
||||
} else
|
||||
full.push({type: 'box', 'mg-y': 'small', wrap: 'pre-wrap', lines: 5, content: truncate(user.description, 1000, undefined, undefined, false)})
|
||||
|
||||
title_tokens = {tag: 'div', class: 'tw-flex tw-align-items-center', content: [
|
||||
{tag: 'div', content: title_tokens},
|
||||
{tag: 'figure', class: 'tw-mg-l-05 ffz-i-verified tw-c-text-link', content: []}
|
||||
]};
|
||||
}
|
||||
full.push({
|
||||
type: 'fieldset',
|
||||
fields: [
|
||||
{
|
||||
name: {type: 'i18n', key: 'embed.twitch.views', phrase: 'Views'},
|
||||
value: {type: 'format', format: 'number', value: user.profileViewCount},
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: {type: 'i18n', key: 'embed.twitch.followers', phrase: 'Followers'},
|
||||
value: {type: 'format', format: 'number', value: user.followers?.totalCount},
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
full.push({
|
||||
type: 'header',
|
||||
subtitle: [{type: 'icon', name: 'twitch'}, ' Twitch']
|
||||
});*/
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null,
|
||||
image: user.profileImageURL,
|
||||
image_square: true,
|
||||
title,
|
||||
title_tokens,
|
||||
desc_1,
|
||||
desc_1_tokens,
|
||||
desc_2,
|
||||
desc_2_tokens
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1},
|
||||
title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,53 +221,51 @@ export const Clips = {
|
|||
return null;
|
||||
|
||||
const clip = result.data.clip,
|
||||
user = clip.broadcaster.displayName,
|
||||
game = clip.game,
|
||||
game_name = game && game.name,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
let desc_1, desc_1_tokens;
|
||||
if ( game_name === 'creative' ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.creative', '{user} being Creative', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', {
|
||||
user
|
||||
});
|
||||
const user = {
|
||||
type: 'link', url: `https://www.twitch.tv/${clip.broadcaster.login}`,
|
||||
content: {
|
||||
type: 'style', weight: 'semibold', color: 'alt-2',
|
||||
content: clip.broadcaster.displayName
|
||||
}
|
||||
};
|
||||
|
||||
} else if ( game ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', {
|
||||
user: {class: 'tw-semibold', content: user},
|
||||
game: {class: 'tw-semibold', content: game_display}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
|
||||
const subtitle = game_display ? {
|
||||
type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: {
|
||||
user,
|
||||
game: game_display
|
||||
});
|
||||
game: {type: 'style', weight: 'semibold', content: game_display}
|
||||
}
|
||||
} : {type: 'i18n', key: 'clip.desc.1', phrase: 'Clip of {user}', content: {user}};
|
||||
|
||||
} else {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1', 'Clip of {user}', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1', 'Clip of {user}', {user});
|
||||
}
|
||||
const curator = clip.curator ? {
|
||||
type: 'link', url: `https://www.twitch.tv/${clip.curator.login}`,
|
||||
content: {
|
||||
type: 'style', color: 'alt-2',
|
||||
content: clip.curator.displayName
|
||||
}
|
||||
} : {type: 'i18n', key: 'clip.unknown', phrase: 'Unknown'};
|
||||
|
||||
const curator = clip.curator ? clip.curator.displayName : this.i18n.t('clip.unknown', 'Unknown');
|
||||
const extra = {
|
||||
type: 'i18n', key: 'clip.desc.2',
|
||||
phrase: 'Clipped by {curator} — {views,number} View{views,en_plural}',
|
||||
content: {
|
||||
curator,
|
||||
views: clip.viewCount
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
image: clip.thumbnailURL,
|
||||
title: clip.title,
|
||||
desc_1,
|
||||
desc_1_tokens,
|
||||
desc_2: this.i18n.t('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', {
|
||||
curator,
|
||||
views: clip.viewCount
|
||||
}),
|
||||
desc_2_tokens: this.i18n.tList('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', {
|
||||
curator: clip.curator ? {class: 'tw-semibold', content: curator} : curator,
|
||||
views: {class: 'tw-semibold', content: this.i18n.formatNumber(clip.viewCount)}
|
||||
})
|
||||
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: clip.thumbnailURL, sfw: false, aspect: 16/9},
|
||||
title: clip.title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -296,49 +301,43 @@ export const Videos = {
|
|||
return null;
|
||||
|
||||
const video = result.data.video,
|
||||
user = video.owner.displayName,
|
||||
game = video.game,
|
||||
game_name = game && game.name,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
let desc_1, desc_1_tokens;
|
||||
if ( game_name === 'creative' ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.creative', '{user} being Creative', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', {
|
||||
user
|
||||
});
|
||||
const user = {
|
||||
type: 'link', url: `https://www.twitch.tv/${video.owner.login}`,
|
||||
content: {
|
||||
type: 'style', weight: 'semibold', color: 'alt-2',
|
||||
content: video.owner.displayName
|
||||
}
|
||||
};
|
||||
|
||||
} else if ( game ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', {
|
||||
user: {class: 'tw-semibold', content: user},
|
||||
game: {class: 'tw-semibold', content: game_display}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
|
||||
const subtitle = game_display ? {
|
||||
type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: {
|
||||
user,
|
||||
game: game_display
|
||||
});
|
||||
game: {type: 'style', weight: 'semibold', content: game_display}
|
||||
}
|
||||
} : {type: 'i18n', key: 'video.desc.1', phrase: 'Video of {user}', content: {user}};
|
||||
|
||||
} else {
|
||||
desc_1_tokens = this.i18n.tList('video.desc.1', 'Video of {user}', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('video.desc.1', 'Video of {user}', {user});
|
||||
}
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
image: video.previewThumbnailURL,
|
||||
title: video.title,
|
||||
desc_1,
|
||||
desc_1_tokens,
|
||||
desc_2: this.i18n.t('video.desc.2', '{length,duration} — {views,number} Views - {date,datetime}', {
|
||||
const extra = {
|
||||
type: 'i18n', key: 'video.desc.2',
|
||||
phrase: '{length,duration} — {views,number} Views — {date,datetime}', content: {
|
||||
length: video.lengthSeconds,
|
||||
views: video.viewCount,
|
||||
date: video.publishedAt
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: video.previewThumbnailURL, sfw: false, aspect: 16/9},
|
||||
title: video.title,
|
||||
subtitle,
|
||||
extra
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,11 @@ const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
|||
// Links
|
||||
// ============================================================================
|
||||
|
||||
const TOOLTIP_VERSION = 4;
|
||||
function datasetBool(value) {
|
||||
return value == null ? null : value === 'true';
|
||||
}
|
||||
|
||||
const TOOLTIP_VERSION = 5;
|
||||
|
||||
export const Links = {
|
||||
type: 'link',
|
||||
|
@ -47,60 +51,94 @@ export const Links = {
|
|||
if ( target.dataset.isMail === 'true' )
|
||||
return [this.i18n.t('tooltip.email-link', 'E-Mail {address}', {address: target.textContent})];
|
||||
|
||||
const url = target.dataset.url || target.href;
|
||||
const url = target.dataset.url || target.href,
|
||||
show_images = datasetBool(target.dataset.forceMedia) ?? this.context.get('tooltip.link-images'),
|
||||
show_unsafe = datasetBool(target.dataset.forceUnsafe) ?? this.context.get('tooltip.link-nsfw-images');
|
||||
|
||||
return this.get_link_info(url).then(data => {
|
||||
return Promise.all([
|
||||
import(/* webpack-chunk-name: 'rich_tokens' */ 'utilities/rich_tokens'),
|
||||
this.get_link_info(url)
|
||||
]).then(([rich_tokens, data]) => {
|
||||
if ( ! data || (data.v || 1) > TOOLTIP_VERSION )
|
||||
return '';
|
||||
|
||||
let content = data.content || data.html || '';
|
||||
const ctx = {
|
||||
tList: (...args) => this.i18n.tList(...args),
|
||||
i18n: this.i18n,
|
||||
allow_media: show_images,
|
||||
allow_unsafe: show_unsafe,
|
||||
onload: tip.update
|
||||
};
|
||||
|
||||
// TODO: Replace timestamps.
|
||||
|
||||
if ( data.urls && data.urls.length > 1 )
|
||||
content += (content.length ? '<hr>' : '') +
|
||||
sanitize(this.i18n.t(
|
||||
'tooltip.link-destination',
|
||||
'Destination: {url}',
|
||||
{url: data.urls[data.urls.length-1][1]}
|
||||
));
|
||||
|
||||
if ( data.unsafe ) {
|
||||
const reasons = Array.from(new Set(data.urls.map(x => x[2]).filter(x => x))).join(', ');
|
||||
content = this.i18n.t(
|
||||
'tooltip.link-unsafe',
|
||||
"Caution: This URL is on Google's Safe Browsing List for: {reasons}",
|
||||
{reasons: sanitize(reasons.toLowerCase())}
|
||||
) + (content.length ? `<hr>${content}` : '');
|
||||
let content;
|
||||
if ( tip.element ) {
|
||||
tip.element.classList.add('ffz-rich-tip');
|
||||
tip.element.classList.add('tw-align-left');
|
||||
}
|
||||
|
||||
const show_image = this.context.get('tooltip.link-images') && (data.image_safe || this.context.get('tooltip.link-nsfw-images'));
|
||||
if ( data.full ) {
|
||||
content = rich_tokens.renderTokens(data.full, createElement, ctx);
|
||||
|
||||
if ( show_image ) {
|
||||
if ( data.image && ! data.image_iframe )
|
||||
content = `<img class="preview-image" src="${sanitize(data.image)}">${content}`
|
||||
} else {
|
||||
if ( data.short ) {
|
||||
content = rich_tokens.renderTokens(data.short, createElement, ctx);
|
||||
} else
|
||||
content = this.i18n.t('card.empty', 'No data was returned.');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if ( tip.element ) {
|
||||
for(const el of tip.element.querySelectorAll('img'))
|
||||
el.addEventListener('load', tip.update);
|
||||
if ( ! data.urls )
|
||||
return content;
|
||||
|
||||
for(const el of tip.element.querySelectorAll('video'))
|
||||
el.addEventListener('loadedmetadata', tip.update);
|
||||
}
|
||||
const url_table = [];
|
||||
for(let i=0; i < data.urls.length; i++) {
|
||||
const url = data.urls[i];
|
||||
|
||||
url_table.push(<tr>
|
||||
<td>{this.i18n.formatNumber(i + 1)}.</td>
|
||||
<td class="tw-c-text-alt-2 tw-pd-x-05 tw-word-break-all">{url.url}</td>
|
||||
<td>{url.flags ? url.flags.map(flag => <span class="tw-pill">{flag.toLowerCase()}</span>) : null}</td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
let url_notice;
|
||||
if ( data.unsafe ) {
|
||||
const reasons = Array.from(new Set(data.urls.map(url => url.flags).flat())).join(', ');
|
||||
url_notice = (<div class="ffz-i-attention">
|
||||
{this.i18n.tList(
|
||||
'tooltip.link-unsafe',
|
||||
"Caution: This URL is on Google's Safe Browsing List for: {reasons}",
|
||||
{reasons: reasons.toLowerCase()}
|
||||
)}
|
||||
</div>);
|
||||
} else if ( data.urls.length > 1 )
|
||||
url_notice = this.i18n.t('tooltip.link-destination', 'Destination: {url}', {
|
||||
url: data.urls[data.urls.length-1].url
|
||||
});
|
||||
|
||||
} else if ( content.length )
|
||||
content = content.replace(/<!--MS-->.*<!--ME-->/g, '');
|
||||
|
||||
if ( data.tooltip_class )
|
||||
tip.element.classList.add(data.tooltip_class);
|
||||
content = (<div>
|
||||
<div class="ffz--shift-hide">
|
||||
{content}
|
||||
{url_notice ? <div class="tw-mg-t-05 tw-border-t tw-pd-t-05 tw-align-center">
|
||||
{url_notice}
|
||||
<div class=" tw-font-size-8">
|
||||
{this.i18n.t('tooltip.shift-detail', '(Shift for Details)')}
|
||||
</div>
|
||||
</div> : null}
|
||||
</div>
|
||||
<div class="ffz--shift-show tw-align-left">
|
||||
<div class="tw-semibold tw-mg-b-05 tw-align-center">
|
||||
{this.i18n.t('tooltip.link.urls', 'Visited URLs')}
|
||||
</div>
|
||||
<table>{url_table}</table>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
return content;
|
||||
|
||||
}).catch(error =>
|
||||
sanitize(this.i18n.t('tooltip.error', 'An error occurred. ({error})', {error}))
|
||||
);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
return sanitize(this.i18n.t('tooltip.error', 'An error occurred. ({error})', {error}))
|
||||
});
|
||||
},
|
||||
|
||||
process(tokens) {
|
||||
|
|
|
@ -12,6 +12,7 @@ query FFZ_GetVideoInfo($id: ID!) {
|
|||
}
|
||||
owner {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,8 @@
|
|||
v-if="addon.website"
|
||||
:href="addon.website"
|
||||
:title="addon.website"
|
||||
class="tw-button ffz-button--hollow tw-mg-r-1"
|
||||
class="tw-button ffz-button--hollow tw-mg-r-1 ffz-tooltip ffz-tooltip--no-mouse"
|
||||
data-tooltip-type="link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
:href="commit.author.html_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="tw-inline-flex tw-align-items-center tw-link tw-link--inherit tw-mg-x-05"
|
||||
class="tw-inline-flex tw-align-items-center tw-link tw-link--inherit tw-mg-x-05 ffz-tooltip"
|
||||
data-tooltip-type="link"
|
||||
>
|
||||
<figure
|
||||
v-if="commit.author.avatar_url"
|
||||
|
@ -76,7 +77,7 @@
|
|||
v-if="commit.hash"
|
||||
class="tw-font-size-8 tw-c-text-alt-2"
|
||||
>
|
||||
@<a :href="commit.link" target="_blank" rel="noopener noreferrer" class="tw-link tw-link--inherit">{{ commit.hash }}</a>
|
||||
@<a :href="commit.link" target="_blank" rel="noopener noreferrer" class="tw-link tw-link--inherit ffz-tooltip" data-tooltip-type="link">{{ commit.hash }}</a>
|
||||
</div>
|
||||
<time
|
||||
v-if="commit.date"
|
||||
|
|
|
@ -26,15 +26,51 @@
|
|||
</select>
|
||||
<input
|
||||
ref="text"
|
||||
v-model="raw_url"
|
||||
:disabled="! isCustomURL"
|
||||
class="ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input"
|
||||
@blur="updateText"
|
||||
@input="onTextChange"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-mg-b-1">
|
||||
<div class="tw-flex-grow-1" />
|
||||
|
||||
<div class="tw-pd-x-1 tw-checkbox">
|
||||
<input
|
||||
id="force_media"
|
||||
ref="force_media"
|
||||
:checked="force_media"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onCheck"
|
||||
>
|
||||
|
||||
<label for="force_media" class="tw-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.link-provider.allow.media', 'Allow Media') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1 tw-checkbox">
|
||||
<input
|
||||
id="force_unsafe"
|
||||
ref="force_unsafe"
|
||||
:checked="force_unsafe"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onCheck"
|
||||
>
|
||||
|
||||
<label for="force_unsafe" class="tw-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.link-provider.allow.unsafe', 'Allow NSFW') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="refresh"
|
||||
|
@ -44,6 +80,47 @@
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-flex tw-mg-b-1 tw-full-width">
|
||||
<label>
|
||||
{{ t('debug.link-provider.link', 'Chat Link') }}
|
||||
</label>
|
||||
<div class="tw-full-width tw-overflow-hidden">
|
||||
<a
|
||||
v-if="url"
|
||||
ref="link"
|
||||
:href="url"
|
||||
:data-url="url"
|
||||
class="ffz-tooltip"
|
||||
data-tooltip-type="link"
|
||||
data-force-tooltip="true"
|
||||
:data-force-open="force_tooltip ? 'true' : 'false'"
|
||||
:data-force-media="force_media ? 'true' : 'false'"
|
||||
:data-force-unsafe="force_unsafe ? 'true' : 'false'"
|
||||
data-is-mail="false"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{ url }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tw-pd-x-1 tw-checkbox">
|
||||
<input
|
||||
id="force_tooltip"
|
||||
ref="force_tooltip"
|
||||
:checked="force_tooltip"
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
@change="onTooltip"
|
||||
>
|
||||
|
||||
<label for="force_tooltip" class="tw-checkbox__label">
|
||||
<span class="tw-mg-l-1">
|
||||
{{ t('debug.link-provider.force-tooltip', 'Force Tooltip') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-mg-b-1 tw-full-width">
|
||||
<label>
|
||||
{{ t('debug.link-provider.embed', 'Rich Embed') }}
|
||||
|
@ -53,28 +130,26 @@
|
|||
v-if="rich_data"
|
||||
:data="rich_data"
|
||||
:url="url"
|
||||
:force-media="force_media"
|
||||
:force-unsafe="force_unsafe"
|
||||
:events="events"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-mg-b-1 tw-full-width">
|
||||
<label>
|
||||
{{ t('debug.link-provider.link', 'Chat Link') }}
|
||||
{{ t('debug.link-provider.full-embed', 'Full Embed') }}
|
||||
</label>
|
||||
<div class="tw-full-width tw-overflow-hidden">
|
||||
<a
|
||||
v-if="url"
|
||||
:href="url"
|
||||
:data-url="url"
|
||||
class="ffz-tooltip"
|
||||
data-tooltip-type="link"
|
||||
data-force-tooltip="true"
|
||||
data-is-mail="false"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{ url }}
|
||||
</a>
|
||||
<chat-rich
|
||||
v-if="rich_data"
|
||||
:data="rich_data"
|
||||
:url="url"
|
||||
:force-full="true"
|
||||
:force-media="force_media"
|
||||
:force-unsafe="force_unsafe"
|
||||
:events="events"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-mg-b-1 tw-full-width">
|
||||
|
@ -99,12 +174,19 @@ import { debounce } from '../../../utilities/object';
|
|||
|
||||
const STOCK_URLS = [
|
||||
'https://www.twitch.tv/sirstendec',
|
||||
'https://www.twitch.tv/videos/42968068',
|
||||
'https://www.twitch.tv/sirstendec/clip/HedonisticMagnificentSoymilkChocolateRain',
|
||||
'https://clips.twitch.tv/HedonisticMagnificentSoymilkChocolateRain',
|
||||
'https://discord.gg/UrAkGhT',
|
||||
'https://www.youtube.com/watch?v=CAL4WMpBNs0',
|
||||
'https://xkcd.com/221/',
|
||||
'https://github.com/FrankerFaceZ/FrankerFaceZ',
|
||||
'https://twitter.com/frankerfacez',
|
||||
'https://twitter.com/FrankerFaceZ/status/1240717057630625792'
|
||||
'https://twitter.com/FrankerFaceZ/status/1240717057630625792',
|
||||
'http://testsafebrowsing.appspot.com/apiv4/ANY_PLATFORM/MALWARE/URL/',
|
||||
'https://en.wikipedia.org/wiki/Emoji',
|
||||
'https://en.wikipedia.org/wiki/Naginata',
|
||||
'https://www.smbc-comics.com/comic/punishment'
|
||||
]
|
||||
|
||||
export default {
|
||||
|
@ -118,13 +200,26 @@ export default {
|
|||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
const state = window.history.state;
|
||||
let url = state?.ffz_lt_url,
|
||||
is_custom = false;
|
||||
if ( url )
|
||||
is_custom = ! STOCK_URLS.includes(url);
|
||||
else
|
||||
url = STOCK_URLS[Math.floor(Math.random() * STOCK_URLS.length)];
|
||||
|
||||
return {
|
||||
stock_urls: deep_copy(STOCK_URLS),
|
||||
raw_url: STOCK_URLS[Math.floor(Math.random() * STOCK_URLS.length)],
|
||||
raw_url: url,
|
||||
isCustomURL: is_custom,
|
||||
rich_data: null,
|
||||
isCustomURL: false,
|
||||
raw_loading: false,
|
||||
raw_data: null,
|
||||
|
||||
force_media: state.ffz_lt_media ?? true,
|
||||
force_unsafe: state.ffz_lt_unsafe ?? false,
|
||||
force_tooltip: state.ffz_lt_tip ?? false,
|
||||
|
||||
events: {
|
||||
on: (...args) => this.item.getChat().on(...args),
|
||||
off: (...args) => this.item.getChat().off(...args)
|
||||
|
@ -143,24 +238,75 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
raw_url() {
|
||||
if ( ! this.isCustomURL )
|
||||
this.$refs.text.value = this.raw_url;
|
||||
},
|
||||
|
||||
url() {
|
||||
this.rebuildData();
|
||||
this.saveState();
|
||||
|
||||
if ( this.force_tooltip ) {
|
||||
const link = this.$refs.link;
|
||||
if ( ! link || ! this.chat )
|
||||
return;
|
||||
|
||||
const tips = this.chat.resolve('tooltips')?.tips;
|
||||
if ( ! tips )
|
||||
return;
|
||||
|
||||
tips._exit(link);
|
||||
setTimeout(() => tips._enter(link), 250);
|
||||
}
|
||||
},
|
||||
|
||||
rich_data() {
|
||||
this.refreshRaw();
|
||||
},
|
||||
|
||||
force_tooltip() {
|
||||
const link = this.$refs.link;
|
||||
if ( ! link || ! this.chat )
|
||||
return;
|
||||
|
||||
const tips = this.chat.resolve('tooltips')?.tips;
|
||||
if ( ! tips )
|
||||
return;
|
||||
|
||||
if ( this.force_tooltip )
|
||||
tips._enter(link);
|
||||
else
|
||||
tips._exit(link);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.rebuildData = debounce(this.rebuildData, 250);
|
||||
this.refreshRaw = debounce(this.refreshRaw, 250);
|
||||
this.onTextChange = debounce(this.onTextChange, 500);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.chat = this.item.getChat();
|
||||
this.chat.on('chat:update-link-resolver', this.checkRefreshRaw, this);
|
||||
this.rebuildData();
|
||||
|
||||
this.$refs.text.value = this.raw_url;
|
||||
|
||||
|
||||
if ( this.force_tooltip ) {
|
||||
const link = this.$refs.link;
|
||||
if ( ! link || ! this.chat )
|
||||
return;
|
||||
|
||||
const tips = this.chat.resolve('tooltips')?.tips;
|
||||
if ( ! tips )
|
||||
return;
|
||||
|
||||
tips._enter(link);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
@ -169,6 +315,21 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
saveState() {
|
||||
try {
|
||||
window.history.replaceState({
|
||||
...window.history.state,
|
||||
ffz_lt_url: this.raw_url,
|
||||
ffz_lt_media: this.force_media,
|
||||
ffz_lt_unsafe: this.force_unsafe,
|
||||
ffz_lt_tip: this.force_tooltip
|
||||
}, document.title);
|
||||
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
}
|
||||
},
|
||||
|
||||
checkRefreshRaw(url) {
|
||||
if ( ! url || (url && url === this.url) )
|
||||
this.refreshRaw();
|
||||
|
@ -220,8 +381,26 @@ export default {
|
|||
this.isCustomURL = true;
|
||||
},
|
||||
|
||||
updateText() {
|
||||
if ( this.isCustomURL )
|
||||
this.raw_url = this.$refs.text.value;
|
||||
},
|
||||
|
||||
onTextChange() {
|
||||
this.raw_url = this.$refs.text
|
||||
this.updateText();
|
||||
},
|
||||
|
||||
onCheck() {
|
||||
this.force_media = this.$refs.force_media.checked;
|
||||
this.force_unsafe = this.$refs.force_unsafe.checked;
|
||||
|
||||
this.saveState();
|
||||
},
|
||||
|
||||
onTooltip() {
|
||||
this.force_tooltip = this.$refs.force_tooltip.checked;
|
||||
|
||||
this.saveState();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -194,6 +194,17 @@ export default {
|
|||
this.markSeen(item);
|
||||
|
||||
this.currentItem = item;
|
||||
this.restoredItem = true;
|
||||
|
||||
try {
|
||||
window.history.replaceState({
|
||||
...window.history.state,
|
||||
ffzcc: item.full_key
|
||||
}, document.title)
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
let current = item;
|
||||
while(current = current.parent) // eslint-disable-line no-cond-assign
|
||||
current.expanded = true;
|
||||
|
|
|
@ -36,6 +36,7 @@ export default class MainMenu extends Module {
|
|||
|
||||
//this.should_enable = true;
|
||||
|
||||
this.exclusive = false;
|
||||
this.new_seen = false;
|
||||
|
||||
this._settings_tree = null;
|
||||
|
@ -198,6 +199,12 @@ export default class MainMenu extends Module {
|
|||
this.off('site.menu_button:clicked', this.dialog.toggleVisible, this.dialog);
|
||||
}
|
||||
|
||||
openExclusive() {
|
||||
this.exclusive = true;
|
||||
this.dialog.exclusive = true;
|
||||
this.enable().then(() => this.dialog.show());
|
||||
}
|
||||
|
||||
runFix(amount) {
|
||||
this.settings.updateContext({
|
||||
force_chat_fix: (this.settings.get('context.force_chat_fix') || 0) + amount
|
||||
|
@ -278,14 +285,41 @@ export default class MainMenu extends Module {
|
|||
const root = this._vue.$children[0],
|
||||
item = root.currentItem,
|
||||
key = item && item.full_key,
|
||||
wants_old = ! root.restoredItem,
|
||||
state = window.history.state,
|
||||
|
||||
tree = this.getSettingsTree();
|
||||
|
||||
root.nav = tree;
|
||||
root.nav_keys = tree.keys;
|
||||
root.currentItem = tree.keys[key] || (this._wanted_page && tree.keys[this._wanted_page]) || (this.has_update ?
|
||||
tree.keys['home.changelog'] :
|
||||
tree.keys['home']);
|
||||
|
||||
let current, restored = true;
|
||||
|
||||
if ( this._wanted_page )
|
||||
current = tree.keys[this._wanted_page];
|
||||
|
||||
if ( ! current && wants_old ) {
|
||||
if ( state?.ffzcc )
|
||||
current = tree.keys[state.ffzcc];
|
||||
if ( ! current ) {
|
||||
const params = new URL(window.location).searchParams,
|
||||
key = params?.get?.('ffz-settings');
|
||||
current = key && tree.keys[key];
|
||||
}
|
||||
if ( ! current )
|
||||
restored = false;
|
||||
}
|
||||
|
||||
if ( ! current )
|
||||
current = tree.keys[key];
|
||||
|
||||
if ( ! current )
|
||||
current = this.has_update ?
|
||||
tree.keys['home.changelog'] :
|
||||
tree.keys['home'];
|
||||
|
||||
root.currentItem = current;
|
||||
root.restoredItem = restored;
|
||||
|
||||
this._wanted_page = null;
|
||||
}
|
||||
|
@ -813,7 +847,26 @@ export default class MainMenu extends Module {
|
|||
getData() {
|
||||
const settings = this.getSettingsTree(),
|
||||
context = this.getContext(),
|
||||
current = (this._wanted_page && settings.keys[this._wanted_page]) || (this.has_update ? settings.keys['home.changelog'] : settings.keys['home']);
|
||||
state = window.history.state;
|
||||
|
||||
let current, restored = true;
|
||||
if ( this._wanted_page )
|
||||
current = settings.keys[this._wanted_page];
|
||||
if ( ! current && state?.ffzcc ) {
|
||||
current = settings.keys[state.ffzcc];
|
||||
if ( ! current )
|
||||
restored = false;
|
||||
} if ( ! current ) {
|
||||
const params = new URL(window.location).searchParams,
|
||||
key = params?.get?.('ffz-settings');
|
||||
current = key && settings.keys[key];
|
||||
if ( ! current )
|
||||
restored = false;
|
||||
}
|
||||
if ( ! current )
|
||||
current = this.has_update ?
|
||||
settings.keys['home.changelog'] :
|
||||
settings.keys['home'];
|
||||
|
||||
this._wanted_page = null;
|
||||
this.markSeen(current);
|
||||
|
@ -834,6 +887,7 @@ export default class MainMenu extends Module {
|
|||
|
||||
nav: settings,
|
||||
currentItem: current,
|
||||
restoredItem: true, // restored, -- Look into making this smoother later.
|
||||
nav_keys: settings.keys,
|
||||
|
||||
has_unseen,
|
||||
|
|
|
@ -77,12 +77,12 @@ export default class TooltipProvider extends Module {
|
|||
this.on(':cleanup', this.cleanup);
|
||||
}
|
||||
|
||||
|
||||
_createInstance(container, klass = 'ffz-tooltip', default_type) {
|
||||
return new Tooltip(container, klass, {
|
||||
html: true,
|
||||
i18n: this.i18n,
|
||||
live: true,
|
||||
check_modifiers: true,
|
||||
|
||||
delayHide: this.checkDelayHide.bind(this, default_type),
|
||||
delayShow: this.checkDelayShow.bind(this, default_type),
|
||||
|
@ -117,7 +117,6 @@ export default class TooltipProvider extends Module {
|
|||
}
|
||||
|
||||
|
||||
|
||||
onFSChange() {
|
||||
const tip_element = document.fullscreenElement || this.container;
|
||||
if ( tip_element !== this.tip_element ) {
|
||||
|
@ -132,6 +131,7 @@ export default class TooltipProvider extends Module {
|
|||
this.tips.cleanup();
|
||||
}
|
||||
|
||||
|
||||
delegatePopperConfig(default_type, target, tip, pop_opts) {
|
||||
const type = target.dataset.tooltipType || default_type,
|
||||
handler = this.types[type];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue