Files
nexus-timer/src/components/PlayerDisplay.vue
cpu 4cae455892 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
2025-05-19 21:43:00 +02:00

145 lines
4.7 KiB
Vue

<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>