mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-31 23:18:31 +00:00
Directory Support & Apollo InlineFragment fix (#338)
* Directory Support - Channel avatar next to / on-top the stream card - Stream uptime on stream card - Host grouping with a nice UI - Ability to block / hide games and / or their thumbnails Missing / Not working as of right now: - Channel avatar / uptime not showing in Game or Community directories Fix not being able to open host menu on the stream name Fix chat lines where you've been mentioned. "Following" button is now hoverable Not configurable though. Merge remote-tracking branch 'upstream/master' Fix for Apollo's InlineFragments Basic game directory index support (Outsource timeToString to some utility class!!!) Empty merge Fix Github's "You're behind the master branch!" annoyance More work on Directory stuff Community pages are now supported Several other small things * Proper router usage * Several fixes Also removing `package-lock.json` because for whatever reason that was in? * Early return if the container or card are null
This commit is contained in:
parent
2b6e93ab11
commit
6a7a19bf83
9 changed files with 1501 additions and 7467 deletions
7387
package-lock.json
generated
7387
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -99,6 +99,7 @@ Twilight.KNOWN_MODULES = {
|
||||||
Twilight.ROUTES = {
|
Twilight.ROUTES = {
|
||||||
'front-page': '/',
|
'front-page': '/',
|
||||||
'collection': '/collections/:collectionID',
|
'collection': '/collections/:collectionID',
|
||||||
|
'dir': '/directory',
|
||||||
'dir-community': '/communities/:communityName',
|
'dir-community': '/communities/:communityName',
|
||||||
'dir-community-index': '/directory/communities',
|
'dir-community-index': '/directory/communities',
|
||||||
'dir-creative': '/directory/creative',
|
'dir-creative': '/directory/creative',
|
||||||
|
|
|
@ -21,7 +21,12 @@ const CLASSES = {
|
||||||
'player-ext-hover': '.player[data-controls="false"] .player-extensions',
|
'player-ext-hover': '.player[data-controls="false"] .player-extensions',
|
||||||
|
|
||||||
'pinned-cheer': '.pinned-cheer',
|
'pinned-cheer': '.pinned-cheer',
|
||||||
'whispers': '.whispers'
|
'whispers': '.whispers',
|
||||||
|
|
||||||
|
'boxart-hover': '.tw-card .full-width:hover a[data-a-target="live-channel-card-game-link"]',
|
||||||
|
'boxart-hide': '.tw-card a[data-a-target="live-channel-card-game-link"]',
|
||||||
|
'profile-hover-following': '.tw-card .full-width:hover .channel-avatar',
|
||||||
|
'profile-hover-game': '.tw-thumbnail-card .tw-card-img:hover .channel-avatar',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
305
src/sites/twitch-twilight/modules/directory/community.js
Normal file
305
src/sites/twitch-twilight/modules/directory/community.js
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Directory (Following, for now)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import {SiteModule} from 'utilities/module';
|
||||||
|
import {createElement as e} from 'utilities/dom';
|
||||||
|
import {duration_to_string} from 'utilities/time';
|
||||||
|
|
||||||
|
export default class Community extends SiteModule {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.inject('site.fine');
|
||||||
|
this.inject('site.router');
|
||||||
|
this.inject('site.apollo');
|
||||||
|
this.inject('site.css_tweaks');
|
||||||
|
|
||||||
|
this.inject('settings');
|
||||||
|
|
||||||
|
this.GameHeader = this.fine.define(
|
||||||
|
'game-header',
|
||||||
|
n => n.renderFollowButton && n.renderGameDetailsTab
|
||||||
|
);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('GamePage_Game', `query {
|
||||||
|
directory {
|
||||||
|
... on Community {
|
||||||
|
streams {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
createdAt
|
||||||
|
type
|
||||||
|
broadcaster {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
this.ChannelCard = this.fine.define(
|
||||||
|
'community-channel-card',
|
||||||
|
n => n.props && n.props.streamNode
|
||||||
|
);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('GamePage_Game', res => this.router.current.name === 'dir-community' && this.modifyStreams(res), false);
|
||||||
|
|
||||||
|
this.on('settings:changed:show-channel-avatar', value => {
|
||||||
|
this.css_tweaks.toggleHide('profile-hover-game', value === 2);
|
||||||
|
this.router.current.name === 'dir-community' && this.ChannelCard.forceUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('settings:changed:directory.following.uptime', () => this.router.current.name === 'dir-community' && this.ChannelCard.forceUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
||||||
|
const newStreams = [];
|
||||||
|
|
||||||
|
const edges = res.data.directory.streams.edges;
|
||||||
|
for (let i = 0; i < edges.length; i++) {
|
||||||
|
const edge = edges[i];
|
||||||
|
const node = edge.node;
|
||||||
|
|
||||||
|
const s = node.viewersCount = new Number(node.viewersCount || 0);
|
||||||
|
s.profileImageURL = node.broadcaster.profileImageURL;
|
||||||
|
s.createdAt = node.createdAt;
|
||||||
|
|
||||||
|
newStreams.push(edge);
|
||||||
|
}
|
||||||
|
res.data.directory.streams.edges = newStreams;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.GameHeader.ready((cls, instances) => {
|
||||||
|
if (this.router.current.name === 'dir-community') {
|
||||||
|
for(const inst of instances) this.updateButtons(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ChannelCard.ready((cls, instances) => {
|
||||||
|
if (this.router.current.name === 'dir-community') {
|
||||||
|
this.apollo.ensureQuery(
|
||||||
|
'GamePage_Game',
|
||||||
|
'data.directory.streams.edges.0.node.createdAt'
|
||||||
|
);
|
||||||
|
|
||||||
|
for(const inst of instances) this.updateChannelCard(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ChannelCard.on('update', inst => this.router.current.name === 'dir-community' && this.updateChannelCard(inst), this);
|
||||||
|
|
||||||
|
this.ChannelCard.on('mount', inst => this.router.current.name === 'dir-community' && this.updateChannelCard(inst), this);
|
||||||
|
|
||||||
|
this.ChannelCard.on('unmount', inst => this.router.current.name === 'dir-community' && this.updateUptime(inst), this);
|
||||||
|
|
||||||
|
this.css_tweaks.toggleHide('profile-hover-game', this.settings.get('directory.following.show-channel-avatar') === 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUptime(inst) {
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
const card = container && container.querySelector && container.querySelector('.tw-card');
|
||||||
|
|
||||||
|
if (container === null || card === null) {
|
||||||
|
if (inst.updateTimer !== undefined) {
|
||||||
|
clearInterval(inst.updateTimer);
|
||||||
|
inst.updateTimer = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.get('directory.following.uptime') === 0) {
|
||||||
|
if (inst.updateTimer !== undefined) {
|
||||||
|
clearInterval(inst.updateTimer);
|
||||||
|
inst.updateTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inst.uptimeElement !== undefined) {
|
||||||
|
inst.uptimeElement.remove();
|
||||||
|
inst.uptimeElementSpan = inst.uptimeElement = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (inst.updateTimer === undefined) {
|
||||||
|
inst.updateTimer = setInterval(
|
||||||
|
this.updateUptime.bind(this, inst),
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const up_since = new Date(inst.props.streamNode.viewersCount.createdAt);
|
||||||
|
const uptime = up_since && Math.floor((Date.now() - up_since) / 1000) || 0;
|
||||||
|
const uptimeText = duration_to_string(uptime, false, false, false, this.settings.get('directory.following.uptime') === 1);
|
||||||
|
|
||||||
|
if (uptime > 0) {
|
||||||
|
if (inst.uptimeElement === undefined) {
|
||||||
|
inst.uptimeElementSpan = e('span', 'tw-stat__value ffz-uptime', `${uptimeText}`);
|
||||||
|
inst.uptimeElement = e('div', {
|
||||||
|
className: 'c-background-overlay c-text-overlay font-size-6 top-0 right-0 z-default inline-flex absolute mg-05',
|
||||||
|
style: 'padding-left: 4px; padding-right: 4px;'
|
||||||
|
}, [
|
||||||
|
e('span', 'tw-stat__icon',
|
||||||
|
e('figure', 'ffz-i-clock')
|
||||||
|
),
|
||||||
|
inst.uptimeElementSpan
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (card.querySelector('.ffz-uptime') === null) card.appendChild(inst.uptimeElement);
|
||||||
|
} else {
|
||||||
|
inst.uptimeElementSpan.textContent = `${uptimeText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChannelCard(inst) {
|
||||||
|
this.updateUptime(inst);
|
||||||
|
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
const card = container && container.querySelector && container.querySelector('.tw-card');
|
||||||
|
|
||||||
|
if (container === null || card === null) return;
|
||||||
|
|
||||||
|
if (!inst.props.streamNode.viewersCount.createdAt) return;
|
||||||
|
|
||||||
|
// Remove old elements
|
||||||
|
const hiddenBodyCard = card.querySelector('.tw-card-body.hide');
|
||||||
|
if (hiddenBodyCard !== null) hiddenBodyCard.classList.remove('hide');
|
||||||
|
|
||||||
|
const ffzChannelData = card.querySelector('.ffz-channel-data');
|
||||||
|
if (ffzChannelData !== null) ffzChannelData.remove();
|
||||||
|
|
||||||
|
const channelAvatar = card.querySelector('.channel-avatar');
|
||||||
|
if (channelAvatar !== null) channelAvatar.remove();
|
||||||
|
|
||||||
|
if (inst.props.streamNode.viewersCount.profileImageURL) {
|
||||||
|
const avatarSetting = this.settings.get('directory.following.show-channel-avatar');
|
||||||
|
if (avatarSetting === 1) {
|
||||||
|
const cardDiv = card.querySelector('.tw-card-body');
|
||||||
|
const modifiedDiv = e('div', {
|
||||||
|
innerHTML: cardDiv.innerHTML
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarDiv = e('a', {
|
||||||
|
className: 'channel-avatar',
|
||||||
|
href: `/${inst.props.streamNode.broadcaster.login}`,
|
||||||
|
style: 'margin-right: 8px; min-width: 4rem; margin-top: 0.5rem;',
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: inst.props.streamNode.broadcaster.login});
|
||||||
|
}
|
||||||
|
}, e('img', {
|
||||||
|
title: inst.props.streamNode.broadcaster.displayName,
|
||||||
|
src: inst.props.streamNode.viewersCount.profileImageURL,
|
||||||
|
style: 'height: 4rem;'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const cardDivParent = cardDiv.parentElement;
|
||||||
|
|
||||||
|
if (cardDivParent.querySelector('.ffz-channel-data') === null) {
|
||||||
|
cardDiv.classList.add('hide');
|
||||||
|
|
||||||
|
const newCardDiv = e('div', 'ffz-channel-data flex flex-nowrap', [
|
||||||
|
avatarDiv, modifiedDiv
|
||||||
|
]);
|
||||||
|
cardDivParent.appendChild(newCardDiv);
|
||||||
|
}
|
||||||
|
} else if (avatarSetting === 2 || avatarSetting === 3) {
|
||||||
|
const avatarElement = e('a', {
|
||||||
|
className: 'channel-avatar',
|
||||||
|
href: `/${inst.props.streamNode.broadcaster.login}`,
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: inst.props.streamNode.broadcaster.login});
|
||||||
|
}
|
||||||
|
}, e('div', 'live-channel-card__boxart bottom-0 absolute',
|
||||||
|
e('figure', 'tw-aspect tw-aspect--align-top',
|
||||||
|
e('img', {
|
||||||
|
title: inst.props.streamNode.broadcaster.displayName,
|
||||||
|
src: inst.props.streamNode.viewersCount.profileImageURL
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const divToAppend = card.querySelector('figure.tw-aspect');
|
||||||
|
if (divToAppend.querySelector('.channel-avatar') === null) divToAppend.appendChild(avatarElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons(inst) {
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
// We can't get the buttons through querySelector('button ...') so this has to do for now...
|
||||||
|
const buttons = container && container.querySelector && container.querySelector('div > div.align-items-center');
|
||||||
|
|
||||||
|
const ffzButtons = buttons.querySelector('.ffz-buttons');
|
||||||
|
if (ffzButtons !== null) ffzButtons.remove();
|
||||||
|
|
||||||
|
if (buttons.querySelector('.ffz-buttons') === null) {
|
||||||
|
// Block / Unblock Games
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
const gameBlocked = blockedGames.includes(inst.props.directoryName);
|
||||||
|
|
||||||
|
const blockButton = e('button', {
|
||||||
|
className: 'mg-l-1 tw-button ffz-toggle-game-block',
|
||||||
|
style: `background-color: ${gameBlocked ? '#228B22' : '#B22222'};`
|
||||||
|
}, e('span', {
|
||||||
|
className: 'tw-button__text',
|
||||||
|
textContent: `${gameBlocked ? 'Unblock' : 'Block'}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
blockButton.addEventListener('click', () => {
|
||||||
|
const gameName = inst.props.directoryName;
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
if (blockedGames.includes(gameName)) blockedGames.splice(blockedGames.indexOf(gameName), 1);
|
||||||
|
else blockedGames.push(gameName);
|
||||||
|
|
||||||
|
this.settings.provider.set('directory.game.blocked-games', blockedGames);
|
||||||
|
|
||||||
|
this.updateButtons(inst);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide / Unhide Thumbnails
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
const thumbnailBlocked = hiddenThumbnails.includes(inst.props.directoryName);
|
||||||
|
|
||||||
|
const hideThumbnailButton = e('button', {
|
||||||
|
className: 'mg-l-1 tw-button ffz-toggle-thumbnail',
|
||||||
|
style: `background-color: ${thumbnailBlocked ? '#228B22' : '#B22222'};`
|
||||||
|
}, e('span', {
|
||||||
|
className: 'tw-button__text',
|
||||||
|
textContent: `${thumbnailBlocked ? 'Unhide Thumbnails' : 'Hide Thumbnails'}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
hideThumbnailButton.addEventListener('click', () => {
|
||||||
|
const gameName = inst.props.directoryName;
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
if (hiddenThumbnails.includes(gameName)) hiddenThumbnails.splice(hiddenThumbnails.indexOf(gameName), 1);
|
||||||
|
else hiddenThumbnails.push(gameName);
|
||||||
|
|
||||||
|
this.settings.provider.set('directory.game.hidden-thumbnails', hiddenThumbnails);
|
||||||
|
|
||||||
|
this.updateButtons(inst);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ffzButtons = e('div', 'ffz-buttons', [
|
||||||
|
blockButton,
|
||||||
|
hideThumbnailButton
|
||||||
|
]);
|
||||||
|
|
||||||
|
buttons.appendChild(ffzButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
588
src/sites/twitch-twilight/modules/directory/following.js
Normal file
588
src/sites/twitch-twilight/modules/directory/following.js
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Following Page
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import {SiteModule} from 'utilities/module';
|
||||||
|
import {createElement as e} from 'utilities/dom';
|
||||||
|
import {get} from 'utilities/object';
|
||||||
|
import {duration_to_string} from 'utilities/time';
|
||||||
|
|
||||||
|
import Popper from 'popper.js';
|
||||||
|
|
||||||
|
export default class Following extends SiteModule {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.inject('site.fine');
|
||||||
|
this.inject('site.router');
|
||||||
|
this.inject('site.apollo');
|
||||||
|
this.inject('site.css_tweaks');
|
||||||
|
|
||||||
|
this.inject('settings');
|
||||||
|
|
||||||
|
this.settings.add('directory.following.group-hosts', {
|
||||||
|
default: true,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Following >> Placeholder',
|
||||||
|
title: 'Group Hosts',
|
||||||
|
description: 'Only show a given hosted channel once in the directory.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: () => this.isRouteAcceptable() && this.apollo.getQuery('FollowedIndex_CurrentUser').refetch()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('directory.following.uptime', {
|
||||||
|
default: 1,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Following >> Placeholder',
|
||||||
|
title: 'Stream Uptime',
|
||||||
|
description: 'Display the stream uptime on the channel cards.',
|
||||||
|
|
||||||
|
component: 'setting-select-box',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{value: 0, title: 'Disabled'},
|
||||||
|
{value: 1, title: 'Enabled'},
|
||||||
|
{value: 2, title: 'Enabled (with Seconds)'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: () => this.isRouteAcceptable() && this.ChannelCard.forceUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('directory.following.host-menus', {
|
||||||
|
default: 1,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Following >> Placeholder',
|
||||||
|
title: 'Hosted Channel Menus',
|
||||||
|
description: 'Display a menu to select which channel to visit when clicking a hosted channel in the directory.',
|
||||||
|
|
||||||
|
component: 'setting-select-box',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{value: 0, title: 'Disabled'},
|
||||||
|
{value: 1, title: 'When Multiple are Hosting'},
|
||||||
|
{value: 2, title: 'Always'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: () => this.isRouteAcceptable() && this.ChannelCard.forceUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('directory.following.hide-boxart', {
|
||||||
|
default: 0,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Following >> Placeholder',
|
||||||
|
title: 'Hide Boxart',
|
||||||
|
description: 'Do not display boxart over a stream / video thumbnail.',
|
||||||
|
|
||||||
|
component: 'setting-select-box',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{value: 0, title: 'Never'},
|
||||||
|
{value: 1, title: 'On Hover'},
|
||||||
|
{value: 2, title: 'Always'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: value => {
|
||||||
|
this.css_tweaks.toggleHide('boxart-hide', value === 2);
|
||||||
|
this.css_tweaks.toggleHide('boxart-hover', value === 1);
|
||||||
|
if (this.isRouteAcceptable()) this.ChannelCard.forceUpdate()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('directory.following.show-channel-avatar', {
|
||||||
|
default: 1,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Following >> Placeholder',
|
||||||
|
title: 'Show Channel Avatar',
|
||||||
|
description: 'Show channel avatar next or on-top of a stream / video thumbnail.',
|
||||||
|
|
||||||
|
component: 'setting-select-box',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{value: 0, title: 'Never'},
|
||||||
|
{value: 1, title: 'Next to Stream Name'},
|
||||||
|
{value: 2, title: 'On Thumbnail, Hidden on Hover'},
|
||||||
|
{value: 3, title: 'On Thumbnail'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: value => {
|
||||||
|
this.css_tweaks.toggleHide('profile-hover-following', value === 2);
|
||||||
|
if (this.isRouteAcceptable()) this.ChannelCard.forceUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowedIndex_CurrentUser', `query {
|
||||||
|
currentUser {
|
||||||
|
followedLiveUsers {
|
||||||
|
nodes {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
stream {
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
followedHosts {
|
||||||
|
nodes {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
hosting {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
stream {
|
||||||
|
createdAt
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowingLive_CurrentUser', `query {
|
||||||
|
currentUser {
|
||||||
|
followedLiveUsers {
|
||||||
|
nodes {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
stream {
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowingHosts_CurrentUser', `query {
|
||||||
|
currentUser {
|
||||||
|
followedHosts {
|
||||||
|
nodes {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
hosting {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
stream {
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
this.ChannelCard = this.fine.define(
|
||||||
|
'channel-card',
|
||||||
|
n => n.renderGameBoxArt && n.renderContentType
|
||||||
|
);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowedIndex_CurrentUser', res => {
|
||||||
|
res = this.modifyLiveUsers(res);
|
||||||
|
res = this.modifyLiveHosts(res);
|
||||||
|
return res;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowingLive_CurrentUser', res => this.modifyLiveUsers(res), false);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
isRouteAcceptable() {
|
||||||
|
return this.router.current.name === 'dir-following-index'
|
||||||
|
|| this.router.current.name === 'dir-category' && this.router.match[1] === 'following';
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyLiveUsers(res) { // eslint-disable-line class-methods-use-this
|
||||||
|
if (!this.isRouteAcceptable()) return res;
|
||||||
|
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
|
||||||
|
const newLiveNodes = [];
|
||||||
|
|
||||||
|
const nodes = res.data.currentUser.followedLiveUsers.nodes;
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
|
||||||
|
const s = node.stream.viewersCount = new Number(node.stream.viewersCount || 0);
|
||||||
|
s.profileImageURL = node.profileImageURL;
|
||||||
|
s.createdAt = node.stream.createdAt;
|
||||||
|
|
||||||
|
if (node.stream.game && hiddenThumbnails.includes(node.stream.game.name)) node.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
||||||
|
if (!node.stream.game || node.stream.game && !blockedGames.includes(node.stream.game.name)) newLiveNodes.push(node);
|
||||||
|
}
|
||||||
|
res.data.currentUser.followedLiveUsers.nodes = newLiveNodes;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyLiveHosts(res) { // eslint-disable-line class-methods-use-this
|
||||||
|
if (!this.isRouteAcceptable()) return res;
|
||||||
|
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
|
||||||
|
const nodes = res.data.currentUser.followedHosts.nodes;
|
||||||
|
this.hosts = {};
|
||||||
|
const newHostNodes = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
|
||||||
|
const s = node.hosting.stream.viewersCount = new Number(node.hosting.stream.viewersCount || 0);
|
||||||
|
s.profileImageURL = node.hosting.profileImageURL;
|
||||||
|
s.createdAt = node.hosting.stream.createdAt;
|
||||||
|
s.hostData = {
|
||||||
|
channel: node.hosting.login,
|
||||||
|
displayName: node.hosting.displayName
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.hosts[node.hosting.displayName]) {
|
||||||
|
this.hosts[node.hosting.displayName] = {
|
||||||
|
nodes: [node],
|
||||||
|
channels: [node.displayName]
|
||||||
|
};
|
||||||
|
if (node.hosting.stream.game && hiddenThumbnails.includes(node.hosting.stream.game.name)) node.hosting.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
||||||
|
if (!node.hosting.stream.game || node.hosting.stream.game && !blockedGames.includes(node.hosting.stream.game.name)) newHostNodes.push(node);
|
||||||
|
} else {
|
||||||
|
this.hosts[node.hosting.displayName].nodes.push(node);
|
||||||
|
this.hosts[node.hosting.displayName].channels.push(node.displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.get('directory.following.group-hosts')) {
|
||||||
|
res.data.currentUser.followedHosts.nodes = newHostNodes;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.css_tweaks.toggleHide('boxart-hover', this.settings.get('directory.following.hide-boxart') === 1);
|
||||||
|
this.css_tweaks.toggleHide('boxart-hide', this.settings.get('directory.following.hide-boxart') === 2);
|
||||||
|
this.css_tweaks.toggleHide('profile-hover-following', this.settings.get('directory.following.show-channel-avatar') === 2);
|
||||||
|
|
||||||
|
this.ChannelCard.on('update', inst => this.updateChannelCard(inst), this);
|
||||||
|
|
||||||
|
this.ChannelCard.ready((cls, instances) => {
|
||||||
|
if (this.router && this.router.match) {
|
||||||
|
if (this.router.match[1] === 'following') {
|
||||||
|
this.apollo.ensureQuery(
|
||||||
|
'FollowedIndex_CurrentUser',
|
||||||
|
n =>
|
||||||
|
get('data.currentUser.followedLiveUsers.nodes.0.profileImageURL', n) !== undefined
|
||||||
|
||
|
||||||
|
get('data.currentUser.followedHosts.nodes.0.hosting.profileImageURL', n) !== undefined
|
||||||
|
);
|
||||||
|
} else if (this.router.match[1] === 'live') {
|
||||||
|
this.apollo.ensureQuery(
|
||||||
|
'FollowingLive_CurrentUser',
|
||||||
|
'data.currentUser.followedLiveUsers.nodes.0.profileImageURL'
|
||||||
|
);
|
||||||
|
} else if (this.router.match[1] === 'hosts') {
|
||||||
|
this.apollo.ensureQuery(
|
||||||
|
'FollowingHosts_CurrentUser',
|
||||||
|
'data.currentUser.followedHosts.nodes.0.hosting.profileImageURL'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const inst of instances) this.updateChannelCard(inst);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ChannelCard.on('mount', inst => this.updateChannelCard(inst), this);
|
||||||
|
|
||||||
|
this.ChannelCard.on('unmount', inst => this.updateUptime(inst), this);
|
||||||
|
|
||||||
|
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyHostMenu(event) {
|
||||||
|
if (event.target.closest('.ffz-channel-selector-outer') === null && Date.now() > this.hostMenuBuffer) {
|
||||||
|
this.hostMenuPopper && this.hostMenuPopper.destroy();
|
||||||
|
this.hostMenu && this.hostMenu.remove();
|
||||||
|
this.hostMenuPopper = this.hostMenu = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUptime(inst) {
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
const card = container && container.querySelector && container.querySelector('.tw-card .tw-aspect > div');
|
||||||
|
|
||||||
|
// if (container === null || card === null) {
|
||||||
|
// if (inst.updateTimer !== undefined) {
|
||||||
|
// clearInterval(inst.updateTimer);
|
||||||
|
// inst.updateTimer = undefined;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (this.settings.get('directory.following.uptime') === 0) {
|
||||||
|
if (inst.updateTimer !== undefined) {
|
||||||
|
clearInterval(inst.updateTimer);
|
||||||
|
inst.updateTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inst.uptimeElement !== undefined) {
|
||||||
|
inst.uptimeElement.remove();
|
||||||
|
inst.uptimeElementSpan = inst.uptimeElement = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (inst.updateTimer === undefined) {
|
||||||
|
inst.updateTimer = setInterval(
|
||||||
|
this.updateUptime.bind(this, inst),
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const up_since = new Date(inst.props.viewerCount.createdAt);
|
||||||
|
const uptime = up_since && Math.floor((Date.now() - up_since) / 1000) || 0;
|
||||||
|
const uptimeText = duration_to_string(uptime, false, false, false, this.settings.get('directory.following.uptime') === 1);
|
||||||
|
|
||||||
|
if (uptime > 0) {
|
||||||
|
if (inst.uptimeElement === undefined) {
|
||||||
|
inst.uptimeElementSpan = e('span', 'tw-stat__value ffz-uptime', `${uptimeText}`);
|
||||||
|
inst.uptimeElement = e('div', {
|
||||||
|
className: 'c-background-overlay c-text-overlay font-size-6 top-0 right-0 z-default inline-flex absolute mg-05',
|
||||||
|
style: 'padding-left: 4px; padding-right: 4px;'
|
||||||
|
}, [
|
||||||
|
e('span', 'tw-stat__icon',
|
||||||
|
e('figure', 'ffz-i-clock')
|
||||||
|
),
|
||||||
|
inst.uptimeElementSpan
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (card.querySelector('.ffz-uptime') === null) card.appendChild(inst.uptimeElement);
|
||||||
|
} else {
|
||||||
|
inst.uptimeElementSpan.textContent = `${uptimeText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showHostMenu(inst, { channels }, event) {
|
||||||
|
if (this.settings.get('directory.following.host-menus') === 0 || this.settings.get('directory.following.host-menus') === 1 && channels.length < 2) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.hostMenuPopper && this.hostMenuPopper.destroy();
|
||||||
|
|
||||||
|
this.hostMenu && this.hostMenu.remove();
|
||||||
|
|
||||||
|
const simplebarContentChildren = [];
|
||||||
|
|
||||||
|
// Hosted Channel Header
|
||||||
|
simplebarContentChildren.push(
|
||||||
|
e('p', {
|
||||||
|
className: 'pd-t-05 pd-x-1 c-text-alt-2',
|
||||||
|
textContent: 'Hosted Channel'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hosted Channel Content
|
||||||
|
simplebarContentChildren.push(
|
||||||
|
e('a', {
|
||||||
|
className: 'tw-interactable',
|
||||||
|
href: inst.props.linkTo.pathname,
|
||||||
|
style: 'padding-top: 0.1rem; padding-bottom: 0.1rem;',
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: inst.props.linkTo.pathname.substring(1)});
|
||||||
|
}
|
||||||
|
}, e('div', 'align-items-center flex flex-row flex-nowrap mg-x-1 mg-y-05',
|
||||||
|
[
|
||||||
|
e('div', {
|
||||||
|
className: 'flex-shrink-0',
|
||||||
|
style: 'overflow: hidden; width: 3rem; height: 3rem;',
|
||||||
|
}, e('img', {
|
||||||
|
src: inst.props.viewerCount.profileImageURL,
|
||||||
|
alt: inst.props.channelName
|
||||||
|
})),
|
||||||
|
e('p', {
|
||||||
|
className: 'ellipsis flex-grow-1 mg-l-1 font-size-5',
|
||||||
|
textContent: inst.props.channelName
|
||||||
|
})
|
||||||
|
]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hosting Channels Header
|
||||||
|
simplebarContentChildren.push(
|
||||||
|
e('p', {
|
||||||
|
className: 'pd-t-05 pd-x-1 c-text-alt-2',
|
||||||
|
textContent: 'Hosting Channels'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hosting Channels Content
|
||||||
|
const hosts = this.hosts[inst.props.channelName];
|
||||||
|
for (let i = 0; i < hosts.nodes.length; i++) {
|
||||||
|
const node = hosts.nodes[i];
|
||||||
|
simplebarContentChildren.push(
|
||||||
|
e('a', {
|
||||||
|
className: 'tw-interactable',
|
||||||
|
href: `/${node.login}`,
|
||||||
|
style: 'padding-top: 0.1rem; padding-bottom: 0.1rem;',
|
||||||
|
}, e('div', 'align-items-center flex flex-row flex-nowrap mg-x-1 mg-y-05',
|
||||||
|
[
|
||||||
|
e('div', {
|
||||||
|
className: 'flex-shrink-0',
|
||||||
|
style: 'overflow: hidden; width: 3rem; height: 3rem;',
|
||||||
|
}, e('img', {
|
||||||
|
src: node.profileImageURL,
|
||||||
|
alt: node.displayName
|
||||||
|
})),
|
||||||
|
e('p', {
|
||||||
|
className: 'ellipsis flex-grow-1 mg-l-1 font-size-5',
|
||||||
|
textContent: node.displayName
|
||||||
|
})
|
||||||
|
]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hostMenu = e('div', 'tw-balloon block',
|
||||||
|
e('div', 'selectable-filter__balloon pd-y-05',
|
||||||
|
e('div', {
|
||||||
|
className: 'scrollable-area',
|
||||||
|
style: 'max-height: 20rem;',
|
||||||
|
'data-simplebar': true,
|
||||||
|
}, [
|
||||||
|
e('div', 'simplebar-track vertical',
|
||||||
|
e('div', 'simplebar-scrollbar')
|
||||||
|
),
|
||||||
|
e('div', 'simplebar-scroll-content',
|
||||||
|
e('div', 'simplebar-content', simplebarContentChildren)
|
||||||
|
)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.body.appendChild(this.hostMenu);
|
||||||
|
|
||||||
|
this.hostMenuPopper = new Popper(document.body, this.hostMenu, {
|
||||||
|
placement: 'bottom-start',
|
||||||
|
modifiers: {
|
||||||
|
flip: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
offset: `${event.clientX - 60}, ${event.clientY - 60}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hostMenuBuffer = Date.now() + 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChannelCard(inst) {
|
||||||
|
this.updateUptime(inst);
|
||||||
|
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
const card = container && container.querySelector && container.querySelector('.tw-card');
|
||||||
|
|
||||||
|
if (container === null || card === null) return;
|
||||||
|
|
||||||
|
const channelCardTitle = card.querySelector('.live-channel-card__title');
|
||||||
|
|
||||||
|
if (channelCardTitle === null) return;
|
||||||
|
|
||||||
|
// Remove old elements
|
||||||
|
const hiddenBodyCard = card.querySelector('.tw-card-body.hide');
|
||||||
|
if (hiddenBodyCard !== null) hiddenBodyCard.classList.remove('hide');
|
||||||
|
|
||||||
|
const ffzChannelData = card.querySelector('.ffz-channel-data');
|
||||||
|
if (ffzChannelData !== null) ffzChannelData.remove();
|
||||||
|
|
||||||
|
const channelAvatar = card.querySelector('.channel-avatar');
|
||||||
|
if (channelAvatar !== null) channelAvatar.remove();
|
||||||
|
|
||||||
|
if (inst.props.viewerCount.profileImageURL) {
|
||||||
|
const hosting = inst.props.viewerCount.hostData;
|
||||||
|
let channel, displayName;
|
||||||
|
if (hosting) {
|
||||||
|
channel = inst.props.viewerCount.hostData.channel;
|
||||||
|
displayName = inst.props.viewerCount.hostData.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarSetting = this.settings.get('directory.following.show-channel-avatar');
|
||||||
|
const cardDiv = card.querySelector('.tw-card-body');
|
||||||
|
const modifiedDiv = e('div', {
|
||||||
|
innerHTML: cardDiv.innerHTML
|
||||||
|
});
|
||||||
|
|
||||||
|
let avatarDiv;
|
||||||
|
if (avatarSetting === 1) {
|
||||||
|
avatarDiv = e('a', {
|
||||||
|
className: 'channel-avatar',
|
||||||
|
href: hosting ? `/${channel}` : inst.props.linkTo.pathname,
|
||||||
|
style: 'margin-right: 8px; min-width: 4rem; margin-top: 0.5rem;'
|
||||||
|
}, e('img', {
|
||||||
|
title: inst.props.channelName,
|
||||||
|
src: inst.props.viewerCount.profileImageURL,
|
||||||
|
style: 'height: 4rem;'
|
||||||
|
}));
|
||||||
|
} else if (avatarSetting === 2 || avatarSetting === 3) {
|
||||||
|
const avatarElement = e('a', {
|
||||||
|
className: 'channel-avatar',
|
||||||
|
href: hosting ? `/${channel}` : inst.props.linkTo.pathname,
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: inst.props.streamNode.broadcaster.login});
|
||||||
|
}
|
||||||
|
}, e('div', 'live-channel-card__boxart bottom-0 absolute',
|
||||||
|
e('figure', 'tw-aspect tw-aspect--align-top',
|
||||||
|
e('img', {
|
||||||
|
title: inst.props.channelName,
|
||||||
|
src: inst.props.viewerCount.profileImageURL
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const divToAppend = card.querySelector('.tw-aspect > div');
|
||||||
|
if (divToAppend.querySelector('.channel-avatar') === null) divToAppend.appendChild(avatarElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardDivParent = cardDiv.parentElement;
|
||||||
|
const ffzChannelData = cardDivParent.querySelector('.ffz-channel-data');
|
||||||
|
if (ffzChannelData === null) {
|
||||||
|
cardDiv.classList.add('hide');
|
||||||
|
|
||||||
|
const newCardDiv = e('div', 'ffz-channel-data flex flex-nowrap', [
|
||||||
|
avatarDiv, modifiedDiv
|
||||||
|
]);
|
||||||
|
cardDivParent.appendChild(newCardDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hosting) {
|
||||||
|
const hostObj = this.hosts[displayName];
|
||||||
|
if (this.settings.get('directory.following.group-hosts')) {
|
||||||
|
const titleLink = card.querySelector('.ffz-channel-data a[data-a-target="live-channel-card-title-link"]');
|
||||||
|
const thumbnailLink = card.querySelector('a[data-a-target="live-channel-card-thumbnail-link"]');
|
||||||
|
|
||||||
|
if (hostObj.channels.length > 1) {
|
||||||
|
const textContent = `${hostObj.channels.length} hosting ${displayName}`;
|
||||||
|
channelCardTitle.textContent
|
||||||
|
= channelCardTitle.title
|
||||||
|
= textContent;
|
||||||
|
|
||||||
|
if (thumbnailLink !== null) thumbnailLink.title = textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleLink !== null) titleLink.onclick = this.showHostMenu.bind(this, inst, hostObj);
|
||||||
|
if (thumbnailLink !== null) thumbnailLink.onclick = this.showHostMenu.bind(this, inst, hostObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
321
src/sites/twitch-twilight/modules/directory/game.js
Normal file
321
src/sites/twitch-twilight/modules/directory/game.js
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Directory (Following, for now)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import {SiteModule} from 'utilities/module';
|
||||||
|
import {createElement as e} from 'utilities/dom';
|
||||||
|
import {duration_to_string} from 'utilities/time';
|
||||||
|
|
||||||
|
export default class Game extends SiteModule {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.inject('site.fine');
|
||||||
|
this.inject('site.router');
|
||||||
|
this.inject('site.apollo');
|
||||||
|
this.inject('site.css_tweaks');
|
||||||
|
|
||||||
|
this.inject('settings');
|
||||||
|
|
||||||
|
this.GameHeader = this.fine.define(
|
||||||
|
'game-header',
|
||||||
|
n => n.renderFollowButton && n.renderGameDetailsTab
|
||||||
|
);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('GamePage_Game', `query {
|
||||||
|
directory {
|
||||||
|
... on Game {
|
||||||
|
streams {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
createdAt
|
||||||
|
type
|
||||||
|
broadcaster {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
this.ChannelCard = this.fine.define(
|
||||||
|
'game-channel-card',
|
||||||
|
n => n.props && n.props.streamNode
|
||||||
|
);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('GamePage_Game', res => this.router.current.name === 'dir-game-index' && this.modifyStreams(res), false);
|
||||||
|
|
||||||
|
this.on('site.directory.following:update-show-channel-avatar', value => {
|
||||||
|
this.css_tweaks.toggleHide('profile-hover-game', value === 2);
|
||||||
|
this.router.current.name === 'dir-game-index' && this.ChannelCard.forceUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('site.directory.following:update-uptime', () => this.router.current.name === 'dir-game-index' && this.ChannelCard.forceUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
||||||
|
const newStreams = [];
|
||||||
|
|
||||||
|
// const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
// const thumbnailBlocked = hiddenThumbnails.includes(res.data.directory.displayName);
|
||||||
|
|
||||||
|
const edges = res.data.directory.streams.edges;
|
||||||
|
for (let i = 0; i < edges.length; i++) {
|
||||||
|
const edge = edges[i];
|
||||||
|
const node = edge.node;
|
||||||
|
|
||||||
|
const s = node.viewersCount = new Number(node.viewersCount || 0);
|
||||||
|
s.profileImageURL = node.broadcaster.profileImageURL;
|
||||||
|
s.createdAt = node.createdAt;
|
||||||
|
|
||||||
|
// if (thumbnailBlocked) edge.node.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
||||||
|
|
||||||
|
newStreams.push(edge);
|
||||||
|
}
|
||||||
|
res.data.directory.streams.edges = newStreams;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.GameHeader.ready((cls, instances) => {
|
||||||
|
if (this.router.current.name === 'dir-game-index') {
|
||||||
|
for(const inst of instances) this.updateButtons(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ChannelCard.ready((cls, instances) => {
|
||||||
|
if (this.router.current.name === 'dir-game-index') {
|
||||||
|
this.apollo.ensureQuery(
|
||||||
|
'GamePage_Game',
|
||||||
|
'data.directory.streams.edges.0.node.createdAt'
|
||||||
|
);
|
||||||
|
|
||||||
|
for(const inst of instances) this.updateChannelCard(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ChannelCard.on('update', inst => this.router.current.name === 'dir-game-index' && this.updateChannelCard(inst), this);
|
||||||
|
|
||||||
|
this.ChannelCard.on('mount', inst => this.router.current.name === 'dir-game-index' && this.updateChannelCard(inst), this);
|
||||||
|
|
||||||
|
this.ChannelCard.on('unmount', inst => this.router.current.name === 'dir-game-index' && this.updateUptime(inst), this);
|
||||||
|
|
||||||
|
this.css_tweaks.toggleHide('profile-hover-game', this.settings.get('directory.following.show-channel-avatar') === 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUptime(inst) {
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
const card = container && container.querySelector && container.querySelector('.tw-thumbnail-card');
|
||||||
|
|
||||||
|
if (container === null || card === null) {
|
||||||
|
if (inst.updateTimer !== undefined) {
|
||||||
|
clearInterval(inst.updateTimer);
|
||||||
|
inst.updateTimer = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.get('directory.following.uptime') === 0) {
|
||||||
|
if (inst.updateTimer !== undefined) {
|
||||||
|
clearInterval(inst.updateTimer);
|
||||||
|
inst.updateTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inst.uptimeElement !== undefined) {
|
||||||
|
inst.uptimeElement.remove();
|
||||||
|
inst.uptimeElementSpan = inst.uptimeElement = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (inst.updateTimer === undefined) {
|
||||||
|
inst.updateTimer = setInterval(
|
||||||
|
this.updateUptime.bind(this, inst),
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const up_since = new Date(inst.props.streamNode.viewersCount.createdAt);
|
||||||
|
const uptime = up_since && Math.floor((Date.now() - up_since) / 1000) || 0;
|
||||||
|
const uptimeText = duration_to_string(uptime, false, false, false, this.settings.get('directory.following.uptime') === 1);
|
||||||
|
|
||||||
|
if (uptime > 0) {
|
||||||
|
if (inst.uptimeElement === undefined) {
|
||||||
|
inst.uptimeElementSpan = e('span', 'tw-stat__value ffz-uptime', `${uptimeText}`);
|
||||||
|
inst.uptimeElement = e('div', {
|
||||||
|
className: 'c-background-overlay c-text-overlay font-size-6 top-0 right-0 z-default inline-flex absolute mg-05',
|
||||||
|
style: 'padding-left: 4px; padding-right: 4px;'
|
||||||
|
}, [
|
||||||
|
e('span', 'tw-stat__icon',
|
||||||
|
e('figure', 'ffz-i-clock')
|
||||||
|
),
|
||||||
|
inst.uptimeElementSpan
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (card.querySelector('.ffz-uptime') === null) card.appendChild(inst.uptimeElement);
|
||||||
|
} else {
|
||||||
|
inst.uptimeElementSpan.textContent = `${uptimeText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChannelCard(inst) {
|
||||||
|
this.updateUptime(inst);
|
||||||
|
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
const card = container && container.querySelector && container.querySelector('.tw-thumbnail-card');
|
||||||
|
|
||||||
|
if (container === null || card === null) return;
|
||||||
|
|
||||||
|
if (!inst.props.streamNode.viewersCount.createdAt || container === null || card === null) return;
|
||||||
|
|
||||||
|
// Remove old elements
|
||||||
|
const hiddenBodyCard = card.querySelector('.tw-card-body.hide');
|
||||||
|
if (hiddenBodyCard !== null) hiddenBodyCard.classList.remove('hide');
|
||||||
|
|
||||||
|
const ffzChannelData = card.querySelector('.ffz-channel-data');
|
||||||
|
if (ffzChannelData !== null) ffzChannelData.remove();
|
||||||
|
|
||||||
|
const channelAvatar = card.querySelector('.channel-avatar');
|
||||||
|
if (channelAvatar !== null) channelAvatar.remove();
|
||||||
|
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
const thumbnailBlocked = hiddenThumbnails.includes(inst.props.directoryName);
|
||||||
|
this.log.warn(inst);
|
||||||
|
|
||||||
|
if (thumbnailBlocked) {
|
||||||
|
card.classList.add('ffz-thumbnail-hidden');
|
||||||
|
} else {
|
||||||
|
card.classList.remove('ffz-thumbnail-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inst.props.streamNode.viewersCount.profileImageURL) {
|
||||||
|
const avatarSetting = this.settings.get('directory.following.show-channel-avatar');
|
||||||
|
if (avatarSetting === 1) {
|
||||||
|
const cardDiv = card.querySelector('.tw-card-body');
|
||||||
|
const modifiedDiv = e('div', {
|
||||||
|
innerHTML: cardDiv.innerHTML
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarDiv = e('a', {
|
||||||
|
className: 'channel-avatar',
|
||||||
|
href: `/${inst.props.streamNode.broadcaster.login}`,
|
||||||
|
style: 'margin-right: 8px; min-width: 4rem; margin-top: 0.5rem;',
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: inst.props.streamNode.broadcaster.login});
|
||||||
|
}
|
||||||
|
}, e('img', {
|
||||||
|
title: inst.props.streamNode.broadcaster.displayName,
|
||||||
|
src: inst.props.streamNode.viewersCount.profileImageURL,
|
||||||
|
style: 'height: 4rem;'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const cardDivParent = cardDiv.parentElement;
|
||||||
|
|
||||||
|
if (cardDivParent.querySelector('.ffz-channel-data') === null) {
|
||||||
|
cardDiv.classList.add('hide');
|
||||||
|
|
||||||
|
const newCardDiv = e('div', 'ffz-channel-data flex flex-nowrap', [
|
||||||
|
avatarDiv, modifiedDiv
|
||||||
|
]);
|
||||||
|
cardDivParent.appendChild(newCardDiv);
|
||||||
|
}
|
||||||
|
} else if (avatarSetting === 2 || avatarSetting === 3) {
|
||||||
|
const avatarElement = e('a', {
|
||||||
|
className: 'channel-avatar',
|
||||||
|
href: `/${inst.props.streamNode.broadcaster.login}`,
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: inst.props.streamNode.broadcaster.login});
|
||||||
|
}
|
||||||
|
}, e('div', 'live-channel-card__boxart bottom-0 absolute',
|
||||||
|
e('figure', 'tw-aspect tw-aspect--align-top',
|
||||||
|
e('img', {
|
||||||
|
title: inst.props.streamNode.broadcaster.displayName,
|
||||||
|
src: inst.props.streamNode.viewersCount.profileImageURL
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const divToAppend = card.querySelector('figure.tw-aspect');
|
||||||
|
if (divToAppend.querySelector('.channel-avatar') === null) divToAppend.appendChild(avatarElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons(inst) {
|
||||||
|
const container = this.fine.getHostNode(inst);
|
||||||
|
// We can't get the buttons through querySelector('button ...') so this has to do for now...
|
||||||
|
const buttons = container && container.querySelector && container.querySelector('div > div.align-items-center');
|
||||||
|
|
||||||
|
const ffzButtons = buttons.querySelector('.ffz-buttons');
|
||||||
|
if (ffzButtons !== null) ffzButtons.remove();
|
||||||
|
|
||||||
|
if (buttons.querySelector('.ffz-buttons') === null) {
|
||||||
|
// Block / Unblock Games
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
const gameBlocked = blockedGames.includes(inst.props.directoryName);
|
||||||
|
|
||||||
|
const blockButton = e('button', {
|
||||||
|
className: 'mg-l-1 tw-button ffz-toggle-game-block',
|
||||||
|
style: `background-color: ${gameBlocked ? '#228B22' : '#B22222'};`
|
||||||
|
}, e('span', {
|
||||||
|
className: 'tw-button__text',
|
||||||
|
textContent: `${gameBlocked ? 'Unblock' : 'Block'}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
blockButton.addEventListener('click', () => {
|
||||||
|
const gameName = inst.props.directoryName;
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
if (blockedGames.includes(gameName)) blockedGames.splice(blockedGames.indexOf(gameName), 1);
|
||||||
|
else blockedGames.push(gameName);
|
||||||
|
|
||||||
|
this.settings.provider.set('directory.game.blocked-games', blockedGames);
|
||||||
|
|
||||||
|
this.updateButtons(inst);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide / Unhide Thumbnails
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
const thumbnailBlocked = hiddenThumbnails.includes(inst.props.directoryName);
|
||||||
|
|
||||||
|
const hideThumbnailButton = e('button', {
|
||||||
|
className: 'mg-l-1 tw-button ffz-toggle-thumbnail',
|
||||||
|
style: `background-color: ${thumbnailBlocked ? '#228B22' : '#B22222'};`
|
||||||
|
}, e('span', {
|
||||||
|
className: 'tw-button__text',
|
||||||
|
textContent: `${thumbnailBlocked ? 'Unhide Thumbnails' : 'Hide Thumbnails'}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
hideThumbnailButton.addEventListener('click', () => {
|
||||||
|
const gameName = inst.props.directoryName;
|
||||||
|
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||||
|
if (hiddenThumbnails.includes(gameName)) hiddenThumbnails.splice(hiddenThumbnails.indexOf(gameName), 1);
|
||||||
|
else hiddenThumbnails.push(gameName);
|
||||||
|
|
||||||
|
this.settings.provider.set('directory.game.hidden-thumbnails', hiddenThumbnails);
|
||||||
|
|
||||||
|
this.updateButtons(inst);
|
||||||
|
this.ChannelCard.forceUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ffzButtons = e('div', 'ffz-buttons', [
|
||||||
|
blockButton,
|
||||||
|
hideThumbnailButton
|
||||||
|
]);
|
||||||
|
|
||||||
|
buttons.appendChild(ffzButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/sites/twitch-twilight/modules/directory/index.js
Normal file
23
src/sites/twitch-twilight/modules/directory/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Directory
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import {SiteModule} from 'utilities/module';
|
||||||
|
|
||||||
|
import Following from './following';
|
||||||
|
import Game from './game';
|
||||||
|
import Community from './community';
|
||||||
|
|
||||||
|
export default class Directory extends SiteModule {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.should_enable = true;
|
||||||
|
|
||||||
|
this.inject(Following);
|
||||||
|
this.inject(Game);
|
||||||
|
this.inject(Community);
|
||||||
|
}
|
||||||
|
}
|
240
src/sites/twitch-twilight/modules/following_link.js
Normal file
240
src/sites/twitch-twilight/modules/following_link.js
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Following Button Modification
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import {SiteModule} from 'utilities/module';
|
||||||
|
import {createElement as e} from 'utilities/dom';
|
||||||
|
import {duration_to_string} from 'utilities/time';
|
||||||
|
|
||||||
|
import Tooltip from 'utilities/tooltip';
|
||||||
|
|
||||||
|
export default class FollowingText extends SiteModule {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.should_enable = true;
|
||||||
|
|
||||||
|
// this.inject('site');
|
||||||
|
this.inject('settings');
|
||||||
|
this.inject('site.router');
|
||||||
|
this.inject('site.apollo');
|
||||||
|
this.inject('i18n');
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowedChannels', `query {
|
||||||
|
currentUser {
|
||||||
|
followedLiveUsers {
|
||||||
|
nodes {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
stream {
|
||||||
|
type
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowedChannels', res => {
|
||||||
|
this.updateFollowing(this.nodes = res.data.currentUser.followedLiveUsers.nodes);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
this.router.on(':route', () => {
|
||||||
|
this.updateFollowing();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFollowing(nodes) {
|
||||||
|
nodes = nodes || this.nodes || [];
|
||||||
|
const followingText = await this.site.awaitElement('.top-nav__nav-link[data-a-target="following-link"]');
|
||||||
|
const topNavContainer = followingText.parentElement;
|
||||||
|
const oldFFZFollowingText = topNavContainer.querySelector('.ffz-following-container');
|
||||||
|
if (oldFFZFollowingText !== null) oldFFZFollowingText.remove();
|
||||||
|
|
||||||
|
const topSpan = e('span', {
|
||||||
|
className: 'top-span',
|
||||||
|
style: 'width: 100%; float: left; text-align: left; border-bottom: 1px black solid; padding-bottom: 8px;',
|
||||||
|
textContent: 'Following'
|
||||||
|
});
|
||||||
|
|
||||||
|
const height = document.body.querySelector('.twilight-root').clientHeight - 50;
|
||||||
|
const max_lines = Math.max(Math.floor(height / 40) - 1, 2);
|
||||||
|
|
||||||
|
let c = 0;
|
||||||
|
let filtered = 0;
|
||||||
|
|
||||||
|
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||||
|
|
||||||
|
const innerContent = [topSpan, e('br')];
|
||||||
|
if (nodes.length) {
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
|
||||||
|
if (blockedGames.includes(node.stream.game.name)) {
|
||||||
|
filtered += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
c += 1;
|
||||||
|
if (c > max_lines) {
|
||||||
|
const div = e('div', {
|
||||||
|
className: 'ffz-following-inner',
|
||||||
|
style: 'padding-top: 16px; padding-bottom: 4px;'
|
||||||
|
}, e('span', {
|
||||||
|
textContent: `And ${this.i18n.formatNumber(nodes.length - max_lines)} more${filtered ? ` (${filtered} hidden)` : ''}`,
|
||||||
|
style: 'float: left; margin-bottom: 8px;'
|
||||||
|
}));
|
||||||
|
innerContent.push(div);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const up_since = new Date(node.stream.createdAt);
|
||||||
|
const uptime = up_since && Math.floor((Date.now() - up_since) / 1000) || 0;
|
||||||
|
const uptimeText = duration_to_string(uptime, false, false, false, true);
|
||||||
|
|
||||||
|
const div = e('div', {
|
||||||
|
className: 'ffz-following-inner',
|
||||||
|
style: 'padding-top: 16px; padding-bottom: 4px;'
|
||||||
|
}, [
|
||||||
|
e('div', {
|
||||||
|
className: 'top-stream-info',
|
||||||
|
style: 'padding-bottom: 16px;'
|
||||||
|
}, [
|
||||||
|
// Username
|
||||||
|
e('a', {
|
||||||
|
class: 'top-nav__nav-link',
|
||||||
|
textContent: node.displayName,
|
||||||
|
style: 'float: left; font-weight: bold; padding: unset; font-size: unset;',
|
||||||
|
href: `/${node.login}`,
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: node.login });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
e('div', {
|
||||||
|
style: 'float: right;'
|
||||||
|
}, [
|
||||||
|
// Uptime
|
||||||
|
e('div', {
|
||||||
|
className: 'ffz-uptime-cont',
|
||||||
|
style: 'float: right;'
|
||||||
|
}, [
|
||||||
|
e('span', 'tw-stat__icon',
|
||||||
|
e('figure', 'ffz-i-clock')
|
||||||
|
),
|
||||||
|
e('span', {
|
||||||
|
textContent: uptimeText
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
// Viewers
|
||||||
|
e('div', {
|
||||||
|
className: 'ffz-viewer-cont',
|
||||||
|
style: 'float: right; margin-right: 4px;'
|
||||||
|
}, [
|
||||||
|
e('span', 'tw-stat__icon',
|
||||||
|
e('figure', 'ffz-i-plus')
|
||||||
|
),
|
||||||
|
e('span', {
|
||||||
|
textContent: `${this.i18n.formatNumber(node.stream.viewersCount)}`
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
e('span', {
|
||||||
|
className: 'ellipsis',
|
||||||
|
textContent: `Playing ${node.stream.game.name}`,
|
||||||
|
style: 'float: left; margin-bottom: 8px; max-width: 100%;'
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
innerContent.push(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered) {
|
||||||
|
const div = e('div', {
|
||||||
|
className: 'ffz-following-inner',
|
||||||
|
style: 'padding-top: 16px; padding-bottom: 4px;'
|
||||||
|
}, e('span', {
|
||||||
|
textContent: `(${filtered} hidden)`,
|
||||||
|
style: 'float: left; margin-bottom: 8px;'
|
||||||
|
}));
|
||||||
|
innerContent.push(div);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const div = e('div', {
|
||||||
|
className: 'ffz-following-inner',
|
||||||
|
style: 'padding-top: 16px; padding-bottom: 4px;'
|
||||||
|
}, e('span', {
|
||||||
|
textContent: `No one you're following is online.`,
|
||||||
|
style: 'float: left; margin-bottom: 8px;'
|
||||||
|
}));
|
||||||
|
innerContent.push(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = e('div', {
|
||||||
|
style: 'padding: 4px;',
|
||||||
|
}, innerContent);
|
||||||
|
|
||||||
|
const tipDiv = e('div', {
|
||||||
|
className: 'ffz-following',
|
||||||
|
tip_content: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newFollowing = e('div', {
|
||||||
|
className: 'top-nav__nav-link ffz-following-container',
|
||||||
|
style: 'padding: 1.5rem 1.5rem 0 0;'
|
||||||
|
}, [
|
||||||
|
e('a', {
|
||||||
|
class: 'top-nav__nav-link',
|
||||||
|
href: '/directory/following',
|
||||||
|
textContent: followingText.title,
|
||||||
|
onclick: event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('dir-following');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
e('span', {
|
||||||
|
className: 'tw-pill tw-pill--brand',
|
||||||
|
textContent: nodes.length,
|
||||||
|
style: 'margin-left: -0.5rem;'
|
||||||
|
}),
|
||||||
|
tipDiv
|
||||||
|
]);
|
||||||
|
|
||||||
|
topNavContainer.insertBefore(newFollowing, followingText);
|
||||||
|
followingText.classList.add('hide');
|
||||||
|
|
||||||
|
newFollowing.tooltip = new Tooltip(tipDiv, newFollowing, {
|
||||||
|
live: false,
|
||||||
|
html: true,
|
||||||
|
interactive: true,
|
||||||
|
delayHide: 250,
|
||||||
|
content: () => tipDiv.tip_content,
|
||||||
|
onShow: (t, tip) => tipDiv.tip = tip,
|
||||||
|
onHide: () => tipDiv.tip = null,
|
||||||
|
popper: {
|
||||||
|
placement: 'bottom',
|
||||||
|
modifiers: {
|
||||||
|
backgroundChange: {
|
||||||
|
order: 900 - 1,
|
||||||
|
enabled: true,
|
||||||
|
fn: data => {
|
||||||
|
data.styles.width = '300px';
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onEnable() {
|
||||||
|
await this.site.awaitElement('.top-nav__nav-link[data-a-target="following-link"]');
|
||||||
|
this.apollo.ensureQuery('FollowedChannels', 'data.currentUser.followedLiveUsers.nodes.0.stream.createdAt');
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,81 +18,12 @@ export default class Apollo extends Module {
|
||||||
this.inject('..web_munch');
|
this.inject('..web_munch');
|
||||||
this.inject('..fine');
|
this.inject('..fine');
|
||||||
|
|
||||||
this.registerModifier('FollowedIndex_CurrentUser', `query {
|
|
||||||
currentUser {
|
|
||||||
followedLiveUsers {
|
|
||||||
nodes {
|
|
||||||
profileImageURL(width: 70)
|
|
||||||
stream {
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
followedHosts {
|
|
||||||
nodes {
|
|
||||||
hosting {
|
|
||||||
profileImageURL(width: 70)
|
|
||||||
stream {
|
|
||||||
createdAt
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`);
|
|
||||||
|
|
||||||
this.registerModifier('FollowingLive_CurrentUser', `query {
|
|
||||||
currentUser {
|
|
||||||
followedLiveUsers {
|
|
||||||
nodes {
|
|
||||||
profileImageURL(width: 70)
|
|
||||||
stream {
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`);
|
|
||||||
|
|
||||||
this.registerModifier('ViewerCard', `query {
|
this.registerModifier('ViewerCard', `query {
|
||||||
targetUser: user {
|
targetUser: user {
|
||||||
createdAt
|
createdAt
|
||||||
profileViewCount
|
profileViewCount
|
||||||
}
|
|
||||||
}`);
|
|
||||||
|
|
||||||
/*this.registerModifier('GamePage_Game', `query {
|
|
||||||
directory {
|
|
||||||
... on Community {
|
|
||||||
streams {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
createdAt
|
|
||||||
type
|
|
||||||
broadcaster {
|
|
||||||
profileImageURL(width: 70)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}`);
|
||||||
... on Game {
|
|
||||||
streams {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
createdAt
|
|
||||||
type
|
|
||||||
broadcaster {
|
|
||||||
profileImageURL(width: 70)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`);*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEnable() {
|
async onEnable() {
|
||||||
|
@ -225,9 +156,9 @@ export default class Apollo extends Module {
|
||||||
modifier = [modifier, parsed];
|
modifier = [modifier, parsed];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mods = pre ?
|
const mods = pre
|
||||||
(this.modifiers[operation] = this.modifiers[operation] || []) :
|
? (this.modifiers[operation] = this.modifiers[operation] || [])
|
||||||
(this.post_modifiers[operation] = this.post_modifiers[operation] || []);
|
: (this.post_modifiers[operation] = this.post_modifiers[operation] || []);
|
||||||
|
|
||||||
mods.push(modifier);
|
mods.push(modifier);
|
||||||
}
|
}
|
||||||
|
@ -352,12 +283,19 @@ function merge(a, b) {
|
||||||
if ( a.selectionSet ) {
|
if ( a.selectionSet ) {
|
||||||
const s = a.selectionSet.selections,
|
const s = a.selectionSet.selections,
|
||||||
selects = {};
|
selects = {};
|
||||||
for(const sel of b.selectionSet.selections)
|
for(const sel of b.selectionSet.selections) {
|
||||||
selects[`${sel.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
|
if (sel.name && sel.name.value) {
|
||||||
|
selects[`${sel.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
|
||||||
|
} else {
|
||||||
|
if (sel.kind === 'InlineFragment') {
|
||||||
|
selects[`${sel.typeCondition.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for(let i=0, l = s.length; i < l; i++) {
|
for(let i=0, l = s.length; i < l; i++) {
|
||||||
const sel = s[i],
|
const sel = s[i],
|
||||||
name = sel.name.value,
|
name = sel.kind === 'InlineFragment' ? (sel.typeCondition.name ? sel.typeCondition.name.value : null) : (sel.name ? sel.name.value : null),
|
||||||
alias = sel.alias ? sel.alias.value : null,
|
alias = sel.alias ? sel.alias.value : null,
|
||||||
key = `${name}:${alias}`,
|
key = `${name}:${alias}`,
|
||||||
other = selects[key];
|
other = selects[key];
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue