first commit
2
client/.browserslistrc
Normal file
@ -0,0 +1,2 @@
|
||||
> 1%
|
||||
last 2 versions
|
9
client/.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
16
client/.eslintrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser"
|
||||
},
|
||||
"rules": {
|
||||
"no-case-declarations": "off",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-console": "off",
|
||||
}
|
||||
}
|
||||
|
8
client/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"vueIndentScriptAndStyle": true
|
||||
}
|
25
client/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false,
|
||||
"files.encoding": "utf8",
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"todo-tree.filtering.excludeGlobs": ["**/node_modules/**"],
|
||||
"eslint.validate": [
|
||||
"vue",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
],
|
||||
"vetur.validation.template": true,
|
||||
"vetur.useWorkspaceDependencies": true,
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "ui"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
0
client/README.md
Normal file
34
client/package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "neko-client",
|
||||
"version": "1.0.0",
|
||||
"description": "Client for neko streaming server",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||
"eventemitter3": "^4.0.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-class-component": "^7.0.2",
|
||||
"vue-notification": "^1.3.20",
|
||||
"vue-property-decorator": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-eslint": "^4.1.0",
|
||||
"@vue/cli-plugin-typescript": "^4.1.0",
|
||||
"@vue/cli-plugin-vuex": "^4.1.0",
|
||||
"@vue/cli-service": "^4.1.0",
|
||||
"@vue/eslint-config-prettier": "^5.0.0",
|
||||
"@vue/eslint-config-typescript": "^4.0.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"prettier": "^1.19.1",
|
||||
"sass-loader": "^8.0.0",
|
||||
"typescript": "~3.5.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
}
|
||||
}
|
BIN
client/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
client/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
client/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
9
client/public/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#19bd9c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
client/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 662 B |
BIN
client/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1003 B |
23
client/public/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>n.eko</title>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#19bd9c">
|
||||
<meta name="msapplication-TileColor" content="#19bd9c">
|
||||
<meta name="theme-color" content="#19bd9c">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but test doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="neko"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
BIN
client/public/mstile-144x144.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
client/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
client/public/mstile-310x150.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
client/public/mstile-310x310.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
client/public/mstile-70x70.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
73
client/public/safari-pinned-tab.svg
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M947 6714 c-329 -50 -672 -278 -782 -519 -15 -33 -31 -67 -35 -75 -6
|
||||
-14 -38 -133 -45 -170 -7 -39 -19 -180 -18 -222 5 -248 89 -392 243 -415 72
|
||||
-11 93 -8 122 22 42 41 53 96 63 300 10 218 29 297 91 383 103 141 273 226
|
||||
454 225 235 -1 423 -138 507 -369 28 -79 44 -179 59 -378 17 -229 60 -380 138
|
||||
-490 97 -136 236 -231 413 -281 237 -68 463 -71 656 -11 52 17 79 11 91 -20
|
||||
26 -68 -174 -126 -409 -119 -133 4 -169 9 -334 51 -9 2 -38 9 -64 16 l-48 12
|
||||
-122 -125 c-201 -204 -418 -542 -540 -839 -55 -135 -119 -349 -141 -473 -3
|
||||
-15 -7 -35 -10 -45 -2 -9 -7 -39 -11 -67 -3 -27 -8 -61 -10 -75 -34 -182 -41
|
||||
-701 -11 -890 2 -14 7 -50 11 -80 10 -86 15 -123 21 -150 3 -14 8 -36 10 -50
|
||||
2 -14 7 -36 10 -50 3 -14 8 -36 10 -50 26 -143 133 -475 200 -617 19 -39 34
|
||||
-74 34 -77 0 -14 127 -236 183 -319 82 -123 233 -274 312 -313 112 -54 216
|
||||
-39 282 41 40 48 53 95 48 179 -5 106 -14 139 -105 421 -38 116 -74 230 -81
|
||||
255 -7 25 -13 47 -14 50 -1 3 -5 25 -9 50 -4 25 -9 54 -11 65 -2 11 -4 30 -4
|
||||
42 -1 12 -9 24 -21 28 -33 11 -193 120 -238 162 -47 45 -56 85 -22 103 24 13
|
||||
29 11 100 -51 50 -43 211 -149 225 -149 4 0 21 -10 38 -23 18 -13 57 -31 87
|
||||
-40 67 -19 292 -63 375 -73 33 -3 69 -8 80 -10 138 -27 594 -23 695 6 16 5 33
|
||||
9 37 9 7 0 8 3 17 87 4 32 9 75 11 94 2 19 7 69 10 110 7 93 16 163 22 172 2
|
||||
5 17 8 33 8 35 0 50 -23 45 -71 -2 -19 -6 -65 -10 -104 -3 -38 -8 -83 -10
|
||||
-100 -2 -16 -7 -57 -10 -90 -8 -86 -15 -142 -25 -205 -5 -30 -12 -71 -15 -90
|
||||
-7 -43 -14 -69 -24 -95 -5 -11 -25 -67 -44 -125 -19 -58 -60 -175 -89 -260
|
||||
-70 -200 -86 -263 -83 -341 2 -83 23 -121 77 -147 108 -50 177 -50 290 -1 144
|
||||
63 229 141 346 317 23 35 42 66 42 69 0 3 8 17 19 31 10 15 56 99 101 187 46
|
||||
88 102 191 124 229 73 122 84 144 129 259 45 113 94 256 101 294 3 12 14 56
|
||||
25 99 71 274 84 536 35 745 -23 97 -18 118 28 119 28 0 45 -38 66 -140 31
|
||||
-154 26 -368 -13 -605 -21 -125 -100 -397 -160 -550 -19 -50 -35 -93 -35 -97
|
||||
0 -18 260 118 290 151 3 3 26 23 51 43 190 153 335 354 397 552 22 69 47 86
|
||||
84 56 l23 -18 -18 -68 c-29 -113 -108 -258 -201 -373 -48 -59 -196 -201 -261
|
||||
-251 -78 -59 -72 -39 -62 -210 21 -327 126 -523 297 -555 68 -12 88 -12 142 5
|
||||
99 30 168 105 168 180 0 45 -35 106 -91 160 -53 51 -62 79 -48 154 4 22 10 55
|
||||
12 71 17 97 60 268 152 600 103 370 109 392 120 485 3 25 8 56 12 70 3 14 7
|
||||
150 8 303 1 231 4 280 16 287 15 10 47 4 64 -12 6 -5 11 -113 12 -276 l1 -267
|
||||
59 3 c226 10 529 142 698 303 l40 38 50 -46 c64 -58 192 -156 236 -181 37 -21
|
||||
72 -16 82 12 11 28 -3 44 -86 100 -45 30 -113 83 -150 118 l-68 63 28 34 29
|
||||
34 40 -21 c21 -11 105 -48 186 -82 122 -52 150 -60 167 -50 26 13 27 52 3 71
|
||||
-19 17 -32 23 -157 75 -76 31 -183 81 -193 89 -1 1 11 42 27 91 39 118 55 242
|
||||
48 378 -3 61 -8 122 -11 136 -3 14 -7 36 -10 50 -29 159 -115 376 -197 497
|
||||
-70 104 -211 256 -349 374 -158 137 -227 183 -370 249 -24 11 -36 132 -39 384
|
||||
-2 212 -6 290 -16 341 -2 11 -4 26 -4 34 -1 11 -15 13 -68 8 -165 -15 -344
|
||||
-127 -561 -349 -137 -141 -299 -346 -368 -465 l-18 -33 35 -32 c146 -136 384
|
||||
-495 539 -813 126 -259 154 -364 100 -378 -32 -8 -55 21 -85 107 -76 221 -303
|
||||
621 -493 869 -178 233 -477 448 -792 572 -87 34 -200 72 -239 80 -9 2 -54 12
|
||||
-101 23 -263 62 -456 62 -815 1 -146 -24 -283 -42 -330 -42 -73 0 -88 3 -158
|
||||
37 -187 90 -286 253 -351 579 -29 146 -34 179 -50 383 -14 174 -169 410 -352
|
||||
536 -74 51 -248 136 -293 143 -19 3 -38 8 -42 11 -20 12 -201 11 -282 -2z
|
||||
m1833 -2648 c0 -12 -18 -47 -40 -80 -22 -32 -40 -62 -40 -66 0 -4 15 -6 33 -3
|
||||
106 12 173 11 190 -4 9 -9 17 -23 17 -32 -1 -36 -18 -44 -124 -56 -59 -7 -109
|
||||
-14 -112 -17 -6 -7 134 -148 146 -148 15 0 22 -38 10 -60 -22 -41 -61 -26
|
||||
-151 59 l-84 80 -18 -52 c-19 -54 -25 -81 -26 -108 -1 -23 -35 -30 -60 -12
|
||||
-24 16 -26 41 -7 103 7 25 16 57 20 72 l6 27 -72 -30 c-59 -24 -77 -28 -96
|
||||
-19 -19 8 -23 17 -20 42 3 31 7 34 85 63 45 16 84 33 88 36 3 4 -13 41 -36 83
|
||||
-36 67 -40 81 -30 102 13 29 51 32 73 7 8 -10 27 -39 42 -65 15 -27 29 -48 31
|
||||
-48 2 0 18 24 35 54 17 30 44 68 59 85 26 28 32 30 55 20 16 -8 26 -20 26 -33z
|
||||
m2668 -96 c33 -20 62 -71 62 -109 0 -12 -7 -38 -16 -60 -54 -135 -243 -72
|
||||
-220 74 16 96 99 141 174 95z m957 -333 c11 -4 30 -22 43 -41 38 -56 30 -135
|
||||
-19 -182 -88 -84 -208 21 -168 148 22 67 78 96 144 75z m-420 -161 c131 -61
|
||||
175 -184 83 -232 -35 -18 -109 -18 -153 0 -49 21 -122 92 -135 132 -34 102 81
|
||||
158 205 100z"/>
|
||||
<path d="M6425 5089 c-95 -14 -204 -58 -454 -184 -171 -86 -188 -96 -184 -117
|
||||
4 -16 23 -32 63 -51 93 -47 226 -152 373 -298 121 -121 137 -134 137 -110 1
|
||||
54 13 113 72 346 64 255 83 391 57 411 -9 6 -33 7 -64 3z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
19
client/public/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "n.eko",
|
||||
"short_name": "n.eko",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#19bd9c",
|
||||
"background_color": "#19bd9c",
|
||||
"display": "standalone"
|
||||
}
|
847
client/src/App.vue
Normal 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>
|
40
client/src/assets/logo.svg
Normal 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 |
358
client/src/assets/styles/_reset.scss
Normal 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;
|
||||
}
|
10
client/src/assets/styles/_variables.scss
Normal 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;
|
23
client/src/assets/styles/main.scss
Normal 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;
|
||||
}
|
||||
|
20
client/src/assets/styles/vendor/_font-awesome.scss
vendored
Normal 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
@ -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
@ -0,0 +1 @@
|
||||
declare module '*.scss' {}
|
13
client/src/types/shims-tsx.d.ts
vendored
Normal 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
@ -0,0 +1,4 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
37
client/tsconfig.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
11
client/vue.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
css: {
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
prependData: `
|
||||
@import "@/assets/styles/_variables.scss";
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|