Add settings
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
561e983ae6
commit
2df8b9a241
|
@ -8,6 +8,7 @@
|
|||
"name": "mercury",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^8.1.2",
|
||||
"animate.css": "^4.1.1",
|
||||
"bennc-js": "git+https://git.jacknet.io/TerribleCodeClub/bennc-js.git",
|
||||
"bootstrap": "^5.1.3",
|
||||
|
@ -22,6 +23,7 @@
|
|||
"vue-router": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.1.9",
|
||||
"@vitejs/plugin-vue": "^2.2.0",
|
||||
"vite": "^2.8.0"
|
||||
}
|
||||
|
@ -41,7 +43,6 @@
|
|||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
|
||||
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
|
@ -52,6 +53,16 @@
|
|||
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
|
||||
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
|
||||
},
|
||||
"node_modules/@types/bootstrap": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.9.tgz",
|
||||
"integrity": "sha512-Tembe6lt7819EUzV5LSG9uuwULm4hdEGV9LZ8QBYpWc0J+a+9DdmJEwZ4FMaXGVJWwumTPSkJ8JQF0/KDAmXYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/color": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
|
||||
|
@ -73,6 +84,21 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
|
||||
"integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
|
@ -198,6 +224,87 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
|
||||
"integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.1.2.tgz",
|
||||
"integrity": "sha512-prI2GzigBUtJNTcwRjJPzUPLFoRZM1RZFR464DFdwgU8TxRFf7dRvuvWFDNbCATzLExHFnGI3zTp9GkXTTZxgQ==",
|
||||
"dependencies": {
|
||||
"@vueuse/metadata": "8.1.2",
|
||||
"@vueuse/shared": "8.1.2",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.1.0",
|
||||
"vue": "^2.6.0 || ^3.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.1.2.tgz",
|
||||
"integrity": "sha512-4Hb9iPUhAz7ghO4hgvB2GV2FOy12qQGdhmQ+9HC6QN/J66DELhmxAvkZAtK5FBqZOSwzKszPqNqoyhRKQrrWGQ==",
|
||||
"dependencies": {
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.1.0",
|
||||
"vue": "^2.6.0 || ^3.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.12.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.4.tgz",
|
||||
"integrity": "sha512-ztPDkFt0TSUdoq1ZI6oD730vgztBkiByhUW7L1cOTebiSBqSYfSQgnhYakYigBkyAybqCTH7h44yZuDJf2xILQ==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-LrPtdiYMleygnGmz8mEmYI9h4Eyo+/igxZWNrwuPnqvL9pIO+8eUpBgPLH5GowKv3Nu0LPZSXSIuaWVJBSU1Cg==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/animate.css": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
|
||||
|
@ -965,14 +1072,23 @@
|
|||
"@popperjs/core": {
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
|
||||
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==",
|
||||
"peer": true
|
||||
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg=="
|
||||
},
|
||||
"@sphinxxxx/color-conversion": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
|
||||
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
|
||||
},
|
||||
"@types/bootstrap": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.9.tgz",
|
||||
"integrity": "sha512-Tembe6lt7819EUzV5LSG9uuwULm4hdEGV9LZ8QBYpWc0J+a+9DdmJEwZ4FMaXGVJWwumTPSkJ8JQF0/KDAmXYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"@types/color": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
|
||||
|
@ -994,6 +1110,21 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||
},
|
||||
"@types/jquery": {
|
||||
"version": "3.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
|
||||
"integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
|
@ -1110,6 +1241,37 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
|
||||
"integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
|
||||
},
|
||||
"@vueuse/core": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.1.2.tgz",
|
||||
"integrity": "sha512-prI2GzigBUtJNTcwRjJPzUPLFoRZM1RZFR464DFdwgU8TxRFf7dRvuvWFDNbCATzLExHFnGI3zTp9GkXTTZxgQ==",
|
||||
"requires": {
|
||||
"@vueuse/metadata": "8.1.2",
|
||||
"@vueuse/shared": "8.1.2",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/shared": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.1.2.tgz",
|
||||
"integrity": "sha512-4Hb9iPUhAz7ghO4hgvB2GV2FOy12qQGdhmQ+9HC6QN/J66DELhmxAvkZAtK5FBqZOSwzKszPqNqoyhRKQrrWGQ==",
|
||||
"requires": {
|
||||
"vue-demi": "*"
|
||||
}
|
||||
},
|
||||
"vue-demi": {
|
||||
"version": "0.12.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.4.tgz",
|
||||
"integrity": "sha512-ztPDkFt0TSUdoq1ZI6oD730vgztBkiByhUW7L1cOTebiSBqSYfSQgnhYakYigBkyAybqCTH7h44yZuDJf2xILQ==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vueuse/metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-LrPtdiYMleygnGmz8mEmYI9h4Eyo+/igxZWNrwuPnqvL9pIO+8eUpBgPLH5GowKv3Nu0LPZSXSIuaWVJBSU1Cg=="
|
||||
},
|
||||
"animate.css": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^8.1.2",
|
||||
"animate.css": "^4.1.1",
|
||||
"bennc-js": "git+https://git.jacknet.io/TerribleCodeClub/bennc-js.git",
|
||||
"bootstrap": "^5.1.3",
|
||||
|
@ -22,6 +23,7 @@
|
|||
"vue-router": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.1.9",
|
||||
"@vitejs/plugin-vue": "^2.2.0",
|
||||
"vite": "^2.8.0"
|
||||
}
|
||||
|
|
37
src/App.vue
37
src/App.vue
|
@ -1,4 +1,16 @@
|
|||
<template>
|
||||
<div ref="settingsModal" class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="staticBackdropLabel">Configure your user</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<UserSettings />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid vh-100">
|
||||
<div class="row vh-100">
|
||||
<router-view />
|
||||
|
@ -7,6 +19,31 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { Modal } from 'bootstrap'
|
||||
import { useMercuryStore } from './stores/mercuryStore'
|
||||
import UserSettings from './components/userSettings/UserSettings.vue'
|
||||
|
||||
const mercuryStore = useMercuryStore()
|
||||
const configured = mercuryStore.configured()
|
||||
|
||||
const settingsModal = ref()
|
||||
|
||||
onMounted(() => {
|
||||
const modal = new Modal(settingsModal.value)
|
||||
|
||||
watch(configured, () => {
|
||||
switch(configured.value) {
|
||||
case true:
|
||||
modal.hide()
|
||||
break
|
||||
case false:
|
||||
modal.show()
|
||||
break
|
||||
}
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export function fromBase64(base64) {
|
||||
return Uint8Array.from(window.atob(base64), c => c.charCodeAt(0))
|
||||
}
|
||||
|
||||
export function toBase64(arr) {
|
||||
return window.btoa(String.fromCharCode(...arr))
|
||||
}
|
||||
|
||||
export function validateBase64(base64) {
|
||||
const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/
|
||||
return base64Regex.test(base64)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
Primary: getComputedStyle(document.documentElement).getPropertyValue('--bs-primary'),
|
||||
Secondary: getComputedStyle(document.documentElement).getPropertyValue('--bs-secondary'),
|
||||
Random: () => { return '#' + Math.floor(Math.random()*16777215).toString(16) }
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
SelfId: -1
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<h3>Add channel</h3>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input v-model="name" type="text" class="form-control" placeholder="Channel name" aria-label="Channel name">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input v-model="key" type="text" class="form-control" placeholder="Channel key (base64)" aria-label="Channel key">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row mt-3">
|
||||
<button @click="addChannel" type="submit" class="btn btn-primary ms-auto">Add channel</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import { useChannelStore } from '../../stores/channelStore'
|
||||
import { fromBase64, toBase64, validateBase64 } from '../../common/base64'
|
||||
|
||||
const channelStore = useChannelStore()
|
||||
const requestUserData = inject('requestUserData')
|
||||
|
||||
const name = ref('')
|
||||
const key = ref('')
|
||||
|
||||
function padKey(key) {
|
||||
const decodedKey = fromBase64(key)
|
||||
const paddedKey = new Uint8Array(16)
|
||||
paddedKey.set(decodedKey)
|
||||
return toBase64(paddedKey)
|
||||
}
|
||||
|
||||
function addChannel() {
|
||||
const channelNames = channelStore.channels.map(channel => channel.name)
|
||||
if (name.value.trim() === '' || key.value.trim() === '') {
|
||||
alert('You must specify a name and key.')
|
||||
return
|
||||
}
|
||||
if (channelNames.includes(name.value.trim())) {
|
||||
alert(`A channel with name '${name.value}' already exists.`)
|
||||
return
|
||||
}
|
||||
if (!validateBase64(key.value.trim())) {
|
||||
alert(`You must specify the key in base64 format.`)
|
||||
return
|
||||
}
|
||||
if (((4 * key.value.trim().length / 3) + 3) & ~3 > 16) {
|
||||
alert('The maximum key length is 16 bytes.')
|
||||
return
|
||||
}
|
||||
const paddedKey = padKey(key.value.trim())
|
||||
channelStore.addChannel(name.value.trim(), paddedKey)
|
||||
requestUserData()
|
||||
name.value = ''
|
||||
key.value = ''
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<h3>Channels</h3>
|
||||
<table class="table mb-5">
|
||||
<thead>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col">Remember</th>
|
||||
<th scope="col" class="text-end">Delete</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="channel in channelStore.channels" :key="channel.id">
|
||||
<td class="w-25">{{ channel.name }}</td>
|
||||
<td class="w-25"><Key :channel="channel" /></td>
|
||||
<td class="w-25">
|
||||
<div class="form-check form-switch">
|
||||
<input v-if="channel.name === 'Default'" class="form-check-input" type="checkbox" checked disabled>
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-25 text-end"><i v-if="channel.name !== 'Default'" class="text-danger text-end bi bi-trash-fill"></i></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useChannelStore } from '../../stores/channelStore'
|
||||
import Key from '../common/Key.vue'
|
||||
|
||||
const channelStore = useChannelStore()
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@ import { computed, ref } from 'vue'
|
|||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
const props = defineProps(['channel'])
|
||||
const key = computed(() => { return props.channel.key.base64 })
|
||||
const key = computed(() => { return props.channel.key })
|
||||
|
||||
const locked = ref(true)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<span class="text-muted">{{ message.received.toLocaleTimeString() }} </span>
|
||||
<span class="fw-bold" :style="{ color: user.color.hex() }">{{ user.name }} </span>
|
||||
<span class="fw-bold" :style="{ color: user.color }">{{ user.name }} </span>
|
||||
<span>{{ message.content }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -9,10 +9,14 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '../../stores/userStore'
|
||||
import Constants from '../../common/constants'
|
||||
import { useMercuryStore } from '../../stores/mercuryStore';
|
||||
|
||||
const props = defineProps(['message'])
|
||||
const message = props.message
|
||||
|
||||
const mercuryStore = useMercuryStore()
|
||||
const userStore = useUserStore()
|
||||
const user = computed(() => { return userStore.getUserById(message.userId)})
|
||||
|
||||
const user = computed(() => { return message.userId === Constants.SelfId ? mercuryStore.user : userStore.getUserById(message.userId) })
|
||||
</script>
|
||||
|
|
|
@ -27,7 +27,7 @@ const sendMessage = () => {
|
|||
if (message.value.trim() === '') {
|
||||
animateCSS(input.value, 'headShake')
|
||||
} else {
|
||||
ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.key.raw))
|
||||
ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.rawKey))
|
||||
messageStore.addMessage(channel.value.id, -1, message.value)
|
||||
message.value = ''
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
<template>
|
||||
|
||||
<div ref="picker" id="picker" class="btn"><div class="label">Set username color</div></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Color from 'color';
|
||||
import Picker from 'vanilla-picker'
|
||||
import { inject, onMounted, ref } from 'vue';
|
||||
import { useUserStore } from '../../stores/userStore';
|
||||
|
||||
const requestUserData = inject('requestUserData')
|
||||
const userStore = useUserStore()
|
||||
|
||||
const self = userStore.getSelf()
|
||||
|
||||
const picker = ref(null)
|
||||
const usernameColor = ref(Color())
|
||||
|
||||
const bootstrapPrimaryColor = getComputedStyle(document.documentElement).getPropertyValue('--bs-primary')
|
||||
|
||||
onMounted(() => {
|
||||
const p = new Picker(picker.value)
|
||||
|
||||
p.setOptions({
|
||||
alpha: false,
|
||||
color: self.color.hex() ?? bootstrapPrimaryColor
|
||||
})
|
||||
|
||||
picker.value.style.backgroundColor = self.color.hex() ?? bootstrapPrimaryColor
|
||||
|
||||
p.onChange = function(color) {
|
||||
picker.value.style.backgroundColor = color.rgbaString
|
||||
}
|
||||
|
||||
p.onOpen = function(color) {
|
||||
usernameColor.value = Color(color.hex)
|
||||
}
|
||||
|
||||
p.onDone = function(color) {
|
||||
usernameColor.value = Color(color.hex)
|
||||
picker.value.style.backgroundColor = color.rgbaString
|
||||
|
||||
userStore.setColor(-1, Color(color.hex))
|
||||
requestUserData()
|
||||
}
|
||||
|
||||
p.onClose = function(color) {
|
||||
picker.value.style.backgroundColor = usernameColor.value.hex()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
#picker .label {
|
||||
background: inherit;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
filter: invert(1) grayscale(1) contrast(9);
|
||||
}
|
||||
</style>
|
|
@ -2,10 +2,10 @@
|
|||
<div class="col-2 bg-dark">
|
||||
<div class="d-flex flex-column vh-100">
|
||||
<div class="d-flex flex-row logo align-items-center py-2">
|
||||
<img src="../assets/logo-dark.png" class="me-2" />
|
||||
<img src="../../assets/logo-dark.png" class="me-2" />
|
||||
<span class="fs-3 text-light">Mercury</span>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="configured">
|
||||
<h5 class="text-muted">Channels</h5>
|
||||
<Channels />
|
||||
<slot name="top"></slot>
|
||||
|
@ -21,10 +21,13 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useMercuryStore } from '../stores/mercuryStore'
|
||||
import Channels from './sidebar/Channels.vue'
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { watch } from 'vue';
|
||||
import { useMercuryStore } from '../../stores/mercuryStore'
|
||||
import Channels from './Channels.vue'
|
||||
|
||||
const mercuryStore = useMercuryStore()
|
||||
const connected = computed (() => { return mercuryStore.connected })
|
||||
const { connected } = storeToRefs(mercuryStore)
|
||||
const configured = mercuryStore.configured()
|
||||
|
||||
</script>
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
|
||||
<div ref="picker" class="btn w-100"><div class="label">Set username color</div></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Color from 'color';
|
||||
import Picker from 'vanilla-picker'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const picker = ref()
|
||||
|
||||
onMounted(() => {
|
||||
var selectedColor = ref(props.modelValue)
|
||||
picker.value.style.backgroundColor = selectedColor.value
|
||||
|
||||
const p = new Picker(picker.value)
|
||||
|
||||
p.setOptions({
|
||||
alpha: false,
|
||||
color: props.modelValue ?? bootstrapPrimaryColor,
|
||||
popup: 'bottom'
|
||||
})
|
||||
|
||||
p.onChange = function(color) {
|
||||
picker.value.style.backgroundColor = Color(color.rgbString).hex()
|
||||
}
|
||||
|
||||
p.onDone = function(color) {
|
||||
selectedColor.value = Color(color.rgbString).hex()
|
||||
picker.value.style.backgroundColor = Color(color.rgbString).hex()
|
||||
emit('update:modelValue', selectedColor.value)
|
||||
}
|
||||
|
||||
p.onClose = function(color) {
|
||||
picker.value.style.backgroundColor = selectedColor.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.label {
|
||||
background: inherit;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
filter: invert(1) grayscale(1) contrast(9);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<input v-model="name" type="text" class="mb-3 form-control" placeholder="Name" aria-label="name" />
|
||||
<ColorPicker v-model="color" />
|
||||
<hr>
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<label v-if="name">Preview:</label>
|
||||
<span class="fw-bold ms-1" :style="{ color: color ?? '#000000' }">{{ name }}</span>
|
||||
<button @click="save" class="btn btn-primary ms-auto">Save</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue'
|
||||
import { useMercuryStore } from '../../stores/mercuryStore'
|
||||
import Colors from '../../common/colors'
|
||||
import ColorPicker from './ColorPicker.vue'
|
||||
|
||||
const requestUserData = inject('requestUserData')
|
||||
const mercuryStore = useMercuryStore()
|
||||
|
||||
const name = ref()
|
||||
const color = ref(Colors.Random())
|
||||
|
||||
// Set initial username.
|
||||
if (mercuryStore.user?.name) {
|
||||
name.value = mercuryStore.user.name
|
||||
}
|
||||
|
||||
// Set initial color.
|
||||
if (mercuryStore.user?.color) {
|
||||
color.value = mercuryStore.user.color
|
||||
}
|
||||
|
||||
function save() {
|
||||
mercuryStore.setUserName(name.value)
|
||||
mercuryStore.setUserColor(color.value)
|
||||
requestUserData()
|
||||
}
|
||||
|
||||
</script>
|
|
@ -4,13 +4,12 @@
|
|||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Colors from '../../common/colors'
|
||||
|
||||
const props = defineProps(['user'])
|
||||
const user = props.user
|
||||
|
||||
const bootstrapSecondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--bs-secondary')
|
||||
|
||||
const color = computed(() => { return user.color?.hex() ?? bootstrapSecondaryColor })
|
||||
const color = computed(() => { return user.color ?? Colors.Secondary })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="d-flex flex-column">
|
||||
<User :user="self" />
|
||||
<User :user="mercuryStore.user" />
|
||||
<div v-for="user in users" :key="user.id" class="d-flex flex-row align-items-center">
|
||||
<User :user="user" />
|
||||
</div>
|
||||
|
@ -9,6 +9,7 @@
|
|||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useMercuryStore } from '../../stores/mercuryStore'
|
||||
import { useUserStore } from '../../stores/userStore'
|
||||
import User from './User.vue'
|
||||
|
||||
|
@ -16,7 +17,6 @@ const props = defineProps(['channel'])
|
|||
const channelId = computed(() => { return props.channel.id })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const mercuryStore = useMercuryStore()
|
||||
const users = computed(() => { return userStore.getUsersByChannelId(channelId.value) })
|
||||
|
||||
const self = userStore.getSelf()
|
||||
</script>
|
||||
|
|
|
@ -16,8 +16,8 @@ import websocket from './plugins/websocket'
|
|||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(websocket)
|
||||
|
||||
app.mount('#app')
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { watch } from 'vue'
|
||||
import Color from 'color'
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
import { MessageTypes, numberToUint16BE, packers, unpackers, unpackIncomingPacket } from 'bennc-js'
|
||||
import { decrypt } from 'romulus-js'
|
||||
import { useMercuryStore } from '../stores/mercuryStore'
|
||||
import { useUserStore } from '../stores/userStore'
|
||||
import { useMessageStore } from '../stores/messageStore'
|
||||
import { useChannelStore } from '../stores/channelStore'
|
||||
import { decrypt } from 'romulus-js'
|
||||
import { provide } from 'vue'
|
||||
|
||||
const CLIENT_ID = 'Mercury'
|
||||
const SUBSCRIBED_MESSAGE_TYPES = [MessageTypes.Basic, MessageTypes.UserDataRequest, MessageTypes.UserDataResponse]
|
||||
|
@ -26,12 +27,13 @@ export default {
|
|||
const messageStore = useMessageStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const configured = mercuryStore.configured()
|
||||
|
||||
function handleBasicMessage(incomingPacket) {
|
||||
// Try decryption with each encryption key.
|
||||
for (let channel of channelStore.channels) {
|
||||
// Attempt decryption of incoming message.
|
||||
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.Basic), channel.key.raw)
|
||||
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.Basic), channel.rawKey)
|
||||
|
||||
// If encryption key is found...
|
||||
if (result.success) {
|
||||
|
@ -53,7 +55,7 @@ export default {
|
|||
// Try decryption with each encryption key.
|
||||
for (let channel of channelStore.channels) {
|
||||
// Attempt decryption of incoming message.
|
||||
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataRequest), channel.key.raw)
|
||||
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataRequest), channel.rawKey)
|
||||
|
||||
// If encryption key is found...
|
||||
if (result.success) {
|
||||
|
@ -62,12 +64,11 @@ export default {
|
|||
|
||||
// Update user in user store.
|
||||
userStore.setName(incomingPacket.senderId, userDataRequest.username)
|
||||
userStore.setColor(incomingPacket.senderId, userDataRequest.colour)
|
||||
userStore.setColor(incomingPacket.senderId, userDataRequest.colour.hex())
|
||||
userStore.addChannel(incomingPacket.senderId, channel.id)
|
||||
|
||||
// Respond to user data request with a user data response.
|
||||
const self = userStore.getSelf()
|
||||
const userDataResponse = packers[MessageTypes.UserDataResponse]({ username: self.name, colour: self.color, clientId: CLIENT_ID }, channel.key.raw)
|
||||
const userDataResponse = packers[MessageTypes.UserDataResponse]({ username: mercuryStore.user.name, colour: Color(mercuryStore.user.color), clientId: CLIENT_ID }, channel.rawKey)
|
||||
ws.send(userDataResponse)
|
||||
|
||||
// Don't process other keys.
|
||||
|
@ -80,7 +81,7 @@ export default {
|
|||
// Try decryption with each encryption key.
|
||||
for (let channel of channelStore.channels) {
|
||||
// Attempt decryption of incoming message.
|
||||
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataResponse), channel.key.raw)
|
||||
const result = decrypt(incomingPacket.data, numberToUint16BE(MessageTypes.UserDataResponse), channel.rawKey)
|
||||
|
||||
// If encryption key is found...
|
||||
if (result.success) {
|
||||
|
@ -89,7 +90,7 @@ export default {
|
|||
|
||||
// Update user in user store.
|
||||
userStore.setName(incomingPacket.senderId, userDataResponse.username)
|
||||
userStore.setColor(incomingPacket.senderId, userDataResponse.colour)
|
||||
userStore.setColor(incomingPacket.senderId, userDataResponse.colour.hex())
|
||||
userStore.addChannel(incomingPacket.senderId, channel.id)
|
||||
|
||||
// Don't process other keys.
|
||||
|
@ -99,9 +100,8 @@ export default {
|
|||
}
|
||||
|
||||
function requestUserData() {
|
||||
const self = userStore.getSelf()
|
||||
channelStore.channels.forEach(channel => {
|
||||
const userDataRequest = packers[MessageTypes.UserDataRequest]({ username: self.name, colour: self.color, clientId: CLIENT_ID }, channel.key.raw)
|
||||
const userDataRequest = packers[MessageTypes.UserDataRequest]({ username: mercuryStore.user.name, colour: Color(mercuryStore.user.color), clientId: CLIENT_ID }, channel.rawKey)
|
||||
ws.send(userDataRequest)
|
||||
})
|
||||
}
|
||||
|
@ -114,7 +114,9 @@ export default {
|
|||
}, 30000)
|
||||
|
||||
setInterval(() => {
|
||||
if (configured.value) {
|
||||
requestUserData()
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
|
@ -127,7 +129,9 @@ export default {
|
|||
})
|
||||
|
||||
// Make a user data request for each channel.
|
||||
if (configured.value) {
|
||||
requestUserData()
|
||||
}
|
||||
})
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
|
@ -159,7 +163,6 @@ export default {
|
|||
// Restart keepalive.
|
||||
clearInterval(timeout)
|
||||
timeout = setInterval(() => {
|
||||
console.log("Keepalive message sent!")
|
||||
ws.send(packers[MessageTypes.Keepalive]())
|
||||
}, 30000)
|
||||
})
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { defineStore } from 'pinia'
|
||||
|
||||
class Key {
|
||||
constructor(base64) {
|
||||
this.base64 = base64
|
||||
this.raw = Uint8Array.from(window.atob(base64), c => c.charCodeAt(0))
|
||||
}
|
||||
}
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { fromBase64 } from '../common/base64'
|
||||
|
||||
export const useChannelStore = defineStore({
|
||||
id: 'channelStore',
|
||||
|
@ -14,12 +9,8 @@ export const useChannelStore = defineStore({
|
|||
{
|
||||
id: 'a798423c-3fbf-434d-9a00-57d7642bff65',
|
||||
name: 'Default',
|
||||
key: new Key('')
|
||||
},
|
||||
{
|
||||
id: '47198ee7-8bf4-41fe-8b42-bb001bd5f140',
|
||||
name: 'HackerNews',
|
||||
key: new Key('HACKERNEWQAAAAAAAAAAAA==')
|
||||
key: 'AAAAAAAAAAAAAAAAAAAAAA==',
|
||||
rawKey: new Uint8Array(16)
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
@ -29,8 +20,13 @@ export const useChannelStore = defineStore({
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
addChannel(channel) {
|
||||
this.channels.push(channel)
|
||||
addChannel(name, key) {
|
||||
this.channels.push({
|
||||
id: uuidv4(),
|
||||
name: name,
|
||||
key: key,
|
||||
rawKey: fromBase64(key)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,14 +1,32 @@
|
|||
import { computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
export const useMercuryStore = defineStore({
|
||||
id: 'mercuryStore',
|
||||
state: () => ({
|
||||
user: useLocalStorage('user', {
|
||||
name: '',
|
||||
color: null
|
||||
}),
|
||||
connected: false
|
||||
}),
|
||||
getters: {},
|
||||
getters: {
|
||||
configured: (state) => {
|
||||
return () => computed(() => {
|
||||
return state.user?.name.trim() !== '' && state.user?.color !== undefined
|
||||
})
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setConnectionState(state) {
|
||||
this.connected = state
|
||||
},
|
||||
setUserName(name) {
|
||||
this.user.name = name
|
||||
},
|
||||
setUserColor(color) {
|
||||
this.user.color = color
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
import Color from 'color'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: 'userStore',
|
||||
state: () => ({
|
||||
users: [
|
||||
{
|
||||
id: -1,
|
||||
name: 'Mercury',
|
||||
color: Color('#ff4000'),
|
||||
channels: [],
|
||||
self: true
|
||||
}
|
||||
]
|
||||
users: []
|
||||
}),
|
||||
getters: {
|
||||
getSelf: (state) => {
|
||||
return (self) => state.users.find(user => user.self === true)
|
||||
},
|
||||
getUserById: (state) => {
|
||||
return (id) => state.users.find(user => user.id === id)
|
||||
},
|
||||
|
@ -32,7 +20,7 @@ export const useUserStore = defineStore({
|
|||
user = {
|
||||
id: newUser.id,
|
||||
name: String(newUser.id),
|
||||
color: Color('#000000'),
|
||||
color: '#000000',
|
||||
channels: []
|
||||
}
|
||||
this.users.push(user)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<template v-if="channel">
|
||||
<Sidebar>
|
||||
<template v-slot:top>
|
||||
<h5 class="text-muted mt-3">Users</h5>
|
||||
|
@ -18,16 +19,30 @@
|
|||
</template>
|
||||
</Content>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Sidebar />
|
||||
<Content>
|
||||
<template v-slot:title>Missing channel</template>
|
||||
<template v-slot:content>
|
||||
<div class="d-flex flex-column text-center">
|
||||
<i class="error text-warning bi bi-cone-striped"></i>
|
||||
<h2>That channel does not exist!</h2>
|
||||
</div>
|
||||
</template>
|
||||
</Content>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
import { useChannelStore } from '../stores/channelStore'
|
||||
import { useMessageStore } from '../stores/messageStore'
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import Sidebar from '../components/sidebar/Sidebar.vue'
|
||||
import Content from '../components/Content.vue'
|
||||
import Key from '../components/common/Key.vue'
|
||||
import UserList from '../components/userlist/UserList.vue'
|
||||
import UserList from '../components/userList/UserList.vue'
|
||||
import Message from '../components/conversation/Message.vue'
|
||||
import MessageInput from '../components/conversation/MessageInput.vue'
|
||||
|
||||
|
@ -43,3 +58,9 @@ const locked = ref(false)
|
|||
onBeforeRouteUpdate(async (to, from) => { locked.value = false })
|
||||
const abcdef = ref(null)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
i.error {
|
||||
font-size: 10rem;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<Sidebar>
|
||||
</Sidebar>
|
||||
<Sidebar />
|
||||
<Content>
|
||||
<template v-slot:title>Welcome to Mercury!</template>
|
||||
<template v-slot:content>
|
||||
|
@ -9,16 +8,16 @@
|
|||
<h1 class="animate__animated animate__zoomIn">
|
||||
Mercury is a web-based BENNC client.
|
||||
</h1>
|
||||
<h2 class="animate__animated animate__zoomIn">
|
||||
<h3 class="animate__animated animate__zoomIn">
|
||||
Go to <router-link :to="{ name: 'Settings' }" class="text-muted"><i class="bi bi-gear-fill"></i> Settings</router-link> to configure your client.
|
||||
</h2>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
</Content>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import Sidebar from '../components/sidebar/Sidebar.vue'
|
||||
import Content from '../components/Content.vue'
|
||||
</script>
|
||||
|
||||
|
@ -26,10 +25,4 @@ import Content from '../components/Content.vue'
|
|||
#logo {
|
||||
filter: invert(80%);
|
||||
}
|
||||
/* h1 {
|
||||
--animate-delay: 0.5s;
|
||||
}
|
||||
h2 {
|
||||
--animate-delay: 0.5s;
|
||||
} */
|
||||
</style>
|
|
@ -5,68 +5,20 @@
|
|||
<template v-slot:content>
|
||||
<h3>User</h3>
|
||||
<div class="mb-5">
|
||||
<div class="input-group mb-3">
|
||||
<input @keyup.enter="saveUsername" v-model="username" type="text" class="form-control" placeholder="Username" aria-label="Username">
|
||||
<button @click="saveUsername" class="btn btn-primary" type="button">Save</button>
|
||||
<UserSettings />
|
||||
</div>
|
||||
<ColorPicker></ColorPicker>
|
||||
</div>
|
||||
<h3>Channels</h3>
|
||||
<table class="table mb-5">
|
||||
<thead>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col" class="text-end">Delete</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="channel in channelStore.channels" :key="channel.id">
|
||||
<td class="w-25">{{ channel.name }}</td>
|
||||
<td class="w-25"><Key :channel="channel" /></td>
|
||||
<td class="w-25 text-end"><i class="deleteChannel bi bi-trash-fill text-danger text-end"></i></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Add channel</h3>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" placeholder="Channel name" aria-label="Channel name">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="password" class="form-control" placeholder="Channel key" aria-label="Channel key">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary float-end">Add channel</button>
|
||||
<ConfiguredChannels />
|
||||
<AddChannel />
|
||||
</template>
|
||||
</Content>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useChannelStore } from '../stores/channelStore'
|
||||
import Key from '../components/common/Key.vue'
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import Sidebar from '../components/sidebar/Sidebar.vue'
|
||||
import Content from '../components/Content.vue'
|
||||
import ColorPicker from '../components/settings/ColorPicker.vue';
|
||||
import { inject, ref } from 'vue';
|
||||
import { useUserStore } from '../stores/userStore';
|
||||
import UserSettings from '../components/userSettings/UserSettings.vue'
|
||||
import ConfiguredChannels from '../components/channelSettings/ConfiguredChannels.vue'
|
||||
import AddChannel from '../components/channelSettings/AddChannel.vue'
|
||||
|
||||
const channelStore = useChannelStore()
|
||||
const userStore = useUserStore()
|
||||
const requestUserData = inject('requestUserData')
|
||||
|
||||
const self = userStore.getSelf()
|
||||
|
||||
const username = ref('')
|
||||
username.value = self.name
|
||||
|
||||
function saveUsername() {
|
||||
userStore.setName(-1, username.value)
|
||||
requestUserData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
.deleteChannel {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue