1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 16:38:31 +00:00
* 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:
SirStendec 2020-07-23 16:00:00 -04:00
parent 3d88836a0e
commit 2f105eb3c4
8 changed files with 223 additions and 12 deletions

View file

@ -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": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,4 +33,8 @@
.ffz-mg-l--05 {
margin-left: -0.5rem !important;
}
.ffz--links {
order: 10;
}