first commit

This commit is contained in:
Craig
2020-01-13 08:05:38 +00:00
commit 0c8af21fab
95 changed files with 5312 additions and 0 deletions

847
client/src/App.vue Normal file
View File

@ -0,0 +1,847 @@
<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: controlling }, '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>
<div class="message" v-if="!connecting">
<span>Please enter the password:</span>
<input type="password" v-model="password" />
<span class="button" @click.stop.prevent="connect">
Connect
</span>
</div>
<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'
const MOUSE_MOVE = 0x01
const MOUSE_UP = 0x02
const MOUSE_DOWN = 0x03
const MOUSE_CLK = 0x04
const KEY_DOWN = 0x05
const KEY_UP = 0x06
const KEY_CLK = 0x07
@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 connected = false
private connecting = false
private controlling = false
private playing = false
private volume = 0
private width = 1280
private height = 720
private state: RTCIceConnectionState = 'disconnected'
private password = ''
private ws?: WebSocket
private peer?: RTCPeerConnection
private channel?: RTCDataChannel
private id?: string
private stream?: MediaStream
private timeout?: number
@Watch('volume')
onVolumeChanged(volume: number) {
if (this._player) {
this._player.volume = this.volume / 100
}
}
mounted() {
window.addEventListener('resize', this.onResise)
this.onResise()
this.volume = this._player.volume * 100
this._volume.value = `${this.volume}`
}
beforeDestroy() {
window.removeEventListener('resize', this.onResise)
}
toggleControl() {
if (!this.ws) {
return
}
if (this.controlling) {
this.ws.send(JSON.stringify({ event: 'control/release' }))
} else {
this.ws.send(JSON.stringify({ event: 'control/request' }))
}
}
toggleMedia() {
if (!this.playing) {
this._player
.play()
.then(() => {
this.playing = true
this.width = this._player.videoWidth
this.height = this._player.videoHeight
this.onResise()
})
.catch(err => {
console.error(err)
})
} else {
this._player.pause()
this.playing = false
}
}
setVolume() {
this.volume = parseInt(this._volume.value)
}
fullscreen() {
this._video.requestFullscreen()
}
connect() {
/*
this.ws = new WebSocket(
`${/https/gi.test(location.protocol) ? 'wss' : 'ws'}://${location.host}/ws?password=${this.password}`,
)
*/
this.ws = new WebSocket(`ws://localhost:3000/ws?password=${this.password}`)
this.ws.onmessage = this.onMessage.bind(this)
this.ws.onerror = event => console.error((event as ErrorEvent).error)
this.ws.onclose = event => this.onClose.bind(this)
this.onConnecting()
this.timeout = setTimeout(this.onTimeout.bind(this), 5000)
}
createPeer() {
if (!this.ws) {
return
}
this.peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] })
this.peer.onicecandidate = event => {
if (event.candidate === null && this.peer!.localDescription) {
this.ws!.send(
JSON.stringify({
event: 'sdp/provide',
sdp: this.peer!.localDescription.sdp,
}),
)
}
}
this.peer.oniceconnectionstatechange = event => {
this.state = this.peer!.iceConnectionState
switch (this.state) {
case 'connected':
this.onConnected()
break
case 'disconnected':
this.onClose()
break
}
}
this.peer.ontrack = this.onTrack.bind(this)
this.peer.addTransceiver('audio', { direction: 'recvonly' })
this.peer.addTransceiver('video', { direction: 'recvonly' })
this.channel = this.peer.createDataChannel('data')
this.peer
.createOffer()
.then(d => this.peer!.setLocalDescription(d))
.catch(err => console.log(err))
}
updateControles(event: 'wheel', data: { x: number; y: number }): void
updateControles(event: 'mousemove', data: { x: number; y: number; rect: DOMRect }): void
updateControles(event: 'mousedown' | 'mouseup' | 'keydown' | 'keyup', data: { key: number }): void
updateControles(event: string, data: any) {
if (!this.controlling) {
return
}
let buffer: ArrayBuffer
let payload: DataView
switch (event) {
case 'mousemove':
buffer = new ArrayBuffer(7)
payload = new DataView(buffer)
payload.setUint8(0, MOUSE_MOVE)
payload.setUint16(1, 4, true)
payload.setUint16(3, Math.round((this.width / data.rect.width) * (data.x - data.rect.left)), true)
payload.setUint16(5, Math.round((this.height / data.rect.height) * (data.y - data.rect.top)), true)
break
case 'wheel':
buffer = new ArrayBuffer(4)
payload = new DataView(buffer)
payload.setUint8(0, MOUSE_CLK)
payload.setUint16(1, 1, true)
const ydir = Math.sign(data.y)
const xdir = Math.sign(data.x)
if ((!xdir && !ydir) || (xdir && ydir)) return
if (ydir && ydir < 0) payload.setUint8(3, 4)
if (ydir && ydir > 0) payload.setUint8(3, 5)
if (xdir && xdir < 0) payload.setUint8(3, 6)
if (xdir && xdir > 0) payload.setUint8(3, 7)
break
case 'mousedown':
buffer = new ArrayBuffer(4)
payload = new DataView(buffer)
payload.setUint8(0, MOUSE_DOWN)
payload.setUint16(1, 1, true)
payload.setUint8(3, data.key)
break
case 'mouseup':
buffer = new ArrayBuffer(4)
payload = new DataView(buffer)
payload.setUint8(0, MOUSE_UP)
payload.setUint16(1, 1, true)
payload.setUint8(3, data.key)
break
case 'keydown':
buffer = new ArrayBuffer(5)
payload = new DataView(buffer)
payload.setUint8(0, KEY_DOWN)
payload.setUint16(1, 2, true)
payload.setUint16(3, data.key, true)
break
case 'keyup':
buffer = new ArrayBuffer(5)
payload = new DataView(buffer)
payload.setUint8(0, KEY_UP)
payload.setUint16(1, 2, true)
payload.setUint16(3, data.key, true)
break
}
// @ts-ignore
if (this.channel && typeof buffer !== 'undefined') {
this.channel.send(buffer)
}
}
getAspect() {
const { width, height } = this
if ((height == 0 && width == 0) || (height == 0 && width != 0) || (height != 0 && width == 0)) {
return null
}
if (height == width) {
return {
horizontal: 1,
vertical: 1,
}
}
let dividend = width
let divisor = height
let gcd = -1
if (height > width) {
dividend = height
divisor = width
}
while (gcd == -1) {
const remainder = dividend % divisor
if (remainder == 0) {
gcd = divisor
} else {
dividend = divisor
divisor = remainder
}
}
return {
horizontal: width / gcd,
vertical: height / gcd,
}
}
onMousePos(e: MouseEvent) {
const rect = this._player.getBoundingClientRect() as DOMRect
this.updateControles('mousemove', {
x: e.clientX,
y: e.clientY,
rect,
})
}
onWheel(e: WheelEvent) {
this.onMousePos(e)
this.updateControles('wheel', { x: e.deltaX, y: e.deltaY })
}
onMouseDown(e: MouseEvent) {
this.onMousePos(e)
this.updateControles('mousedown', { key: e.button })
}
onMouseUp(e: MouseEvent) {
this.onMousePos(e)
this.updateControles('mouseup', { key: e.button })
}
onMouseMove(e: MouseEvent) {
this.onMousePos(e)
}
onMouseEnter(e: MouseEvent) {
this._player.focus()
this.focused = true
}
onMouseLeave(e: MouseEvent) {
this.focused = false
}
onKeyDown(e: KeyboardEvent) {
if (!this.focused) {
return
}
this.updateControles('keydown', { key: e.keyCode })
}
onKeyUp(e: KeyboardEvent) {
if (!this.focused) {
return
}
this.updateControles('keyup', { key: e.keyCode })
}
onResise() {
const aspect = this.getAspect()
if (!aspect) {
return
}
const { horizontal, vertical } = aspect
this._container.style.maxWidth = `${(horizontal / vertical) * this._video.offsetHeight}px`
this._aspect.style.paddingBottom = `${(vertical / horizontal) * 100}%`
}
onMessage(e: MessageEvent) {
const { event, ...payload } = JSON.parse(e.data)
switch (event) {
case 'sdp/reply':
if (!this.peer) {
return
}
this.peer.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: payload.sdp }))
break
case 'identity/provide':
this.id = payload.id
this.createPeer()
break
case 'control/requesting':
this.controlling = true
this.$notify({
group: 'neko',
type: 'info',
title: 'Another user is requesting the controls',
duration: 3000,
speed: 1000,
})
break
case 'control/give':
this.controlling = true
this.$notify({
group: 'neko',
type: 'info',
title: 'You have the controls',
duration: 5000,
speed: 1000,
})
break
case 'control/locked':
this.controlling = false
this.$notify({
group: 'neko',
type: 'info',
title: 'Another user has the controls',
duration: 3000,
speed: 1000,
})
break
case 'control/given':
this.controlling = false
this.$notify({
group: 'neko',
type: 'info',
title: 'Someone has taken the controls',
duration: 5000,
speed: 1000,
})
break
case 'control/release':
this.controlling = false
this.$notify({
group: 'neko',
type: 'info',
title: 'You released the controls',
duration: 5000,
speed: 1000,
})
break
case 'control/released':
this.controlling = false
this.$notify({
group: 'neko',
type: 'info',
title: 'The controls have been released',
duration: 5000,
speed: 1000,
})
break
default:
console.warn(`[NEKO] Unknown message event ${event}`)
}
}
onTrack(event: RTCTrackEvent) {
if (event.track.kind === 'audio') {
return
}
this.stream = event.streams[0]
if (!this.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()
}
}
onTimeout() {
this.connected = false
this.connecting = false
this.$notify({
group: 'neko',
type: 'error',
title: 'Unable to connect to server!',
duration: 5000,
speed: 1000,
})
}
onConnecting() {
this.connecting = true
}
onConnected() {
this.connected = true
this.connecting = false
this.$notify({
group: 'neko',
type: 'success',
title: 'Successfully connected!',
duration: 5000,
speed: 1000,
})
if (this.timeout) {
clearTimeout(this.timeout)
}
}
onClose() {
this.controlling = false
this.connected = false
this.connecting = false
this.ws = undefined
this.peer = undefined
if (this.playing) {
this.toggleMedia()
}
this.$notify({
group: 'neko',
type: 'error',
title: 'Disconnected from server!',
duration: 5000,
speed: 1000,
})
}
}
</script>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<path marker-start="none" marker-end="none" class="st0" d="M47.8,95c-1.9-1-0.5-4.7,0.8-8.3c0.5-1.3,1-2.6,1.3-3.9
c0.1-0.1,0.2-0.2,0.2-0.4c0.7-2.6,0.9-6.2,1.2-8.8l0.1-1.1c0-0.4-0.2-0.7-0.6-0.7c-0.4,0-0.7,0.2-0.7,0.6L50,73.5
c-0.1,1.5-0.3,3.3-0.5,5.1c-6.3,1.4-15.7-0.4-18-1.3c0,0-0.1-0.1-0.1-0.1c-0.1,0-0.2-0.1-0.3-0.2c0-0.1-0.1-0.1-0.2-0.2c0,0,0,0,0,0
c-0.1,0-0.2-0.1-0.3-0.1c0,0,0,0,0,0c-1.3-0.7-3-1.8-3.8-2.7c-0.2-0.3-0.7-0.3-0.9-0.1c-0.3,0.2-0.3,0.7-0.1,0.9
c0.9,1.1,2.8,2.3,4.2,3c0.1,2.1,0.9,4.5,1.7,6.8c1.2,3.5,2.4,7.1,0.8,8.7c-0.8,0.8-1.6,1.1-2.5,1c-2.9-0.3-5.9-4.7-6.7-6.1
c-7.9-12.8-8.6-32.9-1.6-44.8c1.3-2.3,3.3-5.5,5-7.3c0.4-0.4,0.9-0.9,1.5-1.5c0.6-0.5,0.9-0.9,1.2-1.1c1.9,0.6,4.2,1.2,6.7,1.2
c1.6,0,3.4-0.2,5-0.9c0.3-0.1,0.5-0.5,0.4-0.9c-0.1-0.3-0.5-0.5-0.9-0.4c-3.9,1.5-8.2,0.6-11.1-0.4c0,0,0,0,0,0
c-2.4-1.2-3.5-1.9-5-4.2c-1.1-1.8-1.4-4.4-1.6-6.9c-0.1-1.3-0.2-2.5-0.4-3.6c-1-4.6-4.4-6.5-7.2-6.6c-3.5-0.2-6.7,1.8-7.8,4.9
c-0.3,1-0.4,2.3-0.4,3.6c-0.1,1.8-0.1,3.8-1,4.6c-0.3,0.3-0.7,0.4-1.2,0.3c-1.4-0.1-2.4-0.7-3.1-2c-1.7-3.2-0.6-9,0.9-11.4
c2.9-4.9,11.6-8.7,17.1-6c1.4,0.7,4.3,2.1,6,5.1l0.3,0.5c0.4,0.7,0.9,1.6,1,2.1c0.2,0.5,0.3,1.3,0.3,1.8c0.1,2.3,0.8,7,2,9.6
c1.5,3,4.2,4.7,6.9,4.3l6.6-1.1c7.4-1,16.6,2,22.2,7.2c0,0,0.1,0,0.1,0.1c0.1,0.1,0.3,0.3,0.5,0.4c3.1,2.6,8.6,12.1,9.6,15.9
c0.1,0.3,0.3,0.5,0.6,0.5c0.1,0,0.1,0,0.2,0c0.4-0.1,0.6-0.5,0.5-0.8c-1-4.1-6.7-13.9-10.1-16.7c0,0,0,0,0,0
c2.9-4.9,9.7-12.7,14.4-12.1c0.4,0.6,0.4,3.6,0.4,5.6c0,1.1,0,2.3,0.1,3.3c0,0.6,0.1,1.2,0.2,1.7c0,0.3,0.2,0.5,0.5,0.5
c1.6,0.4,6.2,4,9,7.4c0.3,0.3,0.6,0.7,0.9,1.2c1.9,2.7,3,6.5,3.2,8.9c0.3,2.9-0.1,5.5-1.1,7.7c1.1,0.6,2.3,1.1,3.5,1.6
c0.5,0.2,1,0.4,1.4,0.6c0.3,0.1,0.5,0.5,0.4,0.9c-0.1,0.3-0.4,0.4-0.6,0.4c-0.1,0-0.2,0-0.3-0.1c-0.5-0.2-0.9-0.4-1.4-0.6
c-1.2-0.5-2.5-1-3.7-1.6c-0.2,0.4-0.5,0.7-0.7,1c1.3,1.3,2.7,2.4,4.1,3.2c0.3,0.2,0.4,0.6,0.2,0.9c-0.1,0.2-0.3,0.3-0.6,0.3
c-0.1,0-0.2,0-0.3-0.1c-1.5-0.9-3-2.1-4.3-3.4c-0.5,0.5-1,1-1.6,1.4c-3.9,3.1-8.8,3.7-9.9,3.5c0.2-2.3,0.2-4.3,0-7.4
c0-0.4-0.3-0.6-0.7-0.6c-0.4,0-0.6,0.3-0.6,0.7c0.3,4.4,0.2,6.6-0.5,10.6c0,0,0,0,0,0c-0.4,1.4-0.8,3-1.3,4.7
c-1.8,6.4-2.8,10.3-2.5,11.5c0,0.1,0.1,0.3,0.2,0.3c1.5,1.1,2.1,2.3,1.7,3.4c-0.4,1.2-1.8,2-3.3,2.1c-1,0-3.3-0.3-4.4-3.5
c-0.7-1.9-0.8-3.5-0.9-5.4c0-0.5-0.1-0.9-0.1-1.4c3.3-2.3,6.9-6,7.8-10.3c0.1-0.4-0.1-0.7-0.5-0.8c-0.4-0.1-0.7,0.1-0.8,0.5
c-1,4.2-4.7,7.9-8,10c-0.2,0.1-0.4,0.2-0.6,0.3c-0.5,0.2-1.4,0.7-2.2,1c1.8-4.1,4.8-13.2,2.7-19.6c-0.1-0.3-0.5-0.5-0.8-0.4
c-0.4,0.1-0.5,0.5-0.4,0.8c2.2,6.6-1.5,16.5-3.2,19.7c0,0,0,0,0,0c-0.7,1-1.4,2.3-2.1,3.7c-1.9,3.7-4.3,8.3-7.6,9.3
C50.7,96,49.3,95.8,47.8,95z M86.8,53.6c2.1-1.3-1.4-4.3-3.4-3.5c-0.5,0.2-0.8,0.5-0.9,0.8C82,52.3,84.9,54.8,86.8,53.6z M92.5,49.8
c0-1-0.7-1.9-1.6-1.9c-0.9,0-1.6,0.8-1.6,1.9c0,1,0.7,1.9,1.6,1.9C91.7,51.7,92.5,50.8,92.5,49.8z M36.9,48.7
c0.1-0.8,0.4-1.5,0.6-2.2c0.6,0.7,1.3,1.3,1.9,1.9l0.4,0.3c0.1,0.1,0.3,0.2,0.5,0.2c0.2,0,0.4-0.1,0.5-0.2c0.3-0.3,0.2-0.7,0-0.9
l-0.4-0.3c-0.7-0.6-1.3-1.3-1.9-1.9c0.9-0.2,1.8-0.3,2.8-0.3c0.4,0,0.7-0.3,0.7-0.7c0-0.4-0.3-0.7-0.7-0.7c-1,0-1.9,0.1-2.8,0.2
c0.3-0.6,0.7-1.2,1.1-1.7c0.2-0.3,0.2-0.7-0.1-0.9c-0.3-0.2-0.7-0.2-0.9,0.1c-0.5,0.7-1,1.4-1.4,2.2c-0.3-0.5-0.6-1-0.9-1.6
c-0.2-0.3-0.5-0.5-0.9-0.3c-0.3,0.1-0.5,0.5-0.3,0.9c0.3,0.7,0.7,1.3,1.1,2c-0.7,0.3-1.4,0.6-2.3,0.9c-0.3,0.1-0.5,0.5-0.3,0.9
c0.1,0.2,0.4,0.4,0.6,0.4c0.1,0,0.2,0,0.3-0.1c0.7-0.3,1.3-0.5,1.8-0.8c-0.2,0.7-0.4,1.5-0.6,2.3c-0.1,0.4,0.2,0.7,0.5,0.8
c0,0,0.1,0,0.1,0C36.6,49.3,36.9,49.1,36.9,48.7z M78.7,44.9c0-1.1-0.8-1.9-1.7-1.9c-0.9,0-1.7,0.9-1.7,1.9c0,1.1,0.7,1.9,1.7,1.9
C78,46.8,78.7,45.9,78.7,44.9z M82.7,31.9c0-0.2,0-0.4-0.1-0.6c0.2-0.1,0.5-0.3,1.2-0.6c1.9-1,7.5-4,8.9-3.4c0.1,0,0.1,0.1,0.1,0.2
c0.3,1.2-0.3,3.3-0.9,5.6c-0.5,1.9-1,3.8-1.1,5.5C88.3,35.6,84.7,32.6,82.7,31.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,358 @@
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font: inherit;
font-size: 100%;
vertical-align: baseline;
}
/* make sure to set some focus styles for accessibility */
:focus {
outline: 0;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-results-button,
input[type=search]::-webkit-search-results-decoration {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type=search] {
-webkit-appearance: none;
-moz-appearance: none;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
/**
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
max-width: 100%;
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
* Known issue: no IE 6 support.
*/
[hidden] {
display: none;
}
/**
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
* `em` units.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-size: 100%; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/**
* Address `outline` inconsistency between Chrome and other browsers.
*/
a:focus {
outline: none;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/**
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
* 2. Improve image quality when scaled in IE 7.
*/
img {
border: 0; /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */
}
/**
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
*/
figure {
margin: 0;
}
/**
* Correct margin displayed oddly in IE 6/7.
*/
form {
margin: 0;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct color not being inherited in IE 6/7/8/9.
* 2. Correct text not wrapping in Firefox 3.
* 3. Correct alignment displayed oddly in IE 6/7.
*/
legend {
border: 0; /* 1 */
padding: 0;
white-space: normal; /* 2 */
*margin-left: -7px; /* 3 */
}
/**
* 1. Correct font size not being inherited in all browsers.
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
* and Chrome.
* 3. Improve appearance and consistency in all browsers.
*/
button,
input,
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
}
/**
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
* Correct `select` style inheritance in Firefox 4+ and Opera.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
* Known issue: inner spacing remains in IE 6.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
*overflow: visible; /* 4 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* 1. Address box sizing set to content-box in IE 8/9.
* 2. Remove excess padding in IE 8/9.
* 3. Remove excess padding in IE 7.
* Known issue: excess padding remains in IE 6.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
}
/**
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Remove inner padding and border in Firefox 3+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
* 2. Improve readability and alignment in all browsers.
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
html,
button,
input,
select,
textarea {
color: #222;
}
::-moz-selection {
text-shadow: none;
}
::selection {
text-shadow: none;
}
img {
vertical-align: middle;
}
fieldset {
border: 0;
margin: 0;
padding: 0;
}
textarea {
resize: vertical;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}

View File

@ -0,0 +1,10 @@
$style-dark: #2c2c2c;
$style-darker: #1a1a1a;
$style-light: #fafafa;
$style-primary: #19bd9c;
$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;

View File

@ -0,0 +1,23 @@
@charset "utf-8";
// Import variables
@import "variables";
// Reset CSS
@import "reset";
// Import Vendor
@import "vendor/font-awesome";
html, body {
-webkit-font-smoothing: subpixel-antialiased;
background-color: $style-dark;
font-family: $style-font-family;
font-size: $style-font-size;
color: $style-font-color;
overflow: hidden;
width: 100vw;
height: 100vh;
min-width: 320px;
}

View File

@ -0,0 +1,20 @@
// Variables
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
$fa-font-size-base: 16px;
$fa-font-display: auto;
$fa-css-prefix: fa;
$fa-border-color: #eee;
$fa-inverse: #fff;
$fa-li-width: 2em;
$fa-fw-width: (20em / 16);
$fa-primary-opacity: 1;
$fa-secondary-opacity: .4;
$fa-family-default: 'Font Awesome 5 Free';
// Import FA source files
@import "~@fortawesome/fontawesome-free/scss/brands";
@import "~@fortawesome/fontawesome-free/scss/solid";
@import "~@fortawesome/fontawesome-free/scss/regular";
@import "~@fortawesome/fontawesome-free/scss/fontawesome";

12
client/src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import './assets/styles/main.scss'
import Vue from 'vue'
import Notifications from 'vue-notification'
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(Notifications)
new Vue({
render: h => h(App),
}).$mount('#neko')

1
client/src/types/shims-scss.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '*.scss' {}

13
client/src/types/shims-tsx.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}

4
client/src/types/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}