Settings and Notifications update.

- Menu and router updated.
- Notifications component with Toast components added to app.vue
- Form input component added for regular input types
- Settings view and store added.
This commit is contained in:
JaxxMoss 2025-05-16 14:43:48 +02:00
parent 3193127809
commit 57fa6cf2a2
9 changed files with 464 additions and 11 deletions

View file

@ -35,19 +35,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h4>Not authorized!</h4> <h4>Not authorized!</h4>
<p>Click here to start authorization and open the "Devices" page on your PC.</p> <p>Click here to start authorization and open the "Devices" page on your PC.</p>
</AlertComp> </AlertComp>
<NotificationsComp />
</template> </template>
<script setup> <script setup>
import MainMenu from '@/components/base/MainMenu.vue' import MainMenu from '@/components/base/MainMenu.vue'
import { onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import { RouterView, useRoute } from 'vue-router' import { RouterView, useRoute, useRouter } from 'vue-router'
import { useDeviceStore } from './stores/device' import { useDeviceStore } from './stores/device'
import { useSettingStore } from './stores/settings'
import { isLocal } from './services/ApiService' import { isLocal } from './services/ApiService'
import AlertComp from './components/base/AlertComp.vue' import AlertComp from './components/base/AlertComp.vue'
import NotificationsComp from './components/base/NotificationsComp.vue'
import { GetLocalPanel } from './services/PanelService'
const device = useDeviceStore() const device = useDeviceStore()
const settings = useSettingStore()
const route = useRoute() const route = useRoute()
const router = useRouter()
const handshake = ref(false) const handshake = ref(false)
onMounted(() => { onMounted(() => {
@ -55,17 +61,38 @@ onMounted(() => {
// If not present in LocalStorage a new uuidV4 will be generated // If not present in LocalStorage a new uuidV4 will be generated
device.uuid() device.uuid()
if (!isLocal) appHandshake() settings.loadSettings()
if (!isLocal) {
appHandshake()
loadLastPanel()
}
device.$subscribe(() => { device.$subscribe(() => {
if (device.key()) handshake.value = true if (device.key()) handshake.value = true
}) })
}) })
onUpdated(() => {
console.log('App updated')
// loadLastPanel()
})
onUnmounted(() => {
settings.saveSettings()
})
async function appHandshake() { async function appHandshake() {
const hsReq = await device.remoteHandshake() const hsReq = await device.remoteHandshake()
handshake.value = hsReq handshake.value = hsReq
} }
function loadLastPanel() {
if (route.fullPath != GetLocalPanel()) {
router.push(GetLocalPanel())
}
}
</script> </script>
<style scoped> <style scoped>

View file

@ -45,6 +45,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<IconDevices />{{ isLocal() ? 'Devices' : 'Server' }} <IconDevices />{{ isLocal() ? 'Devices' : 'Server' }}
</RouterLink> </RouterLink>
</li> </li>
<li>
<RouterLink @click="menuOpen = false" to="/settings"> <IconSettings />Settings </RouterLink>
</li>
<!-- <li> <!-- <li>
<RouterLink @click="menuOpen = false" to="/settings"> <RouterLink @click="menuOpen = false" to="/settings">
<IconSettings />Settings <IconSettings />Settings

View file

@ -0,0 +1,46 @@
<template>
<div class="mcrm-notifications">
<ToastComp
v-for="(notification, id) in notificationList"
:key="id"
:id="id"
:title="notification.title"
:message="notification.message"
:variant="notification.variant"
:time="notification.time"
:closable="notification.closable"
:notification="id"
/>
</div>
</template>
<script setup>
import { useNoticationStore } from '@/stores/notifications'
import { computed, onMounted } from 'vue'
import ToastComp from './ToastComp.vue'
const notifications = useNoticationStore()
const notificationList = computed(() => notifications.list)
// onMounted(() => {
// notifications.$subscribe((mutation, state) => {
// console.log(mutation, state)
// })
// })
</script>
<style scoped>
@reference "@/assets/main.css";
.mcrm-notifications {
@apply grid
gap-4
fixed
bottom-0
right-0
p-4
z-50
w-[30ch];
}
</style>

View file

@ -0,0 +1,82 @@
<template>
<div :class="`mcrm-toast mcrm-block block__${toastOptions.variant}`" ref="toast">
<ButtonComp v-if="closable" variant="subtle" size="sm" @click="closeToast()">
<IconX />
</ButtonComp>
<h4>{{ title }}</h4>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { useNoticationStore } from '@/stores/notifications'
import { onMounted, reactive, ref } from 'vue'
import ButtonComp from './ButtonComp.vue'
import { IconX } from '@tabler/icons-vue'
const props = defineProps({
title: String,
message: String,
variant: String,
time: Number,
closable: Boolean,
notification: [String, Number],
})
const notifications = useNoticationStore()
const toast = ref(null)
const toastOptions = reactive({
variant: props.variant,
})
onMounted(() => {
if (toastOptions.variant == 'info') toastOptions.variant = 'primary'
setTimeout(() => {
closeToast()
}, props.time)
})
const closeToast = () => {
toast.value.classList.add('closing')
setTimeout(() => {
toast.value.remove()
if (props.notification) notifications.remove(props.notification)
}, 500)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.mcrm-toast {
@apply relative
grid
gap-2
p-4
transition-opacity
duration-400;
h4 {
@apply pr-6
text-base;
}
p {
@apply text-sm opacity-80;
}
&.closing {
@apply opacity-0;
}
button.btn {
@apply absolute
top-2 right-2
p-2;
}
}
</style>

View file

@ -0,0 +1,114 @@
<template>
<div :class="`form-input ${horizontal ? 'horizontal' : ''}`">
<template v-if="label">
<label :for="name">
{{ label }}
</label>
</template>
<template v-if="type != 'radio' && type != 'checkbox'">
<input
:type="type"
:name="name"
:id="name"
:value="value"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:required="required"
:autofocus="autofocus"
@change="$emit('onChange', $event.target.value)"
@input="$emit('onInput', $event.target.value)"
/>
</template>
<template v-else-if="options">
<div class="boolean-group">
<template v-for="option in options" :key="option.value">
<label :for="`${name}-${option.value}`">
{{ option.label }}
</label>
<input
:type="type"
:name="name"
:id="`${name}-${option.value}`"
:disabled="disabled"
:readonly="readonly"
:required="required"
:checked="value == option.value"
:value="option.value"
@change="$emit('onChange', $event.target.value)"
@input="$emit('onInput', $event.target.value)"
/>
</template>
</div>
</template>
<template v-else>
<input
:type="type"
:name="name"
:id="name"
:value="value"
:disabled="disabled"
:readonly="readonly"
:required="required"
:checked="value === true || value === 'true'"
@change="$emit('onChange', $event.target)"
@input="$emit('onInput', $event.target)"
/>
</template>
</div>
</template>
<script setup>
defineProps({
type: String,
name: String,
value: [String, Number, Boolean],
options: Array,
label: String,
placeholder: String,
disabled: Boolean,
readonly: Boolean,
required: Boolean,
autofocus: Boolean,
horizontal: Boolean,
})
defineEmits(['onChange', 'onInput'])
</script>
<style scoped>
@reference "@/assets/main.css";
.form-input {
@apply flex
flex-col
items-center
gap-2;
&.horizontal {
@apply flex-row
justify-between;
input {
@apply max-w-2/3;
}
}
}
.boolean-group {
@apply flex
items-center
gap-2;
}
input[type='checkbox'],
input[type='radio'] {
@apply size-5
cursor-pointer;
}
input[disabled] {
@apply opacity-60
cursor-not-allowed;
}
</style>

View file

@ -60,11 +60,11 @@ const router = createRouter({
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',

View file

@ -0,0 +1,20 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useNoticationStore = defineStore('notications', () => {
const list = ref({})
const add = (notification) => {
list.value[Date.now()] = notification
}
const remove = (id) => {
delete list.value[id]
}
return {
list,
add,
remove,
}
})

74
ui/src/stores/settings.js Normal file
View file

@ -0,0 +1,74 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettingStore = defineStore('settings', () => {
const list = ref({})
const get = (key, all = false) => {
if (key === undefined) return list.value
if (!all) return list.value[key].value
else return list.value[key]
}
const set = (key, value) => {
if (value === 'true' || value === 'false') value = value === 'true'
list.value[key].value = value
saveSettings()
}
const loadSettings = (returnObj = false) => {
const settings = localStorage.getItem('settings')
if (settings) list.value = JSON.parse(settings)
else list.value = loadDefaultSettings()
if (returnObj) return list.value
}
const loadDefaultSettings = () => {
const defaultSettings = {
openLastPanel: {
title: 'Open last panel',
label: 'Open last panel',
description: 'Open the last panel on startup',
type: 'checkbox',
value: true,
remote: true,
},
mcrmPort: {
title: 'Macrame port',
label: 'Port',
description:
'The port that is used by Macrame, changing this will require a restart of the application.',
type: 'number',
value: window.__CONFIG__.MCRM__PORT,
remote: false,
disabled: true,
},
}
return defaultSettings
}
const saveSettings = () => {
localStorage.setItem('settings', JSON.stringify(list.value))
}
const resetSettings = () => {
localStorage.removeItem('settings')
list.value = loadDefaultSettings()
}
return {
list,
get,
set,
loadSettings,
loadDefaultSettings,
saveSettings,
resetSettings,
}
})

View file

@ -20,9 +20,96 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div></div> <div id="settings" class="panel">
<h1 class="flex items-end justify-between !w-full panel__title">
<div>Settings</div>
<ButtonComp variant="subtle" size="sm" @click="settings.resetSettings()">
<IconRestore /> Reset
</ButtonComp>
</h1>
<div class="panel__content">
<div class="settings-container">
<template
class="setting-block mcrm-block block__dark"
v-for="(setting, name) in settingList"
:key="setting"
>
<div
v-if="(setting.remote && !isLocal()) || (!setting.remote && isLocal())"
class="setting-block mcrm-block block__dark"
>
<h4>{{ setting.title }}</h4>
<p class="text-sm">
<em>{{ setting.description }}</em>
</p>
<FormInput
:type="setting.type"
:label="setting.label"
:name="name"
:value="setting.value"
:options="setting.options"
:disabled="setting.disabled"
:horizontal="true"
@onChange="updateSetting(name, setting.type, $event)"
/>
</div>
</template>
</div>
</div>
</div>
</template> </template>
<script setup></script> <script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import { IconRestore } from '@tabler/icons-vue'
import FormInput from '@/components/form/FormInput.vue'
<style lang="scss" scoped></style> import { isLocal } from '@/services/ApiService'
import { useSettingStore } from '@/stores/settings'
import { useNoticationStore } from '@/stores/notifications'
import { computed, onMounted } from 'vue'
const settings = useSettingStore()
const notifications = useNoticationStore()
const settingList = computed(() => settings.list)
onMounted(() => {})
function updateSetting(name, type, target) {
if (type == 'checkbox' || type == 'radio') target.value = target.checked
settings.set(name, target.value)
notifications.add({
title: 'Settings updated',
message: 'Settings have been updated',
variant: 'success',
time: 5000,
closable: true,
})
}
</script>
<style scoped>
@reference "@/assets/main.css";
.settings-container {
@apply grid
grid-cols-1
sm:grid-cols-2
lg:grid-cols-3
items-start
gap-4
pt-8;
.setting-block {
@apply grid
gap-3
content-start;
}
}
</style>