mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +00:00
Hide Vodcasts & Host Button (#353)
* Fix occasional vanishing of stream card uptime, avatar and other info * Additionally fix routing issues with the modified avatar div * Add hide vodcast functionality * Add `vuedraggable` to dependencies * Remove LegacyAPI * Add host button with aut-host management menu Messy code, but it works. * Only add `disabled` class to button if chat connection isn't there * Fix for host button not updating it's text properly after un-/hosting * Add tooltip to host button and use alternative way to get the chat inst. * Rework host button stuff into new metadata All is functional, even though the code might be a mess. * Implement Auto-Host settings tab * Fix reassignment to const, hehe * Custom TMI events for Host and Unhost, plus use existing chat connection * Code adjustments, disabling the button when host is loading, etc. * Address code-review suggestions Translation support and a few other fixes * Remove inline styling * Show error in tooltip if hosting didn't work properly or similar issues * Address change requests * Fix mixup * Fix host options not having a background * Fix styling for the host options This adds a small border * Hide host button on own channel * Fix popper * Move `isChannelHosted` method further up * Adjust handle of auto host menu and fix vodcast hiding * Replace loading icon with text * Add setting for host button; Also another small issue * Fix joining your own channel multiple times
This commit is contained in:
parent
ac35ee5fab
commit
941aab9feb
11 changed files with 767 additions and 367 deletions
|
@ -42,6 +42,7 @@
|
||||||
"sortablejs": "^1.6.1",
|
"sortablejs": "^1.6.1",
|
||||||
"vue": "^2.5.2",
|
"vue": "^2.5.2",
|
||||||
"vue-clickaway": "^2.1.0",
|
"vue-clickaway": "^2.1.0",
|
||||||
"vue-template-compiler": "^2.5.2"
|
"vue-template-compiler": "^2.5.2",
|
||||||
|
"vuedraggable": "^2.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
293
src/api.js
293
src/api.js
|
@ -1,293 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Legacy API
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
|
||||||
import {has} from 'utilities/object';
|
|
||||||
import {EventEmitter} from 'utilities/events';
|
|
||||||
|
|
||||||
|
|
||||||
export default class ApiModule extends Module {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.inject('chat');
|
|
||||||
this.inject('chat.emotes');
|
|
||||||
|
|
||||||
this._apis = {};
|
|
||||||
|
|
||||||
if ( ! this._known_apis ) {
|
|
||||||
this._known_apis = {};
|
|
||||||
const stored_val = localStorage.getItem(`ffz_known_apis`);
|
|
||||||
if ( stored_val !== null )
|
|
||||||
try {
|
|
||||||
this._known_apis = JSON.parse(stored_val);
|
|
||||||
} catch(err) {
|
|
||||||
this.log.error(`Error loading known APIs`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
create(...args) {
|
|
||||||
return new LegacyAPI(this, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class LegacyAPI extends EventEmitter {
|
|
||||||
constructor(instance, name, icon = null, version = null, name_key = null) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.ffz = instance.root;
|
|
||||||
this.parent = instance;
|
|
||||||
|
|
||||||
if ( name ) {
|
|
||||||
for(const id in this.parent._known_apis) {
|
|
||||||
if ( this.parent._known_apis[id] === name ) {
|
|
||||||
this.id = id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! this.id ) {
|
|
||||||
let i = 0;
|
|
||||||
while ( ! this.id ) {
|
|
||||||
if ( ! has(this.parent._known_apis, i) ) {
|
|
||||||
this.id = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( name ) {
|
|
||||||
this.parent._known_apis[this.id] = name;
|
|
||||||
localStorage.ffz_known_apis = JSON.stringify(this.parent._known_apis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.parent._apis[this.id] = this;
|
|
||||||
|
|
||||||
this.emote_sets = {};
|
|
||||||
this.global_sets = [];
|
|
||||||
this.default_sets = [];
|
|
||||||
|
|
||||||
this.badges = {};
|
|
||||||
this.users = {};
|
|
||||||
|
|
||||||
this.name = name || `Extension#${this.id}`;
|
|
||||||
this.name_key = name_key || this.name.replace(/[^A-Z0-9_-]/g, '').toLowerCase();
|
|
||||||
|
|
||||||
if ( /^[0-9]/.test(this.name_key) )
|
|
||||||
this.name_key = `_${this.name_key}`;
|
|
||||||
|
|
||||||
this.icon = icon;
|
|
||||||
this.version = version;
|
|
||||||
|
|
||||||
this.parent.log.info(`Registered New Extension #${this.id} (${this.name_key}): ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log(msg, data) {
|
|
||||||
this.parent.log.info(`Ext #${this.id} (${this.name_key}): ${msg}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
error(msg, error) {
|
|
||||||
this.parent.log.error(`Ext #${this.id} (${this.name_key}): ${msg}`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
register_metadata(key, data) { } // eslint-disable-line
|
|
||||||
unregister_metadata(key, data) { } // eslint-disable-line
|
|
||||||
update_metadata(key, full_update) { } // eslint-disable-line
|
|
||||||
|
|
||||||
|
|
||||||
_load_set(real_id, set_id, data) {
|
|
||||||
if ( ! data )
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const emote_set = Object.assign({
|
|
||||||
source: this.name,
|
|
||||||
icon: this.icon || null,
|
|
||||||
title: 'Global Emoticons',
|
|
||||||
_type: 0
|
|
||||||
}, data, {
|
|
||||||
source_ext: this.id,
|
|
||||||
source_id: set_id,
|
|
||||||
id: real_id,
|
|
||||||
count: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emote_sets[set_id] = emote_set;
|
|
||||||
this.parent.emotes.loadSetData(real_id, emote_set);
|
|
||||||
|
|
||||||
return emote_set;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
load_set(set_id, emote_set) {
|
|
||||||
const real_id = `${this.id}-${set_id}`;
|
|
||||||
return this._load_set(real_id, set_id, emote_set);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
unload_set(set_id) {
|
|
||||||
const real_id = `${this.id}-${set_id}`,
|
|
||||||
emote_set = this.emote_sets[set_id];
|
|
||||||
|
|
||||||
if ( ! emote_set )
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.unregister_global_set(set_id);
|
|
||||||
|
|
||||||
// TODO: Unload sets
|
|
||||||
|
|
||||||
return emote_set;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get_set(set_id) {
|
|
||||||
return this.emote_sets[set_id];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
register_global_set(set_id, emote_set) {
|
|
||||||
const real_id = `${this.id}-${set_id}`;
|
|
||||||
if ( emote_set )
|
|
||||||
emote_set = this.load_set(set_id, emote_set);
|
|
||||||
else
|
|
||||||
emote_set = this.emote_sets[set_id];
|
|
||||||
|
|
||||||
if ( ! emote_set )
|
|
||||||
throw new Error('Invalid set ID.');
|
|
||||||
|
|
||||||
if ( this.parent.emotes.emote_sets && ! this.parent.emotes.emote_sets[real_id] )
|
|
||||||
this.parent.emotes.emote_sets[real_id] = emote_set;
|
|
||||||
|
|
||||||
if ( this.global_sets.indexOf(set_id) === -1 )
|
|
||||||
this.global_sets.push(set_id);
|
|
||||||
|
|
||||||
if ( this.default_sets.indexOf(set_id) === -1 )
|
|
||||||
this.default_sets.push(set_id);
|
|
||||||
|
|
||||||
this.parent.emotes.global_sets.push(`api--${this.id}`, real_id);
|
|
||||||
this.parent.emotes.default_sets.push(`api--${this.id}`, real_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister_global_set(set_id) {
|
|
||||||
const real_id = `${this.id}-${set_id}`,
|
|
||||||
emote_set = this.emote_sets[set_id];
|
|
||||||
|
|
||||||
if ( ! emote_set )
|
|
||||||
return;
|
|
||||||
|
|
||||||
let ind = this.global_sets.indexOf(set_id);
|
|
||||||
if ( ind !== -1 )
|
|
||||||
this.global_sets.splice(ind,1);
|
|
||||||
|
|
||||||
ind = this.default_sets.indexOf(set_id);
|
|
||||||
if ( ind !== -1 )
|
|
||||||
this.default_sets.splice(ind,1);
|
|
||||||
|
|
||||||
this.parent.emote.global_sets.remove(`api--${this.id}`, real_id);
|
|
||||||
this.parent.emote.default_sets.remove(`api--${this.id}`, real_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
register_room_set(room_login, set_id, emote_set) {
|
|
||||||
const real_id = `${this.id}-${set_id}`,
|
|
||||||
room = this.parent.chat.getRoom(null, room_login, true);
|
|
||||||
|
|
||||||
if ( ! room )
|
|
||||||
throw new Error('Room not loaded');
|
|
||||||
|
|
||||||
if ( emote_set ) {
|
|
||||||
emote_set.title = emote_set.title || `Channel: ${room.data && room.data.display_name || room_login}`;
|
|
||||||
emote_set._type = emote_set._type || 1;
|
|
||||||
|
|
||||||
emote_set = this.load_set(set_id, emote_set);
|
|
||||||
} else
|
|
||||||
emote_set = this.emote_sets[set_id];
|
|
||||||
|
|
||||||
if ( ! emote_set )
|
|
||||||
throw new Error('Invalid set ID.');
|
|
||||||
|
|
||||||
if ( this.parent.emotes.emote_sets && ! this.parent.emotes.emote_sets[real_id] )
|
|
||||||
this.parent.emotes.emote_sets[real_id] = emote_set;
|
|
||||||
|
|
||||||
room.emote_sets.push(`api--${this.id}`, real_id);
|
|
||||||
emote_set.users++;
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister_room_set(room_login, set_id) {
|
|
||||||
const real_id = `${this.id}-${set_id}`,
|
|
||||||
emote_set = this.emote_sets[set_id],
|
|
||||||
room = this.parent.chat.getRoom(null, room_login, true);
|
|
||||||
|
|
||||||
if ( ! emote_set || ! room )
|
|
||||||
return;
|
|
||||||
|
|
||||||
room.emote_sets.remove(`api--${this.id}`, real_id);
|
|
||||||
emote_set.users--;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
add_badge() { } // eslint-disable-line
|
|
||||||
remove_badge() { } // eslint-disable-line
|
|
||||||
user_add_badge() { } // eslint-disable-line
|
|
||||||
user_remove_badge() { } // eslint-disable-line
|
|
||||||
room_add_user_badge() { } // eslint-disable-line
|
|
||||||
room_remove_user_badge() { } // eslint-disable-line
|
|
||||||
|
|
||||||
user_add_set(username, set_id) { // eslint-disable-line
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
user_remove_set(username, set_id) { // eslint-disable-line
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
retokenize_messages() { } // eslint-disable-line
|
|
||||||
|
|
||||||
|
|
||||||
register_chat_filter(filter) {
|
|
||||||
this.on('room-message', filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister_chat_filter(filter) {
|
|
||||||
this.off('room-message', filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
iterate_chat_views(func) { } // eslint-disable-line
|
|
||||||
iterate_rooms(func) {
|
|
||||||
if ( func === undefined )
|
|
||||||
func = this.emit.bind(this, 'room-add');
|
|
||||||
|
|
||||||
const chat = this.parent.resolve('chat');
|
|
||||||
for(const room_id in chat.rooms)
|
|
||||||
if ( has(chat.rooms, room_id) )
|
|
||||||
func(room_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
register_on_room_callback(callback, dont_iterate) {
|
|
||||||
const thing = room_id => callback(room_id, this.register_room_set.bind(this, room_id));
|
|
||||||
|
|
||||||
thing.original_func = callback;
|
|
||||||
callback.__wrapped = thing;
|
|
||||||
|
|
||||||
this.on('room-add', thing);
|
|
||||||
if ( ! dont_iterate )
|
|
||||||
this.iterate_rooms(thing);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister_on_room_callback(callback) {
|
|
||||||
if ( ! callback.__wrapped )
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.off('room-add', callback.__wrapped);
|
|
||||||
callback.__wrapped = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
13
src/main.js
13
src/main.js
|
@ -9,7 +9,6 @@ import SettingsManager from './settings/index';
|
||||||
import {TranslationManager} from './i18n';
|
import {TranslationManager} from './i18n';
|
||||||
import SocketClient from './socket';
|
import SocketClient from './socket';
|
||||||
import Site from 'site';
|
import Site from 'site';
|
||||||
import LegacyAPI from './api';
|
|
||||||
import Vue from 'utilities/vue';
|
import Vue from 'utilities/vue';
|
||||||
|
|
||||||
class FrankerFaceZ extends Module {
|
class FrankerFaceZ extends Module {
|
||||||
|
@ -39,8 +38,6 @@ class FrankerFaceZ extends Module {
|
||||||
this.inject('socket', SocketClient);
|
this.inject('socket', SocketClient);
|
||||||
this.inject('site', Site);
|
this.inject('site', Site);
|
||||||
|
|
||||||
this.inject('_api', LegacyAPI);
|
|
||||||
|
|
||||||
this.register('vue', Vue);
|
this.register('vue', Vue);
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,13 +118,3 @@ FrankerFaceZ.utilities = {
|
||||||
|
|
||||||
window.FrankerFaceZ = FrankerFaceZ;
|
window.FrankerFaceZ = FrankerFaceZ;
|
||||||
window.ffz = new FrankerFaceZ();
|
window.ffz = new FrankerFaceZ();
|
||||||
|
|
||||||
// Make FFZ:AP Run
|
|
||||||
FrankerFaceZ.chat_commands = {};
|
|
||||||
FrankerFaceZ.settings_info = {};
|
|
||||||
FrankerFaceZ.utils = {
|
|
||||||
process_int: a => a
|
|
||||||
}
|
|
||||||
window.App = true;
|
|
||||||
if ( window.jQuery )
|
|
||||||
window.jQuery.noty = {themes: {}};
|
|
|
@ -13,15 +13,6 @@ import SettingsContext from './context';
|
||||||
import MigrationManager from './migration';
|
import MigrationManager from './migration';
|
||||||
|
|
||||||
|
|
||||||
const OVERRIDE_GET = {
|
|
||||||
ffz_enable_highlight_sound: false,
|
|
||||||
ffz_highlight_sound_volume: 0,
|
|
||||||
bttv_channel_emotes: true,
|
|
||||||
bttv_global_emotes: true,
|
|
||||||
bttv_gif_emotes: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SettingsManager
|
// SettingsManager
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
@ -331,12 +322,7 @@ export default class SettingsManager extends Module {
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
context(env) { return this.main_context.context(env) }
|
context(env) { return this.main_context.context(env) }
|
||||||
get(key) {
|
get(key) { return this.main_context.get(key); }
|
||||||
if ( has(OVERRIDE_GET, key) )
|
|
||||||
return OVERRIDE_GET[key];
|
|
||||||
|
|
||||||
return this.main_context.get(key);
|
|
||||||
}
|
|
||||||
uses(key) { return this.main_context.uses(key) }
|
uses(key) { return this.main_context.uses(key) }
|
||||||
update(key) { return this.main_context.update(key) }
|
update(key) { return this.main_context.update(key) }
|
||||||
|
|
||||||
|
|
|
@ -428,6 +428,18 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const old_host = this.onHostingEvent;
|
||||||
|
this.onHostingEvent = function (e, _t) {
|
||||||
|
t.emit('tmi:host', e, _t);
|
||||||
|
return old_host.call(i, e, _t);
|
||||||
|
}
|
||||||
|
|
||||||
|
const old_unhost = this.onUnhostEvent;
|
||||||
|
this.onUnhostEvent = function (e, _t) {
|
||||||
|
t.emit('tmi:unhost', e, _t);
|
||||||
|
return old_unhost.call(i, e, _t);
|
||||||
|
}
|
||||||
|
|
||||||
this.postMessage = function(e) {
|
this.postMessage = function(e) {
|
||||||
const original = this._wrapped;
|
const original = this._wrapped;
|
||||||
if ( original ) {
|
if ( original ) {
|
||||||
|
|
|
@ -112,6 +112,20 @@ export default class Following extends SiteModule {
|
||||||
}
|
}
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowedChannels', `query {
|
||||||
|
currentUser {
|
||||||
|
followedLiveUsers {
|
||||||
|
nodes {
|
||||||
|
profileImageURL(width: 70)
|
||||||
|
stream {
|
||||||
|
type
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
this.ChannelCard = this.fine.define(
|
this.ChannelCard = this.fine.define(
|
||||||
'following-channel-card',
|
'following-channel-card',
|
||||||
n => n.renderGameBoxArt && n.renderContentType
|
n => n.renderGameBoxArt && n.renderContentType
|
||||||
|
@ -121,11 +135,13 @@ export default class Following extends SiteModule {
|
||||||
this.modifyLiveUsers(res);
|
this.modifyLiveUsers(res);
|
||||||
this.modifyLiveHosts(res);
|
this.modifyLiveHosts(res);
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
this.on('settings:changed:directory.uptime', () => this.ChannelCard.forceUpdate());
|
this.on('settings:changed:directory.uptime', () => this.ChannelCard.forceUpdate());
|
||||||
this.on('settings:changed:directory.show-channel-avatars', () => this.ChannelCard.forceUpdate());
|
this.on('settings:changed:directory.show-channel-avatars', () => this.ChannelCard.forceUpdate());
|
||||||
this.on('settings:changed:directory.show-boxart', () => this.ChannelCard.forceUpdate());
|
this.on('settings:changed:directory.show-boxart', () => this.ChannelCard.forceUpdate());
|
||||||
|
this.on('settings:changed:directory.hide-vodcasts', () => this.ChannelCard.forceUpdate());
|
||||||
|
|
||||||
|
this.apollo.registerModifier('FollowedChannels', res => this.modifyLiveUsers(res), false);
|
||||||
this.apollo.registerModifier('FollowingLive_CurrentUser', res => this.modifyLiveUsers(res), false);
|
this.apollo.registerModifier('FollowingLive_CurrentUser', res => this.modifyLiveUsers(res), false);
|
||||||
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
|
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
|
||||||
}
|
}
|
||||||
|
@ -170,13 +186,10 @@ export default class Following extends SiteModule {
|
||||||
const s = node.hosting.stream.viewersCount = new Number(node.hosting.stream.viewersCount || 0);
|
const s = node.hosting.stream.viewersCount = new Number(node.hosting.stream.viewersCount || 0);
|
||||||
s.profileImageURL = node.hosting.profileImageURL;
|
s.profileImageURL = node.hosting.profileImageURL;
|
||||||
s.createdAt = node.hosting.stream.createdAt;
|
s.createdAt = node.hosting.stream.createdAt;
|
||||||
s.hostData = {
|
|
||||||
channel: node.hosting.login,
|
|
||||||
displayName: node.hosting.displayName
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this.hosts[node.hosting.displayName]) {
|
if (!this.hosts[node.hosting.displayName]) {
|
||||||
this.hosts[node.hosting.displayName] = {
|
this.hosts[node.hosting.displayName] = {
|
||||||
|
channel: node.hosting.login,
|
||||||
nodes: [node],
|
nodes: [node],
|
||||||
channels: [node.displayName]
|
channels: [node.displayName]
|
||||||
};
|
};
|
||||||
|
@ -194,36 +207,48 @@ export default class Following extends SiteModule {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureQueries () {
|
||||||
|
if (this.router && this.router.match) {
|
||||||
|
this.apollo.ensureQuery(
|
||||||
|
'FollowedChannels',
|
||||||
|
'data.currentUser.followedLiveUsers.nodes.0.profileImageURL'
|
||||||
|
);
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.ChannelCard.ready((cls, instances) => {
|
this.ChannelCard.ready((cls, instances) => {
|
||||||
if (this.router && this.router.match) {
|
this.ensureQueries();
|
||||||
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);
|
for(const inst of instances) this.updateChannelCard(inst);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ChannelCard.on('update', inst => this.updateChannelCard(inst), this);
|
this.ChannelCard.on('update', inst => {
|
||||||
this.ChannelCard.on('mount', inst => this.updateChannelCard(inst), this);
|
this.ensureQueries();
|
||||||
this.ChannelCard.on('unmount', inst => this.parent.clearUptime(inst), this);
|
this.updateChannelCard(inst)
|
||||||
|
}, this);
|
||||||
|
this.ChannelCard.on('mount', this.updateChannelCard, this);
|
||||||
|
this.ChannelCard.on('unmount', this.parent.clearUptime, this);
|
||||||
|
|
||||||
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
|
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
|
||||||
}
|
}
|
||||||
|
@ -246,6 +271,7 @@ export default class Following extends SiteModule {
|
||||||
|
|
||||||
this.hostMenu && this.hostMenu.remove();
|
this.hostMenu && this.hostMenu.remove();
|
||||||
|
|
||||||
|
const hostData = this.hosts[inst.props.channelName];
|
||||||
const simplebarContentChildren = [];
|
const simplebarContentChildren = [];
|
||||||
|
|
||||||
// Hosted Channel Header
|
// Hosted Channel Header
|
||||||
|
@ -260,11 +286,11 @@ export default class Following extends SiteModule {
|
||||||
simplebarContentChildren.push(
|
simplebarContentChildren.push(
|
||||||
e('a', {
|
e('a', {
|
||||||
className: 'tw-interactable',
|
className: 'tw-interactable',
|
||||||
href: `/${inst.props.viewerCount.hostData.channel}`,
|
href: `/${hostData.channel}`,
|
||||||
onclick: event =>
|
onclick: event =>
|
||||||
this.parent.hijackUserClick(
|
this.parent.hijackUserClick(
|
||||||
event,
|
event,
|
||||||
inst.props.viewerCount.hostData.channel,
|
hostData.channel,
|
||||||
this.destroyHostMenu.bind(this)
|
this.destroyHostMenu.bind(this)
|
||||||
)
|
)
|
||||||
}, e('div', 'tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05',
|
}, e('div', 'tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05',
|
||||||
|
@ -292,9 +318,8 @@ export default class Following extends SiteModule {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Hosting Channels Content
|
// Hosting Channels Content
|
||||||
const hosts = this.hosts[inst.props.channelName];
|
for (let i = 0; i < hostData.nodes.length; i++) {
|
||||||
for (let i = 0; i < hosts.nodes.length; i++) {
|
const node = hostData.nodes[i];
|
||||||
const node = hosts.nodes[i];
|
|
||||||
simplebarContentChildren.push(
|
simplebarContentChildren.push(
|
||||||
e('a', {
|
e('a', {
|
||||||
className: 'tw-interactable',
|
className: 'tw-interactable',
|
||||||
|
@ -346,16 +371,17 @@ export default class Following extends SiteModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChannelCard(inst) {
|
updateChannelCard(inst) {
|
||||||
//if (!this.isRouteAcceptable()) return;
|
|
||||||
|
|
||||||
this.parent.updateUptime(inst, 'props.viewerCount.createdAt', '.tw-card .tw-aspect > div');
|
this.parent.updateUptime(inst, 'props.viewerCount.createdAt', '.tw-card .tw-aspect > div');
|
||||||
|
|
||||||
const container = this.fine.getHostNode(inst),
|
const container = this.fine.getHostNode(inst),
|
||||||
card = container && container.querySelector && container.querySelector('.tw-card');
|
card = container && container.querySelector && container.querySelector('.tw-card');
|
||||||
|
|
||||||
if ( container === null || card === null )
|
if ( container === null || card === null )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (inst.props.streamType === 'watch_party')
|
||||||
|
container.parentElement.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts'));
|
||||||
|
|
||||||
// Remove old elements
|
// Remove old elements
|
||||||
const hiddenBodyCard = card.querySelector('.tw-card-body.tw-hide');
|
const hiddenBodyCard = card.querySelector('.tw-card-body.tw-hide');
|
||||||
if (hiddenBodyCard !== null) hiddenBodyCard.classList.remove('tw-hide');
|
if (hiddenBodyCard !== null) hiddenBodyCard.classList.remove('tw-hide');
|
||||||
|
@ -367,11 +393,11 @@ export default class Following extends SiteModule {
|
||||||
if (channelAvatar !== null) channelAvatar.remove();
|
if (channelAvatar !== null) channelAvatar.remove();
|
||||||
|
|
||||||
if (inst.props.viewerCount.profileImageURL) {
|
if (inst.props.viewerCount.profileImageURL) {
|
||||||
const hosting = inst.props.channelNameLinkTo.state.content === 'live_host' && inst.props.viewerCount.hostData;
|
const hosting = inst.props.channelNameLinkTo.state.content === 'live_host' && this.hosts[inst.props.channelName];
|
||||||
let channel, displayName;
|
let channel, displayName;
|
||||||
if (hosting) {
|
if (hosting) {
|
||||||
channel = inst.props.viewerCount.hostData.channel;
|
channel = this.hosts[inst.props.channelName].channel;
|
||||||
displayName = inst.props.viewerCount.hostData.displayName;
|
displayName = inst.props.channelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarSetting = this.settings.get('directory.show-channel-avatars');
|
const avatarSetting = this.settings.get('directory.show-channel-avatars');
|
||||||
|
@ -380,11 +406,26 @@ export default class Following extends SiteModule {
|
||||||
innerHTML: cardDiv.innerHTML
|
innerHTML: cardDiv.innerHTML
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const broadcasterLogin = inst.props.linkTo.pathname.substring(1);
|
||||||
|
modifiedDiv.querySelector('.live-channel-card__channel').onclick = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user', { userName: broadcasterLogin });
|
||||||
|
};
|
||||||
|
modifiedDiv.querySelector('.live-channel-card__videos').onclick = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.router.navigate('user-videos', { userName: broadcasterLogin });
|
||||||
|
};
|
||||||
|
|
||||||
let avatarDiv;
|
let avatarDiv;
|
||||||
if (avatarSetting === 1) {
|
if (avatarSetting === 1) {
|
||||||
avatarDiv = e('a', {
|
avatarDiv = e('a', {
|
||||||
className: 'ffz-channel-avatar tw-mg-r-05 tw-mg-t-05',
|
className: 'ffz-channel-avatar tw-mg-r-05 tw-mg-t-05',
|
||||||
href: hosting ? `/${channel}` : inst.props.linkTo.pathname,
|
href: hosting ? `/${channel}` : inst.props.linkTo.pathname,
|
||||||
|
onclick: event => this.parent.hijackUserClick(event, broadcasterLogin)
|
||||||
}, e('img', {
|
}, e('img', {
|
||||||
title: inst.props.channelName,
|
title: inst.props.channelName,
|
||||||
src: inst.props.viewerCount.profileImageURL
|
src: inst.props.viewerCount.profileImageURL
|
||||||
|
@ -393,7 +434,7 @@ export default class Following extends SiteModule {
|
||||||
const avatarElement = e('a', {
|
const avatarElement = e('a', {
|
||||||
className: 'ffz-channel-avatar',
|
className: 'ffz-channel-avatar',
|
||||||
href: hosting ? `/${channel}` : inst.props.linkTo.pathname,
|
href: hosting ? `/${channel}` : inst.props.linkTo.pathname,
|
||||||
onclick: event => this.parent.hijackUserClick(event, inst.props.streamNode.broadcaster.login)
|
onclick: event => this.parent.hijackUserClick(event, broadcasterLogin)
|
||||||
}, e('div', 'live-channel-card__boxart tw-bottom-0 tw-absolute',
|
}, e('div', 'live-channel-card__boxart tw-bottom-0 tw-absolute',
|
||||||
e('figure', 'tw-aspect tw-aspect--align-top',
|
e('figure', 'tw-aspect tw-aspect--align-top',
|
||||||
e('img', {
|
e('img', {
|
||||||
|
|
|
@ -106,6 +106,20 @@ export default class Directory extends SiteModule {
|
||||||
this.ChannelCard.forceUpdate();
|
this.ChannelCard.forceUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.settings.add('directory.hide-vodcasts', {
|
||||||
|
default: false,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Channels >> Appearance',
|
||||||
|
title: 'Hide Vodcasts',
|
||||||
|
description: 'Hide vodcasts in the directories.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: () => this.ChannelCard.forceUpdate()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,6 +160,10 @@ export default class Directory extends SiteModule {
|
||||||
const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
||||||
|
|
||||||
const container = this.fine.getHostNode(inst);
|
const container = this.fine.getHostNode(inst);
|
||||||
|
|
||||||
|
if (inst.props.streamNode.type === 'watch_party')
|
||||||
|
container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts'));
|
||||||
|
|
||||||
const img = container && container.querySelector && container.querySelector(`${uptimeSel} img`);
|
const img = container && container.querySelector && container.querySelector(`${uptimeSel} img`);
|
||||||
if (img === null) return;
|
if (img === null) return;
|
||||||
|
|
||||||
|
|
171
src/sites/twitch-twilight/modules/host-options.vue
Normal file
171
src/sites/twitch-twilight/modules/host-options.vue
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<template lang="html">
|
||||||
|
<div class="ffz-auto-host-options tw-c-background">
|
||||||
|
<header class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap">
|
||||||
|
<h4>{{ t('metadata.host.title', 'Auto Host Management') }}</h4>
|
||||||
|
</header>
|
||||||
|
<div class="tab tw-overflow-hidden"
|
||||||
|
v-show="activeTab === 'auto-host'"
|
||||||
|
:class="{ active: activeTab === 'auto-host'}">
|
||||||
|
<section class="tw-border-t tw-full-width tw-full-height">
|
||||||
|
<main class="tw-flex-grow-1 scrollable-area" data-simplebar="init">
|
||||||
|
<div class="simplebar-scroll-content">
|
||||||
|
<draggable v-model="hosts" class="simplebar-content" :options="{
|
||||||
|
draggable: '.ffz--host-user',
|
||||||
|
animation: 150,
|
||||||
|
}" @update="rearrangeHosts">
|
||||||
|
<div v-for="host in hosts" class="tw-border-t ffz--host-user" :key="host._id" :data-id="host._id">
|
||||||
|
<div class="tw-interactable">
|
||||||
|
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1">
|
||||||
|
<figure class="ffz-i-ellipsis-vert handle"></figure>
|
||||||
|
<div class="ffz-channel-avatar">
|
||||||
|
<img :src="host.logo" :alt="host.display_name + '(' + host.name + ')'">
|
||||||
|
</div>
|
||||||
|
<p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">{{ host.name }}</p>
|
||||||
|
<div class="tw-flex-grow-1 tw-pd-x-2"></div>
|
||||||
|
<button class="tw-button-icon tw-mg-x-05 ffz--host-remove-user" @click="removeFromHosts">
|
||||||
|
<figure class="ffz-i-trash"></figure>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
<header class="tw-border-t tw-full-width tw-align-items-center tw-flex tw-flex-noxwrap tw-pd-1">
|
||||||
|
<div class="tw-flex-grow-1 tw-pd-x-2"></div>
|
||||||
|
<button class="tw-button tw-button--hollow tw-mg-x-05" :class="{'tw-button--disabled': addedToHosts}" @click="addToAutoHosts">
|
||||||
|
<span class="tw-button__text">{{ t('metadata.host.add-channel', 'Add To Auto Host') }}</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div class="tab tw-overflow-hidden"
|
||||||
|
v-show="activeTab === 'settings'"
|
||||||
|
:class="{ active: activeTab === 'settings'}">
|
||||||
|
<section class="tw-border-t tw-full-width tw-full-height">
|
||||||
|
<main class="tw-flex-grow-1 scrollable-area" data-simplebar="init">
|
||||||
|
<div class="simplebar-scroll-content">
|
||||||
|
<div class="simplebar-content">
|
||||||
|
<div class="tw-pd-1">
|
||||||
|
<div class="ffz--widget ffz--checkbox">
|
||||||
|
<div class="tw-flex tw-align-items-center">
|
||||||
|
<input type="checkbox" class="tw-checkbox__input"
|
||||||
|
id="autoHostSettings:enabled"
|
||||||
|
data-setting="enabled"
|
||||||
|
:checked="autoHostSettings.enabled"
|
||||||
|
@change="updateCheckbox">
|
||||||
|
<label for="autoHostSettings:enabled" class="tw-checkbox__label">
|
||||||
|
{{ t('metadata.host.setting.auto-hosting.title', 'Auto Hosting') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<section class="tw-c-text-alt-2 ffz-checkbox-description">
|
||||||
|
{{ t('metadata.host.setting.auto-hosting.description', 'Toggle all forms of auto hosting: teammates, host list, and similar channels.') }}<br>
|
||||||
|
<a href="https://blog.twitch.tv/grow-your-community-with-auto-hosting-e80c1460f6e1" target="_blank" rel="noopener">{{ t('metadata.host.setting.auto-hosting.link', 'Learn More') }}</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="ffz--widget ffz--checkbox">
|
||||||
|
<div class="tw-flex tw-align-items-center">
|
||||||
|
<input type="checkbox" class="tw-checkbox__input"
|
||||||
|
id="autoHostSettings:team_host"
|
||||||
|
data-setting="team_host"
|
||||||
|
:checked="autoHostSettings.team_host"
|
||||||
|
@change="updateCheckbox">
|
||||||
|
<label for="autoHostSettings:team_host" class="tw-checkbox__label">
|
||||||
|
{{ t('metadata.host.setting.team-hosting.title', 'Team Hosting') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<section class="tw-c-text-alt-2 ffz-checkbox-description">
|
||||||
|
{{ t('metadata.host.setting.team-hosting.description',
|
||||||
|
'Automatically host random channels from your team when you\'re not live. ' +
|
||||||
|
'Team channels will be hosted before any channels in your host list.') }}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="ffz--widget ffz--checkbox">
|
||||||
|
<div class="tw-flex tw-align-items-center">
|
||||||
|
<input type="checkbox" class="tw-checkbox__input"
|
||||||
|
id="autoHostSettings:vodcast_hosting"
|
||||||
|
data-setting="deprioritize_vodcast"
|
||||||
|
:checked="!autoHostSettings.deprioritize_vodcast"
|
||||||
|
@change="updateCheckbox">
|
||||||
|
<label for="autoHostSettings:vodcast_hosting" class="tw-checkbox__label">
|
||||||
|
{{ t('metadata.host.setting.vodcast-hosting.title', 'Vodcast Hosting') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<section class="tw-c-text-alt-2 ffz-checkbox-description">
|
||||||
|
{{ t('metadata.host.setting.vodcast-hosting.description', 'Include Vodcasts in auto host.') }}
|
||||||
|
<a href="https://blog.twitch.tv/vodcast-brings-the-twitch-community-experience-to-uploads-54098498715" target="_blank" rel="noopener">{{ t('metadata.host.setting.vodcast-hosting.link', 'Learn about Vodcasts') }}</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="ffz--widget ffz--checkbox">
|
||||||
|
<div class="tw-flex tw-align-items-center">
|
||||||
|
<input type="checkbox" class="tw-checkbox__input"
|
||||||
|
id="autoHostSettings:recommended_host"
|
||||||
|
data-setting="recommended_host"
|
||||||
|
:checked="autoHostSettings.recommended_host"
|
||||||
|
@change="updateCheckbox">
|
||||||
|
<label for="autoHostSettings:recommended_host" class="tw-checkbox__label">
|
||||||
|
{{ t('metadata.host.setting.recommended-hosting.title', 'Auto Host Channels Similar To Yours') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<section class="tw-c-text-alt-2 ffz-checkbox-description">
|
||||||
|
{{ t('metadata.host.setting.recommended-hosting.description', 'Streamers on your primary team & host list will always be hosted first') }}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="ffz--widget ffz--checkbox">
|
||||||
|
<div class="tw-flex tw-align-items-center">
|
||||||
|
<input type="checkbox" class="tw-checkbox__input"
|
||||||
|
id="autoHostSettings:strategy"
|
||||||
|
data-setting="strategy"
|
||||||
|
:checked="autoHostSettings.strategy === 'random'"
|
||||||
|
@change="updateCheckbox">
|
||||||
|
<label for="autoHostSettings:strategy" class="tw-checkbox__label">
|
||||||
|
{{ t('metadata.host.setting.strategy.title', 'Randomize Host Order') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<section class="tw-c-text-alt-2 ffz-checkbox-description">
|
||||||
|
{{ t('metadata.host.setting.strategy.description',
|
||||||
|
'If enabled, auto-hosts will be picked at random. ' +
|
||||||
|
'Otherwise they\'re picked in order.') }}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="host-options__tabs-container tw-border-t">
|
||||||
|
<div id="host-options__auto-host" class="host-options__tab tw-pd-x-1"
|
||||||
|
@click="setActiveTab('auto-host')"
|
||||||
|
:class="{active: activeTab === 'auto-host'}">
|
||||||
|
<span>{{ t('metadata.host.tab.auto-host', 'Auto Host') }}</span>
|
||||||
|
</div>
|
||||||
|
<div id="host-options__settings" class="host-options__tab tw-pd-x-1"
|
||||||
|
@click="setActiveTab('settings')"
|
||||||
|
:class="{active: activeTab === 'settings'}">
|
||||||
|
<span>{{ t('metadata.host.tab.settings', 'Settings') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from 'vuedraggable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
draggable
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return this.$vnode.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
this.updatePopper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
422
src/sites/twitch-twilight/modules/host_button.js
Normal file
422
src/sites/twitch-twilight/modules/host_button.js
Normal file
|
@ -0,0 +1,422 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Host Button
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import Module from 'utilities/module';
|
||||||
|
import {createElement as e} from 'utilities/dom';
|
||||||
|
|
||||||
|
const HOST_ERRORS = {
|
||||||
|
COMMAND_EXECUTION: {
|
||||||
|
key: 'command-execution',
|
||||||
|
text: 'There was an error executing the host command. Please try again later.',
|
||||||
|
},
|
||||||
|
CHAT_CONNECTION: {
|
||||||
|
key: 'chat-connection',
|
||||||
|
text: 'There was an issue connecting to chat. Please try again later.',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class HostButton extends Module {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.should_enable = true;
|
||||||
|
|
||||||
|
this.inject('site');
|
||||||
|
this.inject('site.fine');
|
||||||
|
this.inject('site.chat');
|
||||||
|
this.inject('i18n');
|
||||||
|
this.inject('metadata');
|
||||||
|
this.inject('settings');
|
||||||
|
|
||||||
|
this.settings.add('metadata.host-button', {
|
||||||
|
default: true,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
path: 'Channel > Metadata >> Player',
|
||||||
|
title: 'Host Button',
|
||||||
|
description: 'Show a host button with the current hosted channel in the tooltip.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: () => {
|
||||||
|
const ffz_user = this.site.getUser(),
|
||||||
|
userLogin = ffz_user && ffz_user.login;
|
||||||
|
|
||||||
|
if (userLogin)
|
||||||
|
this.joinChannel(userLogin);
|
||||||
|
|
||||||
|
this.metadata.updateMetadata('host');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.metadata.definitions.host = {
|
||||||
|
order: 150,
|
||||||
|
button: true,
|
||||||
|
|
||||||
|
disabled: () => {
|
||||||
|
return this._host_updating || this._host_error;
|
||||||
|
},
|
||||||
|
|
||||||
|
click: data => {
|
||||||
|
if (data.channel) this.sendHostUnhostCommand(data.channel.login);
|
||||||
|
},
|
||||||
|
|
||||||
|
popup: async (data, tip) => {
|
||||||
|
const vue = this.resolve('vue'),
|
||||||
|
_host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'),
|
||||||
|
_autoHosts = this.fetchAutoHosts(),
|
||||||
|
_autoHostSettings = this.fetchAutoHostSettings();
|
||||||
|
|
||||||
|
const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]);
|
||||||
|
|
||||||
|
this._auto_host_tip = tip;
|
||||||
|
tip.element.classList.remove('tw-pd-1');
|
||||||
|
tip.element.classList.add('tw-balloon--lg');
|
||||||
|
vue.component('host-options', host_options_vue.default);
|
||||||
|
return this.buildAutoHostMenu(vue, autoHosts, autoHostSettings, data.channel);
|
||||||
|
},
|
||||||
|
|
||||||
|
label: data => {
|
||||||
|
if (!this.settings.get('metadata.host-button')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ffz_user = this.site.getUser(),
|
||||||
|
userLogin = ffz_user && ffz_user.login;
|
||||||
|
|
||||||
|
if (data.channel && data.channel.login === userLogin) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._host_updating) {
|
||||||
|
return 'Updating...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this._last_hosted_channel && this.isChannelHosted(data.channel && data.channel.login))
|
||||||
|
? this.i18n.t('metadata.host.button.unhost', 'Unhost')
|
||||||
|
: this.i18n.t('metadata.host.button.host', 'Host');
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: () => {
|
||||||
|
if (this._host_error) {
|
||||||
|
return this.i18n.t(
|
||||||
|
`metadata.host.button.tooltip.error.${this._host_error.key}`,
|
||||||
|
this._host_error.text);
|
||||||
|
} else {
|
||||||
|
return this.i18n.t('metadata.host.button.tooltip',
|
||||||
|
'Currently hosting: %{channel}',
|
||||||
|
{
|
||||||
|
channel: this._last_hosted_channel || this.i18n.t('metadata.host.button.tooltip.none', 'None')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isChannelHosted(channelLogin) {
|
||||||
|
return this._last_hosted_channel === channelLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendHostUnhostCommand(channel) {
|
||||||
|
if (!this._chat_con) {
|
||||||
|
this._host_error = HOST_ERRORS.CHAT_CONNECTION;
|
||||||
|
this._host_updating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ffz_user = this.site.getUser(),
|
||||||
|
userLogin = ffz_user && ffz_user.login;
|
||||||
|
|
||||||
|
const commandData = {channel: userLogin, username: channel};
|
||||||
|
|
||||||
|
this._host_updating = true;
|
||||||
|
this.metadata.updateMetadata('host');
|
||||||
|
|
||||||
|
this._host_feedback = setTimeout(() => {
|
||||||
|
if (this._last_hosted_channel === null) {
|
||||||
|
this._host_error = HOST_ERRORS.COMMAND_EXECUTION;
|
||||||
|
this._host_updating = false;
|
||||||
|
this.metadata.updateMetadata('host');
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
if (this.isChannelHosted(channel)) {
|
||||||
|
this._chat_con.commands.unhost.execute(commandData);
|
||||||
|
} else {
|
||||||
|
this._chat_con.commands.host.execute(commandData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinChannel(channel) {
|
||||||
|
if (this._chat_con) {
|
||||||
|
if (this.settings.get('metadata.host-button') && !this._chat_con.session.channelstate[`#${channel}`]) {
|
||||||
|
this._chat_con.joinChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hookIntoChatConnection(inst) {
|
||||||
|
const userLogin = inst.props.userLogin;
|
||||||
|
|
||||||
|
if (this._chat_con) {
|
||||||
|
this.joinChannel(userLogin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on('tmi:host', e => {
|
||||||
|
if (e.channel.substring(1) !== userLogin) return;
|
||||||
|
|
||||||
|
clearTimeout(this._host_feedback);
|
||||||
|
this._host_error = false;
|
||||||
|
this._last_hosted_channel = e.target;
|
||||||
|
|
||||||
|
this._host_updating = false;
|
||||||
|
this.metadata.updateMetadata('host');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('tmi:unhost', e => {
|
||||||
|
if (e.channel.substring(1) !== userLogin) return;
|
||||||
|
|
||||||
|
clearTimeout(this._host_feedback);
|
||||||
|
this._host_error = false;
|
||||||
|
this._last_hosted_channel = null;
|
||||||
|
|
||||||
|
this._host_updating = false;
|
||||||
|
this.metadata.updateMetadata('host');
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatServiceClient = inst.chatService.client;
|
||||||
|
|
||||||
|
this._chat_con = chatServiceClient;
|
||||||
|
if (this.settings.get('metadata.host-button'))
|
||||||
|
this.joinChannel(userLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.metadata.updateMetadata('host');
|
||||||
|
|
||||||
|
this.chat.ChatController.ready((cls, instances) => {
|
||||||
|
for(const inst of instances) {
|
||||||
|
if (inst && inst.chatService) this.hookIntoChatConnection(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chat.ChatController.on('mount', this.hookIntoChatConnection, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAutoHostMenu(vue, hosts, autoHostSettings, data) {
|
||||||
|
this._current_channel_id = data.id;
|
||||||
|
this.activeTab = this.activeTab || 'auto-host';
|
||||||
|
|
||||||
|
this.vueEl = new vue.Vue({
|
||||||
|
el: e('div'),
|
||||||
|
render: h => h('host-options', {
|
||||||
|
hosts,
|
||||||
|
autoHostSettings,
|
||||||
|
activeTab: this.activeTab,
|
||||||
|
|
||||||
|
addedToHosts: this.currentRoomInHosts(),
|
||||||
|
addToAutoHosts: () => this.addCurrentRoomToHosts(),
|
||||||
|
rearrangeHosts: event => this.rearrangeHosts(event.oldIndex, event.newIndex),
|
||||||
|
removeFromHosts: event => this.removeUserFromHosts(event),
|
||||||
|
setActiveTab: tab => {
|
||||||
|
this.vueEl.$children[0]._data.activeTab = this.activeTab = tab;
|
||||||
|
},
|
||||||
|
updatePopper: () => {
|
||||||
|
if (this._auto_host_tip) this._auto_host_tip.update();
|
||||||
|
},
|
||||||
|
updateCheckbox: e => {
|
||||||
|
const t = e.target,
|
||||||
|
setting = t.dataset.setting;
|
||||||
|
let state = t.checked;
|
||||||
|
|
||||||
|
if ( setting === 'strategy' )
|
||||||
|
state = state ? 'random' : 'ordered';
|
||||||
|
else if ( setting === 'deprioritize_vodcast' )
|
||||||
|
state = ! state;
|
||||||
|
|
||||||
|
this.updateAutoHostSetting(setting, state);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.vueEl.$el;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAutoHosts() {
|
||||||
|
const user = this.site.getUser();
|
||||||
|
if ( ! user )
|
||||||
|
return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await fetch('https://api.twitch.tv/kraken/autohost/list', {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.twitchtv.v4+json',
|
||||||
|
'Authorization': `OAuth ${user.authToken}`
|
||||||
|
}
|
||||||
|
}).then(r => {
|
||||||
|
if ( r.ok )
|
||||||
|
return r.json();
|
||||||
|
|
||||||
|
throw r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
this.log.error('Error loading auto host list.', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.autoHosts = data.targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAutoHostSettings() {
|
||||||
|
const user = this.site.getUser();
|
||||||
|
if ( ! user )
|
||||||
|
return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await fetch('https://api.twitch.tv/kraken/autohost/settings', {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.twitchtv.v4+json',
|
||||||
|
'Authorization': `OAuth ${user.authToken}`
|
||||||
|
}
|
||||||
|
}).then(r => {
|
||||||
|
if ( r.ok )
|
||||||
|
return r.json();
|
||||||
|
|
||||||
|
throw r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
this.log.error('Error loading auto host settings.', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.autoHostSettings = data.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueHostUpdate() {
|
||||||
|
if (this._host_update_timer) clearTimeout(this._host_update_timer);
|
||||||
|
|
||||||
|
this._host_update_timer = setTimeout(() => {
|
||||||
|
this._host_update_timer = undefined;
|
||||||
|
this.updateAutoHosts(this.autoHosts);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
rearrangeHosts(oldIndex, newIndex) {
|
||||||
|
const host = this.autoHosts.splice(oldIndex, 1)[0];
|
||||||
|
this.autoHosts.splice(newIndex, 0, host);
|
||||||
|
|
||||||
|
this.queueHostUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRoomInHosts() {
|
||||||
|
return this.getAutoHostIDs(this.autoHosts).includes(parseInt(this._current_channel_id, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
addCurrentRoomToHosts() {
|
||||||
|
const newHosts = this.autoHosts.slice(0);
|
||||||
|
newHosts.push({ _id: parseInt(this._current_channel_id, 10)});
|
||||||
|
|
||||||
|
this.updateAutoHosts(newHosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserFromHosts(event) {
|
||||||
|
const id = event.target.closest('.ffz--host-user').dataset.id;
|
||||||
|
|
||||||
|
const newHosts = [];
|
||||||
|
for (let i = 0; i < this.autoHosts.length; i++) {
|
||||||
|
if (this.autoHosts[i]._id != id) newHosts.push(this.autoHosts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateAutoHosts(newHosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAutoHostIDs(hosts) { // eslint-disable-line class-methods-use-this
|
||||||
|
const ids = [];
|
||||||
|
if (hosts) {
|
||||||
|
for (let i = 0; i < hosts.length; i++) {
|
||||||
|
ids.push(hosts[i]._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAutoHosts(newHosts) {
|
||||||
|
const user = this.site.getUser();
|
||||||
|
if ( ! user )
|
||||||
|
return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
const autoHosts = this.getAutoHostIDs(newHosts);
|
||||||
|
form.append('targets', autoHosts.join(','));
|
||||||
|
|
||||||
|
data = await fetch('https://api.twitch.tv/kraken/autohost/list', {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.twitchtv.v4+json',
|
||||||
|
'Authorization': `OAuth ${user.authToken}`
|
||||||
|
},
|
||||||
|
method: autoHosts.length ? 'PUT' : 'DELETE',
|
||||||
|
body: autoHosts.length ? form : undefined
|
||||||
|
}).then(r => {
|
||||||
|
if ( r.ok )
|
||||||
|
return r.json();
|
||||||
|
|
||||||
|
throw r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
this.log.error('Error updating auto host list.', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoHosts = data.targets;
|
||||||
|
if (this.vueEl) {
|
||||||
|
this.vueEl.$children[0]._data.hosts = this.autoHosts;
|
||||||
|
this.vueEl.$children[0]._data.addedToHosts = this.currentRoomInHosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAutoHostSetting(setting, newValue) {
|
||||||
|
const user = this.site.getUser();
|
||||||
|
if ( ! user )
|
||||||
|
return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append(setting, newValue);
|
||||||
|
|
||||||
|
data = await fetch('https://api.twitch.tv/kraken/autohost/settings', {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.twitchtv.v4+json',
|
||||||
|
'Authorization': `OAuth ${user.authToken}`
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
body: form
|
||||||
|
}).then(r => {
|
||||||
|
if ( r.ok )
|
||||||
|
return r.json();
|
||||||
|
|
||||||
|
throw r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
this.log.error('Error updating auto host setting.', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoHostSettings = data.settings;
|
||||||
|
if (this.vueEl) {
|
||||||
|
this.vueEl.$children[0]._data.autoHostSettings = this.autoHostSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
src/sites/twitch-twilight/styles/host_options.scss
Normal file
53
src/sites/twitch-twilight/styles/host_options.scss
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
.ffz-auto-host-options {
|
||||||
|
.scrollable-area {
|
||||||
|
max-height: 25vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--host-user {
|
||||||
|
.handle {
|
||||||
|
padding: 0 0.4rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--host-remove-user {
|
||||||
|
> figure {
|
||||||
|
padding: 0.4rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover { background: #a94444 !important }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> header {
|
||||||
|
padding: .9rem 1rem .9rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz-checkbox-description {
|
||||||
|
padding-left: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-options__tabs-container {
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
> .host-options__tab {
|
||||||
|
position: relative;
|
||||||
|
top: -.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 3rem;
|
||||||
|
margin-right: .5rem;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
border-top: 1px solid #6441a4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,4 +8,6 @@
|
||||||
@import 'chat';
|
@import 'chat';
|
||||||
@import 'directory';
|
@import 'directory';
|
||||||
|
|
||||||
@import 'fixes';
|
@import 'fixes';
|
||||||
|
|
||||||
|
@import 'host_options';
|
Loading…
Add table
Add a link
Reference in a new issue