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 offline mode docs: update documentation to reflect current codebase and MQTT features - Update README.md with global MQTT commands - Enhance architecture.md with comprehensive data model and MQTT state - Update development.md with project structure and workflow - Remove redundant script listings - Fix formatting and organization
This commit is contained in:
145
src/components/PlayerDisplay.vue
Normal file
145
src/components/PlayerDisplay.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['player-area p-2 md:p-4 flex flex-col items-center justify-center text-center relative no-select',
|
||||
areaClass,
|
||||
{ 'opacity-50': player.isSkipped,
|
||||
'bg-green-100 dark:bg-green-800 animate-pulsePositive': player.isTimerRunning && player.currentTimerSec >=0 && !isNextPlayerArea,
|
||||
'bg-red-100 dark:bg-red-900': player.isTimerRunning && player.currentTimerSec < 0 && !isNextPlayerArea,
|
||||
}]"
|
||||
@click="handleTap"
|
||||
v-touch:swipe.up="handleSwipeUp"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="mb-4 md:mb-4 relative"
|
||||
:style="{
|
||||
width: avatarSize.width,
|
||||
height: avatarSize.height,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="player.avatar"
|
||||
:src="player.avatar"
|
||||
alt="Player Avatar"
|
||||
class="rounded-full object-cover border-2 md:border-4 shadow-lg w-full h-full"
|
||||
:class="player.isSkipped ? 'border-gray-500 filter grayscale' : 'border-blue-500 dark:border-blue-400'"
|
||||
/>
|
||||
<DefaultAvatarIcon
|
||||
v-else
|
||||
class="rounded-full object-cover border-2 md:border-4 shadow-lg text-gray-400 dark:text-gray-500 bg-gray-200 dark:bg-gray-700 p-1 md:p-2 w-full h-full"
|
||||
:class="player.isSkipped ? 'border-gray-500 filter grayscale !text-gray-300 dark:!text-gray-600' : 'border-blue-500 dark:border-blue-400'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Player Name -->
|
||||
<h2 class="font-semibold mb-4 md:mb-6 text-5xl sm:text-2xl md:text-3xl lg:text-5xl">
|
||||
{{ player.name }}
|
||||
</h2>
|
||||
<TimerDisplay
|
||||
:seconds="player.currentTimerSec"
|
||||
:is-negative="player.currentTimerSec < 0"
|
||||
:is-pulsating="player.isTimerRunning"
|
||||
class="text-4xl sm:text-5xl md:text-6xl lg:text-6xl xl:text-5xl"
|
||||
/>
|
||||
<p v-if="player.isSkipped" class="text-red-500 dark:text-red-400 mt-1 md:mt-2 font-semibold text-sm md:text-base lg:text-lg">SKIPPED</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
import TimerDisplay from './TimerDisplay.vue';
|
||||
import DefaultAvatarIcon from './DefaultAvatarIcon.vue';
|
||||
|
||||
const vTouch = {
|
||||
mounted: (el, binding) => {
|
||||
if (binding.arg === 'swipe' && binding.modifiers.up) {
|
||||
let touchstartX = 0;
|
||||
let touchstartY = 0;
|
||||
let touchendX = 0;
|
||||
let touchendY = 0;
|
||||
const swipeThreshold = 50;
|
||||
|
||||
el.addEventListener('touchstart', function(event) {
|
||||
touchstartX = event.changedTouches[0].screenX;
|
||||
touchstartY = event.changedTouches[0].screenY;
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('touchend', function(event) {
|
||||
touchendX = event.changedTouches[0].screenX;
|
||||
touchendY = event.changedTouches[0].screenY;
|
||||
handleGesture();
|
||||
}, { passive: true });
|
||||
|
||||
function handleGesture() {
|
||||
const deltaY = touchstartY - touchendY;
|
||||
const deltaX = Math.abs(touchendX - touchstartX);
|
||||
|
||||
if (deltaY > swipeThreshold && deltaY > deltaX) {
|
||||
if (typeof binding.value === 'function') {
|
||||
binding.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
player: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isCurrentPlayerArea: Boolean,
|
||||
isNextPlayerArea: Boolean,
|
||||
areaClass: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['tapped', 'swiped-up']);
|
||||
|
||||
const avatarSize = ref({ width: '120px', height: '120px' });
|
||||
|
||||
const calculateAvatarSize = () => {
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight;
|
||||
const isNormalModeContext = document.querySelector('.player-area')?.parentElement?.classList.contains('flex-col');
|
||||
|
||||
if (isNormalModeContext) {
|
||||
const availableHeight = screenHeight / 2;
|
||||
let size = Math.min(availableHeight * 0.5, screenWidth * 0.4, 175);
|
||||
|
||||
if (screenWidth < 768) { // Mobile
|
||||
size = Math.min(availableHeight * 0.7, screenWidth * 0.6, 230);
|
||||
} else if (screenWidth < 1024) { // Tablet
|
||||
size = Math.min(availableHeight * 0.55, screenWidth * 0.45, 180);
|
||||
}
|
||||
size = Math.max(size, 100);
|
||||
|
||||
avatarSize.value = { width: `${size}px`, height: `${size}px` };
|
||||
} else {
|
||||
avatarSize.value = { width: '120px', height: '120px' };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
calculateAvatarSize();
|
||||
window.addEventListener('resize', calculateAvatarSize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', calculateAvatarSize);
|
||||
});
|
||||
|
||||
const handleTap = () => {
|
||||
if (props.isCurrentPlayerArea && !props.player.isSkipped) {
|
||||
emit('tapped');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwipeUp = () => {
|
||||
if (props.isNextPlayerArea && !props.player.isSkipped) {
|
||||
emit('swiped-up');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user