Compare commits

..

No commits in common. "c77233f07d997f602c26701655772b3e6a1cde07" and "3193127809bff116f53810f7f186be85e0dc7559" have entirely different histories.

16 changed files with 27 additions and 533 deletions

View file

@ -10,7 +10,7 @@ bin = "tmp/main.exe"
[build] [build]
cmd = "go build -o ./tmp/main.exe main.go" cmd = "go build -o ./tmp/main.exe main.go"
include_ext = ["go"] include_ext = ["go"]
exclude_dir = ["ui", "panels", "builds"] exclude_dir = ["fe", "panels", "builds"]
# Restart on file changes # Restart on file changes
[log] [log]

View file

@ -64,7 +64,7 @@ func configFileExists() bool {
} }
func CheckUIDevDir() { func CheckUIDevDir() {
log.Println("Checking UI dev directory...") log.Println("Checking FE dev directory...")
_, err := os.Stat("ui") _, err := os.Stat("ui")
if err != nil { if err != nil {
@ -72,10 +72,10 @@ func CheckUIDevDir() {
return return
} }
copyConfigToUi() copyConfigToFe()
} }
func copyConfigToUi() { func copyConfigToFe() {
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
if err != nil { if err != nil {

View file

@ -35,25 +35,19 @@ 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 { onBeforeUnmount, onMounted, onUnmounted, onUpdated, ref } from 'vue' import { onMounted, ref } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router' import { RouterView, useRoute } 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(() => {
@ -61,38 +55,17 @@ 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()
settings.loadSettings() if (!isLocal) appHandshake()
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,9 +45,6 @@ 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

@ -1,46 +0,0 @@
<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

@ -1,82 +0,0 @@
<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

@ -93,6 +93,8 @@ const server = reactive({
onMounted(async () => { onMounted(async () => {
const serverIP = await device.serverGetIP() const serverIP = await device.serverGetIP()
server.ip = serverIP server.ip = serverIP
// server.port = window.__CONFIG__.MCRM__PORT
// server.fullPath = `http://${server.ip}:${server.port}`
const remoteCount = await device.serverGetRemotes(true) const remoteCount = await device.serverGetRemotes(true)
server.remoteCount = remoteCount server.remoteCount = remoteCount
@ -102,6 +104,8 @@ onMounted(async () => {
const panelCount = await panel.getList(true) const panelCount = await panel.getList(true)
server.panelCount = panelCount server.panelCount = panelCount
console.log(server)
}) })
</script> </script>

View file

@ -1,114 +0,0 @@
<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

@ -21,48 +21,37 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<template> <template>
<div id="panel-view"> <div id="panel-view">
<div class="panel-container" ref="panelContainer" v-html="viewPanel.html"></div> <div class="panel-preview__content" ref="panelView" v-html="viewPanel.html"></div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { isLocal } from '@/services/ApiService'
import { RunMacro } from '@/services/MacroService' import { RunMacro } from '@/services/MacroService'
import { import {
CheckLocalPanel,
PanelButtonListeners, PanelButtonListeners,
PanelDialogListeners, PanelDialogListeners,
RemovePanelScripts, RemovePanelScripts,
RemovePanelStyle, RemovePanelStyle,
SavePanelToLocal,
SetPanelStyle, SetPanelStyle,
StripPanelHTML, StripPanelHTML,
} from '@/services/PanelService' } from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel' import { usePanelStore } from '@/stores/panel'
import { useSettingStore } from '@/stores/settings'
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
const panel = usePanelStore() const panel = usePanelStore()
const settings = useSettingStore()
const props = defineProps({ const props = defineProps({
dirname: String, dirname: String,
}) })
const panelContainer = ref(null) const panelView = ref(null)
const viewPanel = ref({}) const viewPanel = ref({})
const wakeLock = ref(null)
onMounted(async () => { onMounted(async () => {
requestWakeLock()
const currentPanel = await panel.get(props.dirname) const currentPanel = await panel.get(props.dirname)
viewPanel.value = currentPanel viewPanel.value = currentPanel
if (!isLocal() && settings.get('openLastPanel') && !CheckLocalPanel()) SavePanelToLocal()
viewPanel.value.html = StripPanelHTML(viewPanel.value.html, viewPanel.value.aspectRatio) viewPanel.value.html = StripPanelHTML(viewPanel.value.html, viewPanel.value.aspectRatio)
SetPanelStyle(viewPanel.value.style) SetPanelStyle(viewPanel.value.style)
@ -78,8 +67,6 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
RemovePanelStyle() RemovePanelStyle()
RemovePanelScripts() RemovePanelScripts()
wakeLock.value.release()
}) })
const viewPanelListeners = () => { const viewPanelListeners = () => {
@ -87,20 +74,8 @@ const viewPanelListeners = () => {
RunMacro(viewPanel.value.macros[button.id]) RunMacro(viewPanel.value.macros[button.id])
} }
PanelButtonListeners(panelContainer.value, callback) PanelButtonListeners(panelView.value, callback)
PanelDialogListeners(panelContainer.value) PanelDialogListeners(panelView.value)
}
const requestWakeLock = async () => {
try {
if ('wakeLock' in navigator) {
wakeLock.value = await navigator.wakeLock.request('screen')
} else {
console.warn('Wake Lock API not supported')
}
} catch (err) {
console.error(`${err.name}, ${err.message}`)
}
} }
</script> </script>
@ -113,7 +88,7 @@ const requestWakeLock = async () => {
size-full size-full
bg-black; bg-black;
.panel-container { .panel-preview__content {
@apply relative @apply relative
grid grid
justify-center justify-center

View file

@ -31,9 +31,7 @@ import router from '@/router'
const app = createApp(App) const app = createApp(App)
app.use(router)
app.use(createPinia()) app.use(createPinia())
app.use(router)
router.isReady().then(() => { app.mount('#app')
app.mount('#app')
})

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

@ -19,8 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useNoticationStore } from '@/stores/notifications'
export const SetPanelStyle = (styleStr) => { export const SetPanelStyle = (styleStr) => {
const styleEl = document.createElement('style') const styleEl = document.createElement('style')
styleEl.setAttribute('custom_panel_style', true) styleEl.setAttribute('custom_panel_style', true)
@ -120,32 +118,3 @@ export const PanelDialogListeners = (panelEl) => {
}) })
}) })
} }
const getPanelUrl = () => {
const url = new URL(window.location.href)
return url.pathname
}
export const SavePanelToLocal = () => {
localStorage.setItem('last_opened_panel', getPanelUrl())
const notificationStore = useNoticationStore()
notificationStore.add({
message: 'Panel will be opened next launch',
variant: 'success',
time: 1000,
})
}
export const CheckLocalPanel = () => {
const localPanel = localStorage.getItem('last_opened_panel')
if (localPanel) return localPanel == getPanelUrl()
return false
}
export const GetLocalPanel = () => {
return localStorage.getItem('last_opened_panel')
}

View file

@ -1,20 +0,0 @@
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,
}
})

View file

@ -1,74 +0,0 @@
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

@ -45,6 +45,7 @@ import ButtonComp from '@/components/base/ButtonComp.vue'
import PanelEdit from '@/components/panels/PanelEdit.vue' import PanelEdit from '@/components/panels/PanelEdit.vue'
import PanelView from '@/components/panels/PanelView.vue' import PanelView from '@/components/panels/PanelView.vue'
import PanelsOverview from '@/components/panels/PanelsOverview.vue' import PanelsOverview from '@/components/panels/PanelsOverview.vue'
import { isLocal } from '@/services/ApiService'
import { IconArrowLeft } from '@tabler/icons-vue' import { IconArrowLeft } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive } from 'vue' import { onMounted, onUpdated, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'

View file

@ -20,96 +20,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div id="settings" class="panel"> <div></div>
<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 setup></script>
import ButtonComp from '@/components/base/ButtonComp.vue'
import { IconRestore } from '@tabler/icons-vue'
import FormInput from '@/components/form/FormInput.vue'
import { isLocal } from '@/services/ApiService' <style lang="scss" scoped></style>
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>