diff --git a/package.json b/package.json index 9a5103b5..e022f158 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.63.0", + "version": "4.64.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/chat/components/chat-rich.vue b/src/modules/chat/components/chat-rich.vue index a9cd75e0..db4b4f72 100644 --- a/src/modules/chat/components/chat-rich.vue +++ b/src/modules/chat/components/chat-rich.vue @@ -71,7 +71,7 @@ export default { methods: { handleClick(event) { - if ( ! this.events.emit || event.ctrlKey || event.shiftKey ) + if ( ! this.events?.emit || event.ctrlKey || event.shiftKey ) return; const target = event.currentTarget, @@ -392,4 +392,4 @@ export default { } } - \ No newline at end of file + diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 8a6d4ffb..3bb5d468 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -6,12 +6,12 @@ import dayjs from 'dayjs'; -import { DEBUG, LINK_DATA_HOSTS } from 'utilities/constants'; +import { DEBUG, LINK_DATA_HOSTS, RESOLVERS_REQUIRE_TOS } from 'utilities/constants'; import Module, { buildAddonProxy } from 'utilities/module'; import {Color} from 'utilities/color'; import {createElement, ManagedStyle} from 'utilities/dom'; import {getFontsList} from 'utilities/fonts'; -import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars, makeAddonIdChecker} from 'utilities/object'; +import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars, makeAddonIdChecker, deep_copy} from 'utilities/object'; import Badges from './badges'; import Emotes from './emotes'; @@ -109,10 +109,10 @@ export default class Chat extends Module { this.__link_providers = []; this._hl_reasons = {}; - this.addHighlightReason('mention', 'Mentioned'); - this.addHighlightReason('user', 'Highlight User'); - this.addHighlightReason('badge', 'Highlight Badge'); - this.addHighlightReason('term', 'Highlight Term'); + this.addHighlightReason('mention', 'Mentioned', 'Mention'); + this.addHighlightReason('user', 'Highlight User', 'User'); + this.addHighlightReason('badge', 'Highlight Badge', 'Badge'); + this.addHighlightReason('term', 'Highlight Term', 'Term'); // ======================================================================== // Settings @@ -1085,6 +1085,19 @@ export default class Chat extends Module { }); + // Terms of Service Stuff + for(const [key, info] of Object.entries(RESOLVERS_REQUIRE_TOS)) { + this.settings.addUI(`tooltip.tos.${key}`, { + path: 'Chat > Tooltips >> Terms of Service @{"description": "The following services require you to agree to their Terms of Service before we can show you information from their platforms."}', + component: 'tooltip-tos', + item: key, + override_setting: 'agreed-tos', + data: deep_copy(info), + onUIChange: () => this.emit(':update-link-resolver') + }); + } + + this.settings.add('chat.adjustment-mode', { default: null, process(ctx, val) { @@ -1486,6 +1499,8 @@ export default class Chat extends Module { this.socket = this.resolve('socket'); this.pubsub = this.resolve('pubsub'); + this.settings.provider.on('changed', this.onProviderChange, this); + this.on('site.subpump:pubsub-message', this.onPubSub, this); if ( this.context.get('chat.filtering.need-colors') ) @@ -2256,7 +2271,7 @@ export default class Chat extends Module { } - addHighlightReason(key, data) { + addHighlightReason(key, data, label) { if ( typeof key === 'object' && key.key ) { data = key; key = data.key; @@ -2264,16 +2279,26 @@ export default class Chat extends Module { } else if ( typeof data === 'string' ) data = {title: data}; + if ( typeof label === 'string' && label.length > 0 ) + data.label = label; + data.value = data.key = key; if ( ! data.i18n_key ) data.i18n_key = `hl-reason.${key}`; + if ( data.label && ! data.i18n_label ) + data.i18n_label = `${data.i18n_key}.label`; + if ( this._hl_reasons[key] ) throw new Error(`Highlight Reason already exists with key ${key}`); this._hl_reasons[key] = data; } + getHighlightReason(key) { + return this._hl_reasons[key] ?? null; + } + getHighlightReasons() { return Object.values(this._hl_reasons); } @@ -2560,8 +2585,10 @@ export default class Chat extends Module { if ( (info && info[0] && refresh) || (expires && Date.now() > expires) ) info = this._link_info[url] = null; - if ( info && info[0] ) - return no_promises ? info[2] : Promise.resolve(info[2]); + if ( info && info[0] ) { + const out = this.handleLinkToS(info[2]); + return no_promises ? out : Promise.resolve(out); + } if ( no_promises ) return null; @@ -2580,6 +2607,8 @@ export default class Chat extends Module { info[1] = Date.now() + 120000; info[2] = success ? data : null; + data = this.handleLinkToS(data); + if ( callbacks ) for(const cbs of callbacks) cbs[success ? 0 : 1](data); @@ -2612,6 +2641,64 @@ export default class Chat extends Module { }); } + + handleLinkToS(data) { + // Check for YouTube + const agreed = this.settings.provider.get('agreed-tos', []); + const resolvers = data.urls ? new Set(data.urls.map(x => x.resolver).filter(x => x)) : null; + if ( resolvers ) { + for(const [key, info] of Object.entries(RESOLVERS_REQUIRE_TOS)) { + if ( resolvers.has(key) && ! agreed.includes(key) ) { + return { + ...data, + url: null, + short: [ + { + type: 'box', + content: + info.i18n_key + ? {type: 'i18n', key: info.i18n_key, phrase: info.label} + : info.label + }, + { + type: 'flex', + 'justify-content': 'center', + 'align-items': 'center', + content: { + type: 'open_settings', + item: 'chat.tooltips' + } + } + ], + mid: null, + full: null + } + } + } + } + + return data; + } + + + agreeToTerms(service) { + const agreed = this.settings.provider.get('agreed-tos', []); + if ( agreed.includes(service) ) + return; + + this.settings.provider.set('agreed-tos', [...agreed, service]); + this.emit(':update-link-resolver'); + } + + + onProviderChange(key, value) { + if ( key !== 'agreed-tos' ) + return; + + this.emit(':update-link-resolver'); + } + + fixLinkInfo(data) { if ( ! data ) return data; diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 85c1b1ff..66037801 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -110,6 +110,8 @@ export const Links = { fragments: data.fragments, i18n_prefix: data.i18n_prefix, + tooltip: true, + allow_media: show_images, allow_unsafe: show_unsafe, onload: () => requestAnimationFrame(() => tip.update()) @@ -2230,4 +2232,4 @@ export const TwitchEmotes = { return out; } -} \ No newline at end of file +} diff --git a/src/modules/main_menu/components/tooltip-tos.vue b/src/modules/main_menu/components/tooltip-tos.vue new file mode 100644 index 00000000..81b909a5 --- /dev/null +++ b/src/modules/main_menu/components/tooltip-tos.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js index 35f4cdb4..ee518646 100644 --- a/src/modules/main_menu/index.js +++ b/src/modules/main_menu/index.js @@ -250,8 +250,30 @@ export default class MainMenu extends Module { }); this.scheduleUpdate(); + + this.on(':open', evt => { + // If we're on a page with minimal root, we want to open settings + // in a popout as we're almost certainly within Popout Chat. + const layout = this.resolve('site.layout'), + item = evt.item, + event = evt.event; + + if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) { + if ( ! this.openPopout(item) ) + evt.errored = true; + return; + } + + if ( item ) + this.requestPage(item); + if ( this.showing ) + return; + + this.emit('site.menu_button:clicked'); + }); } + openPopout(item) { const win = window.open( `https://twitch.tv/popout/frankerfacez/chat?ffz-settings${item ? `=${encodeURIComponent(item)}` : ''}`, diff --git a/src/modules/main_menu/provider-mixin.js b/src/modules/main_menu/provider-mixin.js index b5da3b24..0ca8a0ea 100644 --- a/src/modules/main_menu/provider-mixin.js +++ b/src/modules/main_menu/provider-mixin.js @@ -8,13 +8,15 @@ export default { value: undefined, has_value: false, + provider_key: this.item.override_setting ?? this.item.setting, + _unseen: false } }, created() { const provider = this.context.provider, - setting = this.item.setting; + setting = this.provider_key; provider.on('changed', this._providerChange, this); @@ -70,7 +72,7 @@ export default { methods: { _providerChange(key, val, deleted) { - if ( key !== this.item.setting ) + if ( key !== this.provider_key ) return; if ( deleted ) { @@ -92,7 +94,7 @@ export default { if ( typeof validate === 'function' ) return validate(value, this.item, this); else - throw new Error(`Invalid Validator for ${this.item.setting}`); + throw new Error(`Invalid Validator for ${this.provider_key}`); } return true; @@ -100,7 +102,7 @@ export default { set(value) { const provider = this.context.provider, - setting = this.item.setting; + setting = this.provider_key; // TODO: Run validation. @@ -124,7 +126,7 @@ export default { clear() { const provider = this.context.provider, - setting = this.item.setting; + setting = this.provider_key; provider.delete(setting); this.value = this.default_value; @@ -134,4 +136,4 @@ export default { this.item.onUIChange(this.value, this); } } -} \ No newline at end of file +} diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index 221f2626..84d9b771 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -13,6 +13,7 @@ import Twilight from 'site'; import Module from 'utilities/module'; import SUB_STATUS from './sub_status.gql'; +import { FFZEvent } from 'src/utilities/events'; //import Tooltip from 'src/utilities/tooltip'; const TIERS = { @@ -1419,7 +1420,15 @@ export default class EmoteMenu extends Module { } clickSettings(event) { // eslint-disable-line class-methods-use-this - const layout = t.resolve('site.layout'); + const evt = new FFZEvent({ + item: 'chat.emote_menu', + event, + errored: false + }); + + t.emit('main_menu:open', evt); + + /*const layout = t.resolve('site.layout'); if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) { const win = window.open( 'https://twitch.tv/popout/frankerfacez/chat?ffz-settings', @@ -1440,7 +1449,7 @@ export default class EmoteMenu extends Module { } t.emit('site.menu_button:clicked'); - } + }*/ } /*clickRefresh(event) { diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 707f9ebc..9a91b3ee 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -284,6 +284,21 @@ export default class ChatHook extends Module { // Settings + this.settings.add('chat.filtering.show-reasons', { + default: false, + ui: { + path: 'Chat > Filtering > General >> Appearance', + title: 'Display Reasons', + description: 'If this is enabled, the reasons a given message was highlighted will be displayed alongside the message. This is a simple display. Enable the debugging option below in Behavior for more details, but be aware that the debugging option has a slight performance impact compared to this.', + component: 'setting-select-box', + data: [ + {value: false, title: 'Disabled'}, + {value: 1, title: 'Above Message'}, + {value: 2, title: 'Inline'} + ] + } + }); + this.settings.add('chat.disable-handling', { default: null, requires: ['context.disable-chat-processing'], diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 6ee2cc75..e5acba64 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -927,6 +927,52 @@ other {# messages were deleted by a moderator.} } } + // Are we listing highlight reasons? + let highlight_tags = null; + const hl_position = t.chat.context.get('chat.filtering.show-reasons'); + + if ( msg.highlights?.size && hl_position ) { + highlight_tags = []; + + for(const tag of msg.highlights) { + const reason = t.chat._hl_reasons[tag]; + let label, + tooltip; + + if ( reason ) { + tooltip = reason.i18n_key + ? t.i18n.t(reason.i18n_key, reason.title) + : reason.title; + + label = reason.i18n_label + ? t.i18n.t(reason.i18n_label, reason.label) + : reason.label; + + if ( label === tooltip ) + tooltip = null; + } + + if ( ! label ) + label = tag; + + highlight_tags.push(e('span', { + className: `ffz-pill ffz-highlight-tag${reason ? ' ffz-tooltip' : ''}`, + 'data-title': tooltip + }, label)); + } + + if ( highlight_tags.length > 0 ) + highlight_tags = e('span', { + className: `ffz-highlight-tags ${ + hl_position === 1 + ? 'ffz-highlight-tags__above' + : 'tw-mg-r-05' + }` + }, highlight_tags); + else + highlight_tags = null; + } + // Check to see if we have message content to render. const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, current_user), has_message = tokens.length > 0 || ! notice; @@ -1014,6 +1060,7 @@ other {# messages were deleted by a moderator.} timestamp, t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this), this.renderInlineHighlight ? this.renderInlineHighlight() : null, + hl_position === 2 ? highlight_tags : null, // Badges e('span', { @@ -1076,22 +1123,32 @@ other {# messages were deleted by a moderator.} const actions = t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this); - if ( is_raw ) - notice.ffz_target.unshift(notice.ffz_icon ?? null, timestamp, actions); + if ( is_raw ) { + notice.ffz_target.unshift( + notice.ffz_icon ?? null, + timestamp, + actions, + hl_position === 2 ? highlight_tags : null + ); - else + } else notice = [ notice.ffz_icon ?? null, timestamp, actions, + hl_position === 2 ? highlight_tags : null, notice ]; } else { - if ( notice.ffz_icon ) + if ( is_raw ) { + if ( notice.ffz_icon ) + notice.ffz_target.unshift(notice.ffz_icon); + + } else if ( notice.ffz_icon ) notice = [ notice.ffz_icon, - notice + notice, ]; message = e( @@ -1114,14 +1171,19 @@ other {# messages were deleted by a moderator.} className: 'tw-c-text-alt-2' }, notice); - if ( message ) + if ( highlight_tags && hl_position === 1 ) { + out = [highlight_tags, notice, message ?? null]; + } else if ( message ) out = [notice, message]; else out = notice; } else { klass = `${klass} chat-line__message`; - out = message; + if ( highlight_tags && hl_position === 1 ) + out = [highlight_tags, message]; + else + out = message; } // Check for hover actions, as those require we wrap the output in a few extra elements. @@ -1393,4 +1455,4 @@ other {# messages were deleted by a moderator.} this.emit('chat:updated-lines'); } -} \ No newline at end of file +} diff --git a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx index 93cd7843..6d472802 100644 --- a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx @@ -7,6 +7,7 @@ import Twilight from 'site'; import Module from 'utilities/module'; import {createElement} from 'utilities/dom'; +import { FFZEvent } from 'src/utilities/events'; export default class SettingsMenu extends Module { constructor(...args) { @@ -391,6 +392,26 @@ export default class SettingsMenu extends Module { } click(inst, event) { + const target = event.currentTarget, + page = target && target.dataset && target.dataset.page; + + const evt = new FFZEvent({ + item: page, + event, + errored: false + }); + + this.emit('main_menu:open', evt); + if ( evt.errored ) { + this.cant_window = true; + this.SettingsMenu.forceUpdate(); + return; + } + + this.closeMenu(inst); + } + + /*old_click(inst, event) { // If we're on a page with minimal root, we want to open settings // in a popout as we're almost certainly within Popout Chat. const layout = this.resolve('site.layout'); @@ -425,5 +446,5 @@ export default class SettingsMenu extends Module { } this.closeMenu(inst); - } -} \ No newline at end of file + }*/ +} diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss index 619e6dcd..a3a8da6c 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss @@ -2,7 +2,7 @@ .mention-fragment--recipient, .ffz--mention-me { border-radius: .5rem; - padding: .3rem; + padding: 0 .3rem; font-weight: 700; color: #fff; @@ -12,4 +12,4 @@ color: #000; background-color: rgba(255,255,255,0.5); } -} \ No newline at end of file +} diff --git a/src/sites/twitch-twilight/styles/color_normalizer.scss b/src/sites/twitch-twilight/styles/color_normalizer.scss index 553ad417..25b28930 100644 --- a/src/sites/twitch-twilight/styles/color_normalizer.scss +++ b/src/sites/twitch-twilight/styles/color_normalizer.scss @@ -21,6 +21,7 @@ .video-chat, .qa-vod-chat, .extensions-popover-view-layout, + .modview-dock-widget__preview__body > div, .video-card { background-color: var(--color-background-base) !important; } diff --git a/src/utilities/constants.ts b/src/utilities/constants.ts index 492ec1b3..d002680f 100644 --- a/src/utilities/constants.ts +++ b/src/utilities/constants.ts @@ -134,7 +134,8 @@ export const RERENDER_SETTINGS = [ 'chat.replies.style', 'chat.bits.cheer-notice', 'chat.filtering.hidden-tokens', - 'chat.hype.message-style' + 'chat.hype.message-style', + 'chat.filtering.show-reasons' ] as const; /** @@ -362,3 +363,16 @@ export enum EmoteTypes { /** Emote unlocked via following a channel. */ Follower }; + + +export const RESOLVERS_REQUIRE_TOS = { + 'YouTube': { + label: 'You must agree to the YouTube Terms of Service to view this embed.', + i18n_key: 'embed.warn.youtube', + + i18n_links: 'embed.warn.youtube.links', + links: `To view YouTube embeds, you must agree to YouTube's Terms of Service: +* [Terms of Service](https://www.youtube.com/t/terms) +* [Privacy Policy](https://policies.google.com/privacy)` + } +} as Record; diff --git a/src/utilities/rich_tokens.js b/src/utilities/rich_tokens.js index b57dbbc0..fed7987f 100644 --- a/src/utilities/rich_tokens.js +++ b/src/utilities/rich_tokens.js @@ -7,6 +7,7 @@ import {has} from 'utilities/object'; import Markdown from 'markdown-it'; import MILA from 'markdown-it-link-attributes'; +import { FFZEvent } from './events'; export const VERSION = 9; @@ -16,7 +17,8 @@ const validate = (input, valid) => valid.includes(input) ? input : null; const VALID_WEIGHTS = ['regular', 'bold', 'semibold'], VALID_COLORS = ['base', 'alt', 'alt-2', 'link'], - VALID_SIZES = ['1', '2,' ,'3','4','5','6','7','8'], + VALID_COLORS_TWO = ['youtube'], + VALID_SIZES = ['1', '2', '3', '4', '5', '6', '7', '8'], VALID_WRAPS = ['nowrap', 'pre-wrap'], VALID_PADDING = { @@ -330,6 +332,49 @@ TOKEN_TYPES.box = function(token, createElement, ctx) { } +// ============================================================================ +// Token Type: open_settings +// ============================================================================ + +TOKEN_TYPES.open_settings = function(token, createElement, ctx) { + + if ( ctx.tooltip ) + return null; + + const handler = event => { + const evt = new FFZEvent({ + item: token.item, + event, + errored: false + }); + + window.FrankerFaceZ.get().emit('main_menu:open', evt); + } + + const label = ctx.i18n.t('embed.show-settings', 'Open Settings'); + + if ( ctx.vue ) + return createElement('button', { + class: 'tw-button', + on: { + click: handler + } + }, [ + createElement('span', { + class: 'tw-button__text' + }, [ + label + ]) + ]); + + return createElement('button', { + className: 'tw-button', + onClick: handler + }, createElement('span', { + className: 'tw-button__text' + }, label)); +} + // ============================================================================ // Token Type: Conditional @@ -606,13 +651,6 @@ function header_vue(token, h, ctx) { }, out.content)); } - content = h('div', { - class: [ - 'tw-flex tw-full-width tw-overflow-hidden', - token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1' - ] - }, content); - let bgtoken = resolveToken(token.sfw_background, ctx); const nsfw_bg_token = resolveToken(token.background, ctx); if ( nsfw_bg_token && canShowImage(nsfw_bg_token, ctx) ) @@ -632,6 +670,54 @@ function header_vue(token, h, ctx) { background = renderWithCapture(token.background, h, ctx, token.markdown).content; } + let subtok = resolveToken(token.sub_logo, ctx); + if ( ! token.compact && subtok && canShowImage(subtok, ctx) ) { + const aspect = subtok.aspect; + + let image; + + if ( subtok.type === 'image' ) + image = render_image({ + ...subtok, + aspect: undefined + }, h, ctx); + + if ( subtok.type === 'icon' ) + image = h('figure', { + class: `ffz-i-${subtok.name}` + }); + + if ( image ) { + image = h('div', { + class: `ffz--header-sublogo tw-flex-shrink-0 ${subtok.youtube_dumb ? 'tw-mg-l-05 tw-mg-r-1' : 'tw-mg-r-05'}${aspect ? ' ffz--header-aspect' : ''}`, + style: { + width: aspect ? `${aspect * 2}rem` : null + } + }, [image]); + + const title = content.shift(); + + content = [ + title, + h('div', { + class: 'tw-flex tw-full-width tw-align-items-center' + }, [ + image, + h('div', { + class: `tw-flex tw-full-width tw-overflow-hidden tw-justify-content-center tw-flex-column tw-flex-grow-1` + }, content) + ]) + ]; + } + } + + content = h('div', { + class: [ + 'tw-flex tw-full-width tw-overflow-hidden', + token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1' + ] + }, content); + let imtok = resolveToken(token.sfw_image, ctx); const nsfw_token = resolveToken(token.image, ctx); if ( nsfw_token && canShowImage(nsfw_token, ctx) ) @@ -759,6 +845,47 @@ function header_normal(token, createElement, ctx) { background = renderWithCapture(token.background, createElement, ctx, token.markdown).content; } + let subtok = resolveToken(token.sub_logo, ctx); + if ( ! token.compact && subtok && canShowImage(subtok, ctx) ) { + const aspect = subtok.aspect; + + let image; + + if ( subtok.type === 'image' ) + image = render_image({ + ...subtok, + aspect: undefined + }, createElement, ctx); + + if ( subtok.type === 'icon' ) + image = createElement('figure', { + className: `ffz-i-${subtok.name}` + }); + + if ( image ) { + image = createElement('div', { + className: `ffz--header-sublogo tw-flex-shrink-0 ${subtok.youtube_dumb ? 'tw-mg-l-05 tw-mg-r-1' : 'tw-mg-r-05'}${aspect ? ' ffz--header-aspect' : ''}`, + style: { + width: aspect ? `${aspect * 2}rem` : null + } + }, image); + + const title = content.shift(); + + content = [ + title, + createElement('div', { + className: 'tw-flex tw-full-width tw-align-items-center' + }, [ + image, + createElement('div', { + className: `tw-flex tw-full-width tw-overflow-hidden tw-justify-content-center tw-flex-column tw-flex-grow-1` + }, content) + ]) + ]; + } + } + content = createElement('div', { className: `tw-flex tw-full-width tw-overflow-hidden ${token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'}` }, content); @@ -1317,6 +1444,8 @@ TOKEN_TYPES.style = function(token, createElement, ctx) { if ( token.color ) { if ( VALID_COLORS.includes(token.color) ) classes.push(`tw-c-text-${token.color}`); + else if ( VALID_COLORS_TWO.includes(token.color) ) + classes.push(`ffz-c-text-${token.color}`); else style.color = token.color; } @@ -1421,4 +1550,4 @@ TOKEN_TYPES.tag = function(token, createElement, ctx) { ...attrs, className: token.class || '' }, content); -} \ No newline at end of file +} diff --git a/src/utilities/time.ts b/src/utilities/time.ts index c75503d8..00d2e6d5 100644 --- a/src/utilities/time.ts +++ b/src/utilities/time.ts @@ -27,7 +27,7 @@ export function duration_to_string( days = day_count > 0 ? `${day_count} days, ` : ''; } - const show_hours = (!no_hours || days || hours); + const show_hours = (no_hours === false || days?.length > 0 || hours > 0); return `${days}${ show_hours ? `${days && hours < 10 ? '0' : ''}${hours}:` : '' @@ -68,4 +68,4 @@ export function durationForURL(elapsed: number) { minutes = minutes % 60; return `${hours > 0 ? `${hours}h` : ''}${minutes > 0 ? `${minutes}m` : ''}${seconds > 0 ? `${seconds}s` : ''}`; -} \ No newline at end of file +} diff --git a/styles/chat.scss b/styles/chat.scss index c99358c7..71c3ae2b 100644 --- a/styles/chat.scss +++ b/styles/chat.scss @@ -55,6 +55,12 @@ } } +.ffz--rich-header div span[class="ffz-i-youtube-play"] { + font-size: 166%; + line-height: 1em; + vertical-align: middle; +} + .ffz__tooltip { --ffz-rich-header-outline: var(--color-background-tooltip); .ffz--rich-header--background { @@ -118,6 +124,15 @@ > * { width: 100%; } + + .ffz--overlay .ffz--overlay__bit[data-side=center] { + border-radius: 50%; + aspect-ratio: 1; + + display: flex; + align-items: center; + justify-content: center; + } } .ffz--overlay { @@ -283,6 +298,28 @@ } } + +.ffz-highlight-tags__above { + display: block; + margin-top: -0.5rem; +} + +.ffz-highlight-tag:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.ffz-highlight-tag + .ffz-highlight-tag { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.ffz-highlight-tag { + margin-right: 2px; + background: rgba(127,127,127,0.35); + color: inherit; +} + .ffz--fields { display: flex; margin-top: -.5rem; diff --git a/styles/main.scss b/styles/main.scss index 0cc4607a..7fa545e0 100644 --- a/styles/main.scss +++ b/styles/main.scss @@ -39,6 +39,15 @@ display: inline; } +.ffz__tooltip .ffz-c-text-youtube, +.tw-root--theme-dark .ffz-c-text-youtube { + color: #fff !important; +} +.tw-root--theme-dark .ffz__tooltip .ffz-c-text-youtube, +.ffz-c-text-youtube { + color: #212121 !important; +} + .ffz--player-meta-tray { position: absolute; bottom: 100%; @@ -152,4 +161,4 @@ .ffz-aspect--align-bottom > :not(.ffz-aspect__spacer) { bottom: 0; -} \ No newline at end of file +} diff --git a/styles/native/index.scss b/styles/native/index.scss index 6b5345a9..dad74ad0 100644 --- a/styles/native/index.scss +++ b/styles/native/index.scss @@ -244,6 +244,10 @@ display: flex !important; } +.tw-flex-inline { + display: inline-flex !important; +} + .tw-flex-wrap { flex-wrap: wrap !important; } @@ -2728,4 +2732,4 @@ .tw-core-button-label--dropdown { padding-right: .8rem; padding-right: calc(var(--button-padding-x) - .2rem) -} \ No newline at end of file +}