Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted } from 'vue'
import { addToHistory, keyboardStore, notifications, serialKeyboards } from './store'
import { addSerialLine } from './store/serial'
import { useRouter } from 'vue-router'
import LoadingOverlay from './components/LoadingOverlay.vue'
const router = useRouter()
const store = computed(() => {
return keyboardStore
Expand Down Expand Up @@ -69,6 +70,7 @@ onUnmounted(() => {
</div>
</div>
<router-view></router-view>
<LoadingOverlay />
</template>
<style lang="scss">
html,
Expand Down
70 changes: 70 additions & 0 deletions src/renderer/src/components/BaseModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="props.open"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
@click.self="$emit('close')"
>
<div
class="relative w-full max-w-md rounded-2xl border border-gray-200/20 bg-base-100/80 p-8 shadow-2xl backdrop-blur-md"
>
<h3 v-if="props.title" class="mb-2 text-xl font-semibold">{{ props.title }}</h3>
<div class="py-2">
<slot />
</div>
<div class="mt-6 flex items-center justify-end gap-2">
<button class="btn justify-self-start" @click="$emit('close')">
{{ props.cancelText }}
</button>
<button
v-if="props.secondaryText"
class="btn btn-primary justify-self-center"
@click="$emit('secondary')"
>
{{ props.secondaryText }}
</button>
<div v-else></div>
<button
v-if="props.showConfirm && props.confirmText"
class="btn btn-primary justify-self-end"
@click="$emit('confirm')"
>
{{ props.confirmText }}
</button>
</div>
</div>
</div>
</Transition>
</template>

<script setup lang="ts">
interface Props {
open: boolean
title?: string
confirmText?: string
cancelText?: string
secondaryText?: string
showConfirm?: boolean
}

const props = withDefaults(defineProps<Props>(), {
title: '',
confirmText: '',
cancelText: 'Cancel',
secondaryText: '',
showConfirm: true
})

defineEmits<{
close: []
confirm: []
secondary: []
}>()
</script>
164 changes: 134 additions & 30 deletions src/renderer/src/components/KmkInstaller.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
<template>
<dialog id="update_modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">Attention</h3>
<p class="py-4">
Updating the POG files will overwrite all files on your keyboard generated by POG (e.g.
kb.py, code.py, customkeys.py, etc.)
</p>
<p class="py-4">Be sure to backup your code if you still need any of it.</p>
<div class="flex justify-between">
<div class="btn" @click="closeModal">Abort</div>
<div class="btn btn-error" @click="updatePOG">Update POG files</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<BaseModal
:open="isUpdateOpen"
title="Attention"
confirm-text="Update POG files"
cancel-text="Abort"
@close="isUpdateOpen = false"
@confirm="updatePOG"
>
<p>
Updating the POG files will overwrite all files on your keyboard generated by POG (e.g. kb.py,
code.py, customkeys.py, etc.)
</p>
<p class="pt-2">Be sure to backup your code if you still need any of it.</p>
</BaseModal>

<BaseModal
:open="isRestoreOpen"
title="Restore Configuration"
cancel-text="Cancel"
secondary-text="Keep ID"
confirm-text="Generate New ID"
@close="isRestoreOpen = false"
@secondary="restoreConfig(false)"
@confirm="restoreConfig(true)"
>
<p>
Do you want to generate new ID for the restored configuration? This is only recommended if you
are restoring a configuration from another keyboard.
</p>
</BaseModal>

<div class="mt-4 p-4 text-left">
<p>
<a href="https://kmkfw.io/" target="_blank" class="link">KMK</a> is a capable firmware for
Expand Down Expand Up @@ -52,13 +66,37 @@
v-if="['', 'done'].includes(kmkInstallState)"
class="mt-8 flex flex-col items-center justify-center"
>
<div class="mt-8 grid grid-cols-2 justify-center gap-4">
<div
class="mt-8 grid justify-center gap-4"
:class="{
'grid-cols-1': !keyboardStore.firmwareInstalled,
'grid-cols-2': keyboardStore.firmwareInstalled
}"
>
<button class="btn btn-primary" @click="updateKMK">
{{ keyboardStore.firmwareInstalled ? 'update' : 'install' }} KMK
</button>
<button class="btn btn-primary" @click="openModal">update Firmware</button>
<button v-if="!initialSetup" class="btn btn-primary" @click="isUpdateOpen = true">
update Firmware
</button>
<button v-if="!initialSetup" class="btn btn-primary" @click="backupConfiguration">
Backup Config
</button>
<button
v-if="!initialSetup || keyboardStore.firmwareInstalled"
class="btn btn-primary"
@click="restoreConfiguration"
>
Restore Config
</button>
</div>
<button class="btn btn-primary mt-8" @click="updateKMK">install KMK</button>
<input
ref="fileInput"
type="file"
accept=".json"
style="display: none"
@change="handleFileUpload"
/>
</div>
<div v-if="initialSetup" class="mt-8 flex justify-center">
<button
Expand All @@ -68,7 +106,6 @@
>
Next
</button>
<button class="btn mt-4 block" @click="$emit('next')">I installed KMK manually</button>
</div>

<div
Expand All @@ -84,14 +121,21 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { keyboardStore } from '../store'
import BaseModal from './BaseModal.vue'
import { keyboardStore, addToHistory } from '../store'
import dayjs from 'dayjs'
import { ulid } from 'ulid'
import { saveConfigurationWithLoading } from '../helpers/saveConfigurationWrapper'

const progress = ref(0)
const kmkInstallState = ref('')
const isUpdateOpen = ref(false)
const isRestoreOpen = ref(false)
const fileInput = ref<HTMLInputElement>()
const pendingConfigData = ref<any>(null)

defineProps<{ initialSetup: boolean }>()
defineEmits(['next'])
const props = defineProps<{ initialSetup: boolean }>()
const emit = defineEmits(['next', 'done'])
const startTime = ref(dayjs())
const endTime = ref(dayjs())

Expand All @@ -102,18 +146,78 @@ const updateKMK = async () => {
}

const updatePOG = async () => {
window.api.saveConfiguration(
await saveConfigurationWithLoading(
JSON.stringify({ pogConfig: keyboardStore.serialize(), writeFirmware: true })
)
closeModal()
isUpdateOpen.value = false
}

const openModal = () => {
;(document.getElementById('update_modal') as HTMLDialogElement).showModal()
const backupConfiguration = async () => {
try {
const configData = keyboardStore.serialize()
const jsonString = JSON.stringify(configData, null, 2)
const blob = new Blob([jsonString], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'pog_backup.json'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
} catch (error) {
console.error('Error downloading configuration:', error)
}
}

const closeModal = () => {
;(document.getElementById('update_modal') as HTMLDialogElement).close()
const restoreConfiguration = () => {
fileInput.value?.click()
}

const handleFileUpload = async (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
try {
const configData = JSON.parse(await file.text())
pendingConfigData.value = configData
isRestoreOpen.value = true
} catch (error) {
console.error('Error reading or parsing configuration file:', error)
}
target.value = ''
}

const restoreConfig = async (generateNewIds: boolean) => {
if (!pendingConfigData.value) return

const configData = generateNewIds ? { ...pendingConfigData.value } : pendingConfigData.value
if (generateNewIds) {
configData.id = ulid()
}

try {
isRestoreOpen.value = false

await saveConfigurationWithLoading(
JSON.stringify({ pogConfig: configData, serial: false, writeFirmware: true })
)
if (keyboardStore.path) {
const keyboardData = await window.api.selectKeyboard({ path: keyboardStore.path })
if (keyboardData && !keyboardData.error) {
keyboardStore.import(keyboardData)
}
}
if (keyboardStore.keymap.length === 0) keyboardStore.keymap = [[]]
keyboardStore.coordMapSetup = false

if (props.initialSetup) {
addToHistory(keyboardStore)
}
emit('done')
} catch (e) {
console.error('restore failed', e)
}
}

window.api.onUpdateFirmwareInstallProgress(
Expand Down
28 changes: 13 additions & 15 deletions src/renderer/src/components/LoadingOverlay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
leave-to-class="opacity-0 -translate-y-2"
>
<div
v-if="props.isVisible"
class="fixed left-2 top-2 z-50 w-[350px] rounded-2xl border border-white/20 bg-base-100/10 p-4 text-xs shadow-2xl backdrop-blur-md"
v-if="getLoadingState()"
class="fixed left-2 top-2 z-50 w-[350px] rounded-2xl border border-white/20 bg-base-100/40 p-4 text-xs shadow-2xl backdrop-blur-md"
>
<div class="mb-2 flex items-center gap-3">
<div class="relative h-6 w-6">
Expand Down Expand Up @@ -58,15 +58,9 @@
<script setup lang="ts">
import { onMounted, onUnmounted, watch, ref, computed } from 'vue'
import { serialLogs } from '@renderer/store/serial'
import { getLoadingState, hideLoading } from '../helpers/saveConfigurationWrapper'

interface Props {
isVisible: boolean
usingSerial?: boolean
}

const props = withDefaults(defineProps<Props>(), {
usingSerial: false
})
// No props needed - uses global loading state

const emit = defineEmits<{
(e: 'done'): void
Expand Down Expand Up @@ -171,7 +165,9 @@ const attachListeners = () => {
transitionToWithMin('saved', () => transitionToWithMin('reloading'))
}
}
window.api.onSaveConfigurationProgress(saveProgressHandler)
if (window.api.onSaveConfigurationProgress) {
window.api.onSaveConfigurationProgress(saveProgressHandler)
}
}
if (!unwatchSerial) {
unwatchSerial = watch(
Expand All @@ -190,7 +186,7 @@ const attachListeners = () => {

const detachListeners = () => {
// Remove listeners explicitly to avoid duplicates across multiple saves
if (saveProgressHandler) {
if (saveProgressHandler && window.api.offSaveConfigurationProgress) {
window.api.offSaveConfigurationProgress(saveProgressHandler)
}
if (unwatchSerial) {
Expand All @@ -207,6 +203,7 @@ const scheduleDone = () => {
if (minHideTimeout) clearTimeout(minHideTimeout)
minHideTimeout = setTimeout(() => {
emit('done')
hideLoading()
// reset local state for next run
progress.value = { state: '', completed: 0, total: 0 }
statusPhase.value = ''
Expand All @@ -228,7 +225,7 @@ const scheduleDone = () => {
}

watch(
() => props.isVisible,
() => getLoadingState(),
(visible) => {
if (visible) {
enterPhase('saving')
Expand Down Expand Up @@ -260,11 +257,12 @@ watch(
statusPhase.value = ''
phaseEnterAt = null
}
}
},
{ immediate: true }
)

onMounted(() => {
if (props.isVisible) {
if (getLoadingState()) {
statusPhase.value = 'saving'
attachListeners()
}
Expand Down
Loading