1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-10-19 17:32:00 +00:00

4.0.0-rc1.5

Add an option to hide the mouse over the player. This doesn't work well in Chrome. Oh well, we tried.
Add an option to not automatically join raids for that comfy falling asleep experience.
Fix in-line actions not rendering when they should.
Fix detokenizeMessage for changes to Twitch's message format.
Don't try to preview the `create` clip URL.
Start using functional components where possible for performance.
Stop logging GraphQL errors to save our Sentry quota.

Begin implementing chat rendering on top of Vue. For now, we've got functional components for every type of chat token. We've got a lot of work ahead of us. This will eventually be used for mod card history, chat panes, and maybe even pinned rooms.

Add an event to clean orphan tooltips. Useful for when we click something we know will change DOM elements with active tooltips, like a Close button for example.
This commit is contained in:
SirStendec 2018-05-10 19:56:39 -04:00
parent 9ef7c2aee3
commit 9dc8252df0
58 changed files with 1037 additions and 392 deletions

View file

@ -156,6 +156,8 @@ Twilight.ROUTES = {
'dir-game-index': '/directory/game/:gameName',
'dir-all': '/directory/all/:filter?',
'dir-category': '/directory/:category?',
'dash': '/:userName/dashboard',
'dash-automod': '/:userName/dashboard/settings/automod',
'event': '/event/:eventName',
'popout': '/popout/:userName/chat',
'video': '/videos/:videoID',

View file

@ -16,6 +16,8 @@ export default class Channel extends Module {
this.inject('settings');
this.inject('site.fine');
this.left_raids = new Set;
this.settings.add('channel.hosting.enable', {
default: true,
ui: {
@ -26,16 +28,52 @@ export default class Channel extends Module {
changed: val => this.updateChannelHosting(val)
});
this.settings.add('channel.raids.no-autojoin', {
default: false,
ui: {
path: 'Channel > Behavior >> Raids',
title: 'Do not automatically join raids.',
component: 'setting-check-box'
}
});
this.ChannelPage = this.fine.define(
'channel-page',
n => n.handleHostingChange,
['user']
);
this.RaidController = this.fine.define(
'raid-controller',
n => n.handleLeaveRaid && n.handleJoinRaid,
['user']
);
}
onEnable() {
this.ChannelPage.on('mount', this.wrapChannelPage, this);
this.RaidController.on('mount', this.noAutoRaids, this);
this.RaidController.on('update', this.noAutoRaids, this);
this.RaidController.ready((cls, instances) => {
for(const inst of instances)
this.noAutoRaids(inst);
});
this.ChannelPage.on('update', inst => {
if ( this.settings.get('channel.hosting.enable') || ! inst.state.isHosting )
return;
// We can't do this immediately because the player state
// occasionally screws up if we do.
setTimeout(() => {
inst.ffzExpectedHost = inst.state.videoPlayerSource;
inst.ffzOldHostHandler(null);
});
});
this.ChannelPage.ready((cls, instances) => {
for(const inst of instances)
@ -44,6 +82,22 @@ export default class Channel extends Module {
}
noAutoRaids(inst) {
if ( this.settings.get('channel.raids.no-autojoin') )
setTimeout(() => {
if ( inst.state.raid && inst.hasJoinedCurrentRaid ) {
const id = inst.state.raid.id;
if ( this.left_raids.has(id) )
return;
this.log.info('Automatically leaving raid:', id);
this.left_raids.add(id);
inst.handleLeaveRaid();
}
});
}
wrapChannelPage(inst) {
if ( inst._ffz_hosting_wrapped )
return;

View file

@ -193,8 +193,8 @@ export default class EmoteMenu extends Module {
this.MenuWrapper = this.fine.wrap('ffz-emote-menu');
this.MenuSection = this.fine.wrap('ffz-menu-section');
this.MenuEmote = this.fine.wrap('ffz-menu-emote');
//this.MenuSection = this.fine.wrap('ffz-menu-section');
//this.MenuEmote = this.fine.wrap('ffz-menu-emote');
}
onEnable() {
@ -281,62 +281,48 @@ export default class EmoteMenu extends Module {
React = this.web_munch.getModule('react'),
createElement = React && React.createElement;
this.MenuEmote = class FFZMenuEmote extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
this.MenuEmote = function({source, data, lock, locked, all_locked, onClickEmote}) {
const handle_click = e => {
if ( ! t.emotes.handleClick(e) )
onClickEmote(data.name);
};
handleClick(event) {
if ( ! t.emotes.handleClick(event) )
this.props.onClickEmote(this.props.data.name);
}
const sellout = lock ?
all_locked ?
t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', lock) :
t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', lock)
: null;
render() {
const data = this.props.data,
lock = this.props.lock,
locked = this.props.locked,
favorite = data.favorite,
sellout = lock ?
this.props.all_locked ?
t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', lock) :
t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', lock)
: null;
return (<button
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}`}
data-tooltip-type="emote"
data-provider={data.provider}
data-id={data.id}
data-set={data.set_id}
data-code={data.code}
data-variant={data.variant}
data-no-source={this.props.source}
data-name={data.name}
aria-label={data.name}
data-locked={data.locked}
data-sellout={sellout}
onClick={!data.locked && this.handleClick}
>
<figure class="emote-picker__emote-figure">
<img
class={`emote-picker__emote-image${data.emoji ? ' ffz-emoji' : ''}`}
src={data.src}
srcSet={data.srcSet}
alt={data.name}
height={data.height ? `${data.height}px` : null}
width={data.width ? `${data.width}px` : null}
/>
</figure>
{favorite && (<figure class="ffz--favorite ffz-i-star" />)}
{locked && (<figure class="ffz-i-lock" />)}
</button>);
}
return (<button
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}`}
data-tooltip-type="emote"
data-provider={data.provider}
data-id={data.id}
data-set={data.set_id}
data-code={data.code}
data-variant={data.variant}
data-no-source={source}
data-name={data.name}
aria-label={data.name}
data-locked={data.locked}
data-sellout={sellout}
onClick={!data.locked && handle_click}
>
<figure class="emote-picker__emote-figure">
<img
class={`emote-picker__emote-image${data.emoji ? ' ffz-emoji' : ''}`}
src={data.src}
srcSet={data.srcSet}
alt={data.name}
height={data.height ? `${data.height}px` : null}
width={data.width ? `${data.width}px` : null}
/>
</figure>
{data.favorite && (<figure class="ffz--favorite ffz-i-star" />)}
{locked && (<figure class="ffz-i-lock" />)}
</button>)
}
this.fine.wrap('ffz-menu-emote', this.MenuEmote);
this.MenuSection = class FFZMenuSection extends React.Component {
constructor(props) {

View file

@ -4,7 +4,7 @@
// Chat Hooks
// ============================================================================
import {ColorAdjuster, Color} from 'utilities/color';
import {ColorAdjuster} from 'utilities/color';
import {setChildren} from 'utilities/dom';
import {has, split_chars} from 'utilities/object';
@ -120,7 +120,7 @@ export default class ChatHook extends Module {
this.inject(SettingsMenu);
this.inject(EmoteMenu);
this.inject(TabCompletion);
this.inject(ModCards);
@ -334,12 +334,11 @@ export default class ChatHook extends Module {
grabTypes() {
const ct = this.web_munch.getModule('chat-types');
if ( ct ) {
this.automod_types = ct && ct.a || AUTOMOD_TYPES;
this.chat_types = ct && ct.b || CHAT_TYPES;
this.message_types = ct && ct.c || MESSAGE_TYPES;
this.mod_types = ct && ct.e || MOD_TYPES;
}
this.automod_types = ct && ct.a || AUTOMOD_TYPES;
this.chat_types = ct && ct.b || CHAT_TYPES;
this.message_types = ct && ct.c || MESSAGE_TYPES;
this.mod_types = ct && ct.e || MOD_TYPES;
}

View file

@ -28,6 +28,8 @@ export default class ChatLine extends Module {
this.inject('site.apollo');
this.inject(RichContent);
this.inject('site.chat.mod_cards');
this.inject('chat.actions');
this.ChatLine = this.fine.define(
@ -127,6 +129,9 @@ export default class ChatLine extends Module {
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r),
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
if ( ! this.ffz_user_click_handler )
this.ffz_user_click_handler = event => event.ctrlKey ? this.usernameClickHandler(event) : t.mod_cards.openCard(r, user, event);
let cls = 'chat-line__message',
out = (tokens.length || ! msg.ffz_type) ? [
this.props.showTimestamps && e('span', {
@ -139,7 +144,7 @@ export default class ChatLine extends Module {
e('a', {
className: 'chat-author__display-name notranslate',
style: { color },
onClick: t.parent.mod_cards.openCustomModCard.bind(t.parent.mod_cards, this, user)
onClick: this.usernameClickHandler, // this.ffz_user_click_handler
}, [
user.userDisplayName,
user.isIntl && e('span', {
@ -274,46 +279,47 @@ export function detokenizeMessage(msg) {
for(let i=0; i < l; i++) {
const part = parts[i],
type = part.type,
content = part.content;
if ( type === 0 )
if ( ! content )
continue;
if ( typeof content === 'string' )
ret = content;
else if ( type === 1 )
else if ( content.recipient )
ret = `@${content.recipient}`;
else if ( type === 2 )
ret = content.displayText;
else if ( content.url )
ret = content.url;
else if ( type === 3 ) {
if ( content.cheerAmount ) {
ret = `${content.alt}${content.cheerAmount}`;
else if ( content.cheerAmount )
ret = `${content.alt}${content.cheerAmount}`;
} else {
const url = (content.images.themed ? content.images.dark : content.images.sources)['1x'],
match = /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url),
id = match && match[1];
else if ( content.images ) {
const url = (content.images.themed ? content.images.dark : content.images.sources),
match = url && /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url['1x']),
id = match && match[1];
ret = content.alt;
ret = content.alt;
if ( id ) {
const em = emotes[id] = emotes[id] || [],
offset = last_type > 0 ? 1 : 0;
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
}
if ( id ) {
const em = emotes[id] = emotes[id] || [],
offset = last_type > 0 ? 1 : 0;
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
}
if ( last_type > 0 )
ret = ` ${ret}`;
} else if ( type === 4 )
ret = `https://clips.twitch.tv/${content.slug}`;
} else
continue;
if ( ret ) {
idx += ret.length;
last_type = type;
out.push(ret);
last_type = part.type;
out.push(ret)
}
}

View file

@ -0,0 +1,265 @@
<template>
<div
:style="{zIndex: z}"
class="ffz-mod-card tw-elevation-3 tw-c-background-alt tw-c-text tw-border tw-flex tw-flex-nowrap tw-flex-column"
tabindex="0"
@focusin="onFocus"
@keyup.esc="close"
>
<header
:style="loaded && `background-image: url('${user.bannerImageURL}');`"
class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap tw-relative"
>
<div class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap tw-pd-1 ffz--background-dimmer">
<figure class="tw-avatar tw-avatar--size-50">
<div v-if="loaded" class="tw-overflow-hidden ">
<img
:src="user.profileImageURL"
class="tw-image"
>
</div>
</figure>
<div class="tw-ellipsis tw-inline-block">
<div class="tw-align-items-center tw-mg-l-1 ffz--info-lines">
<h4>
<a :href="`/${login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
{{ displayName }}
</a>
</h4>
<h5
v-if="displayName && displayName.toLowerCase() !== login"
>
<a :href="`/${login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
{{ login }}
</a>
</h5>
<div v-if="loaded">
<span
:data-title="t('viewer-card.views', 'Views')"
class="ffz-tooltip tw-mg-r-05 ffz-i-views"
>
{{ t(null, '%{views|number}', {views: user.profileViewCount}) }}
</span>
<span
:data-title="t('viewer-card.followers', 'Followers')"
class="ffz-tooltip tw-mg-r-05 ffz-i-heart"
>
{{ t(null, '%{followers|number}', {followers: user.followers.totalCount}) }}
</span>
<span
data-title="rawUserAge"
class="ffz-tooltip ffz-i-clock"
>
{ userAge }
</span>
</div>
</div>
</div>
<div class="tw-flex-grow-1 tw-pd-x-2" />
<button
:data-title="t('viewer-card.close', 'Close')"
class="ffz-tooltip tw-button-icon tw-absolute tw-right-0 tw-top-0 tw-mg-t-05 tw-mg-r-05"
@click="close"
>
<span class="tw-button-icon__icon">
<figure class="ffz-i-cancel" />
</span>
</button>
<button
v-show="! pinned"
:data-title="t('viewer-card.pin', 'Pin')"
class="ffz-tooltip tw-button-icon tw-absolute tw-right-0 tw-bottom-0 tw-mg-b-05 tw-mg-r-05"
@click="pin"
>
<span class="tw-button-icon__icon">
<figure class="ffz-i-pin" />
</span>
</button>
</div>
</header>
<error-tab v-if="errored" />
<template v-else-if="loaded">
<section class="tw-c-background">
<div class="mod-cards__tabs-container tw-border-t">
<div
v-for="(data, key) in tabs"
:key="key"
:id="`mod-cards__${key}`"
:class="{active: active_tab === key}"
class="mod-cards__tab tw-pd-x-1"
@click="active_tab = key"
>
<span>{{ data.label }}</span>
</div>
</div>
</section>
<keep-alive>
<component
:is="current_tab.component"
:tab="current_tab"
:channel="channel"
:user="user"
:self="self"
:getFFZ="getFFZ"
@close="close"
/>
</keep-alive>
</template>
<loading-tab v-else />
</div>
</template>
<script>
import LoadingTab from './components/loading-tab.vue';
import ErrorTab from './components/error-tab.vue';
import displace from 'displacejs';
export default {
components: {
'error-tab': ErrorTab,
'loading-tab': LoadingTab
},
props: ['tabs', 'room', 'raw_user', 'pos_x', 'pos_y', 'data', 'getZ', 'getFFZ'],
data() {
return {
active_tab: Object.keys(this.tabs)[0],
z: this.getZ(),
loaded: false,
errored: false,
pinned: false,
user: null,
channel: null,
self: null
}
},
computed: {
login() {
if ( this.loaded )
return this.user.login;
return this.raw_user.login;
},
displayName() {
if ( this.loaded )
return this.user.displayName;
return this.raw_user.displayName || this.raw_user.login;
},
current_tab() {
return this.tabs[this.active_tab];
}
},
beforeMount() {
this.data.then(data => {
this.loaded = true;
this.user = data.data.targetUser;
this.channel = data.data.channelUser;
this.self = data.data.currentUser;
}).catch(err => {
console.error(err); // eslint-disable-line no-console
this.errored = true;
});
},
mounted() {
this._on_resize = this.handleResize.bind(this);
window.addEventListener('resize', this._on_resize);
this.createDrag();
},
beforeDestroy() {
this.destroyDrag();
if ( this._on_resize ) {
window.removeEventListener('resize', this._on_resize);
this._on_resize = null;
}
},
methods: {
constrain() {
const el = this.$el;
let parent = el.parentElement,
moved = false;
if ( ! parent )
parent = document.body;
const box = el.getBoundingClientRect(),
pbox = parent.getBoundingClientRect();
if ( box.top < pbox.top ) {
el.style.top = `${el.offsetTop + (pbox.top - box.top)}px`;
moved = true;
} else if ( box.bottom > pbox.bottom ) {
el.style.top = `${el.offsetTop - (box.bottom - pbox.bottom)}px`;
moved = true;
}
if ( box.left < pbox.left ) {
el.style.left = `${el.offsetLeft + (pbox.left - box.left)}px`;
moved = true;
} else if ( box.right > pbox.right ) {
el.style.left = `${el.offsetLeft - (box.right - pbox.right)}px`;
moved = true;
}
if ( moved && this.displace )
this.displace.reinit();
},
pin() {
this.pinned = true;
this.$emit('pin');
},
close() {
this.$emit('close');
},
createDrag() {
this.$nextTick(() => {
this.displace = displace(this.$el, {
handle: this.$el.querySelector('header'),
highlightInputs: true,
constrain: true
});
})
},
destroyDrag() {
if ( this.displace ) {
this.displace.destroy();
this.displace = null;
}
},
handleResize() {
if ( this.displace )
this.displace.reinit()
},
onFocus() {
console.log('got focus!');
this.z = this.getZ();
},
focus() {
this.$el.focus();
}
}
}
</script>

View file

@ -0,0 +1,11 @@
<template functional>
<div class="tw-align-center tw-pd-1">
<div class="tw-mg-t-1 tw-mg-b-2">
<img
src="//cdn.frankerfacez.com/emoticon/26608/2"
srcSet="//cdn.frankerfacez.com/emoticon/26608/2 1x, //cdn.frankerfacez.com/emoticon/26608/4 2x"
>
</div>
{{ parent.t('viewer-card.error', 'There was an error loading data for this user.') }}
</div>
</template>

View file

@ -0,0 +1,5 @@
<template functional>
<div class="tw-align-center tw-pd-1">
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
</div>
</template>

View file

@ -1,6 +1,6 @@
<template>
<section class="tw-background-c tw-relative">
<div class="tw-c-background tw-full-width tw-flex tw-flex-row tw-pd-r-05 tw-pd-l-1 tw-pd-y-1">
<div class="tw-background-c tw-full-width tw-flex tw-flex-row tw-pd-r-05 tw-pd-l-1 tw-pd-y-1">
<div class="tw-mg-r-05">
<div class="tw-inline-block">
<button class="tw-button">

View file

@ -0,0 +1,50 @@
<template>
<div>
<error-tab v-if="errored" />
<loading-tab v-else-if="loading" />
<template v-else>
{{ JSON.stringify(data) }}
</template>
</div>
</template>
<script>
import LoadingTab from './loading-tab.vue';
import ErrorTab from './error-tab.vue';
export default {
components: {
'loading-tab': LoadingTab,
'error-tab': ErrorTab
},
props: ['tab', 'channel', 'user', 'self', 'getFFZ'],
data() {
return {
loading: true,
errored: false,
data: null
}
},
mounted() {
const socket = this.getFFZ().resolve('socket');
if ( ! socket ) {
this.errored = true;
return;
}
socket.call('get_name_history', this.user.login).then(data => {
this.loading = false;
this.data = data;
}).catch(err => {
console.error(err);
this.errored = true;
})
}
}
</script>

View file

@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script>
export default {
}
</script>

View file

@ -1,25 +1,50 @@
query($userLogin: String) {
user(login: $userLogin) {
bannerImageURL
displayName
query FFZ_ViewerCard($targetLogin: String!, $channelID: ID!) {
targetUser: user(login: $targetLogin) {
id
login
profileImageURL(width: 50)
displayName
bannerImageURL
profileImageURL(width: 70)
createdAt
profileViewCount
followers {
totalCount
}
profileViewCount
self {
friendship {
... on FriendEdge {
node {
displayName
id
login
}
}
...friendButtonFragment
}
channelUser: user(id: $channelID) {
id
login
displayName
subscriptionProducts {
id
price
url
emoteSetID
emotes {
id
}
}
self {
isModerator
}
}
currentUser {
id
login
roles {
isSiteAdmin
isStaff
isGlobalMod
}
}
}
fragment friendButtonFragment on User {
id
self {
friendship {
__typename
}
}
}

View file

@ -6,6 +6,7 @@
import Module from 'utilities/module';
import {has} from 'utilities/object';
import {createElement} from 'utilities/dom';
import GET_USER_INFO from './get_user_info.gql';
@ -14,104 +15,162 @@ export default class ModCards extends Module {
constructor(...args) {
super(...args);
this.inject('site.apollo');
this.inject('i18n');
this.inject('settings');
this.inject('site.apollo');
this.lastZIndex = 9001;
this.open_mod_cards = {};
this.tabs = {};
this.last_z = 9000;
this.open_cards = {};
this.unpinned_card = null;
this.addTab('main', {
visible: () => true,
visible: true,
label: 'Main',
pill: 0,
pill: 3,
data: (user, room) => ({
}),
component: () => import('./components/main.vue')
component: () => import(/* webpackChunkName: 'viewer-cards' */ './components/main.vue')
});
this.addTab('stats', {
label: 'Stats',
component: () => import(/* webpackChunkName: 'viewer-cards' */ './components/stats.vue')
});
this.addTab('name-history', {
label: 'Name History',
component: () => import(/* webpackChunkName: 'viewer-cards' */ './components/name-history.vue')
});
}
addTab(key, data) {
if (this.tabs[key]) return;
if ( this.tabs[key] )
return this.log.warn(`Attempted to re-define known tab "${key}"`);
this.tabs[key] = data;
}
async openCustomModCard(t, user, e) {
// Old mod-card
// t.usernameClickHandler(e);
const posX = Math.min(window.innerWidth - 300, e.clientX),
posY = Math.min(window.innerHeight - 300, e.clientY),
room = {
id: t.props.channelID,
login: t.props.message.roomLogin
},
currentUser = {
isModerator: t.props.isCurrentUserModerator,
isStaff: t.props.isCurrentUserStaff
onEnable() {
this.vue = this.resolve('vue');
}
async loadVue() {
if ( this._vue_loaded )
return;
const [, card_component] = await Promise.all([
this.vue.enable(),
import(/* webpackChunkName: 'viewer-cards' */ './card.vue')
]);
this.vue.component('viewer-card', card_component.default);
this._vue_loaded = true;
}
async openCard(room, user, event) {
if ( user.userLogin && ! user.login )
user = {
login: user.userLogin,
id: user.userID,
displayName: user.userDisplayName,
};
if (this.open_mod_cards[user.userLogin]) {
this.open_mod_cards[user.userLogin].style.zIndex = ++this.lastZIndex;
const old_card = this.open_cards[user.login];
if ( old_card ) {
old_card.$el.style.zIndex = ++this.last_z;
old_card.focus();
return;
}
const vue = this.resolve('vue'),
_mod_card_vue = import(/* webpackChunkName: "mod-card" */ './mod-card.vue'),
_user_info = this.apollo.client.query({
query: GET_USER_INFO,
variables: {
userLogin: user.userLogin
}
});
let pos_x = event ? event.clientX : window.innerWidth / 2,
pos_y = event ? event.clientY + 15 : window.innerHeight / 2;
const [, mod_card_vue, user_info] = await Promise.all([vue.enable(), _mod_card_vue, _user_info]);
if ( this.unpinned_card ) {
const card = this.unpinned_card;
vue.component('mod-card', mod_card_vue.default);
pos_x = card.$el.offsetLeft;
pos_y = card.$el.offsetTop;
const mod_card = this.open_mod_cards[user.userLogin] = this.buildModCard(vue, user_info.data.user, room, currentUser);
card.close();
}
const main = document.querySelector('.twilight-root>.tw-full-height');
main.appendChild(mod_card);
mod_card.style.left = `${posX}px`;
mod_card.style.top = `${posY}px`;
}
buildModCard(vue, user, room, currentUser) {
this.log.info(user);
const vueEl = new vue.Vue({
el: createElement('div'),
render: h => {
const vueModCard = h('mod-card', {
activeTab: Object.keys(this.tabs)[0],
tabs: this.tabs,
user,
room,
currentUser,
rawUserAge: this.i18n.toLocaleString(new Date(user.createdAt)),
userAge: this.i18n.toHumanTime((new Date() - new Date(user.createdAt)) / 1000),
setActiveTab: tab => vueModCard.data.activeTab = tab,
focus: el => {
el.style.zIndex = ++this.lastZIndex;
},
close: () => {
this.open_mod_cards[user.login].remove();
this.open_mod_cards[user.login] = null;
}
});
return vueModCard;
// We start this first...
const user_info = this.apollo.client.query({
query: GET_USER_INFO,
variables: {
targetLogin: user.login,
channelID: room.id
}
});
return vueEl.$el;
// But we only wait on loading Vue, since we can show a loading indicator.
await this.loadVue();
// Display the card.
this.unpinned_card = this.open_cards[user.login] = this.buildCard(
room,
user,
user_info,
pos_x,
pos_y,
);
}
buildCard(room, user, data, pos_x, pos_y) {
let child;
const component = new this.vue.Vue({
el: createElement('div'),
render: h => h('viewer-card', {
props: {
tabs: this.tabs,
room,
raw_user: user,
data,
getFFZ: () => this,
getZ: () => ++this.last_z
},
on: {
close: () => {
const el = component.$el;
el.remove();
component.$destroy();
if ( this.unpinned_card === child )
this.unpinned_card = null;
if ( this.open_cards[user.login] === child )
this.open_cards[user.login] = null;
this.emit('tooltips:cleanup');
},
pin: () => {
if ( this.unpinned_card === child )
this.unpinned_card = null;
}
}
})
});
child = component.$children[0];
const el = component.$el;
el.style.top = `${pos_y}px`;
el.style.left = `${pos_x}px`
const container = document.querySelector('.twilight-root>.tw-full-height,.twilight-minimal-root>.tw-full-height');
container.appendChild(el);
requestAnimationFrame(() => child.constrain());
return child;
}
}

View file

@ -1,139 +0,0 @@
<template>
<div
class="ffz-mod-card tw-elevation-3 tw-c-background-alt tw-c-text tw-border tw-flex tw-flex-nowrap tw-flex-column"
tabindex="-1"
@focusin="doFocus"
>
<header
:style="`background-image: url('${user.bannerImageURL}');`"
class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap tw-relative"
>
<div class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap tw-pd-1 ffz--background-dimmer">
<div class="tw-inline-block">
<figure class="tw-avatar tw-avatar--size-50">
<div class="tw-overflow-hidden ">
<img
:src="user.profileImageURL"
class="tw-image"
>
</div>
</figure>
</div>
<div class="tw-ellipsis tw-inline-block">
<div class="tw-align-items-center tw-mg-l-1 ffz--info-lines">
<h4>
<a :href="`/${user.login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
{{ user.displayName }}
</a>
</h4>
<h5
v-if="user.displayName && user.displayName.toLowerCase() !== user.login.toLowerCase()"
>
<a :href="`/${user.login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
{{ user.login }}
</a>
</h5>
<div>
<span class="tw-mg-r-05">
<figure class="ffz-i-info tw-inline"/>
{{ user.profileViewCount }}
</span>
<span class="tw-mg-r-05">
<figure class="ffz-i-heart tw-inline"/>
{{ user.followers.totalCount }}
</span>
<span
:data-title="rawUserAge"
data-tooltip-type="html"
class="ffz-tooltip"
>
<figure class="ffz-i-clock tw-inline"/>
{{ userAge }}
</span>
</div>
</div>
</div>
<div class="tw-flex-grow-1 tw-pd-x-2"/>
<div class="tw-inline-block">
<button class="tw-button-icon tw-absolute tw-right-0 tw-top-0 tw-mg-t-05 tw-mg-r-05" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-cancel" />
</span>
</button>
<button class="tw-button-icon tw-absolute tw-right-0 tw-bottom-0 tw-mg-b-05 tw-mg-r-05" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-ignore" />
</span>
</button>
</div>
</div>
</header>
<section class="tw-background-c">
<div class="mod-cards__tabs-container tw-border-t">
<div
v-for="(data, key) in tabs"
:key="key"
:id="`mod-cards__${key}`"
:class="{active: activeTab === key}"
class="mod-cards__tab tw-pd-x-1"
@click="setActiveTab(key)"
>
<span>{{ data.label }}</span>
</div>
</div>
</section>
<component
v-for="(tab, key) in tabs"
v-if="tab.visible && activeTab === key"
:is="tab.component"
:tab="tab"
:user="user"
:room="room"
:current-user="currentUser"
:key="key"
@close="close"
/>
</div>
</template>
<script>
import displace from 'displacejs';
export default {
data() {
return this.$vnode.data;
},
mounted() {
this.createDrag();
},
beforeDestroy() {
this.destroyDrag();
},
methods: {
destroyDrag() {
if ( this.displace ) {
this.displace.destroy();
this.displace = null;
}
},
createDrag() {
this.$nextTick(() => {
this.displace = displace(this.$el, {
handle: this.$el.querySelector('header'),
highlightInputs: true,
constrain: true
});
})
},
doFocus() {
this.focus(this.$el);
}
}
}
</script>

View file

@ -0,0 +1,3 @@
.player:hover:not([data-controls="true"]):not([data-paused="true"]):not([data-ended="true"]) {
cursor: none;
}

View file

@ -184,6 +184,15 @@ export default class Player extends Module {
}
});
this.settings.add('player.hide-mouse', {
default: true,
ui: {
path: 'Channel > Player >> General',
title: "Hide mouse when controls aren't visible.",
component: 'setting-check-box'
},
changed: val => this.css_tweaks.toggle('player-hide-mouse', val)
});
}
updateHideExtensions(val) {
@ -199,6 +208,7 @@ export default class Player extends Module {
this.css_tweaks.toggle('player-ext-mouse', !this.settings.get('player.ext-interaction'));
this.css_tweaks.toggle('theatre-no-whispers', this.settings.get('player.theatre.no-whispers'));
this.css_tweaks.toggle('theatre-metadata', this.settings.get('player.theatre.metadata'));
this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse'));
this.css_tweaks.toggleHide('player-event-bar', this.settings.get('player.hide-event-bar'));
this.css_tweaks.toggleHide('player-rerun-bar', this.settings.get('player.hide-rerun-bar'));
this.updateHideExtensions();
@ -257,6 +267,7 @@ export default class Player extends Module {
process(inst) {
this.addResetButton(inst);
this.addEndedListener(inst);
this.addStateTags(inst);
this.addControlVisibility(inst);
this.updateVolumeScroll(inst);
}
@ -298,6 +309,17 @@ export default class Player extends Module {
inst._ffz_autoplay_handler = null;
}
if ( inst._ffz_on_state ) {
if ( p ) {
off(p, 'ended', inst._ffz_on_state);
off(p, 'pause', inst._ffz_on_state);
off(p, 'playing', inst._ffz_on_state);
off(p, 'error', inst._ffz_on_state);
}
inst._ffz_on_state = null;
}
}
@ -347,6 +369,40 @@ export default class Player extends Module {
}
addStateTags(inst) {
const p = inst.player;
if ( ! p )
return;
if ( inst._ffz_on_state ) {
off(p, 'ended', inst._ffz_on_state);
off(p, 'pause', inst._ffz_on_state);
off(p, 'playing', inst._ffz_on_state);
off(p, 'error', inst._ffz_on_state);
}
const f = inst._ffz_on_state = () => this.updateStateTags(inst);
on(p, 'ended', f);
on(p, 'pause', f);
on(p, 'playing', f);
on(p, 'error', f);
f();
}
updateStateTags(inst) { // eslint-disable-line class-methods-use-this
const p = inst.playerRef,
player = inst.player;
if ( ! p || ! player )
return;
p.dataset.ended = player.ended;
p.dataset.paused = player.paused;
}
disableAutoplay(inst) {
const p = inst.player;
if ( ! p )

View file

@ -111,10 +111,6 @@
cursor: pointer;
}
.loading {
animation: ffz-rotateplane 1.2s infinite linear;
}
.ffz--menu-badge {
width: 1.8rem; height: 1.8rem;
}