diff --git a/package.json b/package.json index d77a768d..57695b02 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index 9c9f1ebc..ad16df2d 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -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 ); diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.jsx similarity index 71% rename from src/sites/twitch-twilight/modules/channel.js rename to src/sites/twitch-twilight/modules/channel.jsx index 00e87283..75b9468f 100644 --- a/src/sites/twitch-twilight/modules/channel.js +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -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 = ; + 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 = {text}; + 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 ) { diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index 56fd18d7..ff8dfc61 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -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'); diff --git a/src/sites/twitch-twilight/modules/host_button/index.js b/src/sites/twitch-twilight/modules/host_button/index.js index 39da2bc2..7755d0bc 100644 --- a/src/sites/twitch-twilight/modules/host_button/index.js +++ b/src/sites/twitch-twilight/modules/host_button/index.js @@ -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 ) diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index d112171f..7f1ed6c8 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -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(); diff --git a/src/sites/twitch-twilight/styles/channel.scss b/src/sites/twitch-twilight/styles/channel.scss index 58b93957..351a6a74 100644 --- a/src/sites/twitch-twilight/styles/channel.scss +++ b/src/sites/twitch-twilight/styles/channel.scss @@ -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; } diff --git a/styles/main.scss b/styles/main.scss index 1bfcf8c7..36f35753 100644 --- a/styles/main.scss +++ b/styles/main.scss @@ -33,4 +33,8 @@ .ffz-mg-l--05 { margin-left: -0.5rem !important; +} + +.ffz--links { + order: 10; } \ No newline at end of file