mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 07:19:26 +00:00
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:
parent
3193127809
commit
57fa6cf2a2
9 changed files with 464 additions and 11 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
46
ui/src/components/base/NotificationsComp.vue
Normal file
46
ui/src/components/base/NotificationsComp.vue
Normal 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>
|
||||||
82
ui/src/components/base/ToastComp.vue
Normal file
82
ui/src/components/base/ToastComp.vue
Normal 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>
|
||||||
114
ui/src/components/form/FormInput.vue
Normal file
114
ui/src/components/form/FormInput.vue
Normal 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>
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
20
ui/src/stores/notifications.js
Normal file
20
ui/src/stores/notifications.js
Normal 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
74
ui/src/stores/settings.js
Normal 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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue