1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-25 07:20:54 +00:00
This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo...

* Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted.
* Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube.
* Changed: Made the background of highlighted words in chat messages slightly smaller.
* Fixed: A few page elements in mod view not being themed correctly.
* Fixed: Timestamps displaying with an hour when they obviously do not need to.
* API Added: `main_menu:open` event for a general way to open the main menu.
* API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition.
* API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
This commit is contained in:
SirStendec 2023-12-19 16:24:33 -05:00
parent 80931479c1
commit 6e78fd7cab
19 changed files with 528 additions and 47 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.63.0", "version": "4.64.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",

View file

@ -71,7 +71,7 @@ export default {
methods: { methods: {
handleClick(event) { handleClick(event) {
if ( ! this.events.emit || event.ctrlKey || event.shiftKey ) if ( ! this.events?.emit || event.ctrlKey || event.shiftKey )
return; return;
const target = event.currentTarget, const target = event.currentTarget,

View file

@ -6,12 +6,12 @@
import dayjs from 'dayjs'; 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 Module, { buildAddonProxy } from 'utilities/module';
import {Color} from 'utilities/color'; import {Color} from 'utilities/color';
import {createElement, ManagedStyle} from 'utilities/dom'; import {createElement, ManagedStyle} from 'utilities/dom';
import {getFontsList} from 'utilities/fonts'; 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 Badges from './badges';
import Emotes from './emotes'; import Emotes from './emotes';
@ -109,10 +109,10 @@ export default class Chat extends Module {
this.__link_providers = []; this.__link_providers = [];
this._hl_reasons = {}; this._hl_reasons = {};
this.addHighlightReason('mention', 'Mentioned'); this.addHighlightReason('mention', 'Mentioned', 'Mention');
this.addHighlightReason('user', 'Highlight User'); this.addHighlightReason('user', 'Highlight User', 'User');
this.addHighlightReason('badge', 'Highlight Badge'); this.addHighlightReason('badge', 'Highlight Badge', 'Badge');
this.addHighlightReason('term', 'Highlight Term'); this.addHighlightReason('term', 'Highlight Term', 'Term');
// ======================================================================== // ========================================================================
// Settings // 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', { this.settings.add('chat.adjustment-mode', {
default: null, default: null,
process(ctx, val) { process(ctx, val) {
@ -1486,6 +1499,8 @@ export default class Chat extends Module {
this.socket = this.resolve('socket'); this.socket = this.resolve('socket');
this.pubsub = this.resolve('pubsub'); this.pubsub = this.resolve('pubsub');
this.settings.provider.on('changed', this.onProviderChange, this);
this.on('site.subpump:pubsub-message', this.onPubSub, this); this.on('site.subpump:pubsub-message', this.onPubSub, this);
if ( this.context.get('chat.filtering.need-colors') ) 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 ) { if ( typeof key === 'object' && key.key ) {
data = key; data = key;
key = data.key; key = data.key;
@ -2264,16 +2279,26 @@ export default class Chat extends Module {
} else if ( typeof data === 'string' ) } else if ( typeof data === 'string' )
data = {title: data}; data = {title: data};
if ( typeof label === 'string' && label.length > 0 )
data.label = label;
data.value = data.key = key; data.value = data.key = key;
if ( ! data.i18n_key ) if ( ! data.i18n_key )
data.i18n_key = `hl-reason.${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] ) if ( this._hl_reasons[key] )
throw new Error(`Highlight Reason already exists with key ${key}`); throw new Error(`Highlight Reason already exists with key ${key}`);
this._hl_reasons[key] = data; this._hl_reasons[key] = data;
} }
getHighlightReason(key) {
return this._hl_reasons[key] ?? null;
}
getHighlightReasons() { getHighlightReasons() {
return Object.values(this._hl_reasons); 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) ) if ( (info && info[0] && refresh) || (expires && Date.now() > expires) )
info = this._link_info[url] = null; info = this._link_info[url] = null;
if ( info && info[0] ) if ( info && info[0] ) {
return no_promises ? info[2] : Promise.resolve(info[2]); const out = this.handleLinkToS(info[2]);
return no_promises ? out : Promise.resolve(out);
}
if ( no_promises ) if ( no_promises )
return null; return null;
@ -2580,6 +2607,8 @@ export default class Chat extends Module {
info[1] = Date.now() + 120000; info[1] = Date.now() + 120000;
info[2] = success ? data : null; info[2] = success ? data : null;
data = this.handleLinkToS(data);
if ( callbacks ) if ( callbacks )
for(const cbs of callbacks) for(const cbs of callbacks)
cbs[success ? 0 : 1](data); 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) { fixLinkInfo(data) {
if ( ! data ) if ( ! data )
return data; return data;

View file

@ -110,6 +110,8 @@ export const Links = {
fragments: data.fragments, fragments: data.fragments,
i18n_prefix: data.i18n_prefix, i18n_prefix: data.i18n_prefix,
tooltip: true,
allow_media: show_images, allow_media: show_images,
allow_unsafe: show_unsafe, allow_unsafe: show_unsafe,
onload: () => requestAnimationFrame(() => tip.update()) onload: () => requestAnimationFrame(() => tip.update())

View file

@ -0,0 +1,67 @@
<template lang="html">
<section class="ffz--widget ffz--service-tos">
<h4>{{ key }}</h4>
<markdown class="tw-mg-b-05" :source="linkText" />
<button
v-if="hasAccepted"
class="tw-button tw-button--disabled"
disabled
>
<span class="tw-button__text">
{{ t('tooltip.has-accepted', 'You have accepted the Terms of Service.') }}
</span>
</button>
<button
v-else
@click="accept"
class="tw-button"
>
<span class="tw-button__text">
{{ t('tooltip.accept-tos', 'Accept Terms of Service') }}
</span>
</button>
</section>
</template>
<script>
import ProviderMixin from '../provider-mixin';
import { deep_copy } from 'utilities/object';
let last_id = 0;
export default {
mixins: [ProviderMixin],
props: ['item', 'context'],
data() {
return {
id: last_id++,
key: this.item.item
}
},
computed: {
linkText() {
if ( this.data.i18n_links )
return this.t(this.data.i18n_links, this.data.links);
return this.data.links;
},
hasAccepted() {
return Array.isArray(this.value) && this.value.includes(this.key)
}
},
methods: {
accept() {
const val = Array.isArray(this.value) ? [...this.value] : [];
val.push(this.key);
this.set(val);
}
}
}
</script>

View file

@ -250,8 +250,30 @@ export default class MainMenu extends Module {
}); });
this.scheduleUpdate(); 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) { openPopout(item) {
const win = window.open( const win = window.open(
`https://twitch.tv/popout/frankerfacez/chat?ffz-settings${item ? `=${encodeURIComponent(item)}` : ''}`, `https://twitch.tv/popout/frankerfacez/chat?ffz-settings${item ? `=${encodeURIComponent(item)}` : ''}`,

View file

@ -8,13 +8,15 @@ export default {
value: undefined, value: undefined,
has_value: false, has_value: false,
provider_key: this.item.override_setting ?? this.item.setting,
_unseen: false _unseen: false
} }
}, },
created() { created() {
const provider = this.context.provider, const provider = this.context.provider,
setting = this.item.setting; setting = this.provider_key;
provider.on('changed', this._providerChange, this); provider.on('changed', this._providerChange, this);
@ -70,7 +72,7 @@ export default {
methods: { methods: {
_providerChange(key, val, deleted) { _providerChange(key, val, deleted) {
if ( key !== this.item.setting ) if ( key !== this.provider_key )
return; return;
if ( deleted ) { if ( deleted ) {
@ -92,7 +94,7 @@ export default {
if ( typeof validate === 'function' ) if ( typeof validate === 'function' )
return validate(value, this.item, this); return validate(value, this.item, this);
else else
throw new Error(`Invalid Validator for ${this.item.setting}`); throw new Error(`Invalid Validator for ${this.provider_key}`);
} }
return true; return true;
@ -100,7 +102,7 @@ export default {
set(value) { set(value) {
const provider = this.context.provider, const provider = this.context.provider,
setting = this.item.setting; setting = this.provider_key;
// TODO: Run validation. // TODO: Run validation.
@ -124,7 +126,7 @@ export default {
clear() { clear() {
const provider = this.context.provider, const provider = this.context.provider,
setting = this.item.setting; setting = this.provider_key;
provider.delete(setting); provider.delete(setting);
this.value = this.default_value; this.value = this.default_value;

View file

@ -13,6 +13,7 @@ import Twilight from 'site';
import Module from 'utilities/module'; import Module from 'utilities/module';
import SUB_STATUS from './sub_status.gql'; import SUB_STATUS from './sub_status.gql';
import { FFZEvent } from 'src/utilities/events';
//import Tooltip from 'src/utilities/tooltip'; //import Tooltip from 'src/utilities/tooltip';
const TIERS = { const TIERS = {
@ -1419,7 +1420,15 @@ export default class EmoteMenu extends Module {
} }
clickSettings(event) { // eslint-disable-line class-methods-use-this 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)) ) { if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
const win = window.open( const win = window.open(
'https://twitch.tv/popout/frankerfacez/chat?ffz-settings', 'https://twitch.tv/popout/frankerfacez/chat?ffz-settings',
@ -1440,7 +1449,7 @@ export default class EmoteMenu extends Module {
} }
t.emit('site.menu_button:clicked'); t.emit('site.menu_button:clicked');
} }*/
} }
/*clickRefresh(event) { /*clickRefresh(event) {

View file

@ -284,6 +284,21 @@ export default class ChatHook extends Module {
// Settings // 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', { this.settings.add('chat.disable-handling', {
default: null, default: null,
requires: ['context.disable-chat-processing'], requires: ['context.disable-chat-processing'],

View file

@ -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. // Check to see if we have message content to render.
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, current_user), const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, current_user),
has_message = tokens.length > 0 || ! notice; has_message = tokens.length > 0 || ! notice;
@ -1014,6 +1060,7 @@ other {# messages were deleted by a moderator.}
timestamp, timestamp,
t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this), t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this),
this.renderInlineHighlight ? this.renderInlineHighlight() : null, this.renderInlineHighlight ? this.renderInlineHighlight() : null,
hl_position === 2 ? highlight_tags : null,
// Badges // Badges
e('span', { 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); const actions = t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this);
if ( is_raw ) if ( is_raw ) {
notice.ffz_target.unshift(notice.ffz_icon ?? null, timestamp, actions); notice.ffz_target.unshift(
notice.ffz_icon ?? null,
timestamp,
actions,
hl_position === 2 ? highlight_tags : null
);
else } else
notice = [ notice = [
notice.ffz_icon ?? null, notice.ffz_icon ?? null,
timestamp, timestamp,
actions, actions,
hl_position === 2 ? highlight_tags : null,
notice notice
]; ];
} else { } else {
if ( is_raw ) {
if ( notice.ffz_icon ) if ( notice.ffz_icon )
notice.ffz_target.unshift(notice.ffz_icon);
} else if ( notice.ffz_icon )
notice = [ notice = [
notice.ffz_icon, notice.ffz_icon,
notice notice,
]; ];
message = e( message = e(
@ -1114,13 +1171,18 @@ other {# messages were deleted by a moderator.}
className: 'tw-c-text-alt-2' className: 'tw-c-text-alt-2'
}, notice); }, notice);
if ( message ) if ( highlight_tags && hl_position === 1 ) {
out = [highlight_tags, notice, message ?? null];
} else if ( message )
out = [notice, message]; out = [notice, message];
else else
out = notice; out = notice;
} else { } else {
klass = `${klass} chat-line__message`; klass = `${klass} chat-line__message`;
if ( highlight_tags && hl_position === 1 )
out = [highlight_tags, message];
else
out = message; out = message;
} }

View file

@ -7,6 +7,7 @@
import Twilight from 'site'; import Twilight from 'site';
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import { FFZEvent } from 'src/utilities/events';
export default class SettingsMenu extends Module { export default class SettingsMenu extends Module {
constructor(...args) { constructor(...args) {
@ -391,6 +392,26 @@ export default class SettingsMenu extends Module {
} }
click(inst, event) { 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 // 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. // in a popout as we're almost certainly within Popout Chat.
const layout = this.resolve('site.layout'); const layout = this.resolve('site.layout');
@ -425,5 +446,5 @@ export default class SettingsMenu extends Module {
} }
this.closeMenu(inst); this.closeMenu(inst);
} }*/
} }

View file

@ -2,7 +2,7 @@
.mention-fragment--recipient, .mention-fragment--recipient,
.ffz--mention-me { .ffz--mention-me {
border-radius: .5rem; border-radius: .5rem;
padding: .3rem; padding: 0 .3rem;
font-weight: 700; font-weight: 700;
color: #fff; color: #fff;

View file

@ -21,6 +21,7 @@
.video-chat, .video-chat,
.qa-vod-chat, .qa-vod-chat,
.extensions-popover-view-layout, .extensions-popover-view-layout,
.modview-dock-widget__preview__body > div,
.video-card { .video-card {
background-color: var(--color-background-base) !important; background-color: var(--color-background-base) !important;
} }

View file

@ -134,7 +134,8 @@ export const RERENDER_SETTINGS = [
'chat.replies.style', 'chat.replies.style',
'chat.bits.cheer-notice', 'chat.bits.cheer-notice',
'chat.filtering.hidden-tokens', 'chat.filtering.hidden-tokens',
'chat.hype.message-style' 'chat.hype.message-style',
'chat.filtering.show-reasons'
] as const; ] as const;
/** /**
@ -362,3 +363,16 @@ export enum EmoteTypes {
/** Emote unlocked via following a channel. */ /** Emote unlocked via following a channel. */
Follower 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<string, any>;

View file

@ -7,6 +7,7 @@
import {has} from 'utilities/object'; import {has} from 'utilities/object';
import Markdown from 'markdown-it'; import Markdown from 'markdown-it';
import MILA from 'markdown-it-link-attributes'; import MILA from 'markdown-it-link-attributes';
import { FFZEvent } from './events';
export const VERSION = 9; export const VERSION = 9;
@ -16,7 +17,8 @@ const validate = (input, valid) => valid.includes(input) ? input : null;
const VALID_WEIGHTS = ['regular', 'bold', 'semibold'], const VALID_WEIGHTS = ['regular', 'bold', 'semibold'],
VALID_COLORS = ['base', 'alt', 'alt-2', 'link'], 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_WRAPS = ['nowrap', 'pre-wrap'],
VALID_PADDING = { 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 // Token Type: Conditional
@ -606,13 +651,6 @@ function header_vue(token, h, ctx) {
}, out.content)); }, 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); let bgtoken = resolveToken(token.sfw_background, ctx);
const nsfw_bg_token = resolveToken(token.background, ctx); const nsfw_bg_token = resolveToken(token.background, ctx);
if ( nsfw_bg_token && canShowImage(nsfw_bg_token, 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; 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); let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx); const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, 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; 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', { 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'}` 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); }, content);
@ -1317,6 +1444,8 @@ TOKEN_TYPES.style = function(token, createElement, ctx) {
if ( token.color ) { if ( token.color ) {
if ( VALID_COLORS.includes(token.color) ) if ( VALID_COLORS.includes(token.color) )
classes.push(`tw-c-text-${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 else
style.color = token.color; style.color = token.color;
} }

View file

@ -27,7 +27,7 @@ export function duration_to_string(
days = day_count > 0 ? `${day_count} days, ` : ''; 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}${ return `${days}${
show_hours ? `${days && hours < 10 ? '0' : ''}${hours}:` : '' show_hours ? `${days && hours < 10 ? '0' : ''}${hours}:` : ''

View file

@ -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__tooltip {
--ffz-rich-header-outline: var(--color-background-tooltip); --ffz-rich-header-outline: var(--color-background-tooltip);
.ffz--rich-header--background { .ffz--rich-header--background {
@ -118,6 +124,15 @@
> * { > * {
width: 100%; 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 { .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 { .ffz--fields {
display: flex; display: flex;
margin-top: -.5rem; margin-top: -.5rem;

View file

@ -39,6 +39,15 @@
display: inline; 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 { .ffz--player-meta-tray {
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;

View file

@ -244,6 +244,10 @@
display: flex !important; display: flex !important;
} }
.tw-flex-inline {
display: inline-flex !important;
}
.tw-flex-wrap { .tw-flex-wrap {
flex-wrap: wrap !important; flex-wrap: wrap !important;
} }