mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-31 06:58:30 +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 = {
|
||||
'front-page': '/',
|
||||
'collection': '/collections/:collectionID',
|
||||
'dir': '/directory',
|
||||
'dir-community': '/communities/:communityName',
|
||||
'dir-community-index': '/directory/communities',
|
||||
'dir-creative': '/directory/creative',
|
||||
|
|
|
@ -21,7 +21,12 @@ const CLASSES = {
|
|||
'player-ext-hover': '.player[data-controls="false"] .player-extensions',
|
||||
|
||||
'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('..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 {
|
||||
targetUser: user {
|
||||
createdAt
|
||||
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() {
|
||||
|
@ -225,9 +156,9 @@ export default class Apollo extends Module {
|
|||
modifier = [modifier, parsed];
|
||||
}
|
||||
|
||||
const mods = pre ?
|
||||
(this.modifiers[operation] = this.modifiers[operation] || []) :
|
||||
(this.post_modifiers[operation] = this.post_modifiers[operation] || []);
|
||||
const mods = pre
|
||||
? (this.modifiers[operation] = this.modifiers[operation] || [])
|
||||
: (this.post_modifiers[operation] = this.post_modifiers[operation] || []);
|
||||
|
||||
mods.push(modifier);
|
||||
}
|
||||
|
@ -352,12 +283,19 @@ function merge(a, b) {
|
|||
if ( a.selectionSet ) {
|
||||
const s = a.selectionSet.selections,
|
||||
selects = {};
|
||||
for(const sel of b.selectionSet.selections)
|
||||
for(const sel of b.selectionSet.selections) {
|
||||
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++) {
|
||||
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,
|
||||
key = `${name}:${alias}`,
|
||||
other = selects[key];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue