From 216f0043e5bbba27b2de912e89029d0761fc4ad1 Mon Sep 17 00:00:00 2001 From: cpu Date: Fri, 9 May 2025 13:11:49 +0200 Subject: [PATCH] overlay for the hotkey --- README.md | 5 +- public/service-worker.js | 2 +- src/components/HotkeyCaptureOverlay.vue | 79 ++++++++ src/components/PlayerForm.vue | 140 +++++++------ src/views/SetupView.vue | 250 ++++++++++++------------ 5 files changed, 273 insertions(+), 203 deletions(-) create mode 100644 src/components/HotkeyCaptureOverlay.vue diff --git a/README.md b/README.md index 551c6c8..3111bfe 100644 --- a/README.md +++ b/README.md @@ -187,8 +187,7 @@ npm run dev Open it in your browser: [http://localhost:8080/](http://localhost:8080/) - -When done, do not forget to update the cache version in the `service-worker.js` +When done, do not forget to update the `CACHE_VERSION` in the `service-worker.js`. It is the indicator for the PWA that the new version is available. ```bash ver=$(grep -oP "CACHE_VERSION = 'nexus-timer-cache-v\K[0-9]+" public/service-worker.js) sed -i "s/nexus-timer-cache-v$ver/nexus-timer-cache-v$((ver+1))/" public/service-worker.js @@ -212,4 +211,4 @@ View real-time logs ```bash journalctl -fu virt-nexus-timer.service ``` -The previously installed PWA should now offer an upgrade \ No newline at end of file +The previously installed PWA should update automatically or offer an upgrade \ No newline at end of file diff --git a/public/service-worker.js b/public/service-worker.js index 186e767..a496094 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_VERSION = 'nexus-timer-cache-v4'; +const CACHE_VERSION = 'nexus-timer-cache-v5'; const APP_SHELL_URLS = [ // '/', // Let NetworkFirst handle '/' '/manifest.json', diff --git a/src/components/HotkeyCaptureOverlay.vue b/src/components/HotkeyCaptureOverlay.vue new file mode 100644 index 0000000..66e920a --- /dev/null +++ b/src/components/HotkeyCaptureOverlay.vue @@ -0,0 +1,79 @@ + + + + + \ No newline at end of file diff --git a/src/components/PlayerForm.vue b/src/components/PlayerForm.vue index bdd5453..c44d71d 100644 --- a/src/components/PlayerForm.vue +++ b/src/components/PlayerForm.vue @@ -3,20 +3,18 @@

{{ isEditing ? 'Edit Player' : 'Add New Player' }}

- +
-

{{ currentTimeFormatError }}

-
@@ -36,39 +34,45 @@

{{ cameraError }}

+ -
-
+ + +
@@ -78,32 +82,65 @@ import { useStore } from 'vuex'; import { CameraService } from '../services/CameraService'; import { formatTime, parseTime } from '../utils/timeFormatter'; import DefaultAvatarIcon from './DefaultAvatarIcon.vue'; +import HotkeyCaptureOverlay from './HotkeyCaptureOverlay.vue'; // Import overlay import { AudioService } from '../services/AudioService'; -const props = defineProps({ - player: Object, -}); +const props = defineProps({ player: Object }); const emit = defineEmits(['close', 'save']); - const store = useStore(); const DEFAULT_AVATAR_MARKER = null; const isEditing = computed(() => !!props.player); -const editablePlayer = reactive({ - id: null, - name: '', - avatar: DEFAULT_AVATAR_MARKER, - initialTimerSec: 3600, - currentTimerSec: 3600, - hotkey: '', +const editablePlayer = reactive({ /* ... same as before ... */ + id: null, name: '', avatar: DEFAULT_AVATAR_MARKER, initialTimerSec: 3600, currentTimerSec: 3600, hotkey: '', }); - const currentTimeFormatted = ref('60:00'); const currentTimeFormatError = ref(''); const cameraError = ref(''); - const maxNegativeSeconds = computed(() => store.getters.maxNegativeTimeReached); +// --- State for Player Hotkey Capture --- +const isCapturingPlayerHotkey = ref(false); + +const startCapturePlayerHotkey = () => { + isCapturingPlayerHotkey.value = true; +}; + +const handlePlayerHotkeyCaptured = (key) => { + isCapturingPlayerHotkey.value = false; + // Validate the captured key (conflicts etc.) + const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key && p.id !== editablePlayer.id); + const globalHotkeyInUseStopPause = store.state.globalHotkeyStopPause === key; + const globalHotkeyInUseRunAll = store.state.globalHotkeyRunAll === key; + + if (existingPlayerHotkey) { + alert(`Hotkey "${key.toUpperCase()}" is already assigned to another player.`); + return; + } + if (globalHotkeyInUseStopPause) { + alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Stop/Pause hotkey.`); + return; + } + if (globalHotkeyInUseRunAll) { + alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Run All Timers hotkey.`); + return; + } + editablePlayer.hotkey = key; +}; + +const cancelCapturePlayerHotkey = () => { + isCapturingPlayerHotkey.value = false; +}; +// --- End Player Hotkey Capture --- + +// ... (onMounted, watch for currentTimeFormatted, validateCurrentTimeFormat, capturePhoto, useDefaultAvatar, submitForm, closeModal remain the same) +// No change to captureHotkey itself, as the overlay handles the key capture event +// The clearHotkey method is still relevant +function clearHotkey(type) { // 'type' is 'player' here + if (type === 'player') { + editablePlayer.hotkey = ''; + } +} onMounted(() => { if (isEditing.value && props.player) { @@ -167,60 +204,18 @@ function useDefaultAvatar() { editablePlayer.avatar = DEFAULT_AVATAR_MARKER; } -function captureHotkey(event, type) { - event.preventDefault(); - let key = event.key; - if (event.code === 'Space') { - key = ' '; - } else if (key.length > 1 && key !== ' ') { - return; - } - key = key.toLowerCase(); - - if (type === 'player') { - const existingPlayerHotkey = store.state.players.some(p => p.hotkey === key && p.id !== editablePlayer.id); - const globalHotkeyInUseStopPause = store.state.globalHotkeyStopPause === key; - const globalHotkeyInUseRunAll = store.state.globalHotkeyRunAll === key; - - if (existingPlayerHotkey) { - alert(`Hotkey "${key.toUpperCase()}" is already assigned to another player.`); - return; - } - if (globalHotkeyInUseStopPause) { - alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Stop/Pause hotkey.`); - return; - } - if (globalHotkeyInUseRunAll) { - alert(`Hotkey "${key.toUpperCase()}" is already assigned as the Global Run All Timers hotkey.`); - return; - } - editablePlayer.hotkey = key; - } -} - - -function clearHotkey(type) { - if (type === 'player') { - editablePlayer.hotkey = ''; - } -} - function submitForm() { validateCurrentTimeFormat(); if (currentTimeFormatError.value) return; - const playerPayload = { ...editablePlayer }; - if (playerPayload.currentTimerSec <= maxNegativeSeconds.value) { playerPayload.isSkipped = true; } else if (playerPayload.isSkipped && playerPayload.currentTimerSec > maxNegativeSeconds.value) { playerPayload.isSkipped = false; } - if (!isEditing.value) { playerPayload.initialTimerSec = playerPayload.currentTimerSec; } - emit('save', playerPayload); closeModal(); } @@ -228,4 +223,5 @@ function submitForm() { function closeModal() { emit('close'); } + \ No newline at end of file diff --git a/src/views/SetupView.vue b/src/views/SetupView.vue index e130124..54d2f38 100644 --- a/src/views/SetupView.vue +++ b/src/views/SetupView.vue @@ -1,13 +1,13 @@ @@ -152,6 +152,7 @@ import { useRouter } from 'vue-router'; import PlayerForm from '../components/PlayerForm.vue'; import { formatTime } from '../utils/timeFormatter'; import DefaultAvatarIcon from '../components/DefaultAvatarIcon.vue'; +import HotkeyCaptureOverlay from '../components/HotkeyCaptureOverlay.vue'; // Import overlay const store = useStore(); const router = useRouter(); @@ -159,94 +160,29 @@ const router = useRouter(); const players = computed(() => store.getters.players); const theme = computed(() => store.getters.theme); const isMuted = computed(() => store.getters.isMuted); - const globalHotkeyStopPause = computed(() => store.getters.globalHotkeyStopPause); const globalHotkeyStopPauseDisplay = computed(() => globalHotkeyStopPause.value ? globalHotkeyStopPause.value.toUpperCase() : ''); - const globalHotkeyRunAll = computed(() => store.getters.globalHotkeyRunAll); const globalHotkeyRunAllDisplay = computed(() => globalHotkeyRunAll.value ? globalHotkeyRunAll.value.toUpperCase() : ''); const showPlayerModal = ref(false); const editingPlayer = ref(null); -const openAddPlayerModal = () => { - if (players.value.length < 99) { - editingPlayer.value = null; - showPlayerModal.value = true; - } else { - alert("Maximum player limit (99) reached."); - } +// --- State for Global Hotkey Capture --- +const isCapturingGlobalHotkey = ref(false); +const currentGlobalHotkeyType = ref(null); // 'stopPause' or 'runAll' + +const startCaptureGlobalHotkey = (type) => { + currentGlobalHotkeyType.value = type; + isCapturingGlobalHotkey.value = true; }; -const openEditPlayerModal = (player) => { - editingPlayer.value = player; - showPlayerModal.value = true; -}; - -const closePlayerModal = () => { - showPlayerModal.value = false; - editingPlayer.value = null; -}; - -const savePlayer = (playerData) => { - if (playerData.id) { - store.dispatch('updatePlayer', playerData); - } else { - store.dispatch('addPlayer', { - name: playerData.name, - avatar: playerData.avatar, - initialTimerSec: playerData.initialTimerSec, - currentTimerSec: playerData.currentTimerSec, - hotkey: playerData.hotkey - }); - } - closePlayerModal(); -}; - -const confirmDeletePlayer = (playerId) => { - if (window.confirm('Are you sure you want to delete this player?')) { - store.dispatch('deletePlayer', playerId); - } -}; - -const shufflePlayers = () => { - store.dispatch('shufflePlayers'); -}; - -const reversePlayers = () => { - store.dispatch('reversePlayers'); -}; - -const movePlayerUp = (index) => { - if (index > 0) { - const newPlayersOrder = [...players.value]; - const temp = newPlayersOrder[index]; - newPlayersOrder[index] = newPlayersOrder[index - 1]; - newPlayersOrder[index - 1] = temp; - store.dispatch('reorderPlayers', newPlayersOrder); - } -}; - -const movePlayerDown = (index) => { - if (index < players.value.length - 1) { - const newPlayersOrder = [...players.value]; - const temp = newPlayersOrder[index]; - newPlayersOrder[index] = newPlayersOrder[index + 1]; - newPlayersOrder[index + 1] = temp; - store.dispatch('reorderPlayers', newPlayersOrder); - } -}; - -const captureGlobalHotkey = (event, type) => { - event.preventDefault(); - let key = event.key; - if (event.code === 'Space') { - key = ' '; - } else if (key.length > 1 && key !== ' ') { - return; - } - key = key.toLowerCase(); +const handleGlobalHotkeyCaptured = (key) => { + isCapturingGlobalHotkey.value = false; + const type = currentGlobalHotkeyType.value; + if (!type) return; + // Validate captured key const isPlayerHotkey = store.state.players.some(p => p.hotkey === key); if (isPlayerHotkey) { alert(`Hotkey "${key.toUpperCase()}" is already assigned to a player.`); @@ -266,8 +202,74 @@ const captureGlobalHotkey = (event, type) => { } store.dispatch('setGlobalHotkeyRunAll', key); } + currentGlobalHotkeyType.value = null; // Reset type }; +const cancelCaptureGlobalHotkey = () => { + isCapturingGlobalHotkey.value = false; + currentGlobalHotkeyType.value = null; +}; +// --- End Global Hotkey Capture --- + +// ... (openAddPlayerModal, openEditPlayerModal, closePlayerModal, savePlayer, confirmDeletePlayer, shufflePlayers, reversePlayers, movePlayerUp, movePlayerDown remain the same) +const openAddPlayerModal = () => { + if (players.value.length < 99) { + editingPlayer.value = null; + showPlayerModal.value = true; + } else { + alert("Maximum player limit (99) reached."); + } +}; +const openEditPlayerModal = (player) => { + editingPlayer.value = player; + showPlayerModal.value = true; +}; +const closePlayerModal = () => { + showPlayerModal.value = false; + editingPlayer.value = null; +}; +const savePlayer = (playerData) => { + if (playerData.id) { + store.dispatch('updatePlayer', playerData); + } else { + store.dispatch('addPlayer', { + name: playerData.name, + avatar: playerData.avatar, + initialTimerSec: playerData.initialTimerSec, + currentTimerSec: playerData.currentTimerSec, + hotkey: playerData.hotkey + }); + } + closePlayerModal(); +}; +const confirmDeletePlayer = (playerId) => { + if (window.confirm('Are you sure you want to delete this player?')) { + store.dispatch('deletePlayer', playerId); + } +}; +const shufflePlayers = () => { store.dispatch('shufflePlayers'); }; +const reversePlayers = () => { store.dispatch('reversePlayers'); }; +const movePlayerUp = (index) => { /* ... as before ... */ + if (index > 0) { + const newPlayersOrder = [...players.value]; + const temp = newPlayersOrder[index]; + newPlayersOrder[index] = newPlayersOrder[index - 1]; + newPlayersOrder[index - 1] = temp; + store.dispatch('reorderPlayers', newPlayersOrder); + } +}; +const movePlayerDown = (index) => { /* ... as before ... */ + if (index < players.value.length - 1) { + const newPlayersOrder = [...players.value]; + const temp = newPlayersOrder[index]; + newPlayersOrder[index] = newPlayersOrder[index + 1]; + newPlayersOrder[index + 1] = temp; + store.dispatch('reorderPlayers', newPlayersOrder); + } +}; + + +// The old captureGlobalHotkey is replaced by startCaptureGlobalHotkey and handleGlobalHotkeyCaptured const clearGlobalHotkey = (type) => { if (type === 'stopPause') { store.dispatch('setGlobalHotkeyStopPause', null); @@ -276,15 +278,10 @@ const clearGlobalHotkey = (type) => { } }; -const toggleTheme = () => { - store.dispatch('toggleTheme'); -}; - -const toggleMute = () => { - store.dispatch('setMuted', !isMuted.value); -}; - -const saveAndClose = () => { +// ... (toggleTheme, toggleMute, saveAndClose, resetPlayerTimersConfirm, fullResetAppConfirm remain the same) +const toggleTheme = () => { store.dispatch('toggleTheme'); }; +const toggleMute = () => { store.dispatch('setMuted', !isMuted.value); }; +const saveAndClose = () => { /* ... as before ... */ store.dispatch('saveState'); if (players.value.length >= 2) { if (store.state.currentPlayerIndex >= players.value.length || store.state.currentPlayerIndex < 0) { @@ -299,16 +296,15 @@ const saveAndClose = () => { alert('At least 2 players are required to start a game.'); } }; - -const resetPlayerTimersConfirm = () => { +const resetPlayerTimersConfirm = () => { /* ... as before ... */ if (window.confirm('Are you sure you want to reset all current players\' timers to their initial values? This will not delete players.')) { store.dispatch('resetGame'); } }; - -const fullResetAppConfirm = () => { +const fullResetAppConfirm = () => { /* ... as before ... */ if (window.confirm('Are you sure you want to reset all app data? This includes all players, settings, and timer states. The app will revert to its default state with 2 predefined players.')) { store.dispatch('fullResetApp'); } }; + \ No newline at end of file