mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
refactor state & public methods.
This commit is contained in:
parent
6177ffc242
commit
619ac3350b
104
src/app.vue
104
src/app.vue
@ -32,20 +32,67 @@
|
|||||||
<td>{{ neko.state.video.playable }}</td>
|
<td>{{ neko.state.video.playable }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>video.playing</th>
|
<th rowspan="2">video.playing</th>
|
||||||
<td><input type="checkbox" v-model="neko.state.video.playing" /></td>
|
<td>{{ neko.state.video.playing }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>video.volume</th>
|
<td>
|
||||||
<td><input type="range" min="0" max="1" v-model="neko.state.video.volume" step="0.01" /></td>
|
<button v-if="!neko.state.video.playing" @click="neko.play()">play</button>
|
||||||
|
<button v-else @click="neko.pause()">pause</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>control.scroll.inverse</th>
|
<th rowspan="2">video.volume</th>
|
||||||
<td><input type="checkbox" v-model="neko.state.control.scroll.inverse" /></td>
|
<td>{{ neko.state.video.volume }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>control.scroll.sensitivity</th>
|
<td>
|
||||||
<td><input type="number" v-model="neko.state.control.scroll.sensitivity" /></td>
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
:value="neko.state.video.volume"
|
||||||
|
@input="neko.setVolume(parseInt($event))"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<th rowspan="2">video.fullscreen</th>
|
||||||
|
<td>{{ neko.state.video.fullscreen }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<td>
|
||||||
|
<button v-if="!neko.state.video.fullscreen" @click="neko.requestFullscreen()">request</button>
|
||||||
|
<button v-else @click="neko.exitFullscreen()">exit</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<th rowspan="2">control.scroll.inverse</th>
|
||||||
|
<td>{{ neko.state.control.scroll.inverse }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<td>
|
||||||
|
<button v-if="!neko.state.control.scroll.inverse" @click="neko.setScrollInverse(true)">switch to inverse</button>
|
||||||
|
<button v-else @click="neko.setScrollInverse(false)">switch to normal</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<th rowspan="2">control.scroll.sensitivity</th>
|
||||||
|
<td>{{ neko.state.control.scroll.sensitivity }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:value="neko.state.control.scroll.sensitivity"
|
||||||
|
@input="neko.setScrollSensitivity(parseInt($event.target.value))"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>control.clipboard.data</th>
|
||||||
|
<td>{{ neko.state.control.clipboard.data }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>control.host</th>
|
<th>control.host</th>
|
||||||
@ -64,13 +111,16 @@
|
|||||||
<td>{{ neko.state.screen.size.rate }}</td>
|
<td>{{ neko.state.screen.size.rate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>screen.configurations</th>
|
<th rowspan="2">screen.configurations</th>
|
||||||
|
<td>Total {{ neko.state.screen.configurations.length }} configurations.</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
:value="Object.values(neko.state.screen.size).join()"
|
:value="Object.values(neko.state.screen.size).join()"
|
||||||
@input="
|
@input="
|
||||||
a = String($event.target.value).split(',')
|
a = String($event.target.value).split(',')
|
||||||
neko.screen.size(parseInt(a[0]), parseInt(a[1]), parseInt(a[2]))
|
neko.setScreenSize(parseInt(a[0]), parseInt(a[1]), parseInt(a[2]))
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@ -83,10 +133,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
|
||||||
<th>screen.fullscreen</th>
|
|
||||||
<td><input type="checkbox" v-model="neko.state.screen.fullscreen" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>member.id</th>
|
<th>member.id</th>
|
||||||
<td>{{ neko.state.member.id }}</td>
|
<td>{{ neko.state.member.id }}</td>
|
||||||
@ -104,9 +150,15 @@
|
|||||||
<td>{{ neko.state.member.is_watching }}</td>
|
<td>{{ neko.state.member.is_watching }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="ok">
|
<tr class="ok">
|
||||||
<th>member.is_controlling</th>
|
<th rowspan="2">member.is_controlling</th>
|
||||||
<td>{{ neko.state.member.is_controlling }}</td>
|
<td>{{ neko.state.member.is_controlling }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="ok">
|
||||||
|
<td>
|
||||||
|
<button v-if="!neko.state.member.is_controlling" @click="neko.controlRequest()">request control</button>
|
||||||
|
<button v-else @click="neko.controlRelease()">release control</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>member.can_watch</th>
|
<th>member.can_watch</th>
|
||||||
<td>{{ neko.state.member.can_watch }}</td>
|
<td>{{ neko.state.member.can_watch }}</td>
|
||||||
@ -126,15 +178,17 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button @click="connect()">Connect</button>
|
<div v-if="loaded && !neko.connected">
|
||||||
<button @click="disconnect()">Disonnect</button>
|
<input type="text" placeholder="URL" v-model="url" />
|
||||||
|
<input type="text" placeholder="Password" v-model="pass" />
|
||||||
|
<input type="text" placeholder="Display Name" v-model="name" />
|
||||||
|
<button @click="connect()">Connect</button>
|
||||||
|
</div>
|
||||||
|
<button v-if="loaded && neko.connected" @click="disconnect()">Disonnect</button>
|
||||||
|
|
||||||
<template v-if="loaded && neko.connected">
|
<template v-if="loaded && neko.connected">
|
||||||
<button v-if="!is_controlling" @click="neko.control.request()">request control</button>
|
<button v-if="!is_controlling" @click="neko.controlRequest()">request control</button>
|
||||||
<button v-else @click="neko.control.release()">release control</button>
|
<button v-else @click="neko.controlRelease()">release control</button>
|
||||||
|
|
||||||
<button v-if="neko.state.video.playing" @click="neko.state.video.playing = false">pause stream</button>
|
|
||||||
<button v-else @click="neko.state.video.playing = true">play stream</button><br />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="container" style="width: 1280px; height: 720px; border: 2px solid red">
|
<div ref="container" style="width: 1280px; height: 720px; border: 2px solid red">
|
||||||
@ -182,8 +236,12 @@
|
|||||||
return this.neko.state.member.is_controlling
|
return this.neko.state.member.is_controlling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
url: string = 'ws://192.168.1.20:3000/'
|
||||||
|
pass: string = 'admin'
|
||||||
|
name: string = 'test'
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.neko.connect('ws://192.168.1.20:3000/', 'admin', 'test')
|
this.neko.connect(this.url, this.pass, this.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
@ -52,12 +52,6 @@
|
|||||||
import NekoState from '~/types/state'
|
import NekoState from '~/types/state'
|
||||||
import Overlay from './overlay.vue'
|
import Overlay from './overlay.vue'
|
||||||
|
|
||||||
export interface NekoEvents {
|
|
||||||
connecting: () => void
|
|
||||||
connected: () => void
|
|
||||||
disconnected: (error?: Error) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'neko-canvas',
|
name: 'neko-canvas',
|
||||||
components: {
|
components: {
|
||||||
@ -67,12 +61,15 @@
|
|||||||
export default class extends Vue {
|
export default class extends Vue {
|
||||||
@Ref('component') readonly _component!: HTMLElement
|
@Ref('component') readonly _component!: HTMLElement
|
||||||
@Ref('container') readonly _container!: HTMLElement
|
@Ref('container') readonly _container!: HTMLElement
|
||||||
@Ref('video') readonly video!: HTMLVideoElement
|
@Ref('video') readonly _video!: HTMLVideoElement
|
||||||
|
|
||||||
private websocket = new NekoWebSocket()
|
websocket = new NekoWebSocket()
|
||||||
private webrtc = new NekoWebRTC()
|
webrtc = new NekoWebRTC()
|
||||||
private observer = new ResizeObserver(this.onResize.bind(this))
|
observer = new ResizeObserver(this.onResize.bind(this))
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// Public state
|
||||||
|
/////////////////////////////
|
||||||
public state = {
|
public state = {
|
||||||
connection: {
|
connection: {
|
||||||
websocket: 'disconnected',
|
websocket: 'disconnected',
|
||||||
@ -86,12 +83,16 @@
|
|||||||
playable: false,
|
playable: false,
|
||||||
playing: false,
|
playing: false,
|
||||||
volume: 0,
|
volume: 0,
|
||||||
|
fullscreen: false,
|
||||||
},
|
},
|
||||||
control: {
|
control: {
|
||||||
scroll: {
|
scroll: {
|
||||||
inverse: true,
|
inverse: true,
|
||||||
sensitivity: 1,
|
sensitivity: 1,
|
||||||
},
|
},
|
||||||
|
clipboard: {
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
host: null,
|
host: null,
|
||||||
},
|
},
|
||||||
screen: {
|
screen: {
|
||||||
@ -101,7 +102,6 @@
|
|||||||
rate: 30,
|
rate: 30,
|
||||||
},
|
},
|
||||||
configurations: [],
|
configurations: [],
|
||||||
fullscreen: false,
|
|
||||||
},
|
},
|
||||||
member: {
|
member: {
|
||||||
id: null,
|
id: null,
|
||||||
@ -116,64 +116,18 @@
|
|||||||
members: [],
|
members: [],
|
||||||
} as NekoState
|
} as NekoState
|
||||||
|
|
||||||
public events = new NekoMessages(this.websocket, this.state)
|
|
||||||
|
|
||||||
public get connected() {
|
public get connected() {
|
||||||
return this.state.connection.websocket == 'connected' && this.state.connection.webrtc == 'connected'
|
return this.state.connection.websocket == 'connected' && this.state.connection.webrtc == 'connected'
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('state.video.playing')
|
/////////////////////////////
|
||||||
onVideoPlayingChanged(play: boolean) {
|
// Public events
|
||||||
if (this.video.paused && play) {
|
/////////////////////////////
|
||||||
this.video.play()
|
public events = new NekoMessages(this.websocket, this.state)
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.video.paused && !play) {
|
|
||||||
this.video.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check if user has tab focused and send via websocket
|
|
||||||
Vue.set(this.state.member, 'is_watching', play)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('state.video.volume')
|
|
||||||
onVideoVolumeChanged(value: number) {
|
|
||||||
if (value < 0 || value > 1) {
|
|
||||||
throw new Error('Out of range. Value must be between 0 and 1.')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video.volume = value
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('state.screen.size')
|
|
||||||
onScreenSizeChanged() {
|
|
||||||
this.onResize()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('state.screen.fullscreen')
|
|
||||||
onScreenFullscreenChanged() {
|
|
||||||
if (document.fullscreenElement !== null) {
|
|
||||||
document.exitFullscreen()
|
|
||||||
} else {
|
|
||||||
this._component.requestFullscreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public control = {
|
|
||||||
request: () => {
|
|
||||||
this.websocket.send('control/request')
|
|
||||||
},
|
|
||||||
release: () => {
|
|
||||||
this.websocket.send('control/release')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
public screen = {
|
|
||||||
size: (width: number, height: number, rate: number) => {
|
|
||||||
this.websocket.send('screen/set', { width, height, rate })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// Public methods
|
||||||
|
/////////////////////////////
|
||||||
public connect(url: string, password: string, name: string) {
|
public connect(url: string, password: string, name: string) {
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
throw new Error('client already connected')
|
throw new Error('client already connected')
|
||||||
@ -189,32 +143,91 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.websocket.disconnect()
|
this.websocket.disconnect()
|
||||||
|
|
||||||
// TODO: reset state
|
|
||||||
Vue.set(this.state.member, 'is_controlling', false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private mounted() {
|
public play() {
|
||||||
// update canvas on resize
|
this._video.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
public pause() {
|
||||||
|
this._video.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
public setVolume(value: number) {
|
||||||
|
if (value < 0 || value > 1) {
|
||||||
|
throw new Error('Out of range. Value must be between 0 and 1.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this._video.volume = value
|
||||||
|
}
|
||||||
|
|
||||||
|
public requestFullscreen() {
|
||||||
|
this._component.requestFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
public exitFullscreen() {
|
||||||
|
document.exitFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
public setScrollInverse(value: boolean = true) {
|
||||||
|
Vue.set(this.state.control.scroll, 'inverse', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public setScrollSensitivity(value: number) {
|
||||||
|
Vue.set(this.state.control.scroll, 'sensitivity', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public setClipboardData(value: number) {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
}
|
||||||
|
|
||||||
|
public controlRequest() {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
this.websocket.send('control/request')
|
||||||
|
}
|
||||||
|
|
||||||
|
public controlRelease() {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
this.websocket.send('control/release')
|
||||||
|
}
|
||||||
|
|
||||||
|
public controlTake() {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
}
|
||||||
|
|
||||||
|
public controlGive(id: string) {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
}
|
||||||
|
|
||||||
|
public controlReset() {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
}
|
||||||
|
|
||||||
|
public setScreenSize(width: number, height: number, rate: number) {
|
||||||
|
// TODO: Via REST API.
|
||||||
|
this.websocket.send('screen/set', { width, height, rate })
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// Component lifecycle
|
||||||
|
/////////////////////////////
|
||||||
|
mounted() {
|
||||||
|
// component size change
|
||||||
this.observer.observe(this._component)
|
this.observer.observe(this._component)
|
||||||
|
|
||||||
// change host
|
// host change
|
||||||
this.events.on('control.host', (id: string | null) => {
|
this.events.on('control.host', (id: string | null) => {
|
||||||
Vue.set(this.state.member, 'is_controlling', id != null && id === this.state.member.id)
|
Vue.set(this.state.member, 'is_controlling', id != null && id === this.state.member.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
// hardcoded webrtc for now
|
// fullscreen change
|
||||||
Vue.set(this.state.connection, 'type', 'webrtc')
|
|
||||||
Vue.set(this.state.connection, 'can_watch', this.webrtc.supported)
|
|
||||||
Vue.set(this.state.connection, 'can_control', this.webrtc.supported)
|
|
||||||
|
|
||||||
this._component.addEventListener('fullscreenchange', () => {
|
this._component.addEventListener('fullscreenchange', () => {
|
||||||
Vue.set(this.state.screen, 'fullscreen', document.fullscreenElement !== null)
|
Vue.set(this.state.video, 'fullscreen', document.fullscreenElement !== null)
|
||||||
this.onResize()
|
this.onResize()
|
||||||
})
|
})
|
||||||
|
|
||||||
// video
|
// video events
|
||||||
VideoRegister(this.video, this.state.video)
|
VideoRegister(this._video, this.state.video)
|
||||||
|
|
||||||
// websocket
|
// websocket
|
||||||
this.websocket.on('message', async (event: string, payload: any) => {
|
this.websocket.on('message', async (event: string, payload: any) => {
|
||||||
@ -241,6 +254,18 @@
|
|||||||
Vue.set(this.state.connection, 'websocket', 'disconnected')
|
Vue.set(this.state.connection, 'websocket', 'disconnected')
|
||||||
this.events.emit('system.websocket', 'disconnected')
|
this.events.emit('system.websocket', 'disconnected')
|
||||||
this.webrtc.disconnect()
|
this.webrtc.disconnect()
|
||||||
|
|
||||||
|
// TODO: reset state
|
||||||
|
Vue.set(this.state, 'member', {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
is_admin: false,
|
||||||
|
is_watching: false,
|
||||||
|
is_controlling: false,
|
||||||
|
can_watch: false,
|
||||||
|
can_control: false,
|
||||||
|
clipboard_access: false,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// webrtc
|
// webrtc
|
||||||
@ -249,14 +274,14 @@
|
|||||||
if (track.kind === 'audio') return
|
if (track.kind === 'audio') return
|
||||||
|
|
||||||
// create stream
|
// create stream
|
||||||
if ('srcObject' in this.video) {
|
if ('srcObject' in this._video) {
|
||||||
this.video.srcObject = streams[0]
|
this._video.srcObject = streams[0]
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.video.src = window.URL.createObjectURL(streams[0]) // for older browsers
|
this._video.src = window.URL.createObjectURL(streams[0]) // for older browsers
|
||||||
}
|
}
|
||||||
|
|
||||||
this.video.play()
|
this._video.play()
|
||||||
})
|
})
|
||||||
this.webrtc.on('connecting', () => {
|
this.webrtc.on('connecting', () => {
|
||||||
Vue.set(this.state.connection, 'webrtc', 'connecting')
|
Vue.set(this.state.connection, 'webrtc', 'connecting')
|
||||||
@ -270,17 +295,29 @@
|
|||||||
Vue.set(this.state.connection, 'webrtc', 'disconnected')
|
Vue.set(this.state.connection, 'webrtc', 'disconnected')
|
||||||
this.events.emit('system.webrtc', 'disconnected')
|
this.events.emit('system.webrtc', 'disconnected')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (this.video) this.video.src = null
|
this._video.src = null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// hardcoded webrtc for now
|
||||||
|
Vue.set(this.state.connection, 'type', 'webrtc')
|
||||||
|
Vue.set(this.state.connection, 'can_watch', this.webrtc.supported)
|
||||||
|
Vue.set(this.state.connection, 'can_control', this.webrtc.supported)
|
||||||
}
|
}
|
||||||
|
|
||||||
private beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
this.webrtc.disconnect()
|
this.webrtc.disconnect()
|
||||||
this.websocket.disconnect()
|
this.websocket.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private onResize() {
|
@Watch('state.video.playing')
|
||||||
|
onVideoPlayingChanged(play: boolean) {
|
||||||
|
// TODO: check if user has tab focused and send via websocket
|
||||||
|
Vue.set(this.state.member, 'is_watching', play)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('state.screen.size')
|
||||||
|
onResize() {
|
||||||
const { width, height } = this.state.screen.size
|
const { width, height } = this.state.screen.size
|
||||||
const screen_ratio = width / height
|
const screen_ratio = width / height
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ export interface Video {
|
|||||||
playable: boolean
|
playable: boolean
|
||||||
playing: boolean
|
playing: boolean
|
||||||
volume: number
|
volume: number
|
||||||
|
fullscreen: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
@ -33,6 +34,7 @@ export interface Video {
|
|||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
export interface Control {
|
export interface Control {
|
||||||
scroll: Scroll
|
scroll: Scroll
|
||||||
|
clipboard: Clipboard
|
||||||
host: Member | null
|
host: Member | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,13 +43,16 @@ export interface Scroll {
|
|||||||
sensitivity: number
|
sensitivity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Clipboard {
|
||||||
|
data: string | null
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Screen
|
// Screen
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
export interface Screen {
|
export interface Screen {
|
||||||
size: ScreenSize
|
size: ScreenSize
|
||||||
configurations: ScreenSize[]
|
configurations: ScreenSize[]
|
||||||
fullscreen: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenSize {
|
export interface ScreenSize {
|
||||||
|
Loading…
Reference in New Issue
Block a user