Archived
2
0

Merge branch 'dev' into sk_lang

This commit is contained in:
m1k1o
2021-04-03 15:20:27 +02:00
114 changed files with 7445 additions and 893 deletions

View File

@ -15,53 +15,55 @@
"scripts": {
"serve": "vue-cli-service serve --mode development",
"build": "vue-cli-service build",
"build:lib": "vue-cli-service build --target lib --name neko-lib 'src/lib.ts'",
"build:emoji": "ts-node --files --project tools/tsconfig.json tools/emoji.ts",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"animejs": "^3.1.0",
"axios": "^0.19.1",
"date-fns": "^2.11.1",
"emoji-datasource": "^5.0.1",
"emojilib": "^2.4.0",
"eventemitter3": "^4.0.0",
"@fortawesome/fontawesome-free": "^5.14.0",
"animejs": "^3.2.0",
"axios": "^0.21.1",
"date-fns": "^2.16.1",
"emoji-datasource": "^6.0.1",
"emojilib": "^3.0.1",
"eventemitter3": "^4.0.7",
"resize-observer-polyfill": "^1.5.1",
"simple-markdown": "^0.7.2",
"sweetalert2": "^9.10.9",
"typed-vuex": "^0.1.17",
"sweetalert2": "^10.15.7",
"typed-vuex": "^0.1.21",
"v-tooltip": "^2.0.3",
"vue": "^2.6.10",
"vue-class-component": "^7.2.3",
"vue": "^2.6.12",
"vue-class-component": "^7.2.6",
"vue-clickaway": "^2.2.2",
"vue-context": "^5.1.0",
"vue-i18n": "^8.16.0",
"vue-context": "^5.2.0",
"vue-i18n": "^8.21.1",
"vue-notification": "^1.3.20",
"vue-property-decorator": "^8.4.1",
"vuex": "^3.1.3"
"vue-property-decorator": "^9.1.2",
"vuex": "^3.5.1"
},
"devDependencies": {
"@types/animejs": "^3.1.0",
"@types/node": "^13.7.0",
"@types/animejs": "^3.1.2",
"@types/node": "^14.14.37",
"@types/vue": "^2.0.0",
"@types/vue-clickaway": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@vue/cli-plugin-babel": "^4.1.0",
"@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",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"@vue/cli-plugin-babel": "^4.5.6",
"@vue/cli-plugin-eslint": "^4.5.6",
"@vue/cli-plugin-typescript": "^4.5.6",
"@vue/cli-plugin-vuex": "^4.5.6",
"@vue/cli-service": "^4.5.12",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/eslint-config-typescript": "^7.0.0",
"core-js": "^3.9.1",
"eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.12.0",
"prettier": "^2.0.2",
"sass-loader": "^8.0.0",
"ts-node": "^8.6.2",
"typescript": "^3.8.3",
"vue-template-compiler": "^2.6.10"
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-vue": "^7.8.0",
"node-sass": "^5.0.0",
"prettier": "^2.1.2",
"sass-loader": "^10.1.1",
"ts-node": "^9.1.1",
"typescript": "^4.2.3",
"vue-template-compiler": "^2.6.12"
}
}

View File

@ -5,13 +5,14 @@
<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">
<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">
<style> /* weird iOS bug, if this is not set right here, video just does not start */ .video-container { width: 100%; height: 100%; } </style>
</head>
<body>
<noscript>

View File

