mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.14.7
* Added: Reset Player is back. It isn't quite as robust as it was before, but it should still prove helpful when faced with certain playback issues. * Added: Clicking the `Stream Uptime` metadata now opens a small pop-up letting the user copy a link to the archived video of the current broadcast, at the current time. Please be aware that archives are not updated in real time and the link will not work for several minutes. * Changed: The `Playback Statistics` metadata now renders as a button to show feedback that you can, in fact, click on it. Doing so toggles the visibility of the player's statistics window. * Fixed: Issue importing a single profile without an update URL causing no data to be imported. (Closes #645) * Fixed: Styles for metadata buttons without borders. * Fixed: Positioning of the PiP and Reset Player buttons. * Fixed: The `Playback Statistics` metadata displaying numbers a bit too precisely for easy reading.
This commit is contained in:
parent
6547497d02
commit
91826e0005
22 changed files with 385 additions and 80 deletions
|
@ -573,6 +573,12 @@
|
|||
"css": "left-open",
|
||||
"code": 59446,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "c8585e1e5b0467f28b70bce765d5840c",
|
||||
"css": "docs",
|
||||
"code": 61637,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.14.6",
|
||||
"version": "4.14.7",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
Binary file not shown.
|
@ -122,6 +122,8 @@
|
|||
|
||||
<glyph glyph-name="github" unicode="" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="docs" unicode="" d="M946 636q23 0 38-16t16-38v-678q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v160h-303q-23 0-38 16t-16 38v375q0 22 11 49t27 42l228 228q15 16 42 27t49 11h232q23 0 38-16t16-38v-183q38 23 71 23h232z m-303-119l-167-167h167v167z m-357 214l-167-167h167v167z m109-361l176 176v233h-214v-233q0-22-15-37t-38-16h-233v-357h286v143q0 22 11 49t27 42z m534-449v643h-215v-232q0-22-15-38t-38-15h-232v-358h500z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="sort-down" unicode="" d="M571 243q0-15-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 10-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
|
||||
|
||||
<glyph glyph-name="sort-up" unicode="" d="M571 457q0-14-10-25t-25-11h-500q-15 0-25 11t-11 25 11 25l250 250q10 11 25 11t25-11l250-250q10-10 10-25z" horiz-adv-x="571.4" />
|
||||
|
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -324,11 +324,10 @@ export default {
|
|||
|
||||
importProfile(profile_data, data) {
|
||||
this.import_profile = profile_data;
|
||||
this.import_profile_data = data;
|
||||
|
||||
if ( profile_data.url ) {
|
||||
this.import_profile_data = data;
|
||||
if ( profile_data.url )
|
||||
return;
|
||||
}
|
||||
|
||||
this.confirmImport(false);
|
||||
},
|
||||
|
@ -342,6 +341,8 @@ export default {
|
|||
if ( ! allow_update )
|
||||
delete profile_data.url;
|
||||
|
||||
console.log('Importing', profile_data, data, this.import_data);
|
||||
|
||||
const prof = this.context.createProfile(profile_data);
|
||||
|
||||
prof.update({
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
|
||||
import {maybe_call} from 'utilities/object';
|
||||
|
||||
import {duration_to_string} from 'utilities/time';
|
||||
import {duration_to_string, durationForURL} from 'utilities/time';
|
||||
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
import Module from 'utilities/module';
|
||||
|
@ -75,6 +75,9 @@ export default class Metadata extends Module {
|
|||
|
||||
|
||||
this.definitions.uptime = {
|
||||
inherit: true,
|
||||
no_arrow: true,
|
||||
|
||||
refresh() { return this.settings.get('metadata.uptime') > 0 },
|
||||
|
||||
setup(data) {
|
||||
|
@ -89,7 +92,8 @@ export default class Metadata extends Module {
|
|||
|
||||
return {
|
||||
created,
|
||||
uptime: created ? Math.floor((now - created.getTime()) / 1000) : -1
|
||||
uptime: created ? Math.floor((now - created.getTime()) / 1000) : -1,
|
||||
getBroadcastID: data.getBroadcastID
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -118,11 +122,66 @@ export default class Metadata extends Module {
|
|||
'(since {since,datetime})',
|
||||
{since: data.created}
|
||||
)}</div>`;
|
||||
},
|
||||
|
||||
async popup(data, tip) {
|
||||
const [permission, broadcast_id] = await Promise.all([
|
||||
navigator.permissions.query({name: 'clipboard-write'}),
|
||||
data.getBroadcastID()
|
||||
]);
|
||||
if ( ! broadcast_id )
|
||||
return (<div>
|
||||
{ this.i18n.t('metadata.uptime-no-id', 'Sorry, we couldn\'t find an archived video for the current broadcast.') }
|
||||
</div>);
|
||||
|
||||
const url = `https://www.twitch.tv/videos/${broadcast_id}${data.uptime > 0 ? `?t=${durationForURL(data.uptime)}` : ''}`,
|
||||
can_copy = permission?.state === 'granted' || permission?.state === 'prompt';
|
||||
|
||||
const copy = can_copy ? e => {
|
||||
navigator.clipboard.writeText(url);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} : null;
|
||||
|
||||
tip.element.classList.add('tw-balloon--lg');
|
||||
|
||||
return (<div>
|
||||
<div class="tw-pd-b-1 tw-mg-b-1 tw-border-b tw-semibold">
|
||||
{ this.i18n.t('metadata.uptime.link-to', 'Link to {time}', {
|
||||
time: duration_to_string(data.uptime, false, false, false, false)
|
||||
}) }
|
||||
</div>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<input
|
||||
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input tw-full-width"
|
||||
type="text"
|
||||
value={url}
|
||||
onFocus={e => e.target.select()}
|
||||
/>
|
||||
{can_copy && <div class="tw-relative tw-tooltip-wrapper tw-mg-l-1">
|
||||
<button
|
||||
class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
|
||||
aria-label={ this.i18n.t('metadata.uptime.copy', 'Copy to Clipboard') }
|
||||
onClick={copy}
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0">
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-docs" />
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="tw-tooltip tw-tooltip--align-right tw-tooltip--up">
|
||||
{ this.i18n.t('metadata.uptime.copy', 'Copy to Clipboard') }
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
this.definitions['player-stats'] = {
|
||||
button: false,
|
||||
button: true,
|
||||
inherit: true,
|
||||
|
||||
refresh() {
|
||||
return this.settings.get('metadata.player-stats')
|
||||
|
@ -170,10 +229,10 @@ export default class Metadata extends Module {
|
|||
videoWidth,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
fps: (maybe_call(player.getVideoFrameRate, player) || 0).toFixed(2),
|
||||
fps: Math.floor(maybe_call(player.getVideoFrameRate, player) || 0),
|
||||
hlsLatencyBroadcaster: player.stats?.broadcasterLatency,
|
||||
hlsLatencyEncoder: player.stats?.transcoderLatency,
|
||||
playbackRate: (maybe_call(player.getVideoBitRate, player) || 0) / 1000,
|
||||
playbackRate: Math.floor((maybe_call(player.getVideoBitRate, player) || 0) / 1000),
|
||||
skippedFrames: maybe_call(player.getDroppedFrames, player),
|
||||
}
|
||||
}
|
||||
|
@ -691,17 +750,8 @@ export default class Metadata extends Module {
|
|||
button = true;
|
||||
|
||||
let btn, popup;
|
||||
const border = maybe_call(def.border, this, data);
|
||||
|
||||
/* let cls = maybe_call(def.button, this, data);
|
||||
|
||||
if ( typeof cls !== 'string' )
|
||||
cls = cls ? 'tw-border-t tw-border-l tw-border-b '
|
||||
|
||||
if ( typeof cls !== 'string' )
|
||||
cls = cls ? 'ffz-button--hollow' : 'tw-button--text';
|
||||
|
||||
const fix = cls === 'tw-button--text';*/
|
||||
const border = maybe_call(def.border, this, data),
|
||||
inherit = maybe_call(def.inherit, this, data);
|
||||
|
||||
if ( typeof icon === 'string' )
|
||||
icon = (<span class="tw-mg-r-05">
|
||||
|
@ -709,29 +759,13 @@ export default class Metadata extends Module {
|
|||
</span>);
|
||||
|
||||
if ( def.popup && def.click ) {
|
||||
/*el = (<span
|
||||
class={`ffz-stat${fix ? ' ffz-fix-padding--left' : ''} tw-inline-flex tw-align-items-center`}
|
||||
data-key={key}
|
||||
tip_content={tooltip}
|
||||
>
|
||||
{btn = (<button class={`tw-button ${cls} ffz-has-stat-arrow`}>
|
||||
{icon}
|
||||
{stat = (<span class="ffz-stat-text tw-button__text" />)}
|
||||
</button>)}
|
||||
{popup = (<button class={`tw-button ${cls} ffz-stat-arrow`}>
|
||||
<span class="tw-button__icon tw-pd-x-0">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>
|
||||
</button>)}
|
||||
</span>);*/
|
||||
|
||||
el = (<div
|
||||
class="tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat tw-mg-l-1 ffz-stat--fix-padding"
|
||||
class={`tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat ffz-stat--fix-padding ${border ? 'tw-mg-l-1' : 'tw-mg-l-05 ffz-mg-r--05'}`}
|
||||
data-key={key}
|
||||
tip_content={tooltip}
|
||||
>
|
||||
{btn = (<button
|
||||
class={`tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-core-button tw-core-button--padded tw-core-button--text tw-c-text-base tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative${border ? ' tw-border-l tw-border-t tw-border-b' : ''}`}
|
||||
class={`tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-core-button tw-core-button--padded tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ${border ? 'tw-border-l tw-border-t tw-border-b' : 'tw-font-size-5 tw-regular'}`}
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center">
|
||||
{icon}
|
||||
|
@ -739,7 +773,7 @@ export default class Metadata extends Module {
|
|||
</div>
|
||||
</button>)}
|
||||
{popup = (<button
|
||||
class={`tw-align-items-center tw-align-middle tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text tw-c-text-base tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative${border ? ' tw-border' : ''}`}
|
||||
class={`tw-align-items-center tw-align-middle tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ${border ? 'tw-border' : 'tw-font-size-5 tw-regular'}`}
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center">
|
||||
<span>
|
||||
|
@ -751,31 +785,19 @@ export default class Metadata extends Module {
|
|||
|
||||
} else
|
||||
btn = popup = el = (<button
|
||||
class={`ffz-stat tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text tw-c-text-base tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative tw-mg-l-1 tw-pd-x-05 ffz-stat--fix-padding${border ? ' tw-border' : ''}`}
|
||||
class={`ffz-stat tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative tw-pd-x-05 ffz-stat--fix-padding ${border ? 'tw-border tw-mg-l-1' : 'tw-font-size-5 tw-regular tw-mg-l-05 ffz-mg-r--05'}`}
|
||||
data-key={key}
|
||||
tip_content={tooltip}
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center">
|
||||
{icon}
|
||||
{stat = (<span class="ffz-stat-text" />)}
|
||||
{def.popup && <span class="tw-mg-l-05">
|
||||
{def.popup && ! def.no_arrow && <span class="tw-mg-l-05">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>}
|
||||
</div>
|
||||
</button>);
|
||||
|
||||
/*btn = popup = el = (<button
|
||||
class={`ffz-stat${fix ? ' ffz-fix-padding' : ''} tw-button tw-inline-flex tw-align-items-center ${cls}`}
|
||||
data-key={key}
|
||||
tip_content={tooltip}
|
||||
>
|
||||
{icon}
|
||||
{stat = <span class="ffz-stat-text tw-button__text" />}
|
||||
{def.popup && <span class="tw-button__icon tw-button__icon--right">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>}
|
||||
</button>);*/
|
||||
|
||||
if ( def.click )
|
||||
btn.addEventListener('click', e => {
|
||||
if ( el._ffz_fading || btn.disabled || btn.classList.contains('disabled') || el.disabled || el.classList.contains('disabled') )
|
||||
|
@ -831,7 +853,7 @@ export default class Metadata extends Module {
|
|||
tooltipClass: 'ffz-metadata-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
|
||||
// Hide the arrow for now, until we re-do our CSS to make it render correctly.
|
||||
arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute',
|
||||
arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
|
||||
arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
|
||||
innerClass: 'tw-pd-1',
|
||||
|
||||
popper: {
|
||||
|
|
|
@ -105,6 +105,58 @@ export default class ChannelBar extends Module {
|
|||
}
|
||||
|
||||
|
||||
getBroadcastID(inst) {
|
||||
const current_id = inst.props?.channel?.stream?.id;
|
||||
if ( current_id === inst._ffz_stream_id ) {
|
||||
if ( Date.now() - inst._ffz_broadcast_saved < 60000 )
|
||||
return Promise.resolve(inst._ffz_broadcast_id);
|
||||
}
|
||||
|
||||
return new Promise(async (s, f) => {
|
||||
if ( inst._ffz_broadcast_updating )
|
||||
return inst._ffz_broadcast_updating.push([s, f]);
|
||||
|
||||
inst._ffz_broadcast_updating = [[s, f]];
|
||||
|
||||
let id, err;
|
||||
|
||||
try {
|
||||
id = await this.twitch_data.getBroadcastID(inst.props.channel.id);
|
||||
} catch(error) {
|
||||
id = null;
|
||||
err = error;
|
||||
}
|
||||
|
||||
const waiters = inst._ffz_broadcast_updating;
|
||||
inst._ffz_broadcast_updating = null;
|
||||
|
||||
if ( current_id !== inst.props?.channel?.stream?.id ) {
|
||||
err = new Error('Outdated');
|
||||
inst._ffz_stream_id = null;
|
||||
inst._ffz_broadcast_saved = 0;
|
||||
inst._ffz_broadcast_id = null;
|
||||
|
||||
for(const pair of waiters)
|
||||
pair[1](err);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
inst._ffz_broadcast_id = id;
|
||||
inst._ffz_broadcast_saved = Date.now();
|
||||
inst._ffz_stream_id = current_id;
|
||||
|
||||
if ( err ) {
|
||||
for(const pair of waiters)
|
||||
pair[1](err);
|
||||
} else {
|
||||
for(const pair of waiters)
|
||||
pair[0](id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async updateUptime(inst) {
|
||||
const current_id = inst?.props?.channel?.id;
|
||||
if ( current_id === inst._ffz_uptime_id ) {
|
||||
|
@ -156,7 +208,8 @@ export default class ChannelBar extends Module {
|
|||
meta: inst._ffz_meta,
|
||||
hosting: false,
|
||||
legacy: true,
|
||||
_inst: inst
|
||||
_inst: inst,
|
||||
getBroadcastID: () => this.getBroadcastID(inst)
|
||||
}
|
||||
|
||||
for(const key of keys)
|
||||
|
|
|
@ -49,6 +49,12 @@ export default class Player extends Module {
|
|||
['user', 'video', 'user-video', 'user-clip']
|
||||
);
|
||||
|
||||
this.PlayerSource = this.fine.define(
|
||||
'player-source',
|
||||
n => n.setSrc && n.setInitialPlaybackSettings,
|
||||
false
|
||||
);
|
||||
|
||||
|
||||
// Settings
|
||||
|
||||
|
@ -67,19 +73,19 @@ export default class Player extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
/*this.settings.add('player.button.reset', {
|
||||
this.settings.add('player.button.reset', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Player > General >> General',
|
||||
title: 'Add a `Reset Player` button to the player controls.',
|
||||
description: "Double-clicking the Reset Player button destroys and recreates Twitch's player, potentially fixing playback issues without a full page refresh.",
|
||||
description: "Double-clicking the Reset Player button attempts to reset the Twitch player's internal state, fixing playback issues without a full page refresh.",
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: () => {
|
||||
for(const inst of this.Player.instances)
|
||||
this.addResetButton(inst);
|
||||
}
|
||||
});*/
|
||||
});
|
||||
|
||||
if ( document.pictureInPictureEnabled )
|
||||
this.settings.add('player.button.pip', {
|
||||
|
@ -386,6 +392,9 @@ export default class Player extends Module {
|
|||
if ( ! this._ffzUpdateState )
|
||||
this._ffzUpdateState = this.ffzUpdateState.bind(this);
|
||||
|
||||
if ( ! this._ffzErrorReset )
|
||||
this._ffzErrorReset = t.addErrorResetButton.bind(t, this);
|
||||
|
||||
const inst = this,
|
||||
old_active = this.setPlayerActive,
|
||||
old_inactive = this.setPlayerInactive;
|
||||
|
@ -415,6 +424,7 @@ export default class Player extends Module {
|
|||
if ( events ) {
|
||||
on(events, 'Playing', this._ffzUpdateState);
|
||||
on(events, 'PlayerError', this._ffzUpdateState);
|
||||
on(events, 'PlayerError', this._ffzErrorReset);
|
||||
on(events, 'Ended', this._ffzUpdateState);
|
||||
on(events, 'Ended', this.ffzOnEnded);
|
||||
on(events, 'Idle', this._ffzUpdateState);
|
||||
|
@ -431,6 +441,7 @@ export default class Player extends Module {
|
|||
if ( events && this._ffzUpdateState ) {
|
||||
off(events, 'Playing', this._ffzUpdateState);
|
||||
off(events, 'PlayerError', this._ffzUpdateState);
|
||||
off(events, 'PlayerError', this._ffzErrorReset);
|
||||
off(events, 'Ended', this._ffzUpdateState);
|
||||
off(events, 'Ended', this.ffzOnEnded);
|
||||
off(events, 'Idle', this._ffzUpdateState);
|
||||
|
@ -440,6 +451,7 @@ export default class Player extends Module {
|
|||
|
||||
this._ffz_state_raf = null;
|
||||
this._ffzUpdateState = null;
|
||||
this._ffzErrorReset = null;
|
||||
this.ffzOnEnded = null;
|
||||
}
|
||||
|
||||
|
@ -573,9 +585,8 @@ export default class Player extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.Player.on('mount', inst => {
|
||||
this.updateGUI(inst);
|
||||
});
|
||||
this.Player.on('mount', this.updateGUI, this);
|
||||
this.Player.on('update', this.updateGUI, this);
|
||||
|
||||
this.Player.on('unmount', inst => {
|
||||
inst.ffzUninstall();
|
||||
|
@ -711,6 +722,7 @@ export default class Player extends Module {
|
|||
|
||||
updateGUI(inst) {
|
||||
this.addPiPButton(inst);
|
||||
this.addResetButton(inst);
|
||||
}
|
||||
|
||||
|
||||
|
@ -753,7 +765,11 @@ export default class Player extends Module {
|
|||
{tip = (<div class="tw-tooltip tw-tooltip--align-right tw-tooltip--up" role="tooltip" />)}
|
||||
</div>);
|
||||
|
||||
container.appendChild(cont);
|
||||
const thing = container.querySelector('button[data-a-target="player-theatre-mode-button"]');
|
||||
if ( thing ) {
|
||||
container.insertBefore(cont, thing.parentElement);
|
||||
} else
|
||||
container.appendChild(cont);
|
||||
|
||||
} else {
|
||||
icon = cont.querySelector('figure');
|
||||
|
@ -792,6 +808,138 @@ export default class Player extends Module {
|
|||
}
|
||||
|
||||
|
||||
addResetButton(inst, tries = 0) {
|
||||
const outer = inst.props.containerRef || this.fine.getChildNode(inst),
|
||||
container = outer && outer.querySelector('.player-controls__right-control-group'),
|
||||
has_reset = this.settings.get('player.button.reset');
|
||||
|
||||
if ( ! container ) {
|
||||
if ( ! has_reset )
|
||||
return;
|
||||
|
||||
if ( tries < 5 )
|
||||
return setTimeout(this.addResetButton.bind(this, inst, (tries || 0) + 1), 250);
|
||||
|
||||
return this.log.warn('Unable to find container element for Reset button.');
|
||||
}
|
||||
|
||||
let tip, btn, cont = container.querySelector('.ffz--player-reset');
|
||||
if ( ! has_reset ) {
|
||||
if ( cont )
|
||||
cont.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! cont ) {
|
||||
cont = (<div class="ffz--player-reset tw-inline-flex tw-relative tw-tooltip-wrapper">
|
||||
{btn = (<button
|
||||
class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay tw-core-button tw-core-button--border tw-core-button--overlay tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
|
||||
type="button"
|
||||
data-a-target="ffz-player-reset-button"
|
||||
onDblClick={this.resetPlayer.bind(this, inst)} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0">
|
||||
<div class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-cancel" />
|
||||
</div>
|
||||
</div>
|
||||
</button>)}
|
||||
{tip = (<div class="tw-tooltip tw-tooltip--align-right tw-tooltip--up" role="tooltip" />)}
|
||||
</div>);
|
||||
|
||||
const thing = container.querySelector('.ffz--player-pip button') || container.querySelector('button[data-a-target="player-theatre-mode-button"]');
|
||||
if ( thing ) {
|
||||
container.insertBefore(cont, thing.parentElement);
|
||||
} else
|
||||
container.appendChild(cont);
|
||||
|
||||
} else {
|
||||
btn = cont.querySelector('button');
|
||||
tip = cont.querySelector('.tw-tooltip');
|
||||
}
|
||||
|
||||
btn.setAttribute('aria-label',
|
||||
tip.textContent = this.i18n.t(
|
||||
'player.reset_button',
|
||||
'Double-Click to Reset Player'
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
addErrorResetButton(inst, tries = 0) {
|
||||
const outer = inst.props.containerRef || this.fine.getChildNode(inst),
|
||||
container = outer && outer.querySelector('.content-overlay-gate'),
|
||||
has_reset = this.settings.get('player.button.reset');
|
||||
|
||||
if ( ! container ) {
|
||||
if ( ! has_reset )
|
||||
return;
|
||||
|
||||
if ( tries < 2 )
|
||||
this.parent.awaitElement(
|
||||
'.autoplay-vod__content-container button',
|
||||
this.props.containerRef || t.fine.getChildNode(this),
|
||||
1000
|
||||
).then(() => {
|
||||
this.addErrorResetButton(inst, (tries || 0) + 1);
|
||||
|
||||
}).catch(() => {
|
||||
this.log.warn('Unable to find container element for Error Reset button.');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let tip, btn, cont = container.querySelector('.ffz--player-reset');
|
||||
if ( ! has_reset ) {
|
||||
if ( cont )
|
||||
cont.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! cont ) {
|
||||
cont = (<div class="ffz--player-reset tw-absolute tw-bottom-0 tw-right-0 tw-tooltip-wrapper tw-mg-1">
|
||||
{btn = (<button
|
||||
class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay tw-core-button tw-core-button--border tw-core-button--overlay tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
|
||||
type="button"
|
||||
data-a-target="ffz-player-reset-button"
|
||||
onDblClick={this.resetPlayer.bind(this, inst)} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0">
|
||||
<div class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-cancel" />
|
||||
</div>
|
||||
</div>
|
||||
</button>)}
|
||||
{tip = (<div class="tw-tooltip tw-tooltip--align-right tw-tooltip--up" role="tooltip" />)}
|
||||
</div>);
|
||||
|
||||
container.appendChild(cont);
|
||||
|
||||
} else {
|
||||
btn = cont.querySelector('button');
|
||||
tip = cont.querySelector('.tw-tooltip');
|
||||
}
|
||||
|
||||
btn.setAttribute('aria-label',
|
||||
tip.textContent = this.i18n.t(
|
||||
'player.reset_button',
|
||||
'Double-Click to Reset Player'
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
resetPlayer(inst) {
|
||||
const player = inst ? (inst.mediaSinkManager ? inst : inst?.props?.mediaPlayerInstance) : null;
|
||||
|
||||
this.PlayerSource.check();
|
||||
for(const inst of this.PlayerSource.instances) {
|
||||
if ( ! player || player === inst.props?.mediaPlayerInstance )
|
||||
inst.setSrc({isNewMediaPlayerInstance: false});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tryTheatreMode(inst) {
|
||||
if ( ! this.settings.get('player.theatre.auto-enter') )
|
||||
return;
|
||||
|
|
|
@ -247,9 +247,9 @@ export default class Fine extends Module {
|
|||
}
|
||||
|
||||
|
||||
findAllMatching(node, criteria, max_depth=15, parents=false, depth=0, traverse_roots=true) {
|
||||
const matches = new Set,
|
||||
crit = n => ! matches.has(n) && criteria(n);
|
||||
findAllMatching(node, criteria, max_depth=15, single_class = true, parents=false, depth=0, traverse_roots=true) {
|
||||
const matches = new Set;
|
||||
let crit = n => ! matches.has(n) && criteria(n);
|
||||
|
||||
while(true) {
|
||||
const match = parents ?
|
||||
|
@ -259,6 +259,11 @@ export default class Fine extends Module {
|
|||
if ( ! match )
|
||||
break;
|
||||
|
||||
if ( single_class && ! matches.size ) {
|
||||
const klass = match.constructor;
|
||||
crit = n => ! matches.has(n) && (n instanceof klass) && criteria(n);
|
||||
}
|
||||
|
||||
matches.add(match);
|
||||
}
|
||||
|
||||
|
@ -377,7 +382,7 @@ export default class Fine extends Module {
|
|||
wrapper._set(data.cls, data.instances);
|
||||
this._known_classes.set(data.cls, wrapper);
|
||||
|
||||
} else {
|
||||
} else if ( routes !== false ) {
|
||||
this._waiting.push(wrapper);
|
||||
this._updateLiveWaiting();
|
||||
}
|
||||
|
@ -517,6 +522,17 @@ export class FineWrapper extends EventEmitter {
|
|||
return Array.from(this.instances);
|
||||
}
|
||||
|
||||
check(node = null, max_depth = 1000) {
|
||||
if ( this._class )
|
||||
return;
|
||||
|
||||
const instances = this.fine.findAllMatching(node, this.criteria, max_depth);
|
||||
if ( instances.size ) {
|
||||
const insts = Array.from(instances);
|
||||
this._set(insts[0].constructor, insts);
|
||||
}
|
||||
}
|
||||
|
||||
ready(fn) {
|
||||
if ( this._class )
|
||||
fn(this._class, this.instances);
|
||||
|
|
11
src/utilities/data/broadcast-id.gql
Normal file
11
src/utilities/data/broadcast-id.gql
Normal file
|
@ -0,0 +1,11 @@
|
|||
query FFZ_BroadcastID($id: ID, $login: String) {
|
||||
user(id: $id, login: $login) {
|
||||
id
|
||||
stream {
|
||||
id
|
||||
archiveVideo {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,5 +82,6 @@ export default [
|
|||
"clip",
|
||||
"youtube-play",
|
||||
"minus",
|
||||
"left-open"
|
||||
"left-open",
|
||||
"docs"
|
||||
];
|
|
@ -39,4 +39,15 @@ export function print_duration(seconds) {
|
|||
seconds %= 60;
|
||||
|
||||
return `${hours > 0 ? `${hours}:${minutes < 10 ? '0' : ''}` : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
}
|
||||
|
||||
|
||||
export function durationForURL(elapsed) {
|
||||
const seconds = elapsed % 60;
|
||||
let minutes = Math.floor(elapsed / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
minutes = minutes % 60;
|
||||
|
||||
return `${hours > 0 ? `${hours}h` : ''}${minutes > 0 ? `${minutes}m` : ''}${seconds > 0 ? `${seconds}s` : ''}`;
|
||||
}
|
|
@ -169,6 +169,23 @@ export default class TwitchData extends Module {
|
|||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Broadcast ID
|
||||
// ========================================================================
|
||||
|
||||
async getBroadcastID(id, login) {
|
||||
const data = await this.queryApollo({
|
||||
query: require('./data/broadcast-id.gql'),
|
||||
variables: {
|
||||
id,
|
||||
login
|
||||
}
|
||||
});
|
||||
|
||||
return get('data.user.stream.archiveVideo.id', data);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Stream Up-Type (Uptime and Type, for Directory Purposes)
|
||||
// ========================================================================
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-github:before { content: '\f09b'; } /* '' */
|
||||
.ffz-i-docs:before { content: '\f0c5'; } /* '' */
|
||||
.ffz-i-sort-down:before { content: '\f0dd'; } /* '' */
|
||||
.ffz-i-sort-up:before { content: '\f0de'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -57,6 +57,7 @@
|
|||
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'ffz-fontello';
|
||||
src: url('../font/ffz-fontello.eot?60288408');
|
||||
src: url('../font/ffz-fontello.eot?60288408#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?60288408') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?60288408') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?60288408') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?60288408#ffz-fontello') format('svg');
|
||||
src: url('../font/ffz-fontello.eot?36275888');
|
||||
src: url('../font/ffz-fontello.eot?36275888#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?36275888') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?36275888') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?36275888') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?36275888#ffz-fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'ffz-fontello';
|
||||
src: url('../font/ffz-fontello.svg?60288408#ffz-fontello') format('svg');
|
||||
src: url('../font/ffz-fontello.svg?36275888#ffz-fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -113,6 +113,7 @@
|
|||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-github:before { content: '\f09b'; } /* '' */
|
||||
.ffz-i-docs:before { content: '\f0c5'; } /* '' */
|
||||
.ffz-i-sort-down:before { content: '\f0dd'; } /* '' */
|
||||
.ffz-i-sort-up:before { content: '\f0de'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
|
|
|
@ -15,4 +15,16 @@
|
|||
|
||||
.ffz-i-zreknarf.loading {
|
||||
animation: ffz-rotateplane 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ffz-c-text-inherit {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.ffz-mg-r--05 {
|
||||
margin-right: -0.5rem !important;
|
||||
}
|
||||
|
||||
.ffz-mg-l--05 {
|
||||
margin-left: -0.5rem !important;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue