Frontend refactor and additions

This commit is contained in:
Jesse Malotaux 2025-04-11 16:49:51 +02:00
parent 5de99b32cd
commit 2a9813e7ac
12 changed files with 195 additions and 26 deletions

View file

@ -18,7 +18,7 @@
<script setup> <script setup>
import MainMenu from '@/components/base/MainMenu.vue' import MainMenu from '@/components/base/MainMenu.vue'
import { onMounted, ref } from 'vue' import { onMounted, onUpdated, ref } from 'vue'
import { RouterView, useRoute } from 'vue-router' import { RouterView, useRoute } from 'vue-router'
import { useDeviceStore } from './stores/device' import { useDeviceStore } from './stores/device'
import { isLocal } from './services/ApiService' import { isLocal } from './services/ApiService'
@ -30,16 +30,20 @@ const route = useRoute()
const handshake = ref(false) const handshake = ref(false)
onMounted(async () => { onMounted(async () => {
// Setting device uuid from localstorage
// If not present in LocalStorage a new uuidV4 will be generated
device.uuid() device.uuid()
const hsReq = await device.remoteHandshake() const hsReq = await device.remoteHandshake()
handshake.value = hsReq handshake.value = hsReq
device.$subscribe((mutation, state) => { device.$subscribe((mutation, state) => {
console.log(mutation) if (device.key()) handshake.value = true
}) })
// Setting device uuid from localstorage })
// If not present in LocalStorage a new uuidV4 will be generated
onUpdated(() => {
console.log(device.key())
}) })
</script> </script>

View file

@ -57,7 +57,8 @@ function toggleAccordion(open = false) {
header { header {
@apply grid @apply grid
grid-cols-[1fr_auto] grid-cols-[1fr_auto]
px-4 py-2; px-4 py-2
cursor-pointer;
} }
.accordion__wrapper { .accordion__wrapper {

View file

@ -0,0 +1,38 @@
<template>
<div v-if="loading" class="loading-component">
<span v-if="text">
{{ text }}
</span>
<IconLoader3 class="duration-1000 animate-spin" />
</div>
</template>
<script setup>
import { IconLoader3 } from '@tabler/icons-vue'
defineProps({
loading: Boolean,
text: String,
})
</script>
<style>
@reference "@/assets/main.css";
&:has(.loading-component) {
@apply relative;
}
.loading-component {
@apply absolute
inset-0
size-full
flex gap-2
flex-col
justify-center
items-center
text-sm
bg-black/50
backdrop-blur-md;
}
</style>

View file

@ -21,10 +21,18 @@
<AlertComp v-if="server.status === 'unauthorized'" variant="info"> <AlertComp v-if="server.status === 'unauthorized'" variant="info">
<div class="grid gap-2"> <div class="grid gap-2">
<strong>Access requested</strong> <strong>Access requested</strong>
<p> <ul class="mb-4">
Navigate to <em class="font-semibold">http://localhost:6970/devices</em> on your pc to <li>Navigate to <em class="font-semibold">http://localhost:6970/devices</em>.</li>
authorize. <li>
</p> <div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
</div>
</li>
<li>Enter the the pin shown on the desktop in the dialog that will appear.</li>
</ul>
<template v-if="server.link == 'checking'"> <template v-if="server.link == 'checking'">
<div class="grid grid-cols-[2rem_1fr] gap-2"> <div class="grid grid-cols-[2rem_1fr] gap-2">
<IconReload class="animate-spin" /> <IconReload class="animate-spin" />
@ -53,11 +61,13 @@
<h3>Server link pin:</h3> <h3>Server link pin:</h3>
<form class="grid gap-4" @submit.prevent="decryptKey()"> <form class="grid gap-4" @submit.prevent="decryptKey()">
<input <input
ref="linkPinInput"
class="input" class="input"
id="input-pin" id="input-pin"
type="text" type="text"
pattern="[0-9]{4}" pattern="[0-9]{4}"
v-model="server.inputPin" v-model="server.inputPin"
autocomplete="off"
/> />
<ButtonComp variant="primary">Enter</ButtonComp> <ButtonComp variant="primary">Enter</ButtonComp>
</form> </form>
@ -75,7 +85,7 @@
// - - if checkAccess -> pingLink -> check for device.tmp (go) // - - if checkAccess -> pingLink -> check for device.tmp (go)
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status // - - if [devicePin] -> handshake -> save key local, close dialog, update server status
import { IconKey, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue' import { IconKey, IconLink, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
import AlertComp from '../base/AlertComp.vue' import AlertComp from '../base/AlertComp.vue'
import ButtonComp from '../base/ButtonComp.vue' import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, onUpdated, reactive, ref } from 'vue' import { onMounted, onUpdated, reactive, ref } from 'vue'
@ -89,6 +99,7 @@ import { appUrl } from '@/services/ApiService'
const device = useDeviceStore() const device = useDeviceStore()
const linkPinDialog = ref() const linkPinDialog = ref()
const linkPinInput = ref()
const server = reactive({ const server = reactive({
host: '', host: '',
@ -105,6 +116,8 @@ onMounted(async () => {
onUpdated(() => { onUpdated(() => {
if (!server.status) checkServerStatus() if (!server.status) checkServerStatus()
if (server.status === 'authorized' && server.inputPin) server.inputPin = ''
}) })
async function checkServerStatus(request = true) { async function checkServerStatus(request = true) {
@ -150,6 +163,7 @@ function pingLink() {
server.encryptedKey = encryptedKey server.encryptedKey = encryptedKey
linkPinDialog.value.toggleDialog(true) linkPinDialog.value.toggleDialog(true)
linkPinInput.value.focus()
}) })
} }

View file

@ -7,10 +7,13 @@
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light"> <div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between w-full gap-4 mb-4"> <h4 class="flex items-center justify-between w-full gap-4 mb-4">
<span class="flex gap-4"> <IconDevices />Remote devices </span> <span class="flex gap-4" v-if="Object.keys(remote.devices).length > 0">
<IconDevices />{{ Object.keys(remote.devices).length }}
{{ Object.keys(remote.devices).length > 1 ? 'Devices' : 'Device' }}
</span>
<ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp> <ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp>
</h4> </h4>
<!-- {{ Object.keys(remote.devices).length }} -->
<template v-if="Object.keys(remote.devices).length > 0"> <template v-if="Object.keys(remote.devices).length > 0">
<template v-for="(remoteDevice, id) in remote.devices" :key="id"> <template v-for="(remoteDevice, id) in remote.devices" :key="id">
<div <div
@ -57,6 +60,46 @@
</div> </div>
</template> </template>
<AccordionComp
class="w-full mt-8 border-t border-t-white/50"
title="How to connect a device?"
>
<div class="grid py-4">
<ul class="space-y-1">
<li>
To connect a device, open <strong>http://{{ server.ip }}:{{ server.port }}</strong> in
a browser on the device.
</li>
<li>Open the menu, and click on <strong>Server.</strong></li>
<li>
The device will automatically request access, if you see "Access requested" on the
device.
</li>
<li>
<div class="inline-flex items-center gap-2">
Click the
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
the devices.
</div>
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
to generate a one-time-pin to link the device.
</div>
</li>
<li>
Enter the pin that is shown on this server in the dialog that will appear on the
device.
</li>
<li>Congratulations! You have linked a device! (Hopefully)</li>
</ul>
</div>
</AccordionComp>
<DialogComp ref="pinDialog"> <DialogComp ref="pinDialog">
<template #content> <template #content>
<div class="grid gap-4"> <div class="grid gap-4">
@ -87,19 +130,29 @@ import ButtonComp from '../base/ButtonComp.vue'
import DialogComp from '../base/DialogComp.vue' import DialogComp from '../base/DialogComp.vue'
import axios from 'axios' import axios from 'axios'
import { appUrl } from '@/services/ApiService' import { appUrl } from '@/services/ApiService'
import AccordionComp from '../base/AccordionComp.vue'
const device = useDeviceStore() const device = useDeviceStore()
const pinDialog = ref() const pinDialog = ref()
const server = reactive({
ip: '',
port: '',
})
const remote = reactive({ devices: [], pinlink: false }) const remote = reactive({ devices: [], pinlink: false })
onMounted(() => { onMounted(async () => {
device.serverGetRemotes() device.serverGetRemotes()
device.$subscribe((mutation, state) => { device.$subscribe((mutation, state) => {
if (state.remote !== remote.devices) remote.devices = device.remote if (state.remote !== remote.devices) remote.devices = device.remote
}) })
const serverIP = await device.serverGetIP()
server.ip = serverIP
server.port = import.meta.env.VITE_MCRM__PORT
}) })
async function startLink(deviceUuid) { async function startLink(deviceUuid) {

View file

@ -2,6 +2,7 @@
<div class="macro-overview mcrm-block block__dark"> <div class="macro-overview mcrm-block block__dark">
<h4 class="border-b-2 border-transparent">Saved Macros</h4> <h4 class="border-b-2 border-transparent">Saved Macros</h4>
<div class="macro-overview__list"> <div class="macro-overview__list">
<LoadComp :loading="macros.loading" text="Loading macros..." />
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i"> <div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
<ButtonComp variant="dark" class="w-full" size="sm"> <ButtonComp variant="dark" class="w-full" size="sm">
<IconKeyboard /> {{ macro.name }} <IconKeyboard /> {{ macro.name }}
@ -23,15 +24,22 @@ import axios from 'axios'
import { appUrl, isLocal } from '@/services/ApiService' import { appUrl, isLocal } from '@/services/ApiService'
import { AuthCall } from '@/services/EncryptService' import { AuthCall } from '@/services/EncryptService'
import { GetMacroList, RunMacro } from '@/services/MacroService' import { GetMacroList, RunMacro } from '@/services/MacroService'
import LoadComp from '../base/LoadComp.vue'
const macros = reactive({ const macros = reactive({
loading: true,
list: [], list: [],
}) })
onMounted(async () => { onMounted(() => {
loadMacroList()
})
const loadMacroList = async () => {
const list = await GetMacroList() const list = await GetMacroList()
macros.list = list macros.list = list
}) macros.loading = false
}
</script> </script>
<style scoped> <style scoped>

View file

@ -89,6 +89,11 @@ function panelItemClick(dir) {
w-full w-full
aspect-[4/3]; aspect-[4/3];
img {
@apply size-full
object-cover;
}
&:not(:has(img)) { &:not(:has(img)) {
@apply bg-sky-950; @apply bg-sky-950;
} }

View file

@ -1,5 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import { useDeviceStore } from '@/stores/device'
import { checkAuth, isLocal } from '@/services/ApiService'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -13,32 +15,36 @@ const router = createRouter({
path: '/panels', path: '/panels',
name: 'panels', name: 'panels',
component: () => import('../views/PanelsView.vue'), component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/panel/edit/:dirname', path: '/panel/edit/:dirname',
name: 'panel-edit', name: 'panel-edit',
component: () => import('../views/PanelsView.vue'), component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/panel/view/:dirname', path: '/panel/view/:dirname',
name: 'panel-view', name: 'panel-view',
component: () => import('../views/PanelsView.vue'), component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/macros', path: '/macros',
name: 'macros', name: 'macros',
component: () => import('../views/MacrosView.vue'), component: () => import('../views/MacrosView.vue'),
meta: { localOnly: true },
}, },
{ {
path: '/devices', path: '/devices',
name: 'devices', name: 'devices',
component: () => import('../views/DevicesView.vue'), component: () => import('../views/DevicesView.vue'),
}, },
{ // {
path: '/settings', // path: '/settings',
name: 'settings', // name: 'settings',
component: () => import('../views/SettingsView.vue'), // component: () => import('../views/SettingsView.vue'),
}, // },
// { // {
// path: '/about', // path: '/about',
// name: 'about', // name: 'about',
@ -50,4 +56,12 @@ const router = createRouter({
], ],
}) })
router.beforeEach(async (to, from, next) => {
const auth = await checkAuth()
if (to.meta.requiresAuth && !auth && !isLocal()) next('/devices')
else if (to.meta.localOnly && !isLocal()) next('/')
else next()
})
export default router export default router

View file

@ -1,3 +1,4 @@
import { useDeviceStore } from '@/stores/device'
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
export const appUrl = () => { export const appUrl = () => {
@ -17,3 +18,15 @@ export const encrypt = (data, key = false) => {
return false return false
} }
} }
export const checkAuth = async () => {
const device = useDeviceStore()
const handshake = await device.remoteHandshake()
if (handshake === true) return true
if (device.key()) return true
return false
}

View file

@ -50,10 +50,19 @@ export const useDeviceStore = defineStore('device', () => {
localStorage.removeItem('deviceKey') localStorage.removeItem('deviceKey')
} }
const serverGetIP = async () => {
const request = await axios.post(appUrl() + '/device/server/ip')
return request.data
}
// Server application // Server application
const serverGetRemotes = async (remoteUuid) => { const serverGetRemotes = async (remoteUuid) => {
axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => { axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
if (data.data.devices) remote.value = data.data.devices // console.log(data.data.devices)
if (data.data.devices) {
console.log(data.data.devices)
remote.value = data.data.devices
}
}) })
} }
@ -100,6 +109,8 @@ export const useDeviceStore = defineStore('device', () => {
shake: encryptAES(keyStr, getDateStr()), shake: encryptAES(keyStr, getDateStr()),
}) })
if (!handshake.data) removeDeviceKey()
return handshake.data return handshake.data
} }
@ -111,6 +122,7 @@ export const useDeviceStore = defineStore('device', () => {
key, key,
setDeviceKey, setDeviceKey,
removeDeviceKey, removeDeviceKey,
serverGetIP,
serverGetRemotes, serverGetRemotes,
serverStartLink, serverStartLink,
remoteCheckServerAccess, remoteCheckServerAccess,

View file

@ -1,5 +1,15 @@
<template> <template>
<div id="dashboard"></div> <div id="dashboard" class="panel">
<h1 class="panel__title">Dashboard</h1>
<div class="panel__content">
<div class="grid gap-1 opacity-50 h-fit">
<em>Hello nothing to see here. Something will be added in the future.</em>
<em>Use the menu to navigate.</em>
<em>Have a nice day!</em>
</div>
</div>
</div>
</template> </template>
<script setup></script> <script setup></script>

View file

@ -1,10 +1,7 @@
<template> <template>
<div id="panels" class="panel"> <div id="panels" class="panel">
<h1 class="flex items-end justify-between !w-full panel__title"> <h1 class="flex items-end justify-between !w-full panel__title">
<div> <div>Panels</div>
Panels
<span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
</div>
<ButtonComp <ButtonComp
v-if="panel.function != 'overview'" v-if="panel.function != 'overview'"
variant="subtle" variant="subtle"