PWA fixed

added systemd service howto

traefik

nginix set_real_ip_from

improved readme

visuals fixed on mobile

labels removed

updated readme

fixed visuals

overlay for the hotkey

disable screen lock

clean up

git precommit hooks

clean up

clean up

update

check for update feature

added build-time information

fixed date

clean up

added hook script

fix

fix

fix

hooks fixed

webhook setup

players stay in run all timers mode

mqtt

mqtt allways connected

mqtt messages work

capturing mqtt in edit player

mqtt in Setup

updated readme

state of the mqtt

Global Pass turn
This commit is contained in:
cpu
2025-05-08 15:36:17 +02:00
parent d741efa62d
commit 19fbae06fc
51 changed files with 6992 additions and 2 deletions

531
src/store/index.js Normal file
View File

@@ -0,0 +1,531 @@
import { createStore } from 'vuex';
import { StorageService } from '../services/StorageService';
import { parseTime, formatTime } from '../utils/timeFormatter';
const MAX_NEGATIVE_SECONDS = -(59 * 60 + 59); // -59:59
const DEFAULT_AVATAR_MARKER = null; // Ensure this is defined
// Define predefined players
const predefinedPlayers = [
{
id: 'predefined-1', // Unique ID for predefined player 1
name: 'Player 1',
avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path if you have one
initialTimerSec: 60 * 60, // 60:00
currentTimerSec: 60 * 60,
hotkey: '1', // Hotkey '1'
isSkipped: false,
isTimerRunning: false,
},
{
id: 'predefined-2', // Unique ID for predefined player 2
name: 'Player 2',
avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path
initialTimerSec: 60 * 60, // 60:00
currentTimerSec: 60 * 60,
hotkey: '2', // Hotkey '2'
isSkipped: false,
isTimerRunning: false,
}
];
const initialState = {
players: JSON.parse(JSON.stringify(predefinedPlayers)), // Start with predefined players (deep copy)
globalHotkeyStopPause: null,
globalHotkeyRunAll: null,
globalHotkeyPassTurn: null,
globalMqttStopPause: null,
globalMqttRunAll: null,
globalMqttPassTurn: null,
mqttBrokerUrl: 'ws://localhost:9001',
mqttConnectDesired: false,
currentPlayerIndex: 0,
gameMode: 'normal',
isMuted: false,
theme: 'dark',
gameRunning: false,
};
// Helper function to create a new player object
const createPlayerObject = (playerData = {}) => ({
id: playerData.id || Date.now().toString() + Math.random(),
name: playerData.name || `Player ${store.state.players.length + 1}`, // Access store carefully here or pass length
avatar: playerData.avatar === undefined ? DEFAULT_AVATAR_MARKER : playerData.avatar,
initialTimerSec: playerData.initialTimerSec || 3600,
currentTimerSec: playerData.currentTimerSec || playerData.initialTimerSec || 3600,
hotkey: playerData.hotkey || null,
mqttChar: playerData.mqttChar || null, // New
isSkipped: playerData.isSkipped || false,
isTimerRunning: playerData.isTimerRunning || false,
});
export default createStore({
state: () => {
const persistedState = StorageService.getState();
if (persistedState) {
let playersToUse = persistedState.players;
if (!playersToUse || (playersToUse.length === 0 && !persistedState.hasOwnProperty('players'))) {
playersToUse = JSON.parse(JSON.stringify(predefinedPlayers.map(p => createPlayerObject(p))));
} else if (persistedState.hasOwnProperty('players') && playersToUse.length === 0) {
playersToUse = [];
}
playersToUse = playersToUse.map(p_persisted => {
const p_base = predefinedPlayers.find(p_def => p_def.id === p_persisted.id) || {};
return createPlayerObject({ ...p_base, ...p_persisted, isTimerRunning: false });
});
return {
...initialState, // Start with all initial state defaults
...persistedState, // Override with persisted values
players: playersToUse, // Specifically set processed players
globalHotkeyPassTurn: persistedState.globalHotkeyPassTurn || initialState.globalHotkeyPassTurn,
globalMqttPassTurn: persistedState.globalMqttPassTurn || initialState.globalMqttPassTurn,
globalHotkeyStopPause: persistedState.globalHotkeyStopPause || initialState.globalHotkeyStopPause,
globalMqttStopPause: persistedState.globalMqttStopPause || initialState.globalMqttStopPause,
globalHotkeyRunAll: persistedState.globalHotkeyRunAll || initialState.globalHotkeyRunAll,
globalMqttRunAll: persistedState.globalMqttRunAll || initialState.globalMqttRunAll,
mqttBrokerUrl: persistedState.mqttBrokerUrl || initialState.mqttBrokerUrl,
mqttConnectDesired: persistedState.hasOwnProperty('mqttConnectDesired') ? persistedState.mqttConnectDesired : initialState.mqttConnectDesired,
gameRunning: false, // Always start non-running
currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0,
};
}
// If no persisted state, deep copy initialState which includes new MQTT fields
const newInitialState = JSON.parse(JSON.stringify(initialState));
newInitialState.players = newInitialState.players.map(p => createPlayerObject(p));
return newInitialState;
},
mutations: {
ADD_PLAYER(state, playerConfig) {
if (state.players.length < 99) {
const newPlayer = createPlayerObject({
name: playerConfig.name,
avatar: playerConfig.avatar,
initialTimerSec: playerConfig.initialTimerSec,
currentTimerSec: playerConfig.currentTimerSec, // Set from form
hotkey: playerConfig.hotkey,
mqttChar: playerConfig.mqttChar // From form
});
state.players.push(newPlayer);
} else {
alert("Maximum player limit (99) reached.");
}
},
UPDATE_PLAYER(state, updatedPlayer) {
const index = state.players.findIndex(p => p.id === updatedPlayer.id);
if (index !== -1) {
state.players[index] = { ...state.players[index], ...updatedPlayer };
}
},
SET_MQTT_BROKER_URL(state, url) {
state.mqttBrokerUrl = url;
state.mqttConnectDesired = true;
},
SET_MQTT_CONNECT_DESIRED(state, desired) {
state.mqttConnectDesired = desired;
},
SET_GLOBAL_HOTKEY_STOP_PAUSE(state, key) {
state.globalHotkeyStopPause = key;
},
SET_GLOBAL_HOTKEY_RUN_ALL(state, key) {
state.globalHotkeyRunAll = key;
},
SET_GLOBAL_HOTKEY_PASS_TURN(state, key) {
state.globalHotkeyPassTurn = key;
},
SET_GLOBAL_MQTT_PASS_TURN(state, char) {
state.globalMqttPassTurn = char;
},
SET_GLOBAL_MQTT_STOP_PAUSE(state, char) {
state.globalMqttStopPause = char;
},
SET_GLOBAL_MQTT_RUN_ALL(state, char) {
state.globalMqttRunAll = char;
},
SET_PLAYERS(state, playersData) { // Used by fullResetApp and potentially reorder
state.players = playersData.map(p => createPlayerObject(p));
},
SET_THEME(state, theme) { state.theme = theme; },
DELETE_PLAYER(state, playerId) {
state.players = state.players.filter(p => p.id !== playerId);
if (state.currentPlayerIndex >= state.players.length && state.players.length > 0) {
state.currentPlayerIndex = state.players.length - 1;
} else if (state.players.length === 0) {
state.currentPlayerIndex = 0;
}
},
REORDER_PLAYERS(state, players) {
state.players = players;
},
SHUFFLE_PLAYERS(state) {
for (let i = state.players.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[state.players[i], state.players[j]] = [state.players[j], state.players[i]];
}
},
REVERSE_PLAYERS(state) {
state.players.reverse();
},
SET_CURRENT_PLAYER_INDEX(state, index) {
state.currentPlayerIndex = index;
},
SET_GAME_MODE(state, mode) {
state.gameMode = mode;
},
SET_IS_MUTED(state, muted) {
state.isMuted = muted;
},
TOGGLE_THEME(state) {
state.theme = state.theme === 'light' ? 'dark' : 'light';
},
SET_THEME(state, theme) {
state.theme = theme;
},
DECREMENT_TIMER(state, { playerIndex }) {
const player = state.players[playerIndex];
if (player && player.isTimerRunning && !player.isSkipped) {
player.currentTimerSec--;
if (player.currentTimerSec < MAX_NEGATIVE_SECONDS) {
player.currentTimerSec = MAX_NEGATIVE_SECONDS;
player.isSkipped = true; // Auto-skip if max negative time reached
player.isTimerRunning = false;
}
}
},
RESET_PLAYER_TIMER(state, playerIndex) {
if (state.players[playerIndex]) {
state.players[playerIndex].currentTimerSec = state.players[playerIndex].initialTimerSec;
state.players[playerIndex].isSkipped = false;
state.players[playerIndex].isTimerRunning = false;
}
},
RESET_ALL_TIMERS(state) {
// When resetting, decide if you want to go back to *only* predefined players
// or reset existing players' timers. The current spec "restores all timers to initial values"
// implies resetting existing players. If it meant reverting to the initial player set,
// this logic would need to change to:
// state.players = JSON.parse(JSON.stringify(predefinedPlayers));
// For now, sticking to resetting current players' timers:
state.players.forEach(player => {
player.currentTimerSec = player.initialTimerSec;
player.isSkipped = false;
player.isTimerRunning = false;
});
state.currentPlayerIndex = 0;
state.gameMode = 'normal';
state.gameRunning = false;
},
START_PLAYER_TIMER(state, playerIndex) {
if(state.players[playerIndex] && !state.players[playerIndex].isSkipped) {
state.players[playerIndex].isTimerRunning = true;
state.gameRunning = true;
}
},
PAUSE_PLAYER_TIMER(state, playerIndex) {
if(state.players[playerIndex]) {
state.players[playerIndex].isTimerRunning = false;
}
if (!state.players.some(p => p.isTimerRunning)) {
state.gameRunning = false;
}
},
PAUSE_ALL_TIMERS(state) {
state.players.forEach(p => p.isTimerRunning = false);
state.gameRunning = false;
},
SET_GAME_RUNNING(state, isRunning) {
state.gameRunning = isRunning;
},
},
actions: {
loadState({ commit, state }) {
// The state initializer already did the main loading from localStorage.
// This action can be used for any *additional* setup after initial hydration
// or to re-apply certain defaults if needed.
// For now, it's mainly a confirmation that persisted state is used.
// Example: ensure theme is applied if it was loaded
// This is already handled by App.vue's watcher, but could be centralized.
// if (state.theme === 'dark') {
// document.documentElement.classList.add('dark');
// } else {
// document.documentElement.classList.remove('dark');
// }
console.log("Store state loaded/initialized.");
// It's good practice for actions to return a Promise if they are async
// or if other parts of the app expect to chain .then()
return Promise.resolve(); // Resolve immediately
},
saveState({ state }) {
StorageService.saveState({
players: state.players.map(p => ({ // Persist mqttChar for players
id: p.id, name: p.name, avatar: p.avatar,
initialTimerSec: p.initialTimerSec, currentTimerSec: p.currentTimerSec,
hotkey: p.hotkey, mqttChar: p.mqttChar, // Save mqttChar
isSkipped: p.isSkipped,
})),
globalHotkeyPassTurn: state.globalHotkeyPassTurn,
globalMqttPassTurn: state.globalMqttPassTurn,
globalHotkeyStopPause: state.globalHotkeyStopPause,
globalHotkeyRunAll: state.globalHotkeyRunAll,
globalMqttStopPause: state.globalMqttStopPause,
globalMqttRunAll: state.globalMqttRunAll,
mqttBrokerUrl: state.mqttBrokerUrl,
mqttConnectDesired: state.mqttConnectDesired,
currentPlayerIndex: state.currentPlayerIndex,
gameMode: state.gameMode,
isMuted: state.isMuted,
theme: state.theme,
});
},
addPlayer({ commit, dispatch }, player) {
commit('ADD_PLAYER', player);
dispatch('saveState');
},
setMqttBrokerUrl({ commit, dispatch }, url) {
commit('SET_MQTT_BROKER_URL', url);
dispatch('saveState');
},
setMqttConnectDesired({ commit, dispatch }, desired) {
commit('SET_MQTT_CONNECT_DESIRED', desired);
dispatch('saveState');
},
setGlobalMqttStopPause({ commit, dispatch }, char) {
commit('SET_GLOBAL_MQTT_STOP_PAUSE', char);
dispatch('saveState');
},
setGlobalMqttRunAll({ commit, dispatch }, char) {
commit('SET_GLOBAL_MQTT_RUN_ALL', char);
dispatch('saveState');
},
setGlobalHotkeyPassTurn({ commit, dispatch }, key) {
commit('SET_GLOBAL_HOTKEY_PASS_TURN', key);
dispatch('saveState');
},
setGlobalMqttPassTurn({ commit, dispatch }, char) {
commit('SET_GLOBAL_MQTT_PASS_TURN', char);
dispatch('saveState');
},
updatePlayer({ commit, dispatch }, player) {
commit('UPDATE_PLAYER', player);
dispatch('saveState');
},
deletePlayer({ commit, dispatch }, playerId) {
commit('DELETE_PLAYER', playerId);
dispatch('saveState');
},
reorderPlayers({commit, dispatch}, players) {
commit('REORDER_PLAYERS', players);
dispatch('saveState');
},
shufflePlayers({commit, dispatch}) {
commit('SHUFFLE_PLAYERS');
dispatch('saveState');
},
reversePlayers({commit, dispatch}) {
commit('REVERSE_PLAYERS');
dispatch('saveState');
},
toggleTheme({ commit, dispatch }) {
commit('TOGGLE_THEME');
dispatch('saveState');
},
setGlobalHotkey({ commit, dispatch }, key) {
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key);
dispatch('saveState');
},
setMuted({ commit, dispatch }, muted) {
commit('SET_IS_MUTED', muted);
dispatch('saveState');
},
resetGame({ commit, dispatch }) {
commit('RESET_ALL_TIMERS');
dispatch('saveState');
},
setGlobalHotkeyStopPause({ commit, dispatch }, key) {
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key);
dispatch('saveState');
},
setGlobalHotkeyRunAll({ commit, dispatch }, key) {
commit('SET_GLOBAL_HOTKEY_RUN_ALL', key);
dispatch('saveState');
},
fullResetApp({ commit, dispatch, state: currentGlobalState }) {
StorageService.clearState();
const freshInitialState = JSON.parse(JSON.stringify(initialState));
freshInitialState.players = freshInitialState.players.map(p => createPlayerObject(p));
commit('SET_PLAYERS', freshInitialState.players);
commit('SET_CURRENT_PLAYER_INDEX', freshInitialState.currentPlayerIndex);
commit('SET_GAME_MODE', freshInitialState.gameMode);
commit('SET_IS_MUTED', freshInitialState.isMuted);
commit('SET_MQTT_BROKER_URL', freshInitialState.mqttBrokerUrl);
commit('SET_MQTT_CONNECT_DESIRED', freshInitialState.mqttConnectDesired);
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause);
commit('SET_GLOBAL_MQTT_STOP_PAUSE', freshInitialState.globalMqttStopPause);
commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll);
commit('SET_GLOBAL_MQTT_RUN_ALL', freshInitialState.globalMqttRunAll);
commit('SET_GLOBAL_HOTKEY_PASS_TURN', freshInitialState.globalHotkeyPassTurn);
commit('SET_GLOBAL_MQTT_PASS_TURN', freshInitialState.globalMqttPassTurn);
if (currentGlobalState.theme !== freshInitialState.theme) {
commit('SET_THEME', freshInitialState.theme);
}
commit('SET_GAME_RUNNING', false);
dispatch('saveState');
},
tick({ commit, state }) {
if (state.gameMode === 'normal') {
if (state.players[state.currentPlayerIndex]?.isTimerRunning) {
commit('DECREMENT_TIMER', { playerIndex: state.currentPlayerIndex });
}
} else if (state.gameMode === 'allTimers') {
state.players.forEach((player, index) => {
if (player.isTimerRunning) {
commit('DECREMENT_TIMER', { playerIndex: index });
}
});
}
},
passTurn({ commit, state, dispatch }) {
const numPlayers = state.players.length;
if (numPlayers === 0) return;
const currentIdx = state.currentPlayerIndex;
const currentPlayerTimerWasRunning = state.players[currentIdx]?.isTimerRunning;
commit('PAUSE_PLAYER_TIMER', currentIdx);
let nextPlayerIndex = (currentIdx + 1) % numPlayers;
let skippedCount = 0;
while(state.players[nextPlayerIndex]?.isSkipped && skippedCount < numPlayers) {
nextPlayerIndex = (nextPlayerIndex + 1) % numPlayers;
skippedCount++;
}
if (skippedCount === numPlayers) {
commit('PAUSE_ALL_TIMERS');
dispatch('saveState');
return;
}
commit('SET_CURRENT_PLAYER_INDEX', nextPlayerIndex);
if (currentPlayerTimerWasRunning && !state.players[nextPlayerIndex].isSkipped) {
commit('START_PLAYER_TIMER', nextPlayerIndex);
} else {
if (state.players[nextPlayerIndex] && !state.players[nextPlayerIndex].isSkipped) {
commit('PAUSE_PLAYER_TIMER', nextPlayerIndex);
}
}
dispatch('saveState');
},
toggleCurrentPlayerTimerNormalMode({ commit, state, dispatch }) {
const player = state.players[state.currentPlayerIndex];
if (!player) return;
if (player.isTimerRunning) {
commit('PAUSE_PLAYER_TIMER', state.currentPlayerIndex);
} else if (!player.isSkipped) {
commit('START_PLAYER_TIMER', state.currentPlayerIndex);
}
dispatch('saveState');
},
togglePlayerTimerAllTimersMode({ commit, state, dispatch }, playerIndex) {
const player = state.players[playerIndex];
if (!player) return;
if (player.isTimerRunning) {
commit('PAUSE_PLAYER_TIMER', playerIndex);
} else if (!player.isSkipped) {
commit('START_PLAYER_TIMER', playerIndex);
}
const anyTimerRunning = state.players.some(p => p.isTimerRunning && !p.isSkipped);
if (!anyTimerRunning && state.players.length > 0 && state.gameMode === 'allTimers') {
// This auto-revert logic is now in GameView.vue watcher for better control over timing
}
dispatch('saveState');
},
globalStopPauseAll({ commit, state, dispatch }) {
if (state.gameMode === 'normal') {
dispatch('toggleCurrentPlayerTimerNormalMode');
} else {
const anyTimerRunning = state.players.some(p => p.isTimerRunning && !p.isSkipped);
if (anyTimerRunning) {
commit('PAUSE_ALL_TIMERS');
} else {
state.players.forEach((player, index) => {
if (!player.isSkipped) {
commit('START_PLAYER_TIMER', index);
}
});
}
}
dispatch('saveState');
},
switchToAllTimersMode({ commit, state, dispatch }) {
commit('SET_GAME_MODE', 'allTimers');
let anyStarted = false;
state.players.forEach((player, index) => {
if (!player.isSkipped) {
commit('START_PLAYER_TIMER', index);
anyStarted = true;
}
});
if(anyStarted) commit('SET_GAME_RUNNING', true);
else commit('SET_GAME_RUNNING', false);
dispatch('saveState');
},
switchToNormalMode({commit, state, dispatch}) {
commit('PAUSE_ALL_TIMERS');
commit('SET_GAME_MODE', 'normal');
// Determine current player for normal mode, respecting skips
let currentIdx = state.currentPlayerIndex;
let skippedCount = 0;
while(state.players[currentIdx]?.isSkipped && skippedCount < state.players.length) {
currentIdx = (currentIdx + 1) % state.players.length;
skippedCount++;
}
if (skippedCount < state.players.length) {
commit('SET_CURRENT_PLAYER_INDEX', currentIdx);
// Timer for this player should remain paused as per PAUSE_ALL_TIMERS
} else {
// All players skipped, game is effectively paused.
commit('SET_GAME_RUNNING', false);
}
dispatch('saveState');
}
},
getters: {
players: state => state.players,
currentPlayer: state => state.players[state.currentPlayerIndex],
nextPlayer: state => {
if (!state.players || state.players.length < 1) return null;
let nextIndex = (state.currentPlayerIndex + 1) % state.players.length;
let count = 0;
while(state.players[nextIndex]?.isSkipped && count < state.players.length) {
nextIndex = (nextIndex + 1) % state.players.length;
count++;
}
return state.players[nextIndex];
},
getPlayerById: (state) => (id) => state.players.find(p => p.id === id),
gameMode: state => state.gameMode,
isMuted: state => state.isMuted,
theme: state => state.theme,
mqttBrokerUrl: state => state.mqttBrokerUrl,
mqttConnectDesired: state => state.mqttConnectDesired,
globalHotkeyStopPause: state => state.globalHotkeyStopPause,
globalMqttStopPause: state => state.globalMqttStopPause,
globalHotkeyRunAll: state => state.globalHotkeyRunAll,
globalMqttRunAll: state => state.globalMqttRunAll,
globalHotkeyPassTurn: state => state.globalHotkeyPassTurn,
globalMqttPassTurn: state => state.globalMqttPassTurn,
totalPlayers: state => state.players.length,
gameRunning: state => state.gameRunning,
maxNegativeTimeReached: () => MAX_NEGATIVE_SECONDS,
}
});