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>
|
||||
<p>Click here to start authorization and open the "Devices" page on your PC.</p>
|
||||
</AlertComp>
|
||||
<NotificationsComp />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MainMenu from '@/components/base/MainMenu.vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import { onBeforeUnmount, onMounted, onUnmounted, onUpdated, ref } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
import { useDeviceStore } from './stores/device'
|
||||
import { useSettingStore } from './stores/settings'
|
||||
import { isLocal } from './services/ApiService'
|
||||
import AlertComp from './components/base/AlertComp.vue'
|
||||
import NotificationsComp from './components/base/NotificationsComp.vue'
|
||||
import { GetLocalPanel } from './services/PanelService'
|
||||
|
||||
const device = useDeviceStore()
|
||||
const settings = useSettingStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const handshake = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -55,17 +61,38 @@ onMounted(() => {
|
|||
// If not present in LocalStorage a new uuidV4 will be generated
|
||||
device.uuid()
|
||||
|
||||
if (!isLocal) appHandshake()
|
||||
settings.loadSettings()
|
||||
|
||||
if (!isLocal) {
|
||||
appHandshake()
|
||||
loadLastPanel()
|
||||
}
|
||||
|
||||
device.$subscribe(() => {
|
||||
if (device.key()) handshake.value = true
|
||||
})
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
console.log('App updated')
|
||||
|
||||
// loadLastPanel()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
settings.saveSettings()
|
||||
})
|
||||
|
||||
async function appHandshake() {
|
||||
const hsReq = await device.remoteHandshake()
|
||||
handshake.value = hsReq
|
||||
}
|
||||
|
||||
function loadLastPanel() {
|
||||
if (route.fullPath != GetLocalPanel()) {
|
||||
router.push(GetLocalPanel())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<IconDevices />{{ isLocal() ? 'Devices' : 'Server' }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink @click="menuOpen = false" to="/settings"> <IconSettings />Settings </RouterLink>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<RouterLink @click="menuOpen = false" to="/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',
|
||||
component: () => import('../views/DevicesView.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/settings',
|
||||
// name: 'settings',
|
||||
// component: () => import('../views/SettingsView.vue'),
|
||||
// },
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('../views/SettingsView.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/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>
|
||||
<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>
|
||||
|
||||
<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