mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-03 16:38:31 +00:00
4.20.17
* Added: Download link for clips. Requires the current user to be an editor of the channel to appear. * Added: Option to hide the Unfollow button from channels to prevent accidentally unfollowing. * Added: Option to add Schedule, Videos, and Clips links to live channel pages. * Fixed: Metadata not rendering on video and clips pages. * Removed: Outdated channel appearance settings that no longer have any effect.
This commit is contained in:
parent
3d88836a0e
commit
2f105eb3c4
8 changed files with 223 additions and 12 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.16",
|
||||
"version": "4.20.17",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -12,6 +12,8 @@ import {duration_to_string, durationForURL} from 'utilities/time';
|
|||
import Tooltip from 'utilities/tooltip';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
const CLIP_URL = /^https:\/\/[^/]+\.twitch\.tv\/.+?\.mp4$/;
|
||||
|
||||
export default class Metadata extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
@ -23,6 +25,19 @@ export default class Metadata extends Module {
|
|||
this.should_enable = true;
|
||||
this.definitions = {};
|
||||
|
||||
this.settings.add('metadata.clip-download', {
|
||||
default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Channel > Metadata >> Clips',
|
||||
title: 'Add a Download button for editors to clip pages.',
|
||||
description: 'This adds a download button beneath the player on clip pages (the main site, not on `clips.twitch.tv`) for broadcasters and their editors.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: () => this.updateMetadata('clip-download')
|
||||
});
|
||||
|
||||
this.settings.add('metadata.player-stats', {
|
||||
default: false,
|
||||
|
||||
|
@ -228,6 +243,45 @@ export default class Metadata extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
this.definitions['clip-download'] = {
|
||||
button: true,
|
||||
inherit: true,
|
||||
|
||||
setup(data) {
|
||||
if ( ! this.settings.get('metadata.clip-download') )
|
||||
return;
|
||||
|
||||
const Player = this.resolve('site.player'),
|
||||
player = Player.current;
|
||||
if ( ! player )
|
||||
return;
|
||||
|
||||
const sink = player.mediaSinkManager || player.core?.mediaSinkManager,
|
||||
src = sink?.video?.src;
|
||||
|
||||
if ( ! src || ! CLIP_URL.test(src) )
|
||||
return;
|
||||
|
||||
const user = this.resolve('site').getUser?.(),
|
||||
is_self = user?.id == data.channel.id;
|
||||
|
||||
if ( is_self || data.getUserSelfImmediate(data.refresh)?.isEditor )
|
||||
return src;
|
||||
},
|
||||
|
||||
label(src) {
|
||||
if ( src )
|
||||
return this.i18n.t('metadata.clip-download', 'Download');
|
||||
},
|
||||
|
||||
icon: 'ffz-i-download',
|
||||
|
||||
click(src) {
|
||||
const link = createElement('a', {target: '_blank', href: src});
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
this.definitions['player-stats'] = {
|
||||
button: true,
|
||||
inherit: true,
|
||||
|
@ -507,6 +561,12 @@ export default class Metadata extends Module {
|
|||
return destroy();
|
||||
|
||||
try {
|
||||
const ref_fn = () => refresh_fn(key);
|
||||
data = {
|
||||
...data,
|
||||
refresh: ref_fn
|
||||
};
|
||||
|
||||
// Process the data if a setup method is defined.
|
||||
if ( def.setup )
|
||||
data = await def.setup.call(this, data);
|
||||
|
@ -515,7 +575,7 @@ export default class Metadata extends Module {
|
|||
const refresh = maybe_call(def.refresh, this, data);
|
||||
if ( refresh )
|
||||
timers[key] = setTimeout(
|
||||
() => refresh_fn(key),
|
||||
ref_fn,
|
||||
typeof refresh === 'number' ? refresh : 1000
|
||||
);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import Module from 'utilities/module';
|
||||
import { Color } from 'utilities/color';
|
||||
import {debounce} from 'utilities/object';
|
||||
import { createElement, setChildren } from 'utilities/dom';
|
||||
|
||||
|
||||
const USER_PAGES = ['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'];
|
||||
|
@ -38,6 +39,16 @@ export default class Channel extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('channel.extra-links', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Channel > Appearance >> General',
|
||||
title: 'Add extra links to live channel pages, next to the streamer\'s name.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: () => this.updateLinks()
|
||||
});
|
||||
|
||||
this.settings.add('channel.hosting.enable', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -65,6 +76,8 @@ export default class Channel extends Module {
|
|||
onEnable() {
|
||||
this.updateChannelColor();
|
||||
|
||||
this.on('i18n:update', this.updateLinks, this);
|
||||
|
||||
this.ChannelRoot.on('mount', this.updateRoot, this);
|
||||
this.ChannelRoot.on('mutate', this.updateRoot, this);
|
||||
this.ChannelRoot.on('unmount', this.removeRoot, this);
|
||||
|
@ -85,6 +98,13 @@ export default class Channel extends Module {
|
|||
this.maybeClickChat();
|
||||
}
|
||||
|
||||
updateLinks() {
|
||||
for(const el of this.InfoBar.instances) {
|
||||
el._ffz_link_login = null;
|
||||
this.updateBar(el);
|
||||
}
|
||||
}
|
||||
|
||||
maybeClickChat() {
|
||||
if ( this.settings.get('channel.auto-click-chat') && this.router.current_name === 'user' ) {
|
||||
const el = document.querySelector('a[data-a-target="channel-home-tab-Chat"]');
|
||||
|
@ -168,9 +188,17 @@ export default class Channel extends Module {
|
|||
el._ffz_cont = null;
|
||||
}
|
||||
|
||||
const want_links = this.settings.get('channel.extra-links');
|
||||
|
||||
if ( el._ffz_links && (! document.contains(el._ffz_links) || ! want_links)) {
|
||||
el._ffz_links.remove();
|
||||
el._ffz_links = null;
|
||||
el._ffz_link_login = null;
|
||||
}
|
||||
|
||||
if ( ! el._ffz_cont ) {
|
||||
const report = el.querySelector('.report-button'),
|
||||
cont = report && report.closest('.tw-flex-wrap.tw-justify-content-end');
|
||||
const report = el.querySelector('.report-button,button[data-test-selector="video-options-button"],button[data-test-selector="clip-options-button"]'),
|
||||
cont = report && (report.closest('.tw-flex-wrap.tw-justify-content-end') || report.closest('.tw-justify-content-end'));
|
||||
|
||||
if ( cont && el.contains(cont) ) {
|
||||
el._ffz_cont = cont;
|
||||
|
@ -180,9 +208,48 @@ export default class Channel extends Module {
|
|||
el._ffz_cont = null;
|
||||
}
|
||||
|
||||
if ( ! el._ffz_links && want_links ) {
|
||||
const link = el.querySelector('a .tw-line-height-heading'),
|
||||
cont = link && link.closest('.tw-flex');
|
||||
|
||||
if ( cont && el.contains(cont) ) {
|
||||
el._ffz_links = <div class="ffz--links tw-mg-l-1"></div>;
|
||||
cont.appendChild(el._ffz_links);
|
||||
}
|
||||
}
|
||||
|
||||
const react = this.fine.getReactInstance(el),
|
||||
props = react?.child?.memoizedProps;
|
||||
|
||||
if ( el._ffz_links && props.channelLogin !== el._ffz_link_login ) {
|
||||
const login = el._ffz_link_login = props.channelLogin;
|
||||
if ( login ) {
|
||||
const make_link = (link, text) => {
|
||||
const a = <a href={link} class="tw-c-text-inherit tw-interactive tw-pd-x-1 tw-font-size-5">{text}</a>;
|
||||
a.addEventListener('click', event => {
|
||||
if ( event.ctrlKey || event.shiftKey || event.altKey )
|
||||
return;
|
||||
|
||||
const history = this.router.history;
|
||||
if ( history ) {
|
||||
event.preventDefault();
|
||||
history.push(link);
|
||||
}
|
||||
});
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
setChildren(el._ffz_links, [
|
||||
make_link(`/${login}/schedule`, this.i18n.t('channel.links.schedule', 'Schedule')),
|
||||
make_link(`/${login}/videos`, this.i18n.t('channel.links.videos', 'Videos')),
|
||||
make_link(`/${login}/clips`, this.i18n.t('channel.links.clips', 'Clips'))
|
||||
]);
|
||||
|
||||
} else
|
||||
el._ffz_links.innerHTML = '';
|
||||
}
|
||||
|
||||
if ( ! el._ffz_cont || ! props?.channelID ) {
|
||||
this.updateSubscription(null);
|
||||
return;
|
||||
|
@ -242,7 +309,7 @@ export default class Channel extends Module {
|
|||
id: props.channelID,
|
||||
login: props.channelLogin,
|
||||
display_name: props.displayName,
|
||||
live: props.isLive,
|
||||
live: props.isLive && ! props.videoID && ! props.clipSlug,
|
||||
live_since: props.liveSince
|
||||
},
|
||||
props,
|
||||
|
@ -261,6 +328,16 @@ export default class Channel extends Module {
|
|||
|
||||
return 0;
|
||||
},
|
||||
getUserSelfImmediate: cb => {
|
||||
const ret = this.getUserSelf(el, props.channelID, true);
|
||||
if ( ret && ret.then ) {
|
||||
ret.then(cb);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
getUserSelf: () => this.getUserSelf(el, props.channelID),
|
||||
getBroadcastID: () => this.getBroadcastID(el, props.channelID)
|
||||
};
|
||||
|
||||
|
@ -317,6 +394,55 @@ export default class Channel extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
getUserSelf(el, channel_id, no_promise) {
|
||||
const cache = el._ffz_self_cache = el._ffz_self_cache || {};
|
||||
if ( channel_id === cache.channel_id ) {
|
||||
if ( Date.now() - cache.saved < 60000 ) {
|
||||
if ( no_promise )
|
||||
return cache.data;
|
||||
return Promise.resolve(cache.data);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(async (s, f) => {
|
||||
if ( cache.updating ) {
|
||||
cache.updating.push([s, f]);
|
||||
return ;
|
||||
}
|
||||
|
||||
cache.channel_id = channel_id;
|
||||
cache.updating = [[s,f]];
|
||||
let data, err;
|
||||
|
||||
try {
|
||||
data = await this.twitch_data.getUserSelf(channel_id);
|
||||
} catch(error) {
|
||||
data = null;
|
||||
err = error;
|
||||
}
|
||||
|
||||
const waiters = cache.updating;
|
||||
cache.updating = null;
|
||||
|
||||
if ( cache.channel_id !== channel_id ) {
|
||||
err = new Error('Outdated');
|
||||
cache.channel_id = null;
|
||||
cache.data = null;
|
||||
cache.saved = 0;
|
||||
for(const pair of waiters)
|
||||
pair[1](err);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cache.data = data;
|
||||
cache.saved = Date.now();
|
||||
|
||||
for(const pair of waiters)
|
||||
err ? pair[1](err) : pair[0](data);
|
||||
});
|
||||
}
|
||||
|
||||
getBroadcastID(el, channel_id) {
|
||||
const cache = el._ffz_bcast_cache = el._ffz_bcast_cache || {};
|
||||
if ( channel_id === cache.channel_id ) {
|
|
@ -11,6 +11,7 @@ import {has} from 'utilities/object';
|
|||
const STYLE_VALIDATOR = document.createElement('span');
|
||||
|
||||
const CLASSES = {
|
||||
'unfollow': '.follow-btn__follow-btn--following',
|
||||
'top-discover': '.navigation-link[data-a-target="discover-link"]',
|
||||
'side-nav': '.side-nav',
|
||||
'side-rec-channels': '.side-nav .recommended-channels,.side-nav .side-nav-section + .side-nav-section:not(.online-friends)',
|
||||
|
@ -335,6 +336,16 @@ export default class CSSTweaks extends Module {
|
|||
changed: () => this.updateFont()
|
||||
});
|
||||
|
||||
this.settings.add('channel.hide-unfollow', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Channel > Appearance >> General',
|
||||
title: 'Hide the Unfollow button.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggleHide('unfollow', val)
|
||||
});
|
||||
|
||||
this.settings.add('channel.hide-live-indicator', {
|
||||
requires: ['context.route.name'],
|
||||
process(ctx, val) {
|
||||
|
@ -359,7 +370,7 @@ export default class CSSTweaks extends Module {
|
|||
changed: val => this.toggle('square-avatars', !val)
|
||||
});
|
||||
|
||||
this.settings.add('channel.hide-not-live-bar', {
|
||||
/*this.settings.add('channel.hide-not-live-bar', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Channel > Appearance >> General',
|
||||
|
@ -368,7 +379,7 @@ export default class CSSTweaks extends Module {
|
|||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggleHide('not-live-bar', val)
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
@ -385,9 +396,10 @@ export default class CSSTweaks extends Module {
|
|||
this.toggleHide('side-offline-channels', this.settings.get('layout.side-nav.hide-offline'));
|
||||
this.toggleHide('prime-offers', !this.settings.get('layout.prime-offers'));
|
||||
this.toggleHide('top-discover', !this.settings.get('layout.discover'));
|
||||
this.toggleHide('unfollow', this.settings.get('channel.hide-unfollow'));
|
||||
|
||||
this.toggle('square-avatars', ! this.settings.get('channel.round-avatars'));
|
||||
this.toggleHide('not-live-bar', this.settings.get('channel.hide-not-live-bar'));
|
||||
//this.toggleHide('not-live-bar', this.settings.get('channel.hide-not-live-bar'));
|
||||
this.toggleHide('channel-live-ind', this.settings.get('channel.hide-live-indicator'));
|
||||
|
||||
const reruns = this.settings.get('layout.side-nav.rerun-style');
|
||||
|
|
|
@ -83,6 +83,9 @@ export default class HostButton extends Module {
|
|||
},
|
||||
|
||||
label: data => {
|
||||
if ( ! data.channel.live )
|
||||
return;
|
||||
|
||||
const ffz_user = this.site.getUser();
|
||||
|
||||
if ( ! this.settings.get('metadata.host-button') || ! ffz_user || ! data.channel || data.channel.login === ffz_user.login )
|
||||
|
|
|
@ -488,7 +488,7 @@ export default class Player extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.hide-rerun-bar', {
|
||||
/*this.settings.add('player.hide-rerun-bar', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Channel > Appearance >> General',
|
||||
|
@ -500,7 +500,7 @@ export default class Player extends Module {
|
|||
this.css_tweaks.toggleHide('player-rerun-bar', val);
|
||||
this.repositionPlayer();
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
this.settings.add('player.hide-mouse', {
|
||||
default: true,
|
||||
|
@ -520,7 +520,7 @@ export default class Player extends Module {
|
|||
this.css_tweaks.toggle('theatre-metadata', this.settings.get('player.theatre.metadata'));
|
||||
this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse'));
|
||||
this.css_tweaks.toggleHide('player-event-bar', this.settings.get('player.hide-event-bar'));
|
||||
this.css_tweaks.toggleHide('player-rerun-bar', this.settings.get('player.hide-rerun-bar'));
|
||||
//this.css_tweaks.toggleHide('player-rerun-bar', this.settings.get('player.hide-rerun-bar'));
|
||||
|
||||
this.updateCaptionsCSS();
|
||||
this.updateHideExtensions();
|
||||
|
|
|
@ -51,7 +51,13 @@
|
|||
margin-bottom: -.7rem !important;
|
||||
}
|
||||
|
||||
.ffz--meta-tray {
|
||||
.ffz--meta-tray:not(.tw-flex-wrap) {
|
||||
& > *:not(.ffz-stat) {
|
||||
order: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.ffz--meta-tray.tw-flex-wrap {
|
||||
& > :first-child {
|
||||
order: 1;
|
||||
}
|
||||
|
|
|
@ -33,4 +33,8 @@
|
|||
|
||||
.ffz-mg-l--05 {
|
||||
margin-left: -0.5rem !important;
|
||||
}
|
||||
|
||||
.ffz--links {
|
||||
order: 10;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue