refactor progress
This commit is contained in:
parent
d497806443
commit
8ba1b68a21
@ -17,7 +17,7 @@ I like cats (Neko is the Japanese word for cat), I'm a weeb/nerd, I own the doma
|
|||||||
### Super easy mode setup
|
### Super easy mode setup
|
||||||
1. Deploy a Server/VPS
|
1. Deploy a Server/VPS
|
||||||
|
|
||||||
*Recomended Specs:*
|
*Recomended Specs:* (Note: these may not be correct, I did a small round testing, 4c/4gb worked fine with small hickups here and there)
|
||||||
|
|
||||||
| Resolution | Cores | Ram | Recommendation |
|
| Resolution | Cores | Ram | Recommendation |
|
||||||
|------------|-------|-------|------------------|
|
|------------|-------|-------|------------------|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"parser": "@typescript-eslint/parser"
|
"parser": "@typescript-eslint/parser"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"vue/valid-v-for": "off",
|
||||||
"no-case-declarations": "off",
|
"no-case-declarations": "off",
|
||||||
"no-dupe-class-members": "off",
|
"no-dupe-class-members": "off",
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
|
@ -19,21 +19,35 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||||
|
"animejs": "^3.1.0",
|
||||||
|
"axios": "^0.19.1",
|
||||||
|
"bulma": "^0.8.0",
|
||||||
|
"date-fns": "^2.9.0",
|
||||||
"eventemitter3": "^4.0.0",
|
"eventemitter3": "^4.0.0",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"simple-markdown": "^0.7.2",
|
||||||
|
"sweetalert2": "^9.6.1",
|
||||||
|
"typed-vuex": "^0.1.15",
|
||||||
|
"v-tooltip": "^2.0.3",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-class-component": "^7.0.2",
|
"vue-class-component": "^7.0.2",
|
||||||
|
"vue-context": "^5.0.0",
|
||||||
"vue-notification": "^1.3.20",
|
"vue-notification": "^1.3.20",
|
||||||
"vue-property-decorator": "^8.3.0",
|
"vue-property-decorator": "^8.3.0",
|
||||||
"typed-vuex": "^0.1.15",
|
"vue-router": "^3.1.5",
|
||||||
"vuex": "^3.1.2"
|
"vuex": "^3.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/animejs": "^3.1.0",
|
||||||
|
"@types/vue": "^2.0.0",
|
||||||
|
"@types/w3c-image-capture": "^1.0.2",
|
||||||
"@vue/cli-plugin-eslint": "^4.1.0",
|
"@vue/cli-plugin-eslint": "^4.1.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.1.0",
|
"@vue/cli-plugin-typescript": "^4.1.0",
|
||||||
"@vue/cli-plugin-vuex": "^4.1.0",
|
"@vue/cli-plugin-vuex": "^4.1.0",
|
||||||
"@vue/cli-service": "^4.1.0",
|
"@vue/cli-service": "^4.1.0",
|
||||||
"@vue/eslint-config-prettier": "^5.0.0",
|
"@vue/eslint-config-prettier": "^5.0.0",
|
||||||
"@vue/eslint-config-typescript": "^4.0.0",
|
"@vue/eslint-config-typescript": "^4.0.0",
|
||||||
|
"autoprefixer": "^9.7.4",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-plugin-prettier": "^3.1.1",
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
|
@ -1,571 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="video-player">
|
|
||||||
<div ref="video" class="video">
|
|
||||||
<div ref="container" class="video-container">
|
|
||||||
<video
|
|
||||||
ref="player"
|
|
||||||
tabindex="0"
|
|
||||||
@click.stop.prevent
|
|
||||||
@contextmenu.stop.prevent
|
|
||||||
@wheel.stop.prevent="onWheel"
|
|
||||||
@mousemove.stop.prevent="onMouseMove"
|
|
||||||
@mousedown.stop.prevent="onMouseDown"
|
|
||||||
@mouseup.stop.prevent="onMouseUp"
|
|
||||||
@mouseenter.stop.prevent="onMouseEnter"
|
|
||||||
@mouseleave.stop.prevent="onMouseLeave"
|
|
||||||
@keydown.stop.prevent="onKeyDown"
|
|
||||||
@keyup.stop.prevent="onKeyUp"
|
|
||||||
/>
|
|
||||||
<div v-if="!playing" class="video-overlay">
|
|
||||||
<i @click.stop.prevent="toggleMedia" class="fas fa-play-circle" />
|
|
||||||
</div>
|
|
||||||
<div ref="aspect" class="aspect" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<div class="neko">
|
|
||||||
<img src="@/assets/logo.svg" alt="n.eko" />
|
|
||||||
<span><b>n</b>.eko</span>
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<i
|
|
||||||
alt="Request Control"
|
|
||||||
:class="[{ enabled: hosting }, 'request', 'fas', 'fa-keyboard']"
|
|
||||||
@click.stop.prevent="toggleControl"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i
|
|
||||||
alt="Play/Pause"
|
|
||||||
:class="[playing ? 'fa-pause-circle' : 'fa-play-circle', 'play', 'fas']"
|
|
||||||
@click.stop.prevent="toggleMedia"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div class="volume">
|
|
||||||
<input
|
|
||||||
@input="setVolume"
|
|
||||||
:class="[volume === 0 ? 'fa-volume-mute' : 'fa-volume-up', 'fas']"
|
|
||||||
ref="volume"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i @click.stop.prevent="fullscreen" alt="Full Screen" class="fullscreen fas fa-expand-alt" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="right"></div>
|
|
||||||
</div>
|
|
||||||
<div class="connect" v-if="!connected">
|
|
||||||
<div class="window">
|
|
||||||
<div class="logo">
|
|
||||||
<img src="@/assets/logo.svg" alt="n.eko" />
|
|
||||||
<span><b>n</b>.eko</span>
|
|
||||||
</div>
|
|
||||||
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
|
|
||||||
<span>Please enter the password:</span>
|
|
||||||
<input type="text" placeholder="Username" v-model="username" />
|
|
||||||
<input type="password" placeholder="Password" v-model="password" />
|
|
||||||
<button type="submit" class="button" @click.stop.prevent="connect">
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div class="spinner" v-if="connecting">
|
|
||||||
<div class="double-bounce1"></div>
|
|
||||||
<div class="double-bounce2"></div>
|
|
||||||
</div>
|
|
||||||
<div class="loader" v-if="connecting" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<notifications group="neko" position="bottom left" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.video-player {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
.video {
|
|
||||||
position: absolute;
|
|
||||||
top: 60px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.video-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 16 / 9 * 100vh;
|
|
||||||
|
|
||||||
video {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
background: #000;
|
|
||||||
|
|
||||||
&::-webkit-media-controls {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba($color: $style-darker, $alpha: 0.2);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
i {
|
|
||||||
cursor: pointer;
|
|
||||||
&::before {
|
|
||||||
font-size: 120px;
|
|
||||||
color: rgba($color: $style-light, $alpha: 0.4);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.aspect {
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 56.25%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 60px;
|
|
||||||
background: $style-darker;
|
|
||||||
padding: 0 50px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.neko {
|
|
||||||
flex: 1; /* shorthand for: flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
width: 150px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
height: 54px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: $style-light;
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 56px;
|
|
||||||
|
|
||||||
b {
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 0 10px;
|
|
||||||
color: $style-light;
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.request {
|
|
||||||
color: rgba($color: $style-light, $alpha: 0.5);
|
|
||||||
|
|
||||||
&.enabled {
|
|
||||||
color: $style-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.volume {
|
|
||||||
display: block;
|
|
||||||
margin-top: 3px;
|
|
||||||
|
|
||||||
input[type='range'] {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
width: 200px;
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: $style-light;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-slider-runnable-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: $style-primary;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
color: $style-light;
|
|
||||||
text-align: center;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba($color: $style-darker, $alpha: 0.8);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.window {
|
|
||||||
width: 300px;
|
|
||||||
background: $style-light;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
color: $style-darker;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: invert(100%);
|
|
||||||
height: 90px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 56px;
|
|
||||||
|
|
||||||
b {
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
span {
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: solid 1px rgba($color: $style-darker, $alpha: 0.4);
|
|
||||||
padding: 3px;
|
|
||||||
line-height: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 4px;
|
|
||||||
background: $style-primary;
|
|
||||||
color: $style-light;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
.double-bounce1,
|
|
||||||
.double-bounce2 {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: $style-primary;
|
|
||||||
opacity: 0.6;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
|
||||||
animation: sk-bounce 2s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.double-bounce2 {
|
|
||||||
-webkit-animation-delay: -1s;
|
|
||||||
animation-delay: -1s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sk-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1);
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
|
||||||
import { EVENT } from '~/client/events'
|
|
||||||
|
|
||||||
@Component({ name: 'stream-video' })
|
|
||||||
export default class extends Vue {
|
|
||||||
@Ref('player') readonly _player!: HTMLVideoElement
|
|
||||||
@Ref('container') readonly _container!: HTMLElement
|
|
||||||
@Ref('aspect') readonly _aspect!: HTMLElement
|
|
||||||
@Ref('video') readonly _video!: HTMLElement
|
|
||||||
@Ref('volume') readonly _volume!: HTMLInputElement
|
|
||||||
|
|
||||||
private focused = false
|
|
||||||
private playing = false
|
|
||||||
private username = ''
|
|
||||||
private password = ''
|
|
||||||
|
|
||||||
get connected() {
|
|
||||||
return this.$accessor.connected
|
|
||||||
}
|
|
||||||
|
|
||||||
get connecting() {
|
|
||||||
return this.$accessor.connecting
|
|
||||||
}
|
|
||||||
|
|
||||||
get hosting() {
|
|
||||||
return this.$accessor.remote.hosting
|
|
||||||
}
|
|
||||||
|
|
||||||
get volume() {
|
|
||||||
return this.$accessor.video.volume
|
|
||||||
}
|
|
||||||
|
|
||||||
get stream() {
|
|
||||||
return this.$accessor.video.stream
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('volume')
|
|
||||||
onVolumeChanged(volume: number) {
|
|
||||||
if (this._player) {
|
|
||||||
this._player.volume = this.volume / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('stream')
|
|
||||||
onStreamChanged(stream?: MediaStream) {
|
|
||||||
if (!this._player || !stream) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('srcObject' in this._player) {
|
|
||||||
this._player.srcObject = this.stream
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
this._player.src = window.URL.createObjectURL(this.stream) // for older browsers
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._player.paused) {
|
|
||||||
this.toggleMedia()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
window.addEventListener('resize', this.onResise)
|
|
||||||
this.onResise()
|
|
||||||
this._player.volume = this.volume / 100
|
|
||||||
this._volume.value = `${this.volume}`
|
|
||||||
this.onStreamChanged(this.stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('resize', this.onResise)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMedia() {
|
|
||||||
if (!this.playing) {
|
|
||||||
this._player
|
|
||||||
.play()
|
|
||||||
.then(() => {
|
|
||||||
const { videoWidth, videoHeight } = this._player
|
|
||||||
this.$accessor.video.setResolution({ width: videoWidth, height: videoHeight })
|
|
||||||
this.playing = true
|
|
||||||
this.onResise()
|
|
||||||
})
|
|
||||||
.catch(err => {})
|
|
||||||
} else {
|
|
||||||
this._player.pause()
|
|
||||||
this.playing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.$client.connect(this.password, this.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleControl() {
|
|
||||||
if (!this.connected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.hosting) {
|
|
||||||
this.$client.sendMessage(EVENT.CONTROL.REQUEST)
|
|
||||||
} else {
|
|
||||||
this.$client.sendMessage(EVENT.CONTROL.RELEASE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVolume() {
|
|
||||||
this.$accessor.video.setVolume(parseInt(this._volume.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fullscreen() {
|
|
||||||
this._video.requestFullscreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMousePos(e: MouseEvent) {
|
|
||||||
const { w, h } = this.$accessor.video.resolution
|
|
||||||
const rect = this._player.getBoundingClientRect()
|
|
||||||
this.$client.sendData('mousemove', {
|
|
||||||
x: Math.round((w / rect.width) * (e.clientX - rect.left)),
|
|
||||||
y: Math.round((h / rect.height) * (e.clientY - rect.top)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onWheel(e: WheelEvent) {
|
|
||||||
if (!this.hosting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.onMousePos(e)
|
|
||||||
this.$client.sendData('wheel', {
|
|
||||||
x: (e.deltaX * -1) / 10,
|
|
||||||
y: (e.deltaY * -1) / 10,
|
|
||||||
}) // TODO: Add user settings
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseDown(e: MouseEvent) {
|
|
||||||
if (!this.hosting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.onMousePos(e)
|
|
||||||
this.$client.sendData('mousedown', { key: e.button })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseUp(e: MouseEvent) {
|
|
||||||
if (!this.hosting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.onMousePos(e)
|
|
||||||
this.$client.sendData('mouseup', { key: e.button })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseMove(e: MouseEvent) {
|
|
||||||
if (!this.hosting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.onMousePos(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseEnter(e: MouseEvent) {
|
|
||||||
this._player.focus()
|
|
||||||
this.focused = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseLeave(e: MouseEvent) {
|
|
||||||
this.focused = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(e: KeyboardEvent) {
|
|
||||||
if (!this.focused || !this.hosting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$client.sendData('keydown', { key: e.keyCode })
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyUp(e: KeyboardEvent) {
|
|
||||||
if (!this.focused || !this.hosting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$client.sendData('keyup', { key: e.keyCode })
|
|
||||||
}
|
|
||||||
|
|
||||||
onResise() {
|
|
||||||
const aspect = this.$accessor.video.aspect
|
|
||||||
if (!aspect) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { horizontal, vertical } = aspect
|
|
||||||
this._container.style.maxWidth = `${(horizontal / vertical) * this._video.offsetHeight}px`
|
|
||||||
this._aspect.style.paddingBottom = `${(vertical / horizontal) * 100}%`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
190
client/src/app.vue
Normal file
190
client/src/app.vue
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<div id="neko">
|
||||||
|
<main class="neko-main">
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="neko">
|
||||||
|
<img src="@/assets/logo.svg" alt="n.eko" />
|
||||||
|
<span><b>n</b>.eko</span>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-bars toggle" @click="toggle" />
|
||||||
|
</div>
|
||||||
|
<div class="video-container">
|
||||||
|
<neko-video ref="video" />
|
||||||
|
</div>
|
||||||
|
<div class="room-container">
|
||||||
|
<neko-members />
|
||||||
|
<div class="room-menu">
|
||||||
|
<div class="settings">
|
||||||
|
<neko-menu />
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<neko-controls />
|
||||||
|
</div>
|
||||||
|
<div class="emoji">
|
||||||
|
<neko-emoji />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<neko-side v-if="side" />
|
||||||
|
<neko-connect v-if="!connected" />
|
||||||
|
<neko-about v-if="about" />
|
||||||
|
<notifications group="neko" position="top left" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#neko {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.neko-main {
|
||||||
|
max-width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
background: $background-tertiary;
|
||||||
|
height: $menu-height;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: block;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32px;
|
||||||
|
background: $background-primary;
|
||||||
|
justify-self: flex-end;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 5px 10px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neko {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 150px;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
background: rgba($color: #000, $alpha: 0.4);
|
||||||
|
max-width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-container {
|
||||||
|
background: $background-tertiary;
|
||||||
|
height: $controls-height;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.room-menu {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
margin-left: 10px;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
margin-right: 10px;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Ref } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
import Connect from '~/components/connect.vue'
|
||||||
|
import Video from '~/components/video.vue'
|
||||||
|
import Menu from '~/components/menu.vue'
|
||||||
|
import Side from '~/components/side.vue'
|
||||||
|
import Controls from '~/components/controls.vue'
|
||||||
|
import Members from '~/components/members.vue'
|
||||||
|
import Emoji from '~/components/emoji.vue'
|
||||||
|
import About from '~/components/about.vue'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'neko',
|
||||||
|
components: {
|
||||||
|
'neko-connect': Connect,
|
||||||
|
'neko-video': Video,
|
||||||
|
'neko-menu': Menu,
|
||||||
|
'neko-side': Side,
|
||||||
|
'neko-controls': Controls,
|
||||||
|
'neko-members': Members,
|
||||||
|
'neko-emoji': Emoji,
|
||||||
|
'neko-about': About,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class extends Vue {
|
||||||
|
@Ref('video') video!: Video
|
||||||
|
|
||||||
|
get about() {
|
||||||
|
return this.$accessor.client.about
|
||||||
|
}
|
||||||
|
|
||||||
|
get side() {
|
||||||
|
return this.$accessor.client.side
|
||||||
|
}
|
||||||
|
|
||||||
|
get connected() {
|
||||||
|
return this.$accessor.connected
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.$accessor.client.toggleSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,10 +1,31 @@
|
|||||||
|
$text-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;;
|
||||||
|
$text-size: 14px;
|
||||||
|
$text-normal: #dcddde;
|
||||||
|
$text-muted: #72767d;
|
||||||
|
$text-link: #00b0f4;
|
||||||
|
|
||||||
|
$interactive-normal: #b9bbbe;
|
||||||
|
$interactive-hover: #dcddde;
|
||||||
|
$interactive-active: #fff;
|
||||||
|
$interactive-muted: #4f545c;
|
||||||
|
|
||||||
|
$background-primary: #36393f;
|
||||||
|
$background-secondary: #2f3136;
|
||||||
|
$background-tertiary: #202225;
|
||||||
|
$background-accent: #4f545c;
|
||||||
|
$background-floating: #18191c;
|
||||||
|
$background-modifier-hover: rgba(79, 84, 92, 0.16);
|
||||||
|
$background-modifier-active: rgba(79, 84, 92, 0.24);
|
||||||
|
$background-modifier-selected: rgba(79, 84, 92, 0.32);
|
||||||
|
$background-modifier-accent: hsla(0, 0%, 100%, 0.06);
|
||||||
|
|
||||||
|
$elevation-low: 0 1px 0 rgba(4, 4, 5, 0.2), 0 1.5px 0 rgba(6, 6, 7, 0.05), 0 2px 0 rgba(4, 4, 5, 0.05);
|
||||||
|
$elevation-high: 0 8px 16px rgba(0, 0, 0, 0.24);
|
||||||
|
|
||||||
$style-dark: #2c2c2c;
|
|
||||||
$style-darker: #1a1a1a;
|
|
||||||
$style-light: #fafafa;
|
|
||||||
$style-primary: #19bd9c;
|
$style-primary: #19bd9c;
|
||||||
|
$style-error: #d32f2f;
|
||||||
|
|
||||||
|
$menu-height: 40px;
|
||||||
|
$controls-height: 125px;
|
||||||
|
$side-width: 300px;
|
||||||
|
|
||||||
$style-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
|
||||||
$style-font-color: $style-dark;
|
|
||||||
$style-font-size: 14px;
|
|
||||||
|
BIN
client/src/assets/styles/fonts/whitney-300.woff
Normal file
BIN
client/src/assets/styles/fonts/whitney-300.woff
Normal file
Binary file not shown.
BIN
client/src/assets/styles/fonts/whitney-400.woff
Normal file
BIN
client/src/assets/styles/fonts/whitney-400.woff
Normal file
Binary file not shown.
BIN
client/src/assets/styles/fonts/whitney-500.woff
Normal file
BIN
client/src/assets/styles/fonts/whitney-500.woff
Normal file
Binary file not shown.
BIN
client/src/assets/styles/fonts/whitney-600.woff
Normal file
BIN
client/src/assets/styles/fonts/whitney-600.woff
Normal file
Binary file not shown.
BIN
client/src/assets/styles/fonts/whitney-700.woff
Normal file
BIN
client/src/assets/styles/fonts/whitney-700.woff
Normal file
Binary file not shown.
@ -8,16 +8,20 @@
|
|||||||
|
|
||||||
// Import Vendor
|
// Import Vendor
|
||||||
@import "vendor/font-awesome";
|
@import "vendor/font-awesome";
|
||||||
|
@import "vendor/font-whitney";
|
||||||
|
@import "vendor/swal";
|
||||||
|
@import "vendor/tooltip";
|
||||||
|
@import "vendor/github";
|
||||||
|
// @import "vendor/bulma";
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
-webkit-font-smoothing: subpixel-antialiased;
|
-webkit-font-smoothing: subpixel-antialiased;
|
||||||
background-color: $style-dark;
|
background-color: $background-tertiary;
|
||||||
font-family: $style-font-family;
|
font-family: $text-family;
|
||||||
font-size: $style-font-size;
|
font-size: $text-size;
|
||||||
color: $style-font-color;
|
color: $text-normal;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
245
client/src/assets/styles/vendor/_bulma.scss
vendored
Normal file
245
client/src/assets/styles/vendor/_bulma.scss
vendored
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
@import "~bulma/sass/utilities/initial-variables.sass";
|
||||||
|
@import "~bulma/sass/utilities/functions.sass";
|
||||||
|
@import "~bulma/sass/utilities/derived-variables.sass";
|
||||||
|
@import "~bulma/sass/utilities/animations.sass";
|
||||||
|
@import "~bulma/sass/utilities/mixins.sass";
|
||||||
|
@import "~bulma/sass/utilities/controls.sass";
|
||||||
|
|
||||||
|
// $black: hsl(0, 0%, 4%);
|
||||||
|
// $black-bis: hsl(0, 0%, 7%);
|
||||||
|
// $black-ter: hsl(0, 0%, 14%);
|
||||||
|
|
||||||
|
// $grey-darker: hsl(0, 0%, 21%);
|
||||||
|
// $grey-dark: hsl(0, 0%, 29%);
|
||||||
|
// $grey: hsl(0, 0%, 48%);
|
||||||
|
// $grey-light: hsl(0, 0%, 71%);
|
||||||
|
// $grey-lighter: hsl(0, 0%, 86%);
|
||||||
|
// $grey-lightest: hsl(0, 0%, 93%);
|
||||||
|
|
||||||
|
// $white-ter: hsl(0, 0%, 96%);
|
||||||
|
// $white-bis: hsl(0, 0%, 98%);
|
||||||
|
// $white: hsl(0, 0%, 100%);
|
||||||
|
|
||||||
|
// $orange: hsl(14, 100%, 53%);
|
||||||
|
// $yellow: hsl(48, 100%, 67%);
|
||||||
|
// $green: hsl(141, 53%, 53%);
|
||||||
|
// $turquoise: hsl(171, 100%, 41%);
|
||||||
|
// $cyan: hsl(204, 71%, 53%);
|
||||||
|
// $blue: hsl(217, 71%, 53%);
|
||||||
|
// $purple: hsl(271, 100%, 71%);
|
||||||
|
// $red: hsl(348, 86%, 61%);
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
$family-sans-serif: $text-family;
|
||||||
|
// $family-monospace: monospace;
|
||||||
|
// $render-mode: optimizeLegibility;
|
||||||
|
|
||||||
|
// $size-1: 3rem;
|
||||||
|
// $size-2: 2.5rem;
|
||||||
|
// $size-3: 2rem;
|
||||||
|
// $size-4: 1.5rem;
|
||||||
|
// $size-5: 1.25rem;
|
||||||
|
// $size-6: 1rem;
|
||||||
|
// $size-7: 0.75rem;
|
||||||
|
|
||||||
|
// $weight-light: 300;
|
||||||
|
// $weight-normal: 400;
|
||||||
|
// $weight-medium: 500;
|
||||||
|
// $weight-semibold: 600;
|
||||||
|
// $weight-bold: 700;
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
// $block-spacing: 1.5rem;
|
||||||
|
|
||||||
|
// Responsiveness
|
||||||
|
// The container horizontal gap, which acts as the offset for breakpoints
|
||||||
|
// $gap: 32px;
|
||||||
|
// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
|
||||||
|
// $tablet: 769px;
|
||||||
|
// 960px container + 4rem
|
||||||
|
// $desktop: 960px + (2 * // $gap);
|
||||||
|
// 1152px container + 4rem
|
||||||
|
// $widescreen: 1152px + (2 * // $gap);
|
||||||
|
// $widescreen-enabled: true;
|
||||||
|
// 1344px container + 4rem
|
||||||
|
// $fullhd: 1344px + (2 * // $gap);
|
||||||
|
// $fullhd-enabled: true;
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
// $easing: ease-out;
|
||||||
|
// $radius-small: 2px;
|
||||||
|
// $radius: 4px;
|
||||||
|
// $radius-large: 6px;
|
||||||
|
// $radius-rounded: 290486px;
|
||||||
|
// $speed: 86ms;
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
// $variable-columns: true;
|
||||||
|
|
||||||
|
// $primary: $turquoise;
|
||||||
|
// $info: $cyan;
|
||||||
|
// $success: $green;
|
||||||
|
// $warning: $yellow;
|
||||||
|
// $danger: $red;
|
||||||
|
|
||||||
|
// $light: $white-ter;
|
||||||
|
// $dark: $grey-darker;
|
||||||
|
|
||||||
|
// Invert colors
|
||||||
|
// $orange-invert: findColorInvert($orange);
|
||||||
|
// $yellow-invert: findColorInvert($yellow);
|
||||||
|
// $green-invert: findColorInvert($green);
|
||||||
|
// $turquoise-invert: findColorInvert($turquoise);
|
||||||
|
// $cyan-invert: findColorInvert($cyan);
|
||||||
|
// $blue-invert: findColorInvert($blue);
|
||||||
|
// $purple-invert: findColorInvert($purple);
|
||||||
|
// $red-invert: findColorInvert($red);
|
||||||
|
|
||||||
|
// $primary-invert: findColorInvert($primary);
|
||||||
|
// $primary-light: findLightColor($primary);
|
||||||
|
// $primary-dark: findDarkColor($primary);
|
||||||
|
// $info-invert: findColorInvert($info);
|
||||||
|
// $info-light: findLightColor($info);
|
||||||
|
// $info-dark: findDarkColor($info);
|
||||||
|
// $success-invert: findColorInvert($success);
|
||||||
|
// $success-light: findLightColor($success);
|
||||||
|
// $success-dark: findDarkColor($success);
|
||||||
|
// $warning-invert: findColorInvert($warning);
|
||||||
|
// $warning-light: findLightColor($warning);
|
||||||
|
// $warning-dark: findDarkColor($warning);
|
||||||
|
// $danger-invert: findColorInvert($danger);
|
||||||
|
// $danger-light: findLightColor($danger);
|
||||||
|
// $danger-dark: findDarkColor($danger);
|
||||||
|
// $light-invert: findColorInvert($light);
|
||||||
|
// $dark-invert: findColorInvert($dark);
|
||||||
|
|
||||||
|
// General colors
|
||||||
|
// $scheme-main: $white;
|
||||||
|
// $scheme-main-bis: $white-bis;
|
||||||
|
// $scheme-main-ter: $white-ter;
|
||||||
|
// $scheme-invert: $black;
|
||||||
|
// $scheme-invert-bis: $black-bis;
|
||||||
|
// $scheme-invert-ter: $black-ter;
|
||||||
|
|
||||||
|
// $background: $white-ter;
|
||||||
|
$border: $grey-light;
|
||||||
|
$border-hover: $grey;
|
||||||
|
// $border-light: $grey-lightest;
|
||||||
|
// $border-light-hover: $grey-light;
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
// $text: $grey-dark;
|
||||||
|
// $text-invert: findColorInvert($text);
|
||||||
|
// $text-light: $grey;
|
||||||
|
// $text-strong: $grey-darker;
|
||||||
|
|
||||||
|
// Code colors
|
||||||
|
// $code: $red;
|
||||||
|
// $code-background: $background;
|
||||||
|
|
||||||
|
// $pre: $text;
|
||||||
|
// $pre-background: $background;
|
||||||
|
|
||||||
|
// Link colors
|
||||||
|
// $link: $blue;
|
||||||
|
// $link-invert: findColorInvert($link);
|
||||||
|
// $link-light: findLightColor($link);
|
||||||
|
// $link-dark: findDarkColor($link);
|
||||||
|
// $link-visited: $purple;
|
||||||
|
|
||||||
|
// $link-hover: $grey-darker;
|
||||||
|
// $link-hover-border: $grey-light;
|
||||||
|
|
||||||
|
// $link-focus: $grey-darker;
|
||||||
|
// $link-focus-border: $blue;
|
||||||
|
|
||||||
|
// $link-active: $grey-darker;
|
||||||
|
// $link-active-border: $grey-dark;
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
// $family-primary: $family-sans-serif;
|
||||||
|
// $family-secondary: $family-sans-serif;
|
||||||
|
// $family-code: $family-monospace;
|
||||||
|
|
||||||
|
// $size-small: $size-7;
|
||||||
|
// $size-normal: $size-6;
|
||||||
|
// $size-medium: $size-5;
|
||||||
|
// $size-large: $size-4;
|
||||||
|
|
||||||
|
// Lists and maps
|
||||||
|
// $custom-colors: null;
|
||||||
|
// $custom-shades: null;
|
||||||
|
|
||||||
|
// $colors: mergeColorMaps(("white": ($white, $black), "black": ($black, $white), "light": ($light, $light-invert), "dark": ($dark, $dark-invert), "primary": ($primary, $primary-invert, $primary-light, $primary-dark), "link": ($link, $link-invert, $link-light, $link-dark), "info": ($info, $info-invert, $info-light, $info-dark), "success": ($success, $success-invert, $success-light, $success-dark), "warning": ($warning, $warning-invert, $warning-light, $warning-dark), "danger": ($danger, $danger-invert, $danger-light, $danger-dark)), $custom-colors);
|
||||||
|
// $shades: mergeColorMaps(("black-bis": $black-bis, "black-ter": $black-ter, "grey-darker": $grey-darker, "grey-dark": $grey-dark, "grey": $grey, "grey-light": $grey-light, "grey-lighter": $grey-lighter, "white-ter": $white-ter, "white-bis": $white-bis), $custom-shades);
|
||||||
|
// $sizes: $size-1 $size-2 $size-3 $size-4 $size-5 $size-6 $size-7;
|
||||||
|
|
||||||
|
@import "~bulma/sass/base/minireset";
|
||||||
|
@import "~bulma/sass/base/generic";
|
||||||
|
@import "~bulma/sass/base/helpers";
|
||||||
|
|
||||||
|
@import "~bulma/sass/elements/box.sass";
|
||||||
|
@import "~bulma/sass/elements/button.sass";
|
||||||
|
@import "~bulma/sass/elements/container.sass";
|
||||||
|
@import "~bulma/sass/elements/content.sass";
|
||||||
|
@import "~bulma/sass/elements/icon.sass";
|
||||||
|
@import "~bulma/sass/elements/image.sass";
|
||||||
|
@import "~bulma/sass/elements/notification.sass";
|
||||||
|
@import "~bulma/sass/elements/progress.sass";
|
||||||
|
@import "~bulma/sass/elements/table.sass";
|
||||||
|
@import "~bulma/sass/elements/tag.sass";
|
||||||
|
@import "~bulma/sass/elements/title.sass";
|
||||||
|
@import "~bulma/sass/elements/other.sass";
|
||||||
|
|
||||||
|
// $input-color: $text-strong;
|
||||||
|
// $input-background-color: $scheme-main;
|
||||||
|
// $input-border-color: $border;
|
||||||
|
// $input-height: $control-height;
|
||||||
|
// $input-shadow: inset 0 0.0625em 0.125em rgba($scheme-invert, 0.05);
|
||||||
|
// $input-placeholder-color: rgba($input-color, 0.3);
|
||||||
|
// $input-hover-color: $text-strong;
|
||||||
|
// $input-hover-border-color: $border-hover;
|
||||||
|
// $input-focus-color: $text-strong;
|
||||||
|
// $input-focus-border-color: $link;
|
||||||
|
// $input-focus-box-shadow-size: 0 0 0 0.125em;
|
||||||
|
// $input-focus-box-shadow-color: rgba($link, 0.25);
|
||||||
|
// $input-disabled-color: $text-light;
|
||||||
|
// $input-disabled-background-color: $background;
|
||||||
|
// $input-disabled-border-color: $background;
|
||||||
|
// $input-disabled-placeholder-color: rgba($input-disabled-color, 0.3);
|
||||||
|
// $input-arrow: $link;
|
||||||
|
// $input-icon-color: $border;
|
||||||
|
// $input-icon-active-color: $text;
|
||||||
|
// $input-radius: $radius;
|
||||||
|
|
||||||
|
@import "~bulma/sass/form/shared.sass";
|
||||||
|
|
||||||
|
// $textarea-padding: $control-padding-horizontal;
|
||||||
|
// $textarea-max-height: 40em;
|
||||||
|
// $textarea-min-height: 8em;
|
||||||
|
@import "~bulma/sass/form/input-textarea.sass";
|
||||||
|
@import "~bulma/sass/form/checkbox-radio.sass";
|
||||||
|
@import "~bulma/sass/form/select.sass";
|
||||||
|
@import "~bulma/sass/form/file.sass";
|
||||||
|
@import "~bulma/sass/form/tools.sass";
|
||||||
|
|
||||||
|
// @import "~bulma/sass/components/breadcrumb.sass";
|
||||||
|
// @import "~bulma/sass/components/card.sass";
|
||||||
|
// @import "~bulma/sass/components/dropdown.sass";
|
||||||
|
// @import "~bulma/sass/components/level.sass";
|
||||||
|
// @import "~bulma/sass/components/list.sass";
|
||||||
|
// @import "~bulma/sass/components/media.sass";
|
||||||
|
// @import "~bulma/sass/components/menu.sass";
|
||||||
|
// @import "~bulma/sass/components/message.sass";
|
||||||
|
// @import "~bulma/sass/components/modal.sass";
|
||||||
|
// @import "~bulma/sass/components/navbar.sass";
|
||||||
|
// @import "~bulma/sass/components/pagination.sass";
|
||||||
|
// @import "~bulma/sass/components/panel.sass";
|
||||||
|
@import "~bulma/sass/components/tabs.sass";
|
||||||
|
|
||||||
|
@import "~bulma/sass/grid/columns.sass";
|
||||||
|
@import "~bulma/sass/grid/tiles.sass";
|
||||||
|
|
||||||
|
// @import "~bulma/sass/layout/hero.sass";
|
||||||
|
// @import "~bulma/sass/layout/section.sass";
|
||||||
|
// @import "~bulma/sass/layout/footer.sass";
|
29
client/src/assets/styles/vendor/_font-whitney.scss
vendored
Normal file
29
client/src/assets/styles/vendor/_font-whitney.scss
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@font-face{
|
||||||
|
font-family:Whitney;
|
||||||
|
font-weight:300;
|
||||||
|
src:url("fonts/whitney-300.woff") format("woff")
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family:Whitney;
|
||||||
|
font-weight:400;
|
||||||
|
src:url("fonts/whitney-400.woff") format("woff")
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family:Whitney;
|
||||||
|
font-weight:500;
|
||||||
|
src:url("fonts/whitney-500.woff") format("woff")
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family:Whitney;
|
||||||
|
font-weight:600;
|
||||||
|
src:url("fonts/whitney-600.woff") format("woff")
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family:Whitney;
|
||||||
|
font-weight:700;
|
||||||
|
src:url("fonts/whitney-700.woff") format("woff")
|
||||||
|
}
|
957
client/src/assets/styles/vendor/_github.scss
vendored
Normal file
957
client/src/assets/styles/vendor/_github.scss
vendored
Normal file
@ -0,0 +1,957 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: octicons-link;
|
||||||
|
src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .octicon {
|
||||||
|
display: inline-block;
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .anchor {
|
||||||
|
float: left;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: -20px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .anchor:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 .octicon-link,
|
||||||
|
.markdown-body h2 .octicon-link,
|
||||||
|
.markdown-body h3 .octicon-link,
|
||||||
|
.markdown-body h4 .octicon-link,
|
||||||
|
.markdown-body h5 .octicon-link,
|
||||||
|
.markdown-body h6 .octicon-link {
|
||||||
|
color: $text-normal;
|
||||||
|
vertical-align: middle;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1:hover .anchor,
|
||||||
|
.markdown-body h2:hover .anchor,
|
||||||
|
.markdown-body h3:hover .anchor,
|
||||||
|
.markdown-body h4:hover .anchor,
|
||||||
|
.markdown-body h5:hover .anchor,
|
||||||
|
.markdown-body h6:hover .anchor {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1:hover .anchor .octicon-link,
|
||||||
|
.markdown-body h2:hover .anchor .octicon-link,
|
||||||
|
.markdown-body h3:hover .anchor .octicon-link,
|
||||||
|
.markdown-body h4:hover .anchor .octicon-link,
|
||||||
|
.markdown-body h5:hover .anchor .octicon-link,
|
||||||
|
.markdown-body h6:hover .anchor .octicon-link {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
color: $text-normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-c {
|
||||||
|
color: #6a737d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-c1,
|
||||||
|
.markdown-body .pl-s .pl-v {
|
||||||
|
color: #005cc5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-e,
|
||||||
|
.markdown-body .pl-en {
|
||||||
|
color: #6f42c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-s .pl-s1,
|
||||||
|
.markdown-body .pl-smi {
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-ent {
|
||||||
|
color: #22863a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-k {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-pds,
|
||||||
|
.markdown-body .pl-s,
|
||||||
|
.markdown-body .pl-s .pl-pse .pl-s1,
|
||||||
|
.markdown-body .pl-sr,
|
||||||
|
.markdown-body .pl-sr .pl-cce,
|
||||||
|
.markdown-body .pl-sr .pl-sra,
|
||||||
|
.markdown-body .pl-sr .pl-sre {
|
||||||
|
color: #032f62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-smw,
|
||||||
|
.markdown-body .pl-v {
|
||||||
|
color: #e36209;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-bu {
|
||||||
|
color: #b31d28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-ii {
|
||||||
|
background-color: #b31d28;
|
||||||
|
color: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-c2 {
|
||||||
|
background-color: #d73a49;
|
||||||
|
color: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-c2:before {
|
||||||
|
content: "^M";
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-sr .pl-cce {
|
||||||
|
color: #22863a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-ml {
|
||||||
|
color: #735c0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mh,
|
||||||
|
.markdown-body .pl-mh .pl-en,
|
||||||
|
.markdown-body .pl-ms {
|
||||||
|
color: #005cc5;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mi {
|
||||||
|
color: #24292e;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mb {
|
||||||
|
color: #24292e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-md {
|
||||||
|
background-color: #ffeef0;
|
||||||
|
color: #b31d28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mi1 {
|
||||||
|
background-color: #f0fff4;
|
||||||
|
color: #22863a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mc {
|
||||||
|
background-color: #ffebda;
|
||||||
|
color: #e36209;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mi2 {
|
||||||
|
background-color: #005cc5;
|
||||||
|
color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-mdr {
|
||||||
|
color: #6f42c1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-ba {
|
||||||
|
color: #586069;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-sg {
|
||||||
|
color: #959da5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-corl {
|
||||||
|
color: #032f62;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:active,
|
||||||
|
.markdown-body a:hover {
|
||||||
|
outline-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body strong {
|
||||||
|
font-weight: inherit;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: .67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code,
|
||||||
|
.markdown-body kbd,
|
||||||
|
.markdown-body pre {
|
||||||
|
font-family: monospace,monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body input {
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body input {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body [type=checkbox] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body input {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
color: $text-link;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid $background-primary;
|
||||||
|
height: 0;
|
||||||
|
margin: 15px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr:before {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr:after {
|
||||||
|
clear: both;
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body td,
|
||||||
|
.markdown-body th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ol,
|
||||||
|
.markdown-body ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ol ol,
|
||||||
|
.markdown-body ul ol {
|
||||||
|
list-style-type: lower-roman;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ol ol ol,
|
||||||
|
.markdown-body ol ul ol,
|
||||||
|
.markdown-body ul ol ol,
|
||||||
|
.markdown-body ul ul ol {
|
||||||
|
list-style-type: lower-alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body dd {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code,
|
||||||
|
.markdown-body pre {
|
||||||
|
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body input::-webkit-inner-spin-button,
|
||||||
|
.markdown-body input::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .border {
|
||||||
|
border: 1px solid #e1e4e8!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .border-0 {
|
||||||
|
border: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .border-bottom {
|
||||||
|
border-bottom: 1px solid #e1e4e8!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .rounded-1 {
|
||||||
|
border-radius: 3px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .bg-white {
|
||||||
|
background-color: #fff!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .bg-gray-light {
|
||||||
|
background-color: #fafbfc!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .text-gray-light {
|
||||||
|
color: $text-normal!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mb-0 {
|
||||||
|
margin-bottom: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .my-2 {
|
||||||
|
margin-bottom: 8px!important;
|
||||||
|
margin-top: 8px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-0 {
|
||||||
|
padding-left: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .py-0 {
|
||||||
|
padding-bottom: 0!important;
|
||||||
|
padding-top: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-1 {
|
||||||
|
padding-left: 4px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-2 {
|
||||||
|
padding-left: 8px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .py-2 {
|
||||||
|
padding-bottom: 8px!important;
|
||||||
|
padding-top: 8px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-3,
|
||||||
|
.markdown-body .px-3 {
|
||||||
|
padding-left: 16px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .px-3 {
|
||||||
|
padding-right: 16px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-4 {
|
||||||
|
padding-left: 24px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-5 {
|
||||||
|
padding-left: 32px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-6 {
|
||||||
|
padding-left: 40px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .f6 {
|
||||||
|
font-size: 12px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .lh-condensed {
|
||||||
|
line-height: 1.25!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .text-bold {
|
||||||
|
font-weight: 600!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body:before {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body:after {
|
||||||
|
clear: both;
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body>:first-child {
|
||||||
|
margin-top: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body>:last-child {
|
||||||
|
margin-bottom: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:not([href]) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote,
|
||||||
|
.markdown-body dl,
|
||||||
|
.markdown-body ol,
|
||||||
|
.markdown-body p,
|
||||||
|
.markdown-body pre,
|
||||||
|
.markdown-body table,
|
||||||
|
.markdown-body ul {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
background-color: #e1e4e8;
|
||||||
|
border: 0;
|
||||||
|
height: .25em;
|
||||||
|
margin: 24px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
background-color: $background-primary;
|
||||||
|
border-left: .25em solid $style-primary;
|
||||||
|
color: $text-normal;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote>:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote>:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body kbd {
|
||||||
|
background-color: #fafbfc;
|
||||||
|
border: 1px solid #c6cbd1;
|
||||||
|
border-bottom-color: #959da5;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 -1px 0 #959da5;
|
||||||
|
color: #444d56;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 10px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2 {
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h5 {
|
||||||
|
font-size: .875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h6 {
|
||||||
|
color: #6a737d;
|
||||||
|
font-size: .85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ol,
|
||||||
|
.markdown-body ul {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ol ol,
|
||||||
|
.markdown-body ol ul,
|
||||||
|
.markdown-body ul ol,
|
||||||
|
.markdown-body ul ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
word-wrap: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li>p {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li+li {
|
||||||
|
margin-top: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body dl {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body dl dt {
|
||||||
|
font-size: 1em;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body dl dd {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
display: block;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table td,
|
||||||
|
.markdown-body table th {
|
||||||
|
border: 1px solid $background-floating;
|
||||||
|
padding: 6px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr {
|
||||||
|
background-color: $background-primary;
|
||||||
|
border-top: 1px solid #c6cbd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr:nth-child(2n) {
|
||||||
|
background-color: $background-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img {
|
||||||
|
box-sizing: content-box;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img[align=right] {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img[align=left] {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
background-color: rgba(27,31,35,.05);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 85%;
|
||||||
|
margin: 0;
|
||||||
|
padding: .2em .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre>code {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .highlight {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .highlight pre {
|
||||||
|
margin-bottom: 0;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .highlight pre,
|
||||||
|
.markdown-body pre {
|
||||||
|
background-color: $background-primary;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 85%;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
display: inline;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
max-width: auto;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .commit-tease-sha {
|
||||||
|
color: #444d56;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-wrapper {
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-wrapper-embedded {
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-num {
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
color: rgba(27,31,35,.3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
min-width: 50px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-num:hover {
|
||||||
|
color: rgba(27,31,35,.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-num:before {
|
||||||
|
content: attr(data-line-number);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-code {
|
||||||
|
line-height: 20px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .blob-code-inner {
|
||||||
|
color: #24292e;
|
||||||
|
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: visible;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-token.active,
|
||||||
|
.markdown-body .pl-token:hover {
|
||||||
|
background: #ffea7f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body kbd {
|
||||||
|
background-color: #fafbfc;
|
||||||
|
border: 1px solid #d1d5da;
|
||||||
|
border-bottom-color: #c6cbd1;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 -1px 0 #c6cbd1;
|
||||||
|
color: #444d56;
|
||||||
|
display: inline-block;
|
||||||
|
font: 11px SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
|
||||||
|
line-height: 10px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :checked+.radio-label {
|
||||||
|
border-color: #0366d6;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="1"] {
|
||||||
|
-moz-tab-size: 1;
|
||||||
|
tab-size: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="2"] {
|
||||||
|
-moz-tab-size: 2;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="3"] {
|
||||||
|
-moz-tab-size: 3;
|
||||||
|
tab-size: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="4"] {
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="5"] {
|
||||||
|
-moz-tab-size: 5;
|
||||||
|
tab-size: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="6"] {
|
||||||
|
-moz-tab-size: 6;
|
||||||
|
tab-size: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="7"] {
|
||||||
|
-moz-tab-size: 7;
|
||||||
|
tab-size: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="8"] {
|
||||||
|
-moz-tab-size: 8;
|
||||||
|
tab-size: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="9"] {
|
||||||
|
-moz-tab-size: 9;
|
||||||
|
tab-size: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="10"] {
|
||||||
|
-moz-tab-size: 10;
|
||||||
|
tab-size: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="11"] {
|
||||||
|
-moz-tab-size: 11;
|
||||||
|
tab-size: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .tab-size[data-tab-size="12"] {
|
||||||
|
-moz-tab-size: 12;
|
||||||
|
tab-size: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .task-list-item {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .task-list-item+.task-list-item {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .task-list-item input {
|
||||||
|
margin: 0 .2em .25em -1.6em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
border-bottom-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-0 {
|
||||||
|
padding-left: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-1 {
|
||||||
|
padding-left: 4px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-2 {
|
||||||
|
padding-left: 8px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-3 {
|
||||||
|
padding-left: 16px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-4 {
|
||||||
|
padding-left: 24px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-5 {
|
||||||
|
padding-left: 32px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-6 {
|
||||||
|
padding-left: 40px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-7 {
|
||||||
|
padding-left: 48px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-8 {
|
||||||
|
padding-left: 64px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-9 {
|
||||||
|
padding-left: 80px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-10 {
|
||||||
|
padding-left: 96px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-11 {
|
||||||
|
padding-left: 112px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .pl-12 {
|
||||||
|
padding-left: 128px!important;
|
||||||
|
}
|
188
client/src/assets/styles/vendor/_swal.scss
vendored
Normal file
188
client/src/assets/styles/vendor/_swal.scss
vendored
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
$swal2-white: #fff;
|
||||||
|
$swal2-black: #000;
|
||||||
|
$swal2-outline-color: transparent;
|
||||||
|
|
||||||
|
// CONTAINER
|
||||||
|
$swal2-container-padding: .625em;
|
||||||
|
|
||||||
|
// BOX MODEL
|
||||||
|
$swal2-width: 32em;
|
||||||
|
$swal2-padding: 1.25em;
|
||||||
|
$swal2-border: none;
|
||||||
|
$swal2-border-radius: .3125em;
|
||||||
|
$swal2-box-shadow: #d9d9d9;
|
||||||
|
|
||||||
|
// ANIMATIONS
|
||||||
|
$swal2-show-animation: swal2-show .3s;
|
||||||
|
$swal2-hide-animation: swal2-hide .15s forwards;
|
||||||
|
|
||||||
|
// BACKGROUND
|
||||||
|
$swal2-background: $background-secondary;
|
||||||
|
|
||||||
|
// TYPOGRAPHY
|
||||||
|
$swal2-font: inherit;
|
||||||
|
$swal2-font-size: 1rem;
|
||||||
|
|
||||||
|
// BACKDROP
|
||||||
|
$swal2-backdrop: rgba($swal2-black, .4);
|
||||||
|
$swal2-backdrop-transition: background-color .1s;
|
||||||
|
|
||||||
|
// ICONS
|
||||||
|
$swal2-icon-size: 5em;
|
||||||
|
$swal2-icon-animations: true;
|
||||||
|
$swal2-icon-margin: 1.25em auto 1.875em;
|
||||||
|
$swal2-icon-zoom: null;
|
||||||
|
$swal2-success: #a5dc86;
|
||||||
|
$swal2-success-border: rgba($swal2-success, .3);
|
||||||
|
$swal2-error: #f27474;
|
||||||
|
$swal2-warning: #f8bb86;
|
||||||
|
$swal2-info: #3fc3ee;
|
||||||
|
$swal2-question: #87adbd;
|
||||||
|
$swal2-icon-font-family: inherit;
|
||||||
|
|
||||||
|
// IMAGE
|
||||||
|
$swal2-image-margin: 1.25em auto;
|
||||||
|
|
||||||
|
// TITLE
|
||||||
|
$swal2-title-margin: 0 0 .4em;
|
||||||
|
$swal2-title-color: $interactive-hover;
|
||||||
|
$swal2-title-font-size: 1.875em;
|
||||||
|
|
||||||
|
// CONTENT
|
||||||
|
$swal2-content-justify-content: center;
|
||||||
|
$swal2-content-margin: 0;
|
||||||
|
$swal2-content-pading: 0;
|
||||||
|
$swal2-content-color: $interactive-hover;
|
||||||
|
$swal2-content-font-size: 1.125em;
|
||||||
|
$swal2-content-font-weight: normal;
|
||||||
|
$swal2-content-line-height: normal;
|
||||||
|
$swal2-content-text-align: center;
|
||||||
|
$swal2-content-word-wrap: break-word;
|
||||||
|
|
||||||
|
// INPUT
|
||||||
|
$swal2-input-margin: 1em auto;
|
||||||
|
$swal2-input-width: 100%;
|
||||||
|
$swal2-input-height: 2.625em;
|
||||||
|
$swal2-input-padding: 0 .75em;
|
||||||
|
$swal2-input-border: 1px solid lighten($swal2-black, 85);
|
||||||
|
$swal2-input-border-radius: .1875em;
|
||||||
|
$swal2-input-box-shadow: inset 0 1px 1px rgba($swal2-black, .06);
|
||||||
|
$swal2-input-focus-border: 1px solid #b4dbed;
|
||||||
|
$swal2-input-focus-outline: none;
|
||||||
|
$swal2-input-focus-box-shadow: 0 0 3px #c4e6f5;
|
||||||
|
$swal2-input-font-size: 1.125em;
|
||||||
|
$swal2-input-background: inherit;
|
||||||
|
$swal2-input-color: inherit;
|
||||||
|
$swal2-input-transition: border-color .3s, box-shadow .3s;
|
||||||
|
|
||||||
|
// TEXTAREA SPECIFIC VARIABLES
|
||||||
|
$swal2-textarea-height: 6.75em;
|
||||||
|
$swal2-textarea-padding: .75em;
|
||||||
|
|
||||||
|
// VALIDATION MESSAGE
|
||||||
|
$swal2-validation-message-justify-content: center;
|
||||||
|
$swal2-validation-message-padding: .625em;
|
||||||
|
$swal2-validation-message-background: lighten($swal2-black, 94);
|
||||||
|
$swal2-validation-message-color: lighten($swal2-black, 40);
|
||||||
|
$swal2-validation-message-font-size: 1em;
|
||||||
|
$swal2-validation-message-font-weight: 300;
|
||||||
|
$swal2-validation-message-icon-background: $swal2-error;
|
||||||
|
$swal2-validation-message-icon-color: $swal2-white;
|
||||||
|
$swal2-validation-message-icon-zoom: null;
|
||||||
|
|
||||||
|
// PROGRESS STEPS
|
||||||
|
$swal2-progress-steps-background: inherit;
|
||||||
|
$swal2-progress-steps-margin: 0 0 1.25em;
|
||||||
|
$swal2-progress-steps-padding: 0;
|
||||||
|
$swal2-progress-steps-font-weight: 600;
|
||||||
|
$swal2-progress-steps-distance: 2.5em;
|
||||||
|
$swal2-progress-step-width: 2em;
|
||||||
|
$swal2-progress-step-height: 2em;
|
||||||
|
$swal2-progress-step-border-radius: 2em;
|
||||||
|
$swal2-progress-step-background: #add8e6;
|
||||||
|
$swal2-progress-step-color: $swal2-white;
|
||||||
|
$swal2-active-step-background: #3085d6;
|
||||||
|
$swal2-active-step-color: $swal2-white;
|
||||||
|
|
||||||
|
// FOOTER
|
||||||
|
$swal2-footer-margin: 1.25em 0 0;
|
||||||
|
$swal2-footer-padding: 1em 0 0;
|
||||||
|
$swal2-footer-border-color: #eee;
|
||||||
|
$swal2-footer-color: lighten($swal2-black, 33);
|
||||||
|
$swal2-footer-font-size: 1em;
|
||||||
|
|
||||||
|
// TIMER POGRESS BAR
|
||||||
|
$swal2-timer-progress-bar-height: .25em;
|
||||||
|
$swal2-timer-progress-bar-background: rgba($swal2-black, .2);
|
||||||
|
|
||||||
|
// CLOSE BUTTON
|
||||||
|
$swal2-close-button-width: 1.2em;
|
||||||
|
$swal2-close-button-height: 1.2em;
|
||||||
|
$swal2-close-button-line-height: 1.2;
|
||||||
|
$swal2-close-button-position: absolute;
|
||||||
|
$swal2-close-button-gap: 0;
|
||||||
|
$swal2-close-button-transition: color .1s ease-out;
|
||||||
|
$swal2-close-button-border: none;
|
||||||
|
$swal2-close-button-border-radius: 0;
|
||||||
|
$swal2-close-button-outline: initial;
|
||||||
|
$swal2-close-button-background: transparent;
|
||||||
|
$swal2-close-button-color: lighten($swal2-black, 80);
|
||||||
|
$swal2-close-button-font-family: serif;
|
||||||
|
$swal2-close-button-font-size: 2.5em;
|
||||||
|
|
||||||
|
// CLOSE BUTTON:HOVER
|
||||||
|
$swal2-close-button-hover-transform: none;
|
||||||
|
$swal2-close-button-hover-color: $swal2-error;
|
||||||
|
$swal2-close-button-hover-background: transparent;
|
||||||
|
|
||||||
|
// ACTIONS
|
||||||
|
$swal2-actions-flex-wrap: wrap;
|
||||||
|
$swal2-actions-align-items: center;
|
||||||
|
$swal2-actions-justify-content: center;
|
||||||
|
$swal2-actions-width: 100%;
|
||||||
|
$swal2-actions-margin: 1.25em auto 0;
|
||||||
|
|
||||||
|
// CONFIRM BUTTON
|
||||||
|
$swal2-confirm-button-border: 0;
|
||||||
|
$swal2-confirm-button-border-radius: .25em;
|
||||||
|
$swal2-confirm-button-background-color: $background-tertiary;
|
||||||
|
$swal2-confirm-button-color: $swal2-white;
|
||||||
|
$swal2-confirm-button-font-size: 1.0625em;
|
||||||
|
|
||||||
|
// CANCEL BUTTON
|
||||||
|
$swal2-cancel-button-border: 0;
|
||||||
|
$swal2-cancel-button-border-radius: .25em;
|
||||||
|
$swal2-cancel-button-background-color: #aaa;
|
||||||
|
$swal2-cancel-button-color: $swal2-white;
|
||||||
|
$swal2-cancel-button-font-size: 1.0625em;
|
||||||
|
|
||||||
|
// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS
|
||||||
|
$swal2-button-darken-hover: rgba($swal2-black, .1);
|
||||||
|
$swal2-button-darken-active: rgba($swal2-black, .2);
|
||||||
|
$swal2-button-focus-outline: none;
|
||||||
|
$swal2-button-focus-background-color: null;
|
||||||
|
$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
|
||||||
|
|
||||||
|
// TOASTS
|
||||||
|
$swal2-toast-show-animation: swal2-toast-show .5s;
|
||||||
|
$swal2-toast-hide-animation: swal2-toast-hide .1s forwards;
|
||||||
|
$swal2-toast-border: none;
|
||||||
|
$swal2-toast-box-shadow: 0 0 .625em #d9d9d9;
|
||||||
|
$swal2-toast-background: $swal2-white;
|
||||||
|
$swal2-toast-close-button-width: .8em;
|
||||||
|
$swal2-toast-close-button-height: .8em;
|
||||||
|
$swal2-toast-close-button-line-height: .8;
|
||||||
|
$swal2-toast-width: auto;
|
||||||
|
$swal2-toast-padding: .625em;
|
||||||
|
$swal2-toast-title-margin: 0 .6em;
|
||||||
|
$swal2-toast-title-font-size: 1em;
|
||||||
|
$swal2-toast-content-font-size: 1em;
|
||||||
|
$swal2-toast-input-font-size: 1em;
|
||||||
|
$swal2-toast-validation-font-size: 1em;
|
||||||
|
$swal2-toast-buttons-font-size: 1em;
|
||||||
|
$swal2-toast-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
|
||||||
|
$swal2-toast-footer-margin: .5em 0 0;
|
||||||
|
$swal2-toast-footer-padding: .5em 0 0;
|
||||||
|
$swal2-toast-footer-font-size: .8em;
|
||||||
|
|
||||||
|
@import "~sweetalert2/src/sweetalert2.scss";
|
109
client/src/assets/styles/vendor/_tooltip.scss
vendored
Normal file
109
client/src/assets/styles/vendor/_tooltip.scss
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
.tooltip {
|
||||||
|
display: block !important;
|
||||||
|
z-index: 10000;
|
||||||
|
|
||||||
|
.tooltip-inner {
|
||||||
|
background: darken($background-floating, $amount: 5);
|
||||||
|
color: $text-normal;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px 10px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
position: absolute;
|
||||||
|
margin: 5px;
|
||||||
|
border-color: darken($background-floating, $amount: 5);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[x-placement^="top"] {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
border-width: 5px 5px 0 5px;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
bottom: -5px;
|
||||||
|
left: calc(50% - 5px);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[x-placement^="bottom"] {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
border-width: 0 5px 5px 5px;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
top: -5px;
|
||||||
|
left: calc(50% - 5px);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[x-placement^="right"] {
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
border-width: 5px 5px 5px 0;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
left: -5px;
|
||||||
|
top: calc(50% - 5px);
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[x-placement^="left"] {
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
border-width: 5px 0 5px 5px;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
right: -5px;
|
||||||
|
top: calc(50% - 5px);
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.popover {
|
||||||
|
$color: $text-normal;
|
||||||
|
|
||||||
|
.popover-inner {
|
||||||
|
background: $color;
|
||||||
|
color: $background-floating;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 5px 30px rgba(black, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-arrow {
|
||||||
|
border-color: $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-hidden='true'] {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .15s, visibility .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-hidden='false'] {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
protected _ws?: WebSocket
|
protected _ws?: WebSocket
|
||||||
protected _peer?: RTCPeerConnection
|
protected _peer?: RTCPeerConnection
|
||||||
protected _channel?: RTCDataChannel
|
protected _channel?: RTCDataChannel
|
||||||
protected _timeout?: number
|
protected _timeout?: NodeJS.Timeout
|
||||||
protected _username?: string
|
protected _username?: string
|
||||||
protected _state: RTCIceConnectionState = 'disconnected'
|
protected _state: RTCIceConnectionState = 'disconnected'
|
||||||
|
|
||||||
@ -264,7 +264,11 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
this.onDisconnected(new Error('connection timeout'))
|
this.onDisconnected(new Error('connection timeout'))
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDisconnected(reason?: Error) {
|
protected onDisconnected(reason?: Error) {
|
||||||
|
if (this._timeout) {
|
||||||
|
clearTimeout(this._timeout)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.socketOpen) {
|
if (this.socketOpen) {
|
||||||
try {
|
try {
|
||||||
this._ws!.close()
|
this._ws!.close()
|
||||||
@ -283,14 +287,14 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
this[EVENT.DISCONNECTED](reason)
|
this[EVENT.DISCONNECTED](reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.MESSAGE](event: string, payload: any) {
|
protected [EVENT.MESSAGE](event: string, payload: any) {
|
||||||
this.emit('warn', `unhandled websocket event '${event}':`, payload)
|
this.emit('warn', `unhandled websocket event '${event}':`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract [EVENT.CONNECTING](): void
|
protected abstract [EVENT.CONNECTING](): void
|
||||||
abstract [EVENT.CONNECTED](): void
|
protected abstract [EVENT.CONNECTED](): void
|
||||||
abstract [EVENT.DISCONNECTED](reason?: Error): void
|
protected abstract [EVENT.DISCONNECTED](reason?: Error): void
|
||||||
abstract [EVENT.TRACK](event: RTCTrackEvent): void
|
protected abstract [EVENT.TRACK](event: RTCTrackEvent): void
|
||||||
abstract [EVENT.DATA](data: any): void
|
protected abstract [EVENT.DATA](data: any): void
|
||||||
abstract [EVENT.IDENTITY.PROVIDE](payload: IdentityPayload): void
|
protected abstract [EVENT.IDENTITY.PROVIDE](payload: IdentityPayload): void
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,9 @@ export const EVENT = {
|
|||||||
DATA: 'DATA',
|
DATA: 'DATA',
|
||||||
|
|
||||||
// Websocket Events
|
// Websocket Events
|
||||||
DISCONNECT: 'disconnect',
|
SYSTEM: {
|
||||||
|
DISCONNECT: 'system/disconnect',
|
||||||
|
},
|
||||||
SIGNAL: {
|
SIGNAL: {
|
||||||
ANSWER: 'signal/answer',
|
ANSWER: 'signal/answer',
|
||||||
PROVIDE: 'signal/provide',
|
PROVIDE: 'signal/provide',
|
||||||
@ -36,20 +38,37 @@ export const EVENT = {
|
|||||||
BAN: 'admin/ban',
|
BAN: 'admin/ban',
|
||||||
KICK: 'admin/kick',
|
KICK: 'admin/kick',
|
||||||
LOCK: 'admin/lock',
|
LOCK: 'admin/lock',
|
||||||
|
UNLOCK: 'admin/unlock',
|
||||||
MUTE: 'admin/mute',
|
MUTE: 'admin/mute',
|
||||||
UNMUTE: 'admin/unmute',
|
UNMUTE: 'admin/unmute',
|
||||||
FORCE: {
|
CONTROL: 'admin/control',
|
||||||
CONTROL: 'admin/force/control',
|
RELEASE: 'admin/release',
|
||||||
RELEASE: 'admin/force/release',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Events = typeof EVENT
|
export type Events = typeof EVENT
|
||||||
export type WebSocketEvents = SystemEvents | ControlEvents | IdentityEvents | MemberEvents | SignalEvents | ChatEvents
|
|
||||||
export type SystemEvents = typeof EVENT.DISCONNECT
|
export type WebSocketEvents =
|
||||||
|
| SystemEvents
|
||||||
|
| ControlEvents
|
||||||
|
| IdentityEvents
|
||||||
|
| MemberEvents
|
||||||
|
| SignalEvents
|
||||||
|
| ChatEvents
|
||||||
|
| AdminEvents
|
||||||
|
|
||||||
|
export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT
|
||||||
export type ControlEvents = typeof EVENT.CONTROL.LOCKED | typeof EVENT.CONTROL.RELEASE | typeof EVENT.CONTROL.REQUEST
|
export type ControlEvents = typeof EVENT.CONTROL.LOCKED | typeof EVENT.CONTROL.RELEASE | typeof EVENT.CONTROL.REQUEST
|
||||||
export type IdentityEvents = typeof EVENT.IDENTITY.PROVIDE | typeof EVENT.IDENTITY.DETAILS
|
export type IdentityEvents = typeof EVENT.IDENTITY.PROVIDE | typeof EVENT.IDENTITY.DETAILS
|
||||||
export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED
|
export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED
|
||||||
export type SignalEvents = typeof EVENT.SIGNAL.ANSWER | typeof EVENT.SIGNAL.PROVIDE
|
export type SignalEvents = typeof EVENT.SIGNAL.ANSWER | typeof EVENT.SIGNAL.PROVIDE
|
||||||
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOJI
|
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOJI
|
||||||
|
export type AdminEvents =
|
||||||
|
| typeof EVENT.ADMIN.BAN
|
||||||
|
| typeof EVENT.ADMIN.KICK
|
||||||
|
| typeof EVENT.ADMIN.LOCK
|
||||||
|
| typeof EVENT.ADMIN.UNLOCK
|
||||||
|
| typeof EVENT.ADMIN.MUTE
|
||||||
|
| typeof EVENT.ADMIN.UNMUTE
|
||||||
|
| typeof EVENT.ADMIN.CONTROL
|
||||||
|
| typeof EVENT.ADMIN.RELEASE
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import EventEmitter from 'eventemitter3'
|
import EventEmitter from 'eventemitter3'
|
||||||
import { BaseClient, BaseEvents } from './base'
|
import { BaseClient, BaseEvents } from './base'
|
||||||
|
import { Member } from './types'
|
||||||
import { EVENT } from './events'
|
import { EVENT } from './events'
|
||||||
import { accessor } from '~/store'
|
import { accessor } from '~/store'
|
||||||
import { IdentityPayload, MemberListPayload, MemberDisconnectPayload, MemberPayload, ControlPayload } from './messages'
|
|
||||||
|
import {
|
||||||
|
DisconnectPayload,
|
||||||
|
IdentityPayload,
|
||||||
|
MemberListPayload,
|
||||||
|
MemberDisconnectPayload,
|
||||||
|
MemberPayload,
|
||||||
|
ControlPayload,
|
||||||
|
ChatPayload,
|
||||||
|
EmojiPayload,
|
||||||
|
AdminPayload,
|
||||||
|
AdminTargetPayload,
|
||||||
|
} from './messages'
|
||||||
|
|
||||||
interface NekoEvents extends BaseEvents {}
|
interface NekoEvents extends BaseEvents {}
|
||||||
|
|
||||||
@ -26,18 +38,21 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
|||||||
super.connect(url, password, username)
|
super.connect(url, password, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get id() {
|
||||||
|
return this.$accessor.user.id
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Internal Events
|
// Internal Events
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
[EVENT.CONNECTING]() {
|
protected [EVENT.CONNECTING]() {
|
||||||
this.$accessor.setConnnecting(true)
|
this.$accessor.setConnnecting()
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.CONNECTED]() {
|
protected [EVENT.CONNECTED]() {
|
||||||
this.$accessor.setConnected(true)
|
this.$accessor.setConnected(true)
|
||||||
this.$accessor.setConnnecting(false)
|
this.$accessor.setConnected(true)
|
||||||
this.$accessor.video.clearStream()
|
|
||||||
this.$accessor.remote.clearHost()
|
|
||||||
this.$vue.$notify({
|
this.$vue.$notify({
|
||||||
group: 'neko',
|
group: 'neko',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -47,11 +62,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.DISCONNECTED](reason?: Error) {
|
protected [EVENT.DISCONNECTED](reason?: Error) {
|
||||||
this.$accessor.setConnected(false)
|
this.$accessor.setConnected(false)
|
||||||
this.$accessor.setConnnecting(false)
|
|
||||||
this.$accessor.video.clearStream()
|
this.$accessor.remote.clearHost()
|
||||||
this.$accessor.user.clearMembers()
|
this.$accessor.user.clearMembers()
|
||||||
|
this.$accessor.video.clear()
|
||||||
|
this.$accessor.chat.clear()
|
||||||
|
|
||||||
this.$vue.$notify({
|
this.$vue.$notify({
|
||||||
group: 'neko',
|
group: 'neko',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -62,61 +80,85 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.TRACK](event: RTCTrackEvent) {
|
protected [EVENT.TRACK](event: RTCTrackEvent) {
|
||||||
if (event.track.kind === 'audio') {
|
const { track, streams } = event
|
||||||
|
if (track.kind === 'audio') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$accessor.video.addStream(event.streams[0])
|
|
||||||
|
this.$accessor.video.addTrack([track, streams[0]])
|
||||||
this.$accessor.video.setStream(0)
|
this.$accessor.video.setStream(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.DATA](data: any) {}
|
protected [EVENT.DATA](data: any) {}
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// System Events
|
||||||
|
/////////////////////////////
|
||||||
|
protected [EVENT.SYSTEM.DISCONNECT]({ message }: DisconnectPayload) {
|
||||||
|
this.onDisconnected(new Error(message))
|
||||||
|
this.$vue.$swal({
|
||||||
|
title: 'Error!',
|
||||||
|
text: message,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'ok',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Identity Events
|
// Identity Events
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
[EVENT.IDENTITY.PROVIDE]({ id }: IdentityPayload) {
|
protected [EVENT.IDENTITY.PROVIDE]({ id }: IdentityPayload) {
|
||||||
this.$accessor.user.setMember(id)
|
this.$accessor.user.setMember(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Member Events
|
// Member Events
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
[EVENT.MEMBER.LIST]({ members }: MemberListPayload) {
|
protected [EVENT.MEMBER.LIST]({ members }: MemberListPayload) {
|
||||||
this.$accessor.user.setMembers(members)
|
this.$accessor.user.setMembers(members)
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.MEMBER.CONNECTED](member: MemberPayload) {
|
protected [EVENT.MEMBER.CONNECTED](member: MemberPayload) {
|
||||||
this.$accessor.user.addMember(member)
|
this.$accessor.user.addMember(member)
|
||||||
|
|
||||||
if (member.id !== this.$accessor.user.id) {
|
if (member.id !== this.id) {
|
||||||
this.$vue.$notify({
|
this.$accessor.chat.addMessage({
|
||||||
group: 'neko',
|
id: member.id,
|
||||||
type: 'info',
|
content: 'connected',
|
||||||
title: `${member.username} connected`,
|
type: 'event',
|
||||||
duration: 5000,
|
created: new Date(),
|
||||||
speed: 1000,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.MEMBER.DISCONNECTED]({ id }: MemberDisconnectPayload) {
|
protected [EVENT.MEMBER.DISCONNECTED]({ id }: MemberDisconnectPayload) {
|
||||||
this.$vue.$notify({
|
const member = this.member(id)
|
||||||
group: 'neko',
|
if (!member) {
|
||||||
type: 'info',
|
return
|
||||||
title: `${this.$accessor.user.members[id].username} disconnected`,
|
}
|
||||||
duration: 5000,
|
|
||||||
speed: 1000,
|
this.$accessor.chat.addMessage({
|
||||||
|
id: member.id,
|
||||||
|
content: 'disconnected',
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$accessor.user.delMember(id)
|
this.$accessor.user.delMember(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Control Events
|
// Control Events
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
[EVENT.CONTROL.LOCKED]({ id }: ControlPayload) {
|
protected [EVENT.CONTROL.LOCKED]({ id }: ControlPayload) {
|
||||||
this.$accessor.remote.setHost(id)
|
this.$accessor.remote.setHost(id)
|
||||||
if (this.$accessor.user.id === id) {
|
const member = this.member(id)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.id === id) {
|
||||||
this.$vue.$notify({
|
this.$vue.$notify({
|
||||||
group: 'neko',
|
group: 'neko',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -125,19 +167,23 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
|||||||
speed: 1000,
|
speed: 1000,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$vue.$notify({
|
this.$accessor.chat.addMessage({
|
||||||
group: 'neko',
|
id: member.id,
|
||||||
type: 'info',
|
content: 'took the controls',
|
||||||
title: `${this.$accessor.user.members[id].username} took the controls`,
|
type: 'event',
|
||||||
duration: 5000,
|
created: new Date(),
|
||||||
speed: 1000,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.CONTROL.RELEASE]({ id }: ControlPayload) {
|
protected [EVENT.CONTROL.RELEASE]({ id }: ControlPayload) {
|
||||||
this.$accessor.remote.clearHost()
|
this.$accessor.remote.clearHost()
|
||||||
if (this.$accessor.user.id === id) {
|
const member = this.member(id)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.id === id) {
|
||||||
this.$vue.$notify({
|
this.$vue.$notify({
|
||||||
group: 'neko',
|
group: 'neko',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -146,34 +192,212 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
|||||||
speed: 1000,
|
speed: 1000,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$vue.$notify({
|
this.$accessor.chat.addMessage({
|
||||||
group: 'neko',
|
id: member.id,
|
||||||
type: 'info',
|
content: 'released the controls',
|
||||||
title: `The controls released from ${this.$accessor.user.members[id].username}`,
|
type: 'event',
|
||||||
duration: 5000,
|
created: new Date(),
|
||||||
speed: 1000,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.CONTROL.REQUEST]({ id }: ControlPayload) {
|
protected [EVENT.CONTROL.REQUEST]({ id }: ControlPayload) {
|
||||||
|
const member = this.member(id)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$vue.$notify({
|
this.$vue.$notify({
|
||||||
group: 'neko',
|
group: 'neko',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: `${this.$accessor.user.members[id].username} has the controls`,
|
title: `${member.username} has the controls`,
|
||||||
text: 'But I let them know you wanted it',
|
text: 'But I let them know you wanted it',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
speed: 1000,
|
speed: 1000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
[EVENT.CONTROL.REQUESTING]({ id }: ControlPayload) {
|
protected [EVENT.CONTROL.REQUESTING]({ id }: ControlPayload) {
|
||||||
|
const member = this.member(id)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$vue.$notify({
|
this.$vue.$notify({
|
||||||
group: 'neko',
|
group: 'neko',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: `${this.$accessor.user.members[id].username} is requesting the controls`,
|
title: `${member.username} is requesting the controls`,
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
speed: 1000,
|
speed: 1000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// Chat Events
|
||||||
|
/////////////////////////////
|
||||||
|
protected [EVENT.CHAT.MESSAGE]({ id, content }: ChatPayload) {
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
type: 'text',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.CHAT.EMOJI]({ id, emoji }: EmojiPayload) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// Admin Events
|
||||||
|
/////////////////////////////
|
||||||
|
protected [EVENT.ADMIN.BAN]({ id, target }: AdminTargetPayload) {
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = this.member(target)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `banned ${member.username}`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.KICK]({ id, target }: AdminTargetPayload) {
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = this.member(target)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `kicked ${member.username}`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.MUTE]({ id, target }: AdminTargetPayload) {
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.user.setMuted({ id: target, muted: true })
|
||||||
|
|
||||||
|
const member = this.member(target)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `muted ${member.username}`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.UNMUTE]({ id, target }: AdminTargetPayload) {
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.user.setMuted({ id: target, muted: false })
|
||||||
|
|
||||||
|
const member = this.member(target)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `unmuted ${member.username}`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.LOCK]({ id }: AdminPayload) {
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `locked the room`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.UNLOCK]({ id }: AdminPayload) {
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `unlocked the room`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.CONTROL]({ id, target }: AdminTargetPayload) {
|
||||||
|
this.$accessor.remote.setHost(id)
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `force took the controls`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = this.member(target)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `took the controls from ${member.username}`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected [EVENT.ADMIN.RELEASE]({ id, target }: AdminTargetPayload) {
|
||||||
|
this.$accessor.remote.clearHost()
|
||||||
|
if (!target) {
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `force released the controls`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = this.member(target)
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.addMessage({
|
||||||
|
id,
|
||||||
|
content: `released the controls from ${member.username}`,
|
||||||
|
type: 'event',
|
||||||
|
created: new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
protected member(id: string): Member | undefined {
|
||||||
|
return this.$accessor.user.members[id]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,25 @@ export type WebSocketPayloads =
|
|||||||
| Member
|
| Member
|
||||||
| ControlPayload
|
| ControlPayload
|
||||||
| ChatPayload
|
| ChatPayload
|
||||||
|
| ChatSendPayload
|
||||||
|
| EmojiSendPayload
|
||||||
|
| AdminPayload
|
||||||
|
|
||||||
export interface WebSocketMessage {
|
export interface WebSocketMessage {
|
||||||
event: WebSocketEvents | string
|
event: WebSocketEvents | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SYSTEM MESSAGES/PAYLOADS
|
||||||
|
*/
|
||||||
|
// system/disconnect
|
||||||
|
export interface DisconnectMessage extends WebSocketMessage, DisconnectPayload {
|
||||||
|
event: typeof EVENT.SYSTEM.DISCONNECT
|
||||||
|
}
|
||||||
|
export interface DisconnectPayload {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
IDENTITY MESSAGES/PAYLOADS
|
IDENTITY MESSAGES/PAYLOADS
|
||||||
*/
|
*/
|
||||||
@ -84,12 +98,49 @@ export interface ControlPayload {
|
|||||||
/*
|
/*
|
||||||
CHAT PAYLOADS
|
CHAT PAYLOADS
|
||||||
*/
|
*/
|
||||||
// chat/send & chat/receive
|
// chat/message
|
||||||
export interface ChatMessage extends WebSocketMessage, ChatPayload {
|
export interface ChatMessage extends WebSocketMessage, ChatPayload {
|
||||||
event: typeof EVENT.CHAT.SEND | typeof EVENT.CHAT.RECEIVE
|
event: typeof EVENT.CHAT.MESSAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatSendPayload {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
export interface ChatPayload {
|
export interface ChatPayload {
|
||||||
id: string
|
id: string
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chat/emoji
|
||||||
|
export interface ChatEmojiMessage extends WebSocketMessage, EmojiPayload {
|
||||||
|
event: typeof EVENT.CHAT.EMOJI
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiPayload {
|
||||||
|
id: string
|
||||||
|
emoji: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiSendPayload {
|
||||||
|
emoji: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ADMIN PAYLOADS
|
||||||
|
*/
|
||||||
|
export interface AdminMessage extends WebSocketMessage, AdminPayload {
|
||||||
|
event: typeof EVENT.MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPayload {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminTargetMessage extends WebSocketMessage, AdminTargetPayload {
|
||||||
|
event: typeof EVENT.CHAT.EMOJI
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminTargetPayload {
|
||||||
|
id: string
|
||||||
|
target?: string
|
||||||
|
}
|
||||||
|
@ -2,4 +2,6 @@ export interface Member {
|
|||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
admin: boolean
|
admin: boolean
|
||||||
|
muted: boolean
|
||||||
|
connected?: boolean
|
||||||
}
|
}
|
||||||
|
170
client/src/components/about.vue
Normal file
170
client/src/components/about.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about" @click.stop.prevent="toggle">
|
||||||
|
<div class="window">
|
||||||
|
<div class="loading" v-if="loading">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="@/assets/logo.svg" alt="n.eko" />
|
||||||
|
<span><b>N</b>.EKO</span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="bounce1"></div>
|
||||||
|
<div class="bounce2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="markdown-body" v-if="!loading" v-html="about"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.about {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba($color: $background-floating, $alpha: 0.8);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.window {
|
||||||
|
max-width: 70vw;
|
||||||
|
background: $background-secondary;
|
||||||
|
border-radius: 5px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $background-tertiary;
|
||||||
|
border: 2px solid $background-primary;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: $background-floating;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 40px 80px 0 80px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 90px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 56px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto 20px auto;
|
||||||
|
|
||||||
|
.bounce1,
|
||||||
|
.bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $style-primary;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
-webkit-animation: bounce 2s infinite ease-in-out;
|
||||||
|
animation: bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce2 {
|
||||||
|
-webkit-animation-delay: -1s;
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
margin: 50px 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
import md, { HtmlOutputRule } from 'simple-markdown'
|
||||||
|
|
||||||
|
@Component({ name: 'neko-about' })
|
||||||
|
export default class extends Vue {
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
get about() {
|
||||||
|
return this.$accessor.client.about_page
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (this.about === '') {
|
||||||
|
this.loading = true
|
||||||
|
this.$http
|
||||||
|
.get<string>('https://raw.githubusercontent.com/nurdism/neko/master/README.md')
|
||||||
|
.then(res => {
|
||||||
|
return this.$http.post('https://api.github.com/markdown', {
|
||||||
|
text: res.data,
|
||||||
|
mode: 'gfm',
|
||||||
|
context: 'github/gollum',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.$accessor.client.setAbout(res.data)
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.$accessor.client.toggleAbout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
416
client/src/components/chat.vue
Normal file
416
client/src/components/chat.vue
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat">
|
||||||
|
<ul class="chat-history" ref="history" @click="onClick">
|
||||||
|
<template v-for="(message, index) in history">
|
||||||
|
<li :key="index" class="message" v-if="message.type === 'text'">
|
||||||
|
<div class="author">
|
||||||
|
<img :src="`https://api.adorable.io/avatars/40/${member(message.id).username}.png`" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="content-head">
|
||||||
|
<span>{{ member(message.id).username }}</span>
|
||||||
|
<span class="timestamp">{{ timestamp(message.created) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<neko-markdown :source="message.content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li :key="index" class="event" v-if="message.type === 'event'">
|
||||||
|
<span
|
||||||
|
v-tooltip="{
|
||||||
|
content: `${member(message.id).username} ${message.content}`,
|
||||||
|
placement: 'left',
|
||||||
|
offset: 3,
|
||||||
|
boundariesElement: 'body',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<strong>{{ member(message.id).username }}</strong>
|
||||||
|
{{ message.content }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
<div class="chat-send">
|
||||||
|
<div class="accent" />
|
||||||
|
<div class="text-container">
|
||||||
|
<textarea placeholder="Send a message" @keydown="onKeyDown" v-model="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chat {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.chat-history {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: $background-tertiary transparent;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $background-tertiary;
|
||||||
|
border: 2px solid $background-primary;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: $background-floating;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep *::selection {
|
||||||
|
background: $text-link;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
flex: 1;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 10px 10px 0px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: contain;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&.message {
|
||||||
|
.author {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $style-primary;
|
||||||
|
margin: 5px 10px 10px 0px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.content-head {
|
||||||
|
span {
|
||||||
|
color: $text-normal;
|
||||||
|
font-weight: 500;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $text-muted;
|
||||||
|
float: left;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::v-deep .content-body {
|
||||||
|
display: flex;
|
||||||
|
color: $text-normal;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
* {
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-width: 225px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $text-link;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px $background-accent solid;
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler {
|
||||||
|
background: $background-tertiary;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $background-secondary;
|
||||||
|
cursor: default;
|
||||||
|
span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: Consolas, Andale Mono WT, Andale Mono, Lucida Console, Lucida Sans Typewriter,
|
||||||
|
DejaVu Sans Mono, Bitstream Vera Sans Mono, Liberation Mono, Nimbus Mono L, Monaco, Courier New,
|
||||||
|
Courier, monospace;
|
||||||
|
background: $background-secondary;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 3px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
text-indent: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: $interactive-normal;
|
||||||
|
border: 1px solid $background-tertiary;
|
||||||
|
background: $background-secondary;
|
||||||
|
padding: 8px 6px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
code {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.event {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
height: 15px;
|
||||||
|
color: $text-muted;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 250px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 15px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
float: right;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 80px;
|
||||||
|
max-height: 80px;
|
||||||
|
padding: 0 10px 10px 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.accent {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba($color: #fff, $alpha: 0.05);
|
||||||
|
margin: 5px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
flex: 1;
|
||||||
|
font-family: $text-family;
|
||||||
|
border: none;
|
||||||
|
caret-color: $text-normal;
|
||||||
|
color: $text-normal;
|
||||||
|
resize: none;
|
||||||
|
margin: 5px;
|
||||||
|
background-color: transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: $background-tertiary transparent;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $background-tertiary;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: $background-floating;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::selection {
|
||||||
|
background: $text-link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
import { formatRelative } from 'date-fns'
|
||||||
|
|
||||||
|
import Markdown from './markdown'
|
||||||
|
|
||||||
|
const length = 512 // max length of message
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'neko-chat',
|
||||||
|
components: {
|
||||||
|
'neko-markdown': Markdown,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class extends Vue {
|
||||||
|
@Ref('history') readonly _history!: HTMLElement
|
||||||
|
|
||||||
|
_content = ''
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.$accessor.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
get history() {
|
||||||
|
return this.$accessor.chat.history
|
||||||
|
}
|
||||||
|
|
||||||
|
get content() {
|
||||||
|
return this._content
|
||||||
|
}
|
||||||
|
|
||||||
|
set content(text: string) {
|
||||||
|
if (text.length > length) {
|
||||||
|
this._content = text.substring(0, length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._content = text
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('history')
|
||||||
|
onHistroyChange() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this._history.scrollTop = this._history.scrollHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this._history.scrollTop = this._history.scrollHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
member(id: string) {
|
||||||
|
return this.$accessor.user.members[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp(time: Date) {
|
||||||
|
return formatRelative(time, new Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(event: { target?: HTMLElement; preventDefault(): void }) {
|
||||||
|
const { target } = event
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.tagName.toLowerCase() === 'span' && target.classList.contains('spoiler')) {
|
||||||
|
target.classList.add('active')
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.parentElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.parentElement.tagName.toLowerCase() === 'span' && target.parentElement.classList.contains('spoiler')) {
|
||||||
|
target.parentElement.classList.add('active')
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (typeof this._content === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._content.length == length) {
|
||||||
|
if (
|
||||||
|
[8, 16, 17, 18, 20, 33, 34, 35, 36, 37, 38, 39, 40, 45, 46, 91, 93, 144].includes(event.keyCode) ||
|
||||||
|
(event.ctrlKey && [67, 65, 88].includes(event.keyCode))
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode !== 13 || event.shiftKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._content === '') {
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$accessor.chat.sendMessage(this._content)
|
||||||
|
|
||||||
|
this._content = ''
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
163
client/src/components/connect.vue
Normal file
163
client/src/components/connect.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div class="connect">
|
||||||
|
<div class="window">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="@/assets/logo.svg" alt="n.eko" />
|
||||||
|
<span><b>n</b>.eko</span>
|
||||||
|
</div>
|
||||||
|
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
|
||||||
|
<span>Please enter the password:</span>
|
||||||
|
<input type="text" placeholder="Username" v-model="username" />
|
||||||
|
<input type="password" placeholder="Password" v-model="password" />
|
||||||
|
<button type="submit" @click.stop.prevent="connect">
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="loader" v-if="connecting">
|
||||||
|
<div class="bounce1"></div>
|
||||||
|
<div class="bounce2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.connect {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba($color: $background-floating, $alpha: 0.8);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.window {
|
||||||
|
width: 300px;
|
||||||
|
background: $background-secondary;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 90px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 56px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
padding: 6px 8px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: $background-tertiary;
|
||||||
|
color: $text-normal;
|
||||||
|
|
||||||
|
&::selection {
|
||||||
|
background: $text-link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
background: $style-primary;
|
||||||
|
color: $text-normal;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 30px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.bounce1,
|
||||||
|
.bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $style-primary;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
-webkit-animation: bounce 2s infinite ease-in-out;
|
||||||
|
animation: bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce2 {
|
||||||
|
-webkit-animation-delay: -1s;
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
@Component({ name: 'neko-connect' })
|
||||||
|
export default class extends Vue {
|
||||||
|
private username = ''
|
||||||
|
private password = ''
|
||||||
|
|
||||||
|
get connecting() {
|
||||||
|
return this.$accessor.connecting
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const { username, password } = this
|
||||||
|
this.$accessor.connect({ username, password })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
163
client/src/components/controls.vue
Normal file
163
client/src/components/controls.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
hosted && !hosting ? 'disabled' : '',
|
||||||
|
!hosted && !hosting ? 'faded' : '',
|
||||||
|
'fas',
|
||||||
|
'fa-keyboard',
|
||||||
|
'request',
|
||||||
|
]"
|
||||||
|
@click.stop.prevent="toggleControl"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i
|
||||||
|
:class="[{ disabled: !playable }, playing ? 'fa-pause-circle' : 'fa-play-circle', 'fas', 'play']"
|
||||||
|
@click.stop.prevent="toggleMedia"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="volume">
|
||||||
|
<i
|
||||||
|
:class="[volume === 0 || muted ? 'fa-volume-mute' : 'fa-volume-up', 'fas']"
|
||||||
|
@click.stop.prevent="toggleMute"
|
||||||
|
/>
|
||||||
|
<input type="range" min="0" max="100" v-model="volume" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
i {
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
&.faded {
|
||||||
|
color: rgba($color: $text-normal, $alpha: 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: rgba($color: $style-error, $alpha: 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
input[type='range'] {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
width: 200px;
|
||||||
|
height: 20px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $interactive-active;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: $style-primary;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $interactive-active;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: $style-primary;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
@Component({ name: 'neko-controls' })
|
||||||
|
export default class extends Vue {
|
||||||
|
get hosted() {
|
||||||
|
return this.$accessor.remote.hosted
|
||||||
|
}
|
||||||
|
|
||||||
|
get hosting() {
|
||||||
|
return this.$accessor.remote.hosting
|
||||||
|
}
|
||||||
|
|
||||||
|
get volume() {
|
||||||
|
return this.$accessor.video.volume
|
||||||
|
}
|
||||||
|
|
||||||
|
set volume(volume: number) {
|
||||||
|
this.$accessor.video.setVolume(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
get muted() {
|
||||||
|
return this.$accessor.video.muted || this.volume === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get playing() {
|
||||||
|
return this.$accessor.video.playing
|
||||||
|
}
|
||||||
|
|
||||||
|
get playable() {
|
||||||
|
return this.$accessor.video.playable
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleControl() {
|
||||||
|
if (!this.playable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$accessor.remote.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMedia() {
|
||||||
|
if (!this.playable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$accessor.video.togglePlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMute() {
|
||||||
|
this.$accessor.video.toggleMute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
48
client/src/components/emoji.vue
Normal file
48
client/src/components/emoji.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="emoji">
|
||||||
|
<ul>
|
||||||
|
<li><i @click.stop.prevent="click('heart')" class="fas fa-heart"></i></li>
|
||||||
|
<li><i @click.stop.prevent="click('poo')" class="fas fa-poo"></i></li>
|
||||||
|
<li><i @click.stop.prevent="click('grin')" class="fas fa-grin-tears"></i></li>
|
||||||
|
<li><i @click.stop.prevent="click('dizzy')" class="fas fa-dizzy"></i></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.emoji {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 5px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'neko-emoji',
|
||||||
|
})
|
||||||
|
export default class extends Vue {
|
||||||
|
click(emoji: string) {
|
||||||
|
this.$swal({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'This feature is not available yet',
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'Cool',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
282
client/src/components/markdown.ts
Normal file
282
client/src/components/markdown.ts
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown'
|
||||||
|
import { Component, Watch, Vue, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
const {
|
||||||
|
blockQuote,
|
||||||
|
inlineCode,
|
||||||
|
codeBlock,
|
||||||
|
autolink,
|
||||||
|
newline,
|
||||||
|
escape,
|
||||||
|
strong,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
url,
|
||||||
|
em,
|
||||||
|
u,
|
||||||
|
br,
|
||||||
|
} = defaultRules
|
||||||
|
|
||||||
|
type Rule = SingleNodeParserRule & HtmlOutputRule
|
||||||
|
|
||||||
|
interface MarkdownRules extends Rules<HtmlOutputRule> {
|
||||||
|
inlineCode: Rule
|
||||||
|
newline: Rule
|
||||||
|
escape: Rule
|
||||||
|
strong: Rule
|
||||||
|
em: Rule
|
||||||
|
u: Rule
|
||||||
|
blockQuote: Rule
|
||||||
|
codeBlock: Rule
|
||||||
|
autolink: Rule
|
||||||
|
url: Rule
|
||||||
|
strike: Rule
|
||||||
|
text: Rule
|
||||||
|
br: Rule
|
||||||
|
emoticon: Rule
|
||||||
|
spoiler: Rule
|
||||||
|
user: Rule
|
||||||
|
channel: Rule
|
||||||
|
role: Rule
|
||||||
|
emoji: Rule
|
||||||
|
everyone: Rule
|
||||||
|
here: Rule
|
||||||
|
link?: Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HTMLAttributes {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarkdownState extends State {}
|
||||||
|
|
||||||
|
function htmlTag(
|
||||||
|
tagName: string,
|
||||||
|
content: string,
|
||||||
|
attributes: HTMLAttributes,
|
||||||
|
state: State = {},
|
||||||
|
isClosed: boolean = true,
|
||||||
|
) {
|
||||||
|
if (!attributes) {
|
||||||
|
attributes = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.class && state.cssModuleNames) {
|
||||||
|
attributes.class = attributes.class
|
||||||
|
.split(' ')
|
||||||
|
.map(cl => state.cssModuleNames[cl] || cl)
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributeString = ''
|
||||||
|
for (const attr in attributes) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(attributes, attr) && attributes[attr]) {
|
||||||
|
attributeString += ` ${md.sanitizeText(attr)}="${md.sanitizeText(attributes[attr])}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unclosedTag = `<${tagName}${attributeString}>`
|
||||||
|
if (isClosed) {
|
||||||
|
return `${unclosedTag}${content}</${tagName}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return unclosedTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const rules: MarkdownRules = {
|
||||||
|
inlineCode,
|
||||||
|
newline,
|
||||||
|
escape,
|
||||||
|
strong,
|
||||||
|
em,
|
||||||
|
u,
|
||||||
|
link,
|
||||||
|
codeBlock: {
|
||||||
|
...codeBlock,
|
||||||
|
match: md.inlineRegex(/^```(([a-z0-9-]+?)\n+)?\n*([^]+?)\n*```/i),
|
||||||
|
parse(capture, parse, state) {
|
||||||
|
return {
|
||||||
|
lang: (capture[2] || '').trim(),
|
||||||
|
content: capture[3] || '',
|
||||||
|
inQuote: state.inQuote || false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return htmlTag('pre', htmlTag('code', node.content, state), {}, state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blockQuote: {
|
||||||
|
...blockQuote,
|
||||||
|
match(source, state, prevSource) {
|
||||||
|
return !/^$|\n *$/.test(prevSource) || state.inQuote
|
||||||
|
? null
|
||||||
|
: /^( *>>> ([\s\S]*))|^( *> [^\n]+(\n *> [^\n]+)*\n?)/.exec(source)
|
||||||
|
},
|
||||||
|
parse(capture, parse, state) {
|
||||||
|
const all = capture[0]
|
||||||
|
const isBlock = Boolean(/^ *>>> ?/.exec(all))
|
||||||
|
const removeSyntaxRegex = isBlock ? /^ *>>> ?/ : /^ *> ?/gm
|
||||||
|
const content = all.replace(removeSyntaxRegex, '')
|
||||||
|
|
||||||
|
state.inQuote = true
|
||||||
|
if (!isBlock) {
|
||||||
|
state.inline = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parse(content, state)
|
||||||
|
|
||||||
|
state.inQuote = state.inQuote || false
|
||||||
|
state.inline = state.inline || false
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: parsed,
|
||||||
|
type: 'blockQuote',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
autolink: {
|
||||||
|
...autolink,
|
||||||
|
parse(capture) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
content: capture[1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
target: capture[1],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return htmlTag(
|
||||||
|
'a',
|
||||||
|
output(node.content, state),
|
||||||
|
{ href: md.sanitizeUrl(node.target) as string, target: '_blank' },
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
...url,
|
||||||
|
parse(capture) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
content: capture[1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
target: capture[1],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return htmlTag(
|
||||||
|
'a',
|
||||||
|
output(node.content, state),
|
||||||
|
{ href: md.sanitizeUrl(node.target) as string, target: '_blank' },
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
strike: {
|
||||||
|
order: md.defaultRules.text.order,
|
||||||
|
match: md.inlineRegex(/^~~([\s\S]+?)~~(?!_)/),
|
||||||
|
parse(capture) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
content: capture[1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
target: capture[1],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return htmlTag('s', output(node.content, state), {}, state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
...text,
|
||||||
|
match: source => /^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff-]|\n\n|\n|\w+:\S|$)/.exec(source),
|
||||||
|
html(node, output, state) {
|
||||||
|
if (state.escapeHTML) {
|
||||||
|
return htmlTag('span', md.sanitizeText(node.content), {}, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlTag('span', node.content, {}, state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
br: {
|
||||||
|
...br,
|
||||||
|
match: md.anyScopeRegex(/^\n/),
|
||||||
|
},
|
||||||
|
emoticon: {
|
||||||
|
order: md.defaultRules.text.order,
|
||||||
|
match: source => /^(¯\\_\(ツ\)_\/¯)/.exec(source),
|
||||||
|
parse(capture) {
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
content: capture[1],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return output(node.content, state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spoiler: {
|
||||||
|
order: 0,
|
||||||
|
match: source => /^\|\|([\s\S]+?)\|\|/.exec(source),
|
||||||
|
parse(capture, parse, state) {
|
||||||
|
return {
|
||||||
|
content: parse(capture[1], state),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return htmlTag('span', output(node.content, state), { class: 'spoiler' }, state) //htmlTag('span', , { }, state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = md.parserFor(rules)
|
||||||
|
const htmlOutput = md.outputFor<HtmlOutputRule, 'html'>(rules, 'html')
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'neko-markdown',
|
||||||
|
})
|
||||||
|
export default class extends Vue {
|
||||||
|
html = ''
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
source!: string
|
||||||
|
|
||||||
|
@Watch('source')
|
||||||
|
onSourceChanged(source: string) {
|
||||||
|
const state: MarkdownState = {
|
||||||
|
inline: true,
|
||||||
|
inQuote: false,
|
||||||
|
escapeHTML: true,
|
||||||
|
cssModuleNames: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.html = htmlOutput(parser(source, state), state)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(createElement: any) {
|
||||||
|
const state: MarkdownState = {
|
||||||
|
inline: true,
|
||||||
|
inQuote: false,
|
||||||
|
escapeHTML: true,
|
||||||
|
cssModuleNames: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.html = htmlOutput(parser(this.source, state), state)
|
||||||
|
|
||||||
|
return createElement('div', {
|
||||||
|
domProps: {
|
||||||
|
innerHTML: this.html,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
354
client/src/components/members.vue
Normal file
354
client/src/components/members.vue
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
<template>
|
||||||
|
<div class="members">
|
||||||
|
<div class="members-container">
|
||||||
|
<ul class="members-list">
|
||||||
|
<li v-if="member">
|
||||||
|
<div :class="[{ host: member.id === host }, 'self', 'member']">
|
||||||
|
<img :src="`https://api.adorable.io/avatars/50/${member.username}.png`" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<template v-for="(member, index) in members">
|
||||||
|
<li
|
||||||
|
v-if="member.id !== id && member.connected"
|
||||||
|
:key="index"
|
||||||
|
v-tooltip="{ content: member.username, placement: 'top', offset: 5, boundariesElement: 'body' }"
|
||||||
|
>
|
||||||
|
<div :class="[{ host: member.id === host, admin: member.admin }, 'member']">
|
||||||
|
<img
|
||||||
|
:src="`https://api.adorable.io/avatars/50/${member.username}.png`"
|
||||||
|
@contextmenu="context($event, { member, index })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<vue-context class="context" ref="menu">
|
||||||
|
<template slot-scope="child" v-if="child.data && admin">
|
||||||
|
<li>
|
||||||
|
<strong>{{ child.data.member.username }}</strong>
|
||||||
|
</li>
|
||||||
|
<li class="seperator" />
|
||||||
|
<li>
|
||||||
|
<span @click="mute(child.data.member)" v-if="!child.data.member.muted">Mute</span>
|
||||||
|
<span @click="unmute(child.data.member)" v-else>Unmute</span>
|
||||||
|
</li>
|
||||||
|
<template v-if="child.data.member.id === host">
|
||||||
|
<li>
|
||||||
|
<span @click="release(child.data.member)">Release Controls</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span @click="control(child.data.member)">Take Controls</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li class="seperator" />
|
||||||
|
<li>
|
||||||
|
<span @click="kick(child.data.member)" style="color: #f04747">Kick</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span @click="ban(child.data.member)" style="color: #f04747">Ban</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</vue-context>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.members {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: $background-secondary $background-tertiary;
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: $background-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $background-secondary;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: $background-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-container {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
padding: 0 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.members-list {
|
||||||
|
white-space: nowrap;
|
||||||
|
clear: both;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.member {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 10px 5px 0 5px;
|
||||||
|
|
||||||
|
&.self {
|
||||||
|
&::before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
content: '\f2bd';
|
||||||
|
background: $background-floating;
|
||||||
|
color: $style-primary;
|
||||||
|
position: absolute;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-left: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.admin {
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
content: '\f3ed';
|
||||||
|
color: $style-primary;
|
||||||
|
background: transparent;
|
||||||
|
position: absolute;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-left: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.host::after {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
content: '\f521';
|
||||||
|
background: $style-primary;
|
||||||
|
color: $background-floating;
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 42px;
|
||||||
|
margin-left: -18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: ' ';
|
||||||
|
height: 45px;
|
||||||
|
width: 2px;
|
||||||
|
background: $background-secondary;
|
||||||
|
margin-top: 13px;
|
||||||
|
margin-left: -9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
background-color: $background-floating;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
min-width: 150px;
|
||||||
|
z-index: 1500;
|
||||||
|
position: fixed;
|
||||||
|
list-style: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: calc(100% - 50px);
|
||||||
|
overflow-y: auto;
|
||||||
|
color: $interactive-normal;
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: $elevation-high;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.seperator {
|
||||||
|
height: 1px;
|
||||||
|
background: $background-secondary;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> strong {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 5px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: 5px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: $background-modifier-hover;
|
||||||
|
color: $interactive-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
import { Member } from '~/client/types'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { VueContext } from 'vue-context'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'neko-members',
|
||||||
|
components: {
|
||||||
|
'vue-context': VueContext,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class extends Vue {
|
||||||
|
@Ref('menu') readonly menu!: any
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.$accessor.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
get admin() {
|
||||||
|
return this.$accessor.user.admin
|
||||||
|
}
|
||||||
|
|
||||||
|
get host() {
|
||||||
|
return this.$accessor.remote.id
|
||||||
|
}
|
||||||
|
|
||||||
|
get member() {
|
||||||
|
return this.$accessor.user.member
|
||||||
|
}
|
||||||
|
|
||||||
|
get members() {
|
||||||
|
return this.$accessor.user.members
|
||||||
|
}
|
||||||
|
|
||||||
|
context(event: MouseEvent, data: any) {
|
||||||
|
if (this.admin) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.menu.open(event, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kick(member: Member) {
|
||||||
|
this.$swal({
|
||||||
|
title: `Kick ${member.username}?`,
|
||||||
|
text: `Are you sure you want to kick ${member.username}?`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Yes',
|
||||||
|
}).then(({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
this.$accessor.user.kick(member)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ban(member: Member) {
|
||||||
|
this.$swal({
|
||||||
|
title: `Ban ${member.username}?`,
|
||||||
|
text: `Are you sure you want to ban ${member.username}? You will need to restart the server to undo this.`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Yes',
|
||||||
|
}).then(({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
this.$accessor.user.ban(member)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mute(member: Member) {
|
||||||
|
this.$swal({
|
||||||
|
title: `Mute ${member.username}?`,
|
||||||
|
text: `Are you sure you want to mute ${member.username}?`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Yes',
|
||||||
|
}).then(({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
this.$accessor.user.mute(member)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unmute(member: Member) {
|
||||||
|
this.$swal({
|
||||||
|
title: `Unmute ${member.username}?`,
|
||||||
|
text: `Are you sure you want to unmute ${member.username}?`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Yes',
|
||||||
|
}).then(({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
this.$accessor.user.unmute(member)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
release(member: Member) {
|
||||||
|
this.$accessor.remote.adminRelease()
|
||||||
|
}
|
||||||
|
|
||||||
|
control(member: Member) {
|
||||||
|
this.$accessor.remote.adminControl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
46
client/src/components/menu.vue
Normal file
46
client/src/components/menu.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li><i @click.stop.prevent="about" class="fas fa-question-circle" /></li>
|
||||||
|
<li>
|
||||||
|
<i
|
||||||
|
class="fas fa-shield-alt"
|
||||||
|
v-tooltip="{
|
||||||
|
content: 'You are logged in as an admin',
|
||||||
|
placement: 'right',
|
||||||
|
offset: 5,
|
||||||
|
boundariesElement: 'body',
|
||||||
|
}"
|
||||||
|
v-if="admin"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
@Component({ name: 'neko-menu' })
|
||||||
|
export default class extends Vue {
|
||||||
|
get admin() {
|
||||||
|
return this.$accessor.user.admin
|
||||||
|
}
|
||||||
|
|
||||||
|
about() {
|
||||||
|
this.$accessor.client.toggleAbout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
12
client/src/components/settings.vue
Normal file
12
client/src/components/settings.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
@Component({ name: 'neko-settings' })
|
||||||
|
export default class extends Vue {}
|
||||||
|
</script>
|
97
client/src/components/side.vue
Normal file
97
client/src/components/side.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<aside class="neko-menu">
|
||||||
|
<div class="tabs-container">
|
||||||
|
<ul>
|
||||||
|
<li :class="{ active: tab === 'chat' }" @click.stop.prevent="change('chat')">
|
||||||
|
<i class="fas fa-comment-alt" />
|
||||||
|
<span>Chat</span>
|
||||||
|
</li>
|
||||||
|
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
|
||||||
|
<i class="fas fa-sliders-h" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="page-container">
|
||||||
|
<neko-chat v-if="tab === 'chat'" />
|
||||||
|
<neko-settings v-if="tab === 'settings'" />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.neko-menu {
|
||||||
|
width: $side-width;
|
||||||
|
background-color: $background-primary;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
background: $background-tertiary;
|
||||||
|
height: $menu-height;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 16px 0 0 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
background: $background-secondary;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
border-bottom: none;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $background-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
max-height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
import Settings from '~/components/settings.vue'
|
||||||
|
import Chat from '~/components/chat.vue'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
name: 'neko',
|
||||||
|
components: {
|
||||||
|
'neko-settings': Settings,
|
||||||
|
'neko-chat': Chat,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class extends Vue {
|
||||||
|
get tab() {
|
||||||
|
return this.$accessor.client.tab
|
||||||
|
}
|
||||||
|
|
||||||
|
change(tab: string) {
|
||||||
|
this.$accessor.client.setTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
364
client/src/components/video.vue
Normal file
364
client/src/components/video.vue
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="component" class="video">
|
||||||
|
<div ref="player" class="player">
|
||||||
|
<div ref="container" class="player-container">
|
||||||
|
<video ref="video" />
|
||||||
|
<div
|
||||||
|
ref="overlay"
|
||||||
|
class="overlay"
|
||||||
|
tabindex="0"
|
||||||
|
@click.stop.prevent
|
||||||
|
@contextmenu.stop.prevent
|
||||||
|
@wheel.stop.prevent="onWheel"
|
||||||
|
@mousemove.stop.prevent="onMouseMove"
|
||||||
|
@mousedown.stop.prevent="onMouseDown"
|
||||||
|
@mouseup.stop.prevent="onMouseUp"
|
||||||
|
@mouseenter.stop.prevent="onMouseEnter"
|
||||||
|
@mouseleave.stop.prevent="onMouseLeave"
|
||||||
|
@keydown.stop.prevent="onKeyDown"
|
||||||
|
@keyup.stop.prevent="onKeyUp"
|
||||||
|
/>
|
||||||
|
<div v-if="!playing" class="player-overlay">
|
||||||
|
<i @click.stop.prevent="toggle" v-if="playable" class="fas fa-play-circle" />
|
||||||
|
</div>
|
||||||
|
<div ref="aspect" class="player-aspect" />
|
||||||
|
</div>
|
||||||
|
<i v-if="!fullscreen" @click.stop.prevent="requestFullscreen" class="expand fas fa-expand"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.player {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.expand {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 15px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background: rgba($color: #fff, $alpha: 0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba($color: #fff, $alpha: 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 16 / 9 * 100vh;
|
||||||
|
|
||||||
|
video {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
background: #000;
|
||||||
|
|
||||||
|
&::-webkit-media-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba($color: #000, $alpha: 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
&::before {
|
||||||
|
font-size: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-aspect {
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 56.25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill'
|
||||||
|
|
||||||
|
@Component({ name: 'neko-video' })
|
||||||
|
export default class extends Vue {
|
||||||
|
@Ref('component') readonly _component!: HTMLElement
|
||||||
|
@Ref('container') readonly _container!: HTMLElement
|
||||||
|
@Ref('overlay') readonly _overlay!: HTMLElement
|
||||||
|
@Ref('aspect') readonly _aspect!: HTMLElement
|
||||||
|
@Ref('player') readonly _player!: HTMLElement
|
||||||
|
@Ref('video') readonly _video!: HTMLVideoElement
|
||||||
|
|
||||||
|
private observer = new ResizeObserver(this.onResise.bind(this))
|
||||||
|
private focused = false
|
||||||
|
private fullscreen = false
|
||||||
|
|
||||||
|
get connected() {
|
||||||
|
return this.$accessor.connected
|
||||||
|
}
|
||||||
|
|
||||||
|
get connecting() {
|
||||||
|
return this.$accessor.connecting
|
||||||
|
}
|
||||||
|
|
||||||
|
get hosting() {
|
||||||
|
return this.$accessor.remote.hosting
|
||||||
|
}
|
||||||
|
|
||||||
|
get volume() {
|
||||||
|
return this.$accessor.video.volume
|
||||||
|
}
|
||||||
|
|
||||||
|
get muted() {
|
||||||
|
return this.$accessor.video.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
get stream() {
|
||||||
|
return this.$accessor.video.stream
|
||||||
|
}
|
||||||
|
|
||||||
|
get playing() {
|
||||||
|
return this.$accessor.video.playing
|
||||||
|
}
|
||||||
|
|
||||||
|
get playable() {
|
||||||
|
return this.$accessor.video.playable
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('volume')
|
||||||
|
onVolumeChanged(volume: number) {
|
||||||
|
if (this._video) {
|
||||||
|
this._video.volume = this.volume / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('muted')
|
||||||
|
onMutedChanged(muted: boolean) {
|
||||||
|
if (this._video) {
|
||||||
|
this._video.muted = muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('stream')
|
||||||
|
onStreamChanged(stream?: MediaStream) {
|
||||||
|
if (!this._video || !stream) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('srcObject' in this._video) {
|
||||||
|
this._video.srcObject = stream
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
this._video.src = window.URL.createObjectURL(this.stream) // for older browsers
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._video.paused && this.playing) {
|
||||||
|
// TODO: auto play setting
|
||||||
|
this.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('playing')
|
||||||
|
onPlayingChanged(playing: boolean) {
|
||||||
|
if (playing) {
|
||||||
|
this.play()
|
||||||
|
} else {
|
||||||
|
this.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this._container.addEventListener('resize', this.onResise)
|
||||||
|
this.onVolumeChanged(this.volume)
|
||||||
|
this.onStreamChanged(this.stream)
|
||||||
|
this.onResise()
|
||||||
|
|
||||||
|
this.observer.observe(this._component)
|
||||||
|
|
||||||
|
this._player.addEventListener('fullscreenchange', () => {
|
||||||
|
this.fullscreen = document.fullscreenElement !== null
|
||||||
|
this.onResise()
|
||||||
|
})
|
||||||
|
|
||||||
|
this._video.addEventListener('canplaythrough', () => {
|
||||||
|
this.$accessor.video.setPlayable(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._video.addEventListener('ended', () => {
|
||||||
|
this.$accessor.video.setPlayable(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._video.addEventListener('error', event => {
|
||||||
|
console.error(event.error)
|
||||||
|
this.$accessor.video.setPlayable(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.observer.disconnect()
|
||||||
|
this.$accessor.video.setPlayable(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
if (!this._video.paused || !this.playable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._video
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
const { videoWidth, videoHeight } = this._video
|
||||||
|
this.$accessor.video.setResolution({ width: videoWidth, height: videoHeight })
|
||||||
|
this.onResise()
|
||||||
|
})
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
if (this._video.paused || !this.playable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._video.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (!this.playable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.playing) {
|
||||||
|
this.$accessor.video.play()
|
||||||
|
} else {
|
||||||
|
this.$accessor.video.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFullscreen() {
|
||||||
|
this._player.requestFullscreen()
|
||||||
|
this.onResise()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMousePos(e: MouseEvent) {
|
||||||
|
const { w, h } = this.$accessor.video.resolution
|
||||||
|
const rect = this._overlay.getBoundingClientRect()
|
||||||
|
this.$client.sendData('mousemove', {
|
||||||
|
x: Math.round((w / rect.width) * (e.clientX - rect.left)),
|
||||||
|
y: Math.round((h / rect.height) * (e.clientY - rect.top)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel(e: WheelEvent) {
|
||||||
|
if (!this.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.onMousePos(e)
|
||||||
|
this.$client.sendData('wheel', {
|
||||||
|
x: (e.deltaX * -1) / 10,
|
||||||
|
y: (e.deltaY * -1) / 10,
|
||||||
|
}) // TODO: Add user settings
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(e: MouseEvent) {
|
||||||
|
if (!this.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.onMousePos(e)
|
||||||
|
this.$client.sendData('mousedown', { key: e.button })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(e: MouseEvent) {
|
||||||
|
if (!this.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.onMousePos(e)
|
||||||
|
this.$client.sendData('mouseup', { key: e.button })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(e: MouseEvent) {
|
||||||
|
if (!this.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.onMousePos(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter(e: MouseEvent) {
|
||||||
|
this._overlay.focus()
|
||||||
|
this.focused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave(e: MouseEvent) {
|
||||||
|
this.focused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!this.focused || !this.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$client.sendData('keydown', { key: e.keyCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyUp(e: KeyboardEvent) {
|
||||||
|
if (!this.focused || !this.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$client.sendData('keyup', { key: e.keyCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
onResise() {
|
||||||
|
const { horizontal, vertical } = this.$accessor.video
|
||||||
|
|
||||||
|
let height = 0
|
||||||
|
if (!this.fullscreen) {
|
||||||
|
const { offsetWidth, offsetHeight } = this._component
|
||||||
|
this._player.style.width = `${offsetWidth}px`
|
||||||
|
this._player.style.height = `${offsetHeight}px`
|
||||||
|
height = offsetHeight
|
||||||
|
} else {
|
||||||
|
const { offsetWidth, offsetHeight } = this._player
|
||||||
|
height = offsetHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
this._container.style.maxWidth = `${(horizontal / vertical) * height}px`
|
||||||
|
this._aspect.style.paddingBottom = `${(vertical / horizontal) * 100}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,21 +1,27 @@
|
|||||||
import './assets/styles/main.scss'
|
import './assets/styles/main.scss'
|
||||||
|
|
||||||
import { EVENT } from '~/client/events'
|
|
||||||
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
import Notifications from 'vue-notification'
|
import Notifications from 'vue-notification'
|
||||||
|
import ToolTip from 'v-tooltip'
|
||||||
import Client from './plugins/neko'
|
import Client from './plugins/neko'
|
||||||
import App from './App.vue'
|
import Axios from './plugins/axios'
|
||||||
|
import Swal from './plugins/swal'
|
||||||
|
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
import app from './app.vue'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.use(Notifications)
|
Vue.use(Notifications)
|
||||||
|
Vue.use(ToolTip)
|
||||||
|
Vue.use(Axios)
|
||||||
|
Vue.use(Swal)
|
||||||
Vue.use(Client)
|
Vue.use(Client)
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
store,
|
store,
|
||||||
render: h => h(App),
|
render: h => h(app),
|
||||||
created() {
|
created() {
|
||||||
this.$client.init(this)
|
this.$client.init(this)
|
||||||
},
|
},
|
||||||
|
46
client/src/plugins/anime.ts
Normal file
46
client/src/plugins/anime.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { PluginObject } from 'vue'
|
||||||
|
import anime, { StaggerOptions, AnimeTimelineInstance, AnimeParams, AnimeInstance } from 'animejs'
|
||||||
|
|
||||||
|
type FunctionBasedParameter = (element: HTMLElement, index: number, length: number) => number
|
||||||
|
type AnimeTarget = string | object | HTMLElement | SVGElement | NodeList | null
|
||||||
|
type AnimeFunc = (params: AnimeParams) => AnimeInstance
|
||||||
|
|
||||||
|
interface Anime {
|
||||||
|
version: string
|
||||||
|
speed: number
|
||||||
|
running: AnimeInstance[]
|
||||||
|
easings: { [EasingFunction: string]: (t: number) => any }
|
||||||
|
remove(targets: AnimeTarget | ReadonlyArray<AnimeTarget>): void
|
||||||
|
get(targets: AnimeTarget, prop: string): string | number
|
||||||
|
path(
|
||||||
|
path: string | HTMLElement | SVGElement | null,
|
||||||
|
percent?: number,
|
||||||
|
): (
|
||||||
|
prop: string,
|
||||||
|
) => {
|
||||||
|
el: HTMLElement | SVGElement
|
||||||
|
property: string
|
||||||
|
totalLength: number
|
||||||
|
}
|
||||||
|
setDashoffset(el: HTMLElement | SVGElement | null): number
|
||||||
|
bezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number
|
||||||
|
stagger(value: number | string | ReadonlyArray<number | string>, options?: StaggerOptions): FunctionBasedParameter
|
||||||
|
set(targets: AnimeTarget, value: { [AnyAnimatedProperty: string]: any }): void
|
||||||
|
// Timeline
|
||||||
|
timeline(params?: AnimeParams | ReadonlyArray<AnimeInstance>): AnimeTimelineInstance
|
||||||
|
random(min: number, max: number): number
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$anime: AnimeFunc & Anime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: PluginObject<undefined> = {
|
||||||
|
install(Vue) {
|
||||||
|
Vue.prototype.$anime = anime
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
16
client/src/plugins/axios.ts
Normal file
16
client/src/plugins/axios.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { PluginObject } from 'vue'
|
||||||
|
import axios, { AxiosStatic } from 'axios'
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$http: AxiosStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: PluginObject<undefined> = {
|
||||||
|
install(Vue) {
|
||||||
|
Vue.prototype.$http = axios
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
@ -1,16 +1,29 @@
|
|||||||
import { PluginObject } from 'vue'
|
import { PluginObject } from 'vue'
|
||||||
import { NekoClient } from '~/client'
|
import { NekoClient } from '~/client'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const $client: NekoClient
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
$client: NekoClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$client: NekoClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const plugin: PluginObject<undefined> = {
|
const plugin: PluginObject<undefined> = {
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
console.log()
|
window.$client = new NekoClient()
|
||||||
const client = new NekoClient()
|
|
||||||
.on('error', error => console.error('[%cNEKO%c] %cERR', 'color: #498ad8;', '', 'color: #d84949;', error))
|
.on('error', error => console.error('[%cNEKO%c] %cERR', 'color: #498ad8;', '', 'color: #d84949;', error))
|
||||||
.on('warn', (...log) => console.warn('[%cNEKO%c] %cWRN', 'color: #498ad8;', '', 'color: #eae364;', ...log))
|
.on('warn', (...log) => console.warn('[%cNEKO%c] %cWRN', 'color: #498ad8;', '', 'color: #eae364;', ...log))
|
||||||
.on('info', (...log) => console.info('[%cNEKO%c] %cINF', 'color: #498ad8;', '', 'color: #4ac94c;', ...log))
|
.on('info', (...log) => console.info('[%cNEKO%c] %cINF', 'color: #498ad8;', '', 'color: #4ac94c;', ...log))
|
||||||
.on('debug', (...log) => console.log('[%cNEKO%c] %cDBG', 'color: #498ad8;', '', 'color: #eae364;', ...log))
|
.on('debug', (...log) => console.log('[%cNEKO%c] %cDBG', 'color: #498ad8;', '', 'color: #eae364;', ...log))
|
||||||
|
|
||||||
Vue.prototype.$client = client
|
Vue.prototype.$client = window.$client
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
client/src/plugins/swal.ts
Normal file
58
client/src/plugins/swal.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import { SweetAlertOptions } from 'sweetalert2'
|
||||||
|
import Swal from 'sweetalert2/dist/sweetalert2.js'
|
||||||
|
|
||||||
|
type VueSwalInstance = typeof Swal.fire
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$swal: VueSwalInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VueConstructor<V extends Vue = Vue> {
|
||||||
|
swal: VueSwalInstance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VueSweetalert2Options extends SweetAlertOptions {
|
||||||
|
// includeCss?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class VueSweetalert2 {
|
||||||
|
static install(vue: Vue | any, options?: VueSweetalert2Options): void {
|
||||||
|
const swalFunction = (...args: [SweetAlertOptions]) => {
|
||||||
|
if (options) {
|
||||||
|
const mixed = Swal.mixin(options)
|
||||||
|
|
||||||
|
return mixed.fire.apply(mixed, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Swal.fire.apply(Swal, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
let methodName: string | number | symbol
|
||||||
|
|
||||||
|
for (methodName in Swal) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (Object.prototype.hasOwnProperty.call(Swal, methodName) && typeof Swal[methodName] === 'function') {
|
||||||
|
// @ts-ignore
|
||||||
|
swalFunction[methodName] = (method => {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
// @ts-ignore
|
||||||
|
return Swal[method].apply(Swal, args)
|
||||||
|
}
|
||||||
|
})(methodName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vue['swal'] = swalFunction
|
||||||
|
|
||||||
|
// add the instance method
|
||||||
|
if (!vue.prototype.hasOwnProperty('$swal')) {
|
||||||
|
vue.prototype.$swal = swalFunction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VueSweetalert2
|
27
client/src/router/index.ts
Normal file
27
client/src/router/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import chat from '~/pages/chat.vue'
|
||||||
|
import about from '~/pages/about.vue'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'chat',
|
||||||
|
component: chat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: 'about',
|
||||||
|
component: about,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: 'history',
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
@ -1,4 +1,6 @@
|
|||||||
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
|
import { EVENT } from '~/client/events'
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
export const namespaced = true
|
export const namespaced = true
|
||||||
|
|
||||||
@ -6,10 +8,11 @@ interface Message {
|
|||||||
id: string
|
id: string
|
||||||
content: string
|
content: string
|
||||||
created: Date
|
created: Date
|
||||||
|
type: 'text' | 'event'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
messages: [] as Message[],
|
history: [] as Message[],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = getterTree(state, {
|
export const getters = getterTree(state, {
|
||||||
@ -18,13 +21,30 @@ export const getters = getterTree(state, {
|
|||||||
|
|
||||||
export const mutations = mutationTree(state, {
|
export const mutations = mutationTree(state, {
|
||||||
addMessage(state, message: Message) {
|
addMessage(state, message: Message) {
|
||||||
state.messages = state.messages.concat([message])
|
state.history = state.history.concat([message])
|
||||||
|
},
|
||||||
|
clear(state) {
|
||||||
|
state.history = []
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const actions = actionTree(
|
export const actions = actionTree(
|
||||||
{ state, getters, mutations },
|
{ state, getters, mutations },
|
||||||
{
|
{
|
||||||
//
|
sendMessage(store, content: string) {
|
||||||
|
if (!accessor.connected || accessor.user.muted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.CHAT.MESSAGE, { content })
|
||||||
|
},
|
||||||
|
|
||||||
|
sendEmoji(store, emoji: string) {
|
||||||
|
if (!accessor.connected || !accessor.user.muted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.CHAT.EMOJI, { emoji })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
46
client/src/store/client.ts
Normal file
46
client/src/store/client.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
|
export const namespaced = true
|
||||||
|
|
||||||
|
export const state = () => {
|
||||||
|
let side = false
|
||||||
|
let _side = localStorage.getItem('side')
|
||||||
|
if (_side) {
|
||||||
|
side = _side === '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
let tab = 'chat'
|
||||||
|
let _tab = localStorage.getItem('tab')
|
||||||
|
if (_tab) {
|
||||||
|
tab = _tab
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
side,
|
||||||
|
about: false,
|
||||||
|
about_page: '',
|
||||||
|
tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getters = getterTree(state, {})
|
||||||
|
|
||||||
|
export const mutations = mutationTree(state, {
|
||||||
|
setTab(state, tab: string) {
|
||||||
|
state.tab = tab
|
||||||
|
localStorage.setItem('tab', tab)
|
||||||
|
},
|
||||||
|
setAbout(state, page: string) {
|
||||||
|
state.about_page = page
|
||||||
|
},
|
||||||
|
toggleAbout(state) {
|
||||||
|
state.about = !state.about
|
||||||
|
},
|
||||||
|
toggleSide(state) {
|
||||||
|
state.side = !state.side
|
||||||
|
localStorage.setItem('side', state.side ? '1' : '0')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const actions = actionTree({ state, getters, mutations }, {})
|
@ -3,8 +3,11 @@ import Vuex from 'vuex'
|
|||||||
import { useAccessor, mutationTree, actionTree } from 'typed-vuex'
|
import { useAccessor, mutationTree, actionTree } from 'typed-vuex'
|
||||||
|
|
||||||
import * as video from './video'
|
import * as video from './video'
|
||||||
|
import * as chat from './chat'
|
||||||
import * as remote from './remote'
|
import * as remote from './remote'
|
||||||
import * as user from './user'
|
import * as user from './user'
|
||||||
|
import * as settings from './settings'
|
||||||
|
import * as client from './client'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
connecting: false,
|
connecting: false,
|
||||||
@ -18,14 +21,18 @@ export const getters = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = mutationTree(state, {
|
export const mutations = mutationTree(state, {
|
||||||
initialiseStore() {
|
initialiseStore(state) {
|
||||||
// TODO: init with localstorage to retrieve save settings
|
console.log('test')
|
||||||
},
|
},
|
||||||
setConnnecting(state, connecting: boolean) {
|
|
||||||
state.connecting = connecting
|
setConnnecting(state) {
|
||||||
|
state.connected = false
|
||||||
|
state.connecting = true
|
||||||
},
|
},
|
||||||
|
|
||||||
setConnected(state, connected: boolean) {
|
setConnected(state, connected: boolean) {
|
||||||
state.connected = connected
|
state.connected = connected
|
||||||
|
state.connecting = false
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -33,6 +40,9 @@ export const actions = actionTree(
|
|||||||
{ state, getters, mutations },
|
{ state, getters, mutations },
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
connect(store, { username, password }: { username: string; password: string }) {
|
||||||
|
$client.connect(password, username)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,7 +50,7 @@ export const storePattern = {
|
|||||||
state,
|
state,
|
||||||
mutations,
|
mutations,
|
||||||
actions,
|
actions,
|
||||||
modules: { video, user, remote },
|
modules: { video, chat, user, remote, settings, client },
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
@ -50,4 +60,10 @@ export const accessor = useAccessor(store, storePattern)
|
|||||||
|
|
||||||
Vue.prototype.$accessor = accessor
|
Vue.prototype.$accessor = accessor
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$accessor: typeof accessor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default store
|
export default store
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
import { Member } from '~/client/types'
|
import { Member } from '~/client/types'
|
||||||
|
import { EVENT } from '~/client/events'
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
export const namespaced = true
|
export const namespaced = true
|
||||||
|
|
||||||
@ -11,6 +13,9 @@ export const getters = getterTree(state, {
|
|||||||
hosting: (state, getters, root) => {
|
hosting: (state, getters, root) => {
|
||||||
return root.user.id === state.id
|
return root.user.id === state.id
|
||||||
},
|
},
|
||||||
|
hosted: (state, getters, root) => {
|
||||||
|
return state.id !== ''
|
||||||
|
},
|
||||||
host: (state, getters, root) => {
|
host: (state, getters, root) => {
|
||||||
return root.user.member[state.id] || null
|
return root.user.member[state.id] || null
|
||||||
},
|
},
|
||||||
@ -32,6 +37,67 @@ export const mutations = mutationTree(state, {
|
|||||||
export const actions = actionTree(
|
export const actions = actionTree(
|
||||||
{ state, getters, mutations },
|
{ state, getters, mutations },
|
||||||
{
|
{
|
||||||
//
|
initialise({ commit }) {
|
||||||
|
//
|
||||||
|
},
|
||||||
|
toggle({ getters }) {
|
||||||
|
if (!accessor.connected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getters.hosting) {
|
||||||
|
$client.sendMessage(EVENT.CONTROL.REQUEST)
|
||||||
|
} else {
|
||||||
|
$client.sendMessage(EVENT.CONTROL.RELEASE)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
request({ getters }) {
|
||||||
|
if (!accessor.connected || !getters.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.CONTROL.REQUEST)
|
||||||
|
},
|
||||||
|
|
||||||
|
release({ getters }) {
|
||||||
|
if (!accessor.connected || getters.hosting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.CONTROL.RELEASE)
|
||||||
|
},
|
||||||
|
|
||||||
|
adminControl() {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.CONTROL)
|
||||||
|
},
|
||||||
|
|
||||||
|
adminRelease() {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.RELEASE)
|
||||||
|
},
|
||||||
|
|
||||||
|
lock() {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.LOCK)
|
||||||
|
},
|
||||||
|
|
||||||
|
unlock() {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.UNLOCK)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
30
client/src/store/settings.ts
Normal file
30
client/src/store/settings.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
|
export const namespaced = true
|
||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
scroll: 10,
|
||||||
|
scroll_invert: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getters = getterTree(state, {})
|
||||||
|
|
||||||
|
export const mutations = mutationTree(state, {
|
||||||
|
setScroll(state, scroll: number) {
|
||||||
|
state.scroll = scroll
|
||||||
|
localStorage.setItem('scroll', `${scroll}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const actions = actionTree(
|
||||||
|
{ state, getters, mutations },
|
||||||
|
{
|
||||||
|
initialise() {
|
||||||
|
const scroll = localStorage.getItem('scroll')
|
||||||
|
if (scroll) {
|
||||||
|
accessor.settings.setScroll(parseInt(scroll))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
@ -1,5 +1,8 @@
|
|||||||
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
import { Member } from '~/client/types'
|
import { Member } from '~/client/types'
|
||||||
|
import { EVENT } from '~/client/events'
|
||||||
|
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
export const namespaced = true
|
export const namespaced = true
|
||||||
|
|
||||||
@ -15,13 +18,23 @@ export const state = () => ({
|
|||||||
export const getters = getterTree(state, {
|
export const getters = getterTree(state, {
|
||||||
member: state => state.members[state.id] || null,
|
member: state => state.members[state.id] || null,
|
||||||
admin: state => (state.members[state.id] ? state.members[state.id].admin : false),
|
admin: state => (state.members[state.id] ? state.members[state.id].admin : false),
|
||||||
|
muted: state => (state.members[state.id] ? state.members[state.id].muted : false),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const mutations = mutationTree(state, {
|
export const mutations = mutationTree(state, {
|
||||||
|
setMuted(state, { id, muted }: { id: string; muted: boolean }) {
|
||||||
|
state.members[id] = {
|
||||||
|
...state.members[id],
|
||||||
|
muted,
|
||||||
|
}
|
||||||
|
},
|
||||||
setMembers(state, members: Member[]) {
|
setMembers(state, members: Member[]) {
|
||||||
const data: Members = {}
|
const data: Members = {}
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
data[member.id] = member
|
data[member.id] = {
|
||||||
|
connected: true,
|
||||||
|
...member,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.members = data
|
state.members = data
|
||||||
},
|
},
|
||||||
@ -31,13 +44,17 @@ export const mutations = mutationTree(state, {
|
|||||||
addMember(state, member: Member) {
|
addMember(state, member: Member) {
|
||||||
state.members = {
|
state.members = {
|
||||||
...state.members,
|
...state.members,
|
||||||
[member.id]: member,
|
[member.id]: {
|
||||||
|
connected: true,
|
||||||
|
...member,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delMember(state, id: string) {
|
delMember(state, id: string) {
|
||||||
const data = { ...state.members }
|
state.members[id] = {
|
||||||
delete data[id]
|
...state.members[id],
|
||||||
state.members = data
|
connected: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearMembers(state) {
|
clearMembers(state) {
|
||||||
state.members = {}
|
state.members = {}
|
||||||
@ -47,6 +64,68 @@ export const mutations = mutationTree(state, {
|
|||||||
export const actions = actionTree(
|
export const actions = actionTree(
|
||||||
{ state, getters, mutations },
|
{ state, getters, mutations },
|
||||||
{
|
{
|
||||||
//
|
ban({ state }, member: string | Member) {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof member === 'string') {
|
||||||
|
member = state.members[member]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.BAN, { id: member.id })
|
||||||
|
},
|
||||||
|
|
||||||
|
kick({ state }, member: string | Member) {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof member === 'string') {
|
||||||
|
member = state.members[member]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.KICK, { id: member.id })
|
||||||
|
},
|
||||||
|
|
||||||
|
mute({ state }, member: string | Member) {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof member === 'string') {
|
||||||
|
member = state.members[member]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.MUTE, { id: member.id })
|
||||||
|
},
|
||||||
|
|
||||||
|
unmute({ state }, member: string | Member) {
|
||||||
|
if (!accessor.connected || !accessor.user.admin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof member === 'string') {
|
||||||
|
member = state.members[member]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.sendMessage(EVENT.ADMIN.UNMUTE, { id: member.id })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1,24 +1,78 @@
|
|||||||
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
export const namespaced = true
|
export const namespaced = true
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => {
|
||||||
index: -1,
|
let volume = 100
|
||||||
streams: [] as MediaStream[],
|
let _volume = localStorage.getItem('volume')
|
||||||
width: 1280,
|
if (_volume) {
|
||||||
height: 720,
|
volume = parseInt(_volume)
|
||||||
volume: 0,
|
}
|
||||||
playing: false,
|
|
||||||
})
|
let muted = false
|
||||||
|
let _muted = localStorage.getItem('muted')
|
||||||
|
if (_muted) {
|
||||||
|
muted = _muted === '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: -1,
|
||||||
|
tracks: [] as MediaStreamTrack[],
|
||||||
|
streams: [] as MediaStream[],
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 9,
|
||||||
|
volume,
|
||||||
|
muted,
|
||||||
|
playing: false,
|
||||||
|
playable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getters = getterTree(state, {
|
export const getters = getterTree(state, {
|
||||||
stream: state => state.streams[state.index],
|
stream: state => state.streams[state.index],
|
||||||
|
track: state => state.tracks[state.index],
|
||||||
resolution: state => ({ w: state.width, h: state.height }),
|
resolution: state => ({ w: state.width, h: state.height }),
|
||||||
aspect: state => {
|
})
|
||||||
const { width, height } = state
|
|
||||||
|
export const mutations = mutationTree(state, {
|
||||||
|
play(state) {
|
||||||
|
if (state.playable) {
|
||||||
|
state.playing = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
pause(state) {
|
||||||
|
if (state.playable) {
|
||||||
|
state.playing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePlay(state) {
|
||||||
|
if (state.playable) {
|
||||||
|
state.playing = !state.playing
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMute(state) {
|
||||||
|
state.muted = !state.muted
|
||||||
|
},
|
||||||
|
|
||||||
|
setPlayable(state, playable: boolean) {
|
||||||
|
if (!playable && state.playing) {
|
||||||
|
state.playing = false
|
||||||
|
}
|
||||||
|
state.playable = playable
|
||||||
|
},
|
||||||
|
|
||||||
|
setResolution(state, { width, height }: { width: number; height: number }) {
|
||||||
|
state.width = width
|
||||||
|
state.height = height
|
||||||
|
|
||||||
if ((height == 0 && width == 0) || (height == 0 && width != 0) || (height != 0 && width == 0)) {
|
if ((height == 0 && width == 0) || (height == 0 && width != 0) || (height != 0 && width == 0)) {
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height == width) {
|
if (height == width) {
|
||||||
@ -47,36 +101,32 @@ export const getters = getterTree(state, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
state.horizontal = width / gcd
|
||||||
horizontal: width / gcd,
|
state.vertical = height / gcd
|
||||||
vertical: height / gcd,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const mutations = mutationTree(state, {
|
|
||||||
setResolution(state, { width, height }: { width: number; height: number }) {
|
|
||||||
state.width = width
|
|
||||||
state.height = height
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setVolume(state, volume: number) {
|
setVolume(state, volume: number) {
|
||||||
state.volume = volume
|
state.volume = volume
|
||||||
|
localStorage.setItem('volume', `${volume}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
setStream(state, index: number) {
|
setStream(state, index: number) {
|
||||||
state.index = index
|
state.index = index
|
||||||
},
|
},
|
||||||
|
|
||||||
addStream(state, stream: MediaStream) {
|
addTrack(state, [track, stream]: [MediaStreamTrack, MediaStream]) {
|
||||||
|
state.tracks = state.tracks.concat([track])
|
||||||
state.streams = state.streams.concat([stream])
|
state.streams = state.streams.concat([stream])
|
||||||
},
|
},
|
||||||
|
|
||||||
delStream(state, index: number) {
|
delTrack(state, index: number) {
|
||||||
state.streams = state.streams.filter((_, i) => i !== index)
|
state.streams = state.streams.filter((_, i) => i !== index)
|
||||||
|
state.tracks = state.tracks.filter((_, i) => i !== index)
|
||||||
},
|
},
|
||||||
|
|
||||||
clearStream(state) {
|
clear(state) {
|
||||||
|
state.index = -1
|
||||||
|
state.tracks = []
|
||||||
state.streams = []
|
state.streams = []
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -84,6 +134,11 @@ export const mutations = mutationTree(state, {
|
|||||||
export const actions = actionTree(
|
export const actions = actionTree(
|
||||||
{ state, getters, mutations },
|
{ state, getters, mutations },
|
||||||
{
|
{
|
||||||
//
|
initialise({ commit }) {
|
||||||
|
const volume = localStorage.getItem('volume')
|
||||||
|
if (volume) {
|
||||||
|
accessor.video.setVolume(parseInt(volume))
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
9
client/src/types/vue.d.ts
vendored
9
client/src/types/vue.d.ts
vendored
@ -1,9 +0,0 @@
|
|||||||
import { NekoClient } from '~/client'
|
|
||||||
import { accessor } from '~/store'
|
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
|
||||||
interface Vue {
|
|
||||||
$accessor: typeof accessor
|
|
||||||
$client: NekoClient
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,9 +10,11 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": [
|
"types": [
|
||||||
"webpack-env"
|
"webpack-env",
|
||||||
|
"w3c-image-capture"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": [
|
"~/*": [
|
||||||
@ -27,7 +29,7 @@
|
|||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"scripthost"
|
"scripthost"
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
|
@ -26,5 +26,5 @@ const ADMIN_LOCK = "admin/lock"
|
|||||||
const ADMIN_MUTE = "admin/mute"
|
const ADMIN_MUTE = "admin/mute"
|
||||||
const ADMIN_UNLOCK = "admin/unlock"
|
const ADMIN_UNLOCK = "admin/unlock"
|
||||||
const ADMIN_UNMUTE = "admin/unmute"
|
const ADMIN_UNMUTE = "admin/unmute"
|
||||||
const ADMIN_FORCE_CONTROL = "admin/force/control"
|
const ADMIN_CONTROL = "admin/control"
|
||||||
const ADMIN_FORCE_RELEASE = "admin/force/release"
|
const ADMIN_RELEASE = "admin/release"
|
||||||
|
@ -72,8 +72,8 @@ type Admin struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminSubject struct {
|
type AdminTarget struct {
|
||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
Subject string `json:"subject"`
|
Target string `json:"target"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ type Session struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"username"`
|
Name string `json:"username"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
Muted bool `json:"-"`
|
Muted bool `json:"muted"`
|
||||||
connected bool
|
connected bool
|
||||||
socket *websocket.Conn
|
socket *websocket.Conn
|
||||||
peer *webrtc.PeerConnection
|
peer *webrtc.PeerConnection
|
||||||
|
@ -49,15 +49,29 @@ func (h *MessageHandler) adminControl(id string, session *session.Session) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
host, ok := h.sessions.GetHost()
|
||||||
|
|
||||||
h.sessions.SetHost(id)
|
h.sessions.SetHost(id)
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if ok {
|
||||||
message.Admin{
|
if err := h.sessions.Brodcast(
|
||||||
Event: event.ADMIN_FORCE_CONTROL,
|
message.AdminTarget{
|
||||||
ID: id,
|
Event: event.ADMIN_CONTROL,
|
||||||
}, nil); err != nil {
|
ID: id,
|
||||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_FORCE_CONTROL)
|
Target: host.ID,
|
||||||
return err
|
}, nil); err != nil {
|
||||||
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_CONTROL)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := h.sessions.Brodcast(
|
||||||
|
message.Admin{
|
||||||
|
Event: event.ADMIN_CONTROL,
|
||||||
|
ID: id,
|
||||||
|
}, nil); err != nil {
|
||||||
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_CONTROL)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -68,15 +82,29 @@ func (h *MessageHandler) adminRelease(id string, session *session.Session) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
host, ok := h.sessions.GetHost()
|
||||||
|
|
||||||
h.sessions.ClearHost()
|
h.sessions.ClearHost()
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if ok {
|
||||||
message.Admin{
|
if err := h.sessions.Brodcast(
|
||||||
Event: event.ADMIN_FORCE_RELEASE,
|
message.AdminTarget{
|
||||||
ID: id,
|
Event: event.ADMIN_RELEASE,
|
||||||
}, nil); err != nil {
|
ID: id,
|
||||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_FORCE_RELEASE)
|
Target: host.ID,
|
||||||
return err
|
}, nil); err != nil {
|
||||||
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_RELEASE)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := h.sessions.Brodcast(
|
||||||
|
message.Admin{
|
||||||
|
Event: event.ADMIN_RELEASE,
|
||||||
|
ID: id,
|
||||||
|
}, nil); err != nil {
|
||||||
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_RELEASE)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -87,12 +115,16 @@ func (h *MessageHandler) adminBan(id string, session *session.Session, payload *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
session, ok := h.sessions.Get(id)
|
target, ok := h.sessions.Get(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
address := session.RemoteAddr()
|
if target.Admin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
address := target.RemoteAddr()
|
||||||
if address == nil {
|
if address == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -107,10 +139,10 @@ func (h *MessageHandler) adminBan(id string, session *session.Session, payload *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if err := h.sessions.Brodcast(
|
||||||
message.AdminSubject{
|
message.AdminTarget{
|
||||||
Event: event.ADMIN_BAN,
|
Event: event.ADMIN_BAN,
|
||||||
Subject: payload.ID,
|
Target: target.ID,
|
||||||
ID: id,
|
ID: id,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_BAN)
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_BAN)
|
||||||
return err
|
return err
|
||||||
@ -124,18 +156,27 @@ func (h *MessageHandler) adminKick(id string, session *session.Session, payload
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.sessions.Kick(payload.ID, message.Disconnect{
|
target, ok := h.sessions.Get(payload.ID)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.Admin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := target.Kick(message.Disconnect{
|
||||||
Event: event.SYSTEM_DISCONNECT,
|
Event: event.SYSTEM_DISCONNECT,
|
||||||
Message: "You have been banned",
|
Message: "You have been kicked",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if err := h.sessions.Brodcast(
|
||||||
message.AdminSubject{
|
message.AdminTarget{
|
||||||
Event: event.ADMIN_KICK,
|
Event: event.ADMIN_KICK,
|
||||||
Subject: payload.ID,
|
Target: target.ID,
|
||||||
ID: id,
|
ID: id,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_KICK)
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_KICK)
|
||||||
return err
|
return err
|
||||||
@ -149,15 +190,22 @@ func (h *MessageHandler) adminMute(id string, session *session.Session, payload
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.sessions.Mute(payload.ID); err != nil {
|
target, ok := h.sessions.Get(payload.ID)
|
||||||
return err
|
if !ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if target.Admin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
target.Muted = true
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if err := h.sessions.Brodcast(
|
||||||
message.AdminSubject{
|
message.AdminTarget{
|
||||||
Event: event.ADMIN_MUTE,
|
Event: event.ADMIN_MUTE,
|
||||||
Subject: payload.ID,
|
Target: target.ID,
|
||||||
ID: id,
|
ID: id,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_UNMUTE)
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_UNMUTE)
|
||||||
return err
|
return err
|
||||||
@ -171,15 +219,18 @@ func (h *MessageHandler) adminUnmute(id string, session *session.Session, payloa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.sessions.Unmute(payload.ID); err != nil {
|
target, ok := h.sessions.Get(payload.ID)
|
||||||
return err
|
if !ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target.Muted = false
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if err := h.sessions.Brodcast(
|
||||||
message.AdminSubject{
|
message.AdminTarget{
|
||||||
Event: event.ADMIN_UNMUTE,
|
Event: event.ADMIN_UNMUTE,
|
||||||
Subject: payload.ID,
|
Target: target.ID,
|
||||||
ID: id,
|
ID: id,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_UNMUTE)
|
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.ADMIN_UNMUTE)
|
||||||
return err
|
return err
|
||||||
|
@ -7,6 +7,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (h *MessageHandler) chat(id string, session *session.Session, payload *message.ChatRecieve) error {
|
func (h *MessageHandler) chat(id string, session *session.Session, payload *message.ChatRecieve) error {
|
||||||
|
if session.Muted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if err := h.sessions.Brodcast(
|
||||||
message.ChatSend{
|
message.ChatSend{
|
||||||
Event: event.CHAT_MESSAGE,
|
Event: event.CHAT_MESSAGE,
|
||||||
@ -20,6 +24,10 @@ func (h *MessageHandler) chat(id string, session *session.Session, payload *mess
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *MessageHandler) chatEmoji(id string, session *session.Session, payload *message.EmojiRecieve) error {
|
func (h *MessageHandler) chatEmoji(id string, session *session.Session, payload *message.EmojiRecieve) error {
|
||||||
|
if session.Muted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.sessions.Brodcast(
|
if err := h.sessions.Brodcast(
|
||||||
message.EmojiSend{
|
message.EmojiSend{
|
||||||
Event: event.CHAT_MESSAGE,
|
Event: event.CHAT_MESSAGE,
|
||||||
|
@ -57,6 +57,7 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
|
|||||||
utils.Unmarshal(&payload, raw, func() error {
|
utils.Unmarshal(&payload, raw, func() error {
|
||||||
return h.webrtc.CreatePeer(id, payload.SDP)
|
return h.webrtc.CreatePeer(id, payload.SDP)
|
||||||
}), "%s failed", header.Event)
|
}), "%s failed", header.Event)
|
||||||
|
|
||||||
// Identity Events
|
// Identity Events
|
||||||
case event.IDENTITY_DETAILS:
|
case event.IDENTITY_DETAILS:
|
||||||
payload := &message.IdentityDetails{}
|
payload := &message.IdentityDetails{}
|
||||||
@ -64,11 +65,13 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
|
|||||||
utils.Unmarshal(payload, raw, func() error {
|
utils.Unmarshal(payload, raw, func() error {
|
||||||
return h.identityDetails(id, session, payload)
|
return h.identityDetails(id, session, payload)
|
||||||
}), "%s failed", header.Event)
|
}), "%s failed", header.Event)
|
||||||
|
|
||||||
// Control Events
|
// Control Events
|
||||||
case event.CONTROL_RELEASE:
|
case event.CONTROL_RELEASE:
|
||||||
return errors.Wrapf(h.controlRelease(id, session), "%s failed", header.Event)
|
return errors.Wrapf(h.controlRelease(id, session), "%s failed", header.Event)
|
||||||
case event.CONTROL_REQUEST:
|
case event.CONTROL_REQUEST:
|
||||||
return errors.Wrapf(h.controlRequest(id, session), "%s failed", header.Event)
|
return errors.Wrapf(h.controlRequest(id, session), "%s failed", header.Event)
|
||||||
|
|
||||||
// Chat Events
|
// Chat Events
|
||||||
case event.CHAT_MESSAGE:
|
case event.CHAT_MESSAGE:
|
||||||
payload := &message.ChatRecieve{}
|
payload := &message.ChatRecieve{}
|
||||||
@ -82,12 +85,15 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
|
|||||||
utils.Unmarshal(payload, raw, func() error {
|
utils.Unmarshal(payload, raw, func() error {
|
||||||
return h.chatEmoji(id, session, payload)
|
return h.chatEmoji(id, session, payload)
|
||||||
}), "%s failed", header.Event)
|
}), "%s failed", header.Event)
|
||||||
|
|
||||||
// Admin Events
|
// Admin Events
|
||||||
case event.ADMIN_LOCK:
|
case event.ADMIN_LOCK:
|
||||||
return errors.Wrapf(h.adminLock(id, session), "%s failed", header.Event)
|
return errors.Wrapf(h.adminLock(id, session), "%s failed", header.Event)
|
||||||
case event.ADMIN_FORCE_CONTROL:
|
case event.ADMIN_UNLOCK:
|
||||||
|
return errors.Wrapf(h.adminUnlock(id, session), "%s failed", header.Event)
|
||||||
|
case event.ADMIN_CONTROL:
|
||||||
return errors.Wrapf(h.adminControl(id, session), "%s failed", header.Event)
|
return errors.Wrapf(h.adminControl(id, session), "%s failed", header.Event)
|
||||||
case event.ADMIN_FORCE_RELEASE:
|
case event.ADMIN_RELEASE:
|
||||||
return errors.Wrapf(h.adminRelease(id, session), "%s failed", header.Event)
|
return errors.Wrapf(h.adminRelease(id, session), "%s failed", header.Event)
|
||||||
case event.ADMIN_BAN:
|
case event.ADMIN_BAN:
|
||||||
payload := &message.Admin{}
|
payload := &message.Admin{}
|
||||||
|
Reference in New Issue
Block a user