1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +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",
"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",

View file

@ -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 {
}
}
</script>
</script>

View file

@ -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;

View file

@ -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;
}
}
}

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.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)}` : ''}`,

View file

@ -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);
}
}
}
}

View file

@ -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) {

View file

@ -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'],

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.
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');
}
}
}

View file

@ -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);
}
}
}*/
}

View file

@ -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);
}
}
}

View file

@ -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;
}

View file

@ -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<string, any>;

View file

@ -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);
}
}

View file

@ -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` : ''}`;
}
}

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-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;

View file

@ -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;
}
}

View file

@ -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)
}
}