@ -29,7 +29,7 @@
<neko-side v-if="side" />
<neko-connect v-if="!connected" />
<neko-about v-if="about" />
<notifications group="neko" position="top left" style="top: 50px;" />
<notifications group="neko" position="top left" :ignoreDuplicates="true" style="top: 50px;pointer-events: none" />
</template>
</div>
</template>
@ -109,25 +109,29 @@
}
@media only screen and (max-width: 600px) {
#neko {
&.expanded {
.neko-main {
transform: translateX(-$side-width);
}
.neko-menu {
transform: translateX(-$side-width);
#neko.expanded {
.neko-main {
transform: translateX(calc(-100% + 65px));
video {
display: none;
}
}
.neko-menu {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 65px;
width: calc(100% - 65px);
}
}
}
@media only screen and (max-width: 768px) {
#neko {
.neko-main {
.room-container {
display: none;
}
}
#neko .neko-main .room-container {
display: none;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<!--
<img :src="`https://ui-avatars.com/api/?name=${seed}&size=${size}`" />
-->
<div
class="avatar"
:style="{
width: size + 'px',
height: size + 'px',
lineHeight: size + 'px',
fontSize: size / 2 + 'px',
backgroundColor: Background(seed),
}"
>
{{ seed.substring(0, 2).toUpperCase() }}
</div>
</template>
<style lang="scss" scoped>
.avatar {
user-select: none;
text-align: center;
background: white;
color: black;
display: inline-block;
overflow: hidden;
border-radius: 50%;
}
</style>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component({
name: 'neko-avatar',
})
export default class extends Vue {
@Prop(String) readonly seed: string | undefined
@Prop(Number) readonly size: number | undefined
Background(seed: string) {
let a = 0,
b = 0,
c = 0
for (let i = 0; i < seed.length; i++) {
a += seed.charCodeAt(i) * 3
b += seed.charCodeAt(i) * 5
c += seed.charCodeAt(i) * 7
}
let x = Math.floor(128 + (a % 128))
let y = Math.floor(128 + (b % 128))
let z = Math.floor(128 + (c % 128))
return 'rgb(' + x + ',' + y + ',' + z + ')'
}
}
</script>

View File

@ -4,7 +4,7 @@
<template v-for="(message, index) in history">
<li :key="index" class="message" v-if="message.type === 'text'">
<div class="author" @contextmenu.stop.prevent="onContext($event, { member: member(message.id) })">
<img :src="`https://api.adorable.io/avatars/40/${member(message.id).displayname}.png`" />
<neko-avatar class="avatar" :seed="member(message.id).displayname" :size="40" />
</div>
<div class="content">
<div class="content-head">
@ -106,7 +106,7 @@
background: $style-primary;
margin: 0px 10px 10px 0px;
img {
.avatar {
width: 100%;
}
}
@ -329,6 +329,7 @@
import Markdown from './markdown'
import Content from './context.vue'
import Emoji from './emoji.vue'
import Avatar from './avatar.vue'
const length = 512 // max length of message
@ -338,6 +339,7 @@
'neko-markdown': Markdown,
'neko-context': Content,
'neko-emoji': Emoji,
'neko-avatar': Avatar,
},
})
export default class extends Vue {

View File

@ -0,0 +1,75 @@
<template>
<div class="clipboard" v-if="opened" @click="$event.stopPropagation()">
<textarea ref="textarea" v-model="clipboard" @focus="$event.target.select()" />
</div>
</template>
<style lang="scss" scoped>
.clipboard {
background-color: $background-primary;
border-radius: 0.25rem;
display: block;
padding: 5px;
position: absolute;
bottom: 10px;
right: 10px;
&,
textarea {
max-width: 320px;
width: 100%;
max-height: 120px;
height: 100%;
}
textarea {
border: 0;
color: $text-normal;
background: none;
&::selection {
background: $text-normal;
}
}
}
</style>
<script lang="ts">
import { Component, Ref, Vue } from 'vue-property-decorator'
@Component({
name: 'neko-clipboard',
})
export default class extends Vue {
@Ref('textarea') readonly _textarea!: HTMLTextAreaElement
private opened: boolean = false
private typing: any = null
get clipboard() {
return this.$accessor.remote.clipboard
}
set clipboard(data: string) {
this.$accessor.remote.setClipboard(data)
if (this.typing) {
clearTimeout(this.typing)
}
this.typing = setTimeout(() => this.$accessor.remote.sendClipboard(this.clipboard), 500)
}
open() {
this.opened = true
document.body.addEventListener('click', this.close)
setTimeout(() => this._textarea.focus(), 0)
}
close() {
this.opened = false
document.body.removeEventListener('click', this.close)
}
}
</script>

View File

@ -6,9 +6,10 @@
<span><b>n</b>.eko</span>
</div>
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
<span>{{ $t('connect.title') }}</span>
<span v-if="!autoPassword">{{ $t('connect.login_title') }}</span>
<span v-else>{{ $t('connect.invitation_title') }}</span>
<input type="text" :placeholder="$t('connect.displayname')" v-model="displayname" />
<input type="password" :placeholder="$t('connect.password')" v-model="password" />
<input type="password" :placeholder="$t('connect.password')" v-model="password" v-if="!autoPassword" />
<button type="submit" @click.stop.prevent="login">
{{ $t('connect.connect') }}
</button>
@ -150,12 +151,21 @@
@Component({ name: 'neko-connect' })
export default class extends Vue {
private displayname = ''
private password = ''
private autoPassword: string | null = new URL(location.href).searchParams.get('pwd')
private displayname: string = ''
private password: string = ''
mounted() {
if (this.$accessor.displayname !== '' && this.$accessor.password !== '') {
this.$accessor.login({ displayname: this.$accessor.displayname, password: this.$accessor.password })
let password = this.$accessor.password
if (this.autoPassword !== null) {
this.removeUrlParam('pwd')
password = this.autoPassword
}
if (this.$accessor.displayname !== '' && password !== '') {
this.$accessor.login({ displayname: this.$accessor.displayname, password })
this.autoPassword = null
}
}
@ -163,8 +173,44 @@
return this.$accessor.connecting
}
login() {
this.$accessor.login({ displayname: this.displayname, password: this.password })
removeUrlParam(param: string) {
let url = document.location.href
let urlparts = url.split('?')
if (urlparts.length >= 2) {
let urlBase = urlparts.shift()
let queryString = urlparts.join('?')
let prefix = encodeURIComponent(param) + '='
let pars = queryString.split(/[&;]/g)
for (let i = pars.length; i-- > 0; ) {
if (pars[i].lastIndexOf(prefix, 0) !== -1) {
pars.splice(i, 1)
}
}
url = urlBase + (pars.length > 0 ? '?' + pars.join('&') : '')
window.history.pushState('', document.title, url)
}
}
async login() {
let password = this.password
if (this.autoPassword !== null) {
password = this.autoPassword
}
try {
await this.$accessor.login({ displayname: this.displayname, password })
this.autoPassword = null
} catch (err) {
this.$swal({
title: this.$t('connect.error') as string,
text: err.message,
icon: 'error',
})
}
}
}
</script>

View File

@ -3,7 +3,7 @@
<template slot-scope="child" v-if="child.data">
<li class="header">
<div class="user">
<img :src="`https://api.adorable.io/avatars/25/${child.data.member.displayname}.png`" />
<neko-avatar class="avatar" :seed="child.data.member.displayname" :size="25" />
<strong>{{ child.data.member.displayname }}</strong>
</div>
</li>
@ -39,10 +39,10 @@
<template v-if="admin && !child.data.member.admin">
<li class="seperator" />
<li>
<span @click="kick(child.data.member)" style="color: #f04747;">{{ $t('context.kick') }}</span>
<span @click="kick(child.data.member)" style="color: #f04747">{{ $t('context.kick') }}</span>
</li>
<li>
<span @click="ban(child.data.member)" style="color: #f04747;">{{ $t('context.ban') }}</span>
<span @click="ban(child.data.member)" style="color: #f04747">{{ $t('context.ban') }}</span>
</li>
</template>
</template>
@ -80,7 +80,7 @@
align-content: center;
padding: 5px 0;
img {
.avatar {
width: 25px;
height: 25px;
border-radius: 50%;
@ -137,11 +137,13 @@
// @ts-ignore
import { VueContext } from 'vue-context'
import Avatar from './avatar.vue'
@Component({
name: 'neko-context',
components: {
'vue-context': VueContext,
'neko-avatar': Avatar,
},
})
export default class extends Vue {

View File

@ -4,7 +4,7 @@
<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.displayname}.png`" />
<neko-avatar class="avatar" :seed="member.displayname" :size="50" />
</div>
</li>
<template v-for="(member, index) in members">
@ -13,11 +13,11 @@
:key="index"
v-tooltip="{ content: member.displayname, placement: 'bottom', offset: -15, boundariesElement: 'body' }"
>
<div :class="[{ host: member.id === host, admin: member.admin }, 'member']">
<img
:src="`https://api.adorable.io/avatars/50/${member.displayname}.png`"
@contextmenu.stop.prevent="onContext($event, { member })"
/>
<div
:class="[{ host: member.id === host, admin: member.admin }, 'member']"
@contextmenu.stop.prevent="onContext($event, { member })"
>
<neko-avatar class="avatar" :seed="member.displayname" :size="50" />
</div>
</li>
</template>
@ -130,7 +130,7 @@
border-radius: 50%;
}
img {
.avatar {
border-radius: 50%;
overflow: hidden;
width: 100%;
@ -161,11 +161,13 @@
import { Member } from '~/neko/types'
import Content from './context.vue'
import Avatar from './avatar.vue'
@Component({
name: 'neko-members',
components: {
'neko-context': Content,
'neko-avatar': Avatar,
},
})
export default class extends Vue {

View File

@ -13,6 +13,13 @@
v-if="admin"
/>
</li>
<li>
<select v-model="$i18n.locale">
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
{{ lang }}
</option>
</select>
</li>
</ul>
</template>
@ -28,10 +35,33 @@
}
}
}
select {
appearance: none;
background-color: $background-tertiary;
border: 1px solid $background-primary;
color: white;
cursor: pointer;
border-radius: 5px;
height: 24px;
vertical-align: text-bottom;
display: inline-block;
option {
font-weight: normal;
color: $text-normal;
background-color: $background-tertiary;
}
&:hover {
border: 1px solid $background-primary;
}
}
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { messages } from '~/locale'
@Component({ name: 'neko-menu' })
export default class extends Vue {
@ -39,6 +69,10 @@
return this.$accessor.user.admin
}
get langs() {
return Object.keys(messages)
}
about() {
this.$accessor.client.toggleAbout()
}

View File

@ -39,15 +39,24 @@
<span>{{ $t('setting.keyboard_layout') }}</span>
<label class="select">
<select v-model="keyboard_layout">
<option
v-for="(name, code) in keyboard_layouts_list"
:key="code"
:value="code"
>{{ name }}</option>
<option v-for="(name, code) in keyboard_layouts_list" :key="code" :value="code">{{ name }}</option>
</select>
<span />
</label>
</li>
<template v-if="admin">
<li>
<span>{{ $t('setting.broadcast_is_active') }}</span>
<label class="switch">
<input type="checkbox" v-model="broadcast_is_active" />
<span />
</label>
</li>
<li>
<span>{{ $t('setting.broadcast_url') }}</span>
<input v-model="broadcast_url" :disabled="broadcast_is_active" class="input" />
</li>
</template>
<li v-if="connected">
<button @click.stop.prevent="logout">{{ $t('logout') }}</button>
</li>
@ -198,20 +207,33 @@
.select {
max-width: 120px;
text-align: right;
select:hover {
border: 1px solid $background-secondary;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: block;
width: 100%;
max-width: 100%;
padding: 4px;
height: 30px;
text-align: right;
padding: 0 5px 0 10px;
margin: 0;
line-height: 30px;
font-weight: bold;
border: 0;
border-radius: 12px;
color: black;
background-color: $style-primary;
font-size: 12px;
text-overflow: ellipsis;
border: 1px solid transparent;
border-radius: 5px;
color: white;
background-color: $background-tertiary;
font-weight: lighter;
cursor: pointer;
option {
font-weight: normal;
@ -220,6 +242,30 @@
}
}
}
.input {
display: block;
height: 30px;
text-align: right;
padding: 0 10px;
margin-left: 10px;
line-height: 30px;
text-overflow: ellipsis;
border: 1px solid transparent;
border-radius: 5px;
color: white;
background-color: $background-tertiary;
font-weight: lighter;
user-select: auto;
&::selection {
background: $text-normal;
}
&[disabled] {
background: none;
}
}
}
}
}
@ -230,6 +276,12 @@
@Component({ name: 'neko-settings' })
export default class extends Vue {
private broadcast_url: string = ''
get admin() {
return this.$accessor.user.admin
}
get connected() {
return this.$accessor.connected
}
@ -282,6 +334,27 @@
return this.$accessor.settings.keyboard_layout
}
get broadcast_is_active() {
return this.$accessor.settings.broadcast_is_active
}
set broadcast_is_active(value: boolean) {
if (value) {
this.$accessor.settings.broadcastCreate(this.broadcast_url)
} else {
this.$accessor.settings.broadcastDestroy()
}
}
get broadcast_url_remote() {
return this.$accessor.settings.broadcast_url
}
@Watch('broadcast_url_remote', { immediate: true })
onBroadcastUrlChange() {
this.broadcast_url = this.broadcast_url_remote
}
set keyboard_layout(value: string) {
this.$accessor.settings.setKeyboardLayout(value)
this.$accessor.remote.changeKeyboard()

View File

@ -2,7 +2,7 @@
<div ref="component" class="video">
<div ref="player" class="player">
<div ref="container" class="player-container">
<video ref="video" />
<video ref="video" playsinline />
<div class="emotes">
<template v-for="(emote, index) in emotes">
<neko-emote :id="index" :key="index" />
@ -26,11 +26,31 @@
</div>
<div ref="aspect" class="player-aspect" />
</div>
<ul v-if="!fullscreen" class="video-menu">
<ul v-if="!fullscreen" class="video-menu top">
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
<li v-if="admin"><i @click.stop.prevent="onResolution" class="fas fa-desktop"></i></li>
<li class="request-control">
<i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
@click.stop.prevent="toggleControl"
/>
</li>
</ul>
<neko-resolution ref="resolution" />
<ul v-if="!fullscreen" class="video-menu bottom">
<li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)">
<i @click.stop.prevent="onClipboard" class="fas fa-clipboard"></i>
</li>
<li>
<i
v-if="pip_available"
@click.stop.prevent="requestPictureInPicture"
v-tooltip="{ content: 'Picture-in-Picture', placement: 'left', offset: 5, boundariesElement: 'body' }"
class="fas fa-external-link-alt"
/>
</li>
</ul>
<neko-resolution ref="resolution" v-if="admin" />
<neko-clipboard ref="clipboard" v-if="hosting && (!clipboard_read_available || !clipboard_write_available)" />
</div>
</div>
</template>
@ -49,7 +69,14 @@
.video-menu {
position: absolute;
right: 20px;
top: 15px;
&.top {
top: 15px;
}
&.bottom {
bottom: 15px;
}
li {
margin: 0 0 10px 0;
@ -64,6 +91,28 @@
text-align: center;
color: rgba($color: #fff, $alpha: 0.6);
cursor: pointer;
&.faded {
color: rgba($color: $text-normal, $alpha: 0.4);
}
&.disabled {
color: rgba($color: $style-error, $alpha: 0.4);
}
}
&.request-control {
display: none;
}
@media (max-width: 768px) {
&.request-control {
display: inline-block;
}
}
&:last-child {
margin: 0;
}
}
}
@ -139,7 +188,9 @@
import Emote from './emote.vue'
import Resolution from './resolution.vue'
import Clipboard from './clipboard.vue'
// @ts-ignore
import GuacamoleKeyboard from '~/utils/guacamole-keyboard.ts'
@Component({
@ -147,6 +198,7 @@
components: {
'neko-emote': Emote,
'neko-resolution': Resolution,
'neko-clipboard': Clipboard,
},
})
export default class extends Vue {
@ -157,6 +209,7 @@
@Ref('player') readonly _player!: HTMLElement
@Ref('video') readonly _video!: HTMLVideoElement
@Ref('resolution') readonly _resolution!: any
@Ref('clipboard') readonly _clipboard!: any
private keyboard = GuacamoleKeyboard()
private observer = new ResizeObserver(this.onResise.bind(this))
@ -179,6 +232,10 @@
return this.$accessor.remote.hosting
}
get hosted() {
return this.$accessor.remote.hosted
}
get volume() {
return this.$accessor.video.volume
}
@ -219,6 +276,19 @@
return this.$accessor.settings.scroll_invert
}
get pip_available() {
//@ts-ignore
return typeof document.createElement('video').requestPictureInPicture === 'function'
}
get clipboard_read_available() {
return 'clipboard' in navigator && typeof navigator.clipboard.readText === 'function'
}
get clipboard_write_available() {
return 'clipboard' in navigator && typeof navigator.clipboard.writeText === 'function'
}
get clipboard() {
return this.$accessor.remote.clipboard
}
@ -292,7 +362,7 @@
@Watch('clipboard')
onClipboardChanged(clipboard: string) {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
if (this.clipboard_write_available) {
navigator.clipboard.writeText(clipboard).catch(console.error)
}
}
@ -328,13 +398,12 @@
this.$accessor.video.setPlayable(false)
})
this._video.addEventListener('error', event => {
this._video.addEventListener('error', (event) => {
this.$log.error(event.error)
this.$accessor.video.setPlayable(false)
})
document.addEventListener('focusin', this.onFocus.bind(this))
document.addEventListener('focusout', this.onBlur.bind(this))
/* Initialize Guacamole Keyboard */
this.keyboard.onkeydown = (key: number) => {
@ -359,7 +428,6 @@
this.observer.disconnect()
this.$accessor.video.setPlayable(false)
document.removeEventListener('focusin', this.onFocus.bind(this))
document.removeEventListener('focusout', this.onBlur.bind(this))
/* Guacamole Keyboard does not provide destroy functions */
}
@ -374,7 +442,7 @@
.then(() => {
this.onResise()
})
.catch(err => this.$log.error)
.catch((err) => this.$log.error)
} catch (err) {
this.$log.error(err)
}
@ -400,8 +468,37 @@
}
}
toggleControl() {
if (!this.playable) {
return
}
this.$accessor.remote.toggle()
}
requestFullscreen() {
this._player.requestFullscreen()
if (typeof this._player.requestFullscreen === 'function') {
this._player.requestFullscreen()
//@ts-ignore
} else if (typeof this._player.webkitRequestFullscreen === 'function') {
//@ts-ignore
this._player.webkitRequestFullscreen()
//@ts-ignore
} else if (typeof this._player.webkitEnterFullscreen === 'function') {
//@ts-ignore
this._player.webkitEnterFullscreen()
//@ts-ignore
} else if (typeof this._player.msRequestFullScreen === 'function') {
//@ts-ignore
this._player.msRequestFullScreen()
}
this.onResise()
}
requestPictureInPicture() {
//@ts-ignore
this._video.requestPictureInPicture()
this.onResise()
}
@ -410,10 +507,10 @@
return
}
if (this.hosting && navigator.clipboard && typeof navigator.clipboard.readText === 'function') {
if (this.hosting && this.clipboard_read_available) {
navigator.clipboard
.readText()
.then(text => {
.then((text) => {
if (this.clipboard !== text) {
this.$accessor.remote.setClipboard(text)
this.$accessor.remote.sendClipboard(text)
@ -423,14 +520,6 @@
}
}
onBlur() {
if (!this.focused || !this.hosting || this.locked) {
return
}
this.keyboard.reset()
}
onMousePos(e: MouseEvent) {
const { w, h } = this.$accessor.video.resolution
const rect = this._overlay.getBoundingClientRect()
@ -480,16 +569,34 @@
if (!this.hosting || this.locked) {
return
}
this.onMousePos(e)
}
onMouseEnter(e: MouseEvent) {
if (this.hosting) {
this.$accessor.remote.syncKeyboardModifierState({
capsLock: e.getModifierState('CapsLock'),
numLock: e.getModifierState('NumLock'),
scrollLock: e.getModifierState('ScrollLock'),
})
}
this._overlay.focus()
this.onFocus()
this.focused = true
}
onMouseLeave(e: MouseEvent) {
if (this.hosting) {
this.$accessor.remote.setKeyboardModifierState({
capsLock: e.getModifierState('CapsLock'),
numLock: e.getModifierState('NumLock'),
scrollLock: e.getModifierState('ScrollLock'),
})
}
this.keyboard.reset()
this.focused = false
}
@ -512,5 +619,9 @@
onResolution(event: MouseEvent) {
this._resolution.open(event)
}
onClipboard(event: MouseEvent) {
this._clipboard.open(event)
}
}
</script>

83
client/src/lib.ts Normal file
View File

@ -0,0 +1,83 @@
import { accessor as neko } from './store'
import { PluginObject } from 'vue'
// Plugins
import Logger from './plugins/log'
import Client from './plugins/neko'
import Axios from './plugins/axios'
import Swal from './plugins/swal'
import Anime from './plugins/anime'
import { i18n } from './plugins/i18n'
// Components
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 Emotes from '~/components/emotes.vue'
import About from '~/components/about.vue'
import Header from '~/components/header.vue'
import Chat from '~/components/chat.vue'
import Clipboard from '~/components/clipboard.vue'
import Emoji from '~/components/emoji.vue'
import Emote from '~/components/emote.vue'
import Context from '~/components/context.vue'
import Markdown from '~/components/markdown'
import Avatar from '~/components/avatar.vue'
// Vue
import Vue from 'vue'
import ToolTip from 'v-tooltip'
Vue.use(ToolTip)
const exportMixin = {
computed: {
$accessor() {
return neko
},
$client () {
return window.$client
}
},
}
const plugini18n: PluginObject<undefined> = {
install(Vue) {
Vue.prototype.i18n = i18n
Vue.prototype.$t = i18n.t.bind(i18n)
},
}
function extend (component: any) {
return component
.use(plugini18n)
.use(Logger)
.use(Axios)
.use(Swal)
.use(Anime)
.use(Client)
.extend(exportMixin)
}
export const NekoConnect = extend(Connect)
export const NekoVideo = extend(Video)
export const NekoMenu = extend(Menu)
export const NekoSide = extend(Side)
export const NekoControls = extend(Controls)
export const NekoMembers = extend(Members)
export const NekoEmotes = extend(Emotes)
export const NekoAbout = extend(About)
export const NekoHeader = extend(Header)
export const NekoChat = extend(Chat)
export const NekoClipboard = extend(Clipboard)
export const NekoEmoji = extend(Emoji)
export const NekoEmote = extend(Emote)
export const NekoMarkdown = extend(Markdown)
export const NekoContext = extend(Context)
export const NekoAvatar = extend(Avatar)
neko.initialise()
export default neko

View File

@ -10,10 +10,12 @@ export const side = {
}
export const connect = {
title: 'Please Login',
displayname: 'Display Name',
login_title: 'Please Login',
invitation_title: 'You have been invited to this room',
displayname: 'Enter your display name',
password: 'Password',
connect: 'Connect',
error: 'Login error',
}
export const context = {
@ -48,10 +50,10 @@ export const controls = {
}
export const room = {
lock: 'Lock Room',
unlock: 'Unlock Room',
locked: 'Room Locked',
unlocked: 'Room Unlocked',
lock: 'Lock Room (for users)',
unlock: 'Unlock Room (for users)',
locked: 'Room Locked (for users)',
unlocked: 'Room Unlocked (for users)',
}
export const setting = {
@ -60,7 +62,9 @@ export const setting = {
autoplay: 'Autoplay Video',
ignore_emotes: 'Ignore Emotes',
chat_sound: 'Play Chat Sound',
keyboard_layout: 'Change Keyboard Layout',
keyboard_layout: 'Keyboard Layout',
broadcast_is_active: 'Broadcast Enabled',
broadcast_url: 'RTMP url',
}
export const connection = {

View File

@ -0,0 +1,97 @@
export const logout = 'salir'
export const unsupported = 'este navegador no soporta webrtc'
export const admin_loggedin = 'Registrado como admin'
export const you = 'Tú'
export const send_a_message = 'Enviar un mensaje'
export const side = {
chat: 'Chat',
settings: 'Configuración',
}
export const connect = {
login_title: 'Por favor regístrate',
invitation_title: 'Te han invitado a esta sala',
displayname: 'Introduce tu nombre',
password: 'Contraseña',
connect: 'Conectar',
error: 'Error de login',
}
export const context = {
ignore: 'Ignorar',
unignore: 'No ignorar',
mute: 'Silenciar',
unmute: 'No silenciar',
release: 'Forzar liberar los controles',
take: 'Forzar obtener los controles',
give: 'Dar los controles',
kick: 'Echar',
ban: 'Bloquear IP',
confirm: {
kick_title: 'Echar a {name}?',
kick_text: 'Seguro que quiere echar a {name}?',
ban_title: 'Bloquear a {name}?',
ban_text: 'Seguroq ue quieres bloquear a {name}? Necesitarás reiniciar el servidor para deshacer esta acción.',
mute_title: 'Silenciar a {name}?',
mute_text: 'Seguro que quieres silenciar a {name}?',
unmute_title: 'Dejar de silenciar a {name}?',
unmute_text: 'Seguro que quieres dejar de silenciar a {name}?',
button_yes: 'Sí',
button_cancel: 'Cancelar',
},
}
export const controls = {
release: 'Controles liberador',
request: 'Controles solicitados',
lock: 'Controles bloqueados',
unlock: 'Controles desbloqueados',
}
export const room = {
lock: 'Bloquear sala (para usuarios)',
unlock: 'Desbloquear sala (para usuarios)',
locked: 'Sala bloqueada (para usuarios)',
unlocked: 'Sala desbloqueada (para usuarios)',
}
export const setting = {
scroll: 'Sensibilidad del Scroll',
scroll_invert: 'Invertir Scroll',
autoplay: 'Auto Reproducir Video',
ignore_emotes: 'Ignorar Emotes',
chat_sound: 'Reproducir Sonidos Chat',
keyboard_layout: 'Keyboard Layout',
broadcast_is_active: 'Habilitar Broadcast',
broadcast_url: 'RTMP url',
}
export const connection = {
logged_out: 'Has salido!',
connected: 'Connectado correctamente',
disconnected: 'Has sido desconectado',
button_confirm: 'De acuerdo',
}
export const notifications = {
connected: '{name} se ha conectado',
disconnected: '{name} se ha desconnectado',
controls_taken: '{name} tiene los controles',
controls_taken_force: 'controles confiscados',
controls_taken_steal: 'cogió los controles de {name}',
controls_released: '{name} ha liberado los controles',
controls_released_force: 'controles liberados',
controls_released_steal: 'controles liberados de {name}',
controls_given: 'controles asignados a {name}',
controls_has: '{name} tiene los controles',
controls_has_alt: 'Pero le diré que quieres los controles',
controls_requesting: '{name} quiere los controles',
resolution: 'resolución cambiada a {width}x{height}@{rate}',
banned: '{name} bloqueado',
kicked: '{name} expulsado',
muted: '{name} silenciado',
unmuted: '{name} no silenciado',
room_locked: 'bloqueó la sala',
room_unlocked: 'desbloqueó la sala',
}

View File

@ -1,5 +1,7 @@
import * as en from './en-us'
import * as es from './es-sp'
export const messages = {
en,
es,
}

View File

@ -2,7 +2,7 @@ import EventEmitter from 'eventemitter3'
import { OPCODE } from './data'
import { EVENT, WebSocketEvents } from './events'
import { WebSocketMessages, WebSocketPayloads, SignalProvidePayload } from './messages'
import { WebSocketMessages, WebSocketPayloads, SignalProvidePayload, SignalCandidatePayload } from './messages'
export interface BaseEvents {
info: (...message: any[]) => void
@ -19,6 +19,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
protected _displayname?: string
protected _state: RTCIceConnectionState = 'disconnected'
protected _id = ''
protected _candidates: RTCIceCandidate[] = []
get id() {
return this._id
@ -52,18 +53,18 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
}
if (displayname === '') {
throw new Error('Must add a displayname') // TODO: Better handling
throw new Error('Display Name cannot be empty.')
}
this._displayname = displayname
this[EVENT.CONNECTING]()
try {
this._ws = new WebSocket(`${url}ws?password=${password}`)
this._ws = new WebSocket(`${url}?password=${encodeURIComponent(password)}`)
this.emit('debug', `connecting to ${this._ws.url}`)
this._ws.onmessage = this.onMessage.bind(this)
this._ws.onerror = event => this.onError.bind(this)
this._ws.onclose = event => this.onDisconnected.bind(this, new Error('websocket closed'))
this._ws.onerror = (event) => this.onError.bind(this)
this._ws.onclose = (event) => this.onDisconnected.bind(this, new Error('websocket closed'))
this._timeout = setTimeout(this.onTimeout.bind(this), 15000)
} catch (err) {
this.onDisconnected(err)
@ -75,17 +76,43 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
clearTimeout(this._timeout)
}
if (this.socketOpen) {
if (this._ws) {
// reset all events
this._ws.onmessage = () => {}
this._ws.onerror = () => {}
this._ws.onclose = () => {}
try {
this._ws!.close()
this._ws.close()
} catch (err) {}
this._ws = undefined
}
if (this.peerConnected) {
if (this._channel) {
// reset all events
this._channel.onmessage = () => {}
this._channel.onerror = () => {}
this._channel.onclose = () => {}
try {
this._peer!.close()
this._channel.close()
} catch (err) {}
this._channel = undefined
}
if (this._peer) {
// reset all events
this._peer.onconnectionstatechange = () => {}
this._peer.onsignalingstatechange = () => {}
this._peer.oniceconnectionstatechange = () => {}
this._peer.ontrack = () => {}
try {
this._peer.close()
} catch (err) {}
this._peer = undefined
}
@ -179,15 +206,15 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
})
}
this._peer.onconnectionstatechange = event => {
this._peer.onconnectionstatechange = (event) => {
this.emit('debug', `peer connection state changed`, this._peer ? this._peer.connectionState : undefined)
}
this._peer.onsignalingstatechange = event => {
this._peer.onsignalingstatechange = (event) => {
this.emit('debug', `peer signaling state changed`, this._peer ? this._peer.signalingState : undefined)
}
this._peer.oniceconnectionstatechange = event => {
this._peer.oniceconnectionstatechange = (event) => {
this._state = this._peer!.iceConnectionState
this.emit('debug', `peer ice connection state changed: ${this._peer!.iceConnectionState}`)
@ -220,9 +247,15 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._channel.onclose = this.onDisconnected.bind(this, new Error('peer data channel closed'))
this._peer.setRemoteDescription({ type: 'offer', sdp })
for (const candidate of this._candidates) {
this._peer.addIceCandidate(candidate)
}
this._candidates = []
this._peer
.createAnswer()
.then(d => {
.then((d) => {
this._peer!.setLocalDescription(d)
this._ws!.send(
JSON.stringify({
@ -232,7 +265,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
}),
)
})
.catch(err => this.emit('error', err))
.catch((err) => this.emit('error', err))
}
private onMessage(e: MessageEvent) {
@ -247,6 +280,17 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
return
}
if (event === EVENT.SIGNAL.CANDIDATE) {
const { data } = payload as SignalCandidatePayload
const candidate: RTCIceCandidate = JSON.parse(data)
if (this._peer) {
this._peer.addIceCandidate(candidate)
} else {
this._candidates.push(candidate)
}
return
}
// @ts-ignore
if (typeof this[event] === 'function') {
// @ts-ignore

View File

@ -14,6 +14,7 @@ export const EVENT = {
SIGNAL: {
ANSWER: 'signal/answer',
PROVIDE: 'signal/provide',
CANDIDATE: 'signal/candidate',
},
MEMBER: {
LIST: 'member/list',
@ -27,7 +28,7 @@ export const EVENT = {
REQUESTING: 'control/requesting',
CLIPBOARD: 'control/clipboard',
GIVE: 'control/give',
KEYBOARD: 'control/keyboard'
KEYBOARD: 'control/keyboard',
},
CHAT: {
MESSAGE: 'chat/message',
@ -38,6 +39,11 @@ export const EVENT = {
RESOLUTION: 'screen/resolution',
SET: 'screen/set',
},
BROADCAST: {
STATUS: 'broadcast/status',
CREATE: 'broadcast/create',
DESTROY: 'broadcast/destroy',
},
ADMIN: {
BAN: 'admin/ban',
KICK: 'admin/kick',
@ -60,6 +66,7 @@ export type WebSocketEvents =
| SignalEvents
| ChatEvents
| ScreenEvents
| BroadcastEvents
| AdminEvents
export type ControlEvents =
@ -72,10 +79,15 @@ export type ControlEvents =
export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT
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 | typeof EVENT.SIGNAL.CANDIDATE
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
export type BroadcastEvents =
| typeof EVENT.BROADCAST.STATUS
| typeof EVENT.BROADCAST.CREATE
| typeof EVENT.BROADCAST.DESTROY
export type AdminEvents =
| typeof EVENT.ADMIN.BAN
| typeof EVENT.ADMIN.KICK

View File

@ -18,6 +18,7 @@ import {
ControlClipboardPayload,
ScreenConfigurationsPayload,
ScreenResolutionPayload,
BroadcastStatusPayload,
AdminPayload,
AdminTargetPayload,
} from './messages'
@ -27,10 +28,21 @@ interface NekoEvents extends BaseEvents {}
export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
private $vue!: Vue
private $accessor!: typeof accessor
private url!: string
init(vue: Vue) {
const url =
process.env.NODE_ENV === 'development'
? `ws://${location.host.split(':')[0]}:${process.env.VUE_APP_SERVER_PORT}/ws`
: location.protocol.replace(/^http/, 'ws') + '//' + location.host + location.pathname.replace(/\/$/, '') + '/ws'
this.initWithURL(vue, url)
}
initWithURL(vue: Vue, url: string) {
this.$vue = vue
this.$accessor = vue.$accessor
this.url = url
}
private cleanup() {
@ -42,12 +54,7 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
}
login(password: string, displayname: string) {
const url =
process.env.NODE_ENV === 'development'
? `ws://${location.host.split(':')[0]}:${process.env.VUE_APP_SERVER_PORT}/`
: `${/https/gi.test(location.protocol) ? 'wss' : 'ws'}://${location.host}/`
this.connect(url, password, displayname)
this.connect(this.url, password, displayname)
}
logout() {
@ -83,6 +90,11 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
protected [EVENT.DISCONNECTED](reason?: Error) {
this.cleanup()
if (reason && reason.message == 'kicked') {
this.$accessor.logout()
}
this.$vue.$notify({
group: 'neko',
type: 'error',
@ -326,6 +338,13 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
})
}
/////////////////////////////
// Broadcast Events
/////////////////////////////
protected [EVENT.BROADCAST.STATUS](payload: BroadcastStatusPayload) {
this.$accessor.settings.broadcastStatus(payload)
}
/////////////////////////////
// Admin Events
/////////////////////////////

View File

@ -15,6 +15,7 @@ export type WebSocketMessages =
| WebSocketMessage
| SignalProvideMessage
| SignalAnswerMessage
| SignalCandidateMessage
| MemberListMessage
| MemberConnectMessage
| MemberDisconnectMessage
@ -26,6 +27,7 @@ export type WebSocketMessages =
export type WebSocketPayloads =
| SignalProvidePayload
| SignalAnswerPayload
| SignalCandidatePayload
| MemberListPayload
| Member
| ControlPayload
@ -37,6 +39,8 @@ export type WebSocketPayloads =
| ScreenResolutionPayload
| ScreenConfigurationsPayload
| AdminPayload
| BroadcastStatusPayload
| BroadcastCreatePayload
export interface WebSocketMessage {
event: WebSocketEvents | string
@ -76,6 +80,14 @@ export interface SignalAnswerPayload {
displayname: string
}
// signal/candidate
export interface SignalCandidateMessage extends WebSocketMessage, SignalCandidatePayload {
event: typeof EVENT.SIGNAL.CANDIDATE
}
export interface SignalCandidatePayload {
data: string
}
/*
MEMBER MESSAGES/PAYLOADS
*/
@ -122,7 +134,10 @@ export interface ControlClipboardPayload {
}
export interface ControlKeyboardPayload {
layout: string
layout?: string
capsLock?: boolean
numLock?: boolean
scrollLock?: boolean
}
/*
@ -174,6 +189,18 @@ export interface ScreenConfigurationsPayload {
configurations: ScreenConfigurations
}
/*
BROADCAST PAYLOADS
*/
export interface BroadcastCreatePayload {
url: string
}
export interface BroadcastStatusPayload {
url: string
isActive: boolean
}
/*
ADMIN PAYLOADS
*/

View File

@ -25,10 +25,10 @@ class VueSweetalert2 {
if (options) {
const mixed = Swal.mixin(options)
return mixed.fire.apply(mixed, args)
return mixed.fire(...args)
}
return Swal.fire.apply(Swal, args)
return Swal.fire(...args)
}
let methodName: string | number | symbol
@ -40,7 +40,7 @@ class VueSweetalert2 {
swalFunction[methodName] = ((method) => {
return (...args: any[]) => {
// @ts-ignore
return Swal[method].apply(Swal, args)
return Swal[method](...args)
}
})(methodName)
}

View File

@ -60,7 +60,7 @@ export const actions = actionTree(
{
initialise() {
$http
.get<Emojis>('/emoji.json')
.get<Emojis>('emoji.json')
.then((req) => {
for (const group of req.data.groups) {
accessor.emoji.addGroup(group)

View File

@ -3,12 +3,17 @@ import { Member } from '~/neko/types'
import { EVENT } from '~/neko/events'
import { accessor } from '~/store'
const keyboardModifierState = (capsLock: boolean, numLock: boolean, scrollLock: boolean) =>
Number(capsLock) + 2 * Number(numLock) + 4 * Number(scrollLock)
export const namespaced = true
export const state = () => ({
id: '',
clipboard: '',
locked: false,
keyboardModifierState: -1,
})
export const getters = getterTree(state, {
@ -36,6 +41,10 @@ export const mutations = mutationTree(state, {
state.clipboard = clipboard
},
setKeyboardModifierState(state, { capsLock, numLock, scrollLock }) {
state.keyboardModifierState = keyboardModifierState(capsLock, numLock, scrollLock)
},
setLocked(state, locked: boolean) {
state.locked = locked
},
@ -44,6 +53,7 @@ export const mutations = mutationTree(state, {
state.id = ''
state.clipboard = ''
state.locked = false
state.keyboardModifierState = -1
},
})
@ -71,7 +81,7 @@ export const actions = actionTree(
},
request({ getters }) {
if (!accessor.connected || !getters.hosting) {
if (!accessor.connected || getters.hosting) {
return
}
@ -79,7 +89,7 @@ export const actions = actionTree(
},
release({ getters }) {
if (!accessor.connected || getters.hosting) {
if (!accessor.connected || !getters.hosting) {
return
}
@ -140,6 +150,15 @@ export const actions = actionTree(
}
$client.sendMessage(EVENT.CONTROL.KEYBOARD, { layout: accessor.settings.keyboard_layout })
}
},
syncKeyboardModifierState({ state, getters }, { capsLock, numLock, scrollLock }) {
if (state.keyboardModifierState === keyboardModifierState(capsLock, numLock, scrollLock)) {
return
}
accessor.remote.setKeyboardModifierState({ capsLock, numLock, scrollLock })
$client.sendMessage(EVENT.CONTROL.KEYBOARD, { capsLock, numLock, scrollLock })
},
},
)

View File

@ -1,5 +1,6 @@
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
import { get, set } from '~/utils/localstorage'
import { EVENT } from '~/neko/events'
import { accessor } from '~/store'
export const namespaced = true
@ -18,6 +19,9 @@ export const state = () => {
keyboard_layout: get<string>('keyboard_layout', 'us'),
keyboard_layouts_list: {} as KeyboardLayouts,
broadcast_is_active: false,
broadcast_url: '',
}
}
@ -57,6 +61,10 @@ export const mutations = mutationTree(state, {
setKeyboardLayoutsList(state, value: KeyboardLayouts) {
state.keyboard_layouts_list = value
},
setBroadcastStatus(state, { url, isActive }) {
state.broadcast_url = url
state.broadcast_is_active = isActive
},
})
export const actions = actionTree(
@ -64,12 +72,21 @@ export const actions = actionTree(
{
initialise() {
$http
.get<KeyboardLayouts>('/keyboard_layouts.json')
.get<KeyboardLayouts>('keyboard_layouts.json')
.then((req) => {
accessor.settings.setKeyboardLayoutsList(req.data)
console.log(req.data)
})
.catch(console.error)
},
broadcastStatus({ getters }, { url, isActive }) {
accessor.settings.setBroadcastStatus({ url, isActive })
},
broadcastCreate({ getters }, url: string) {
$client.sendMessage(EVENT.BROADCAST.CREATE, { url })
},
broadcastDestroy({ getters }) {
$client.sendMessage(EVENT.BROADCAST.DESTROY)
},
},
)

View File

@ -48,6 +48,11 @@ export const mutations = mutationTree(state, {
state.id = id
},
addMember(state, member: Member) {
// remove html tags
const tmp = document.createElement('div')
tmp.innerHTML = member.displayname
member.displayname = tmp.textContent || tmp.innerText || ''
state.members = {
...state.members,
[member.id]: {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file

View File

@ -4,41 +4,41 @@ export interface GuacamoleKeyboardInterface {
/**
* Fired whenever the user presses a key with the element associated
* with this Guacamole.Keyboard in focus.
*
*
* @event
* @param {Number} keysym The keysym of the key being pressed.
* @return {Boolean} true if the key event should be allowed through to the
* browser, false otherwise.
*/
onkeydown?: (keysym: number) => boolean;
onkeydown?: (keysym: number) => boolean
/**
* Fired whenever the user releases a key with the element associated
* with this Guacamole.Keyboard in focus.
*
*
* @event
* @param {Number} keysym The keysym of the key being released.
*/
onkeyup?: (keysym: number) => void;
onkeyup?: (keysym: number) => void
/**
* Marks a key as pressed, firing the keydown event if registered. Key
* repeat for the pressed key will start after a delay if that key is
* not a modifier. The return value of this function depends on the
* return value of the keydown event handler, if any.
*
*
* @param {Number} keysym The keysym of the key to press.
* @return {Boolean} true if event should NOT be canceled, false otherwise.
*/
press: (keysym: number) => boolean;
press: (keysym: number) => boolean
/**
* Marks a key as released, firing the keyup event if registered.
*
*
* @param {Number} keysym The keysym of the key to release.
*/
release: (keysym: number) => void;
release: (keysym: number) => void
/**
* Presses and releases the keys necessary to type the given string of
* text.
@ -46,14 +46,14 @@ export interface GuacamoleKeyboardInterface {
* @param {String} str
* The string to type.
*/
type: (str: string) => void;
type: (str: string) => void
/**
* Resets the state of this keyboard, releasing all keys, and firing keyup
* events for each released key.
*/
reset: () => void;
reset: () => void
/**
* Attaches event listeners to the given Element, automatically translating
* received key, input, and composition events into simple keydown/keyup
@ -64,13 +64,13 @@ export interface GuacamoleKeyboardInterface {
* The Element to attach event listeners to for the sake of handling
* key or input events.
*/
listenTo: (element: Element | Document) => void;
listenTo: (element: Element | Document) => void
}
export default function(element?: Element): GuacamoleKeyboardInterface {
var Keyboard = {};
export default function (element?: Element): GuacamoleKeyboardInterface {
const Keyboard = {}
GuacamoleKeyboard.bind(Keyboard, element)();
GuacamoleKeyboard.bind(Keyboard, element)()
return Keyboard as GuacamoleKeyboardInterface;
return Keyboard as GuacamoleKeyboardInterface
}

View File

@ -2,7 +2,7 @@ export function makeid(length: number) {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (var i = 0; i < length; i++) {
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result

View File

@ -13,7 +13,7 @@ export function set<T extends string | number | boolean>(key: string, val: T) {
}
export function get<T extends string | number | boolean>(key: string, def: T): T {
let store = localStorage.getItem(key)
const store = localStorage.getItem(key)
if (store) {
switch (typeof def) {
case 'number':

View File

@ -5,12 +5,14 @@ module.exports = {
css: {
loaderOptions: {
sass: {
prependData: `
additionalData: `
@import "@/assets/styles/_variables.scss";
`,
},
},
},
publicPath: './',
assetsDir: './',
configureWebpack: {
resolve: {
alias: {