mirror of
https://github.com/wizarrrr/wizarr.git
synced 2026-07-31 07:17:10 -04:00
- Updated title block in frame.html for clarity. - Improved localization for the "Next" button in steps.html. - Refactored the WizardController JavaScript for better readability and organization. - Added detailed comments to the wizard architecture for future reference. - Enhanced button visibility and state management based on interaction requirements. - Implemented swipe gesture handling for mobile navigation. - Updated subproject commit to indicate a dirty state.
341 lines
13 KiB
HTML
341 lines
13 KiB
HTML
<section class="py-8 bg-gray-50 dark:bg-gray-900 animate__animated animate__fadeIn animate__faster min-h-screen">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<!-- Header with New Invite button -->
|
|
<div class="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _("Dashboard") }}</h1>
|
|
</div>
|
|
<button id="newInviteBtn"
|
|
class="bg-primary hover:bg-primary_hover text-white font-medium px-6 py-3 rounded-lg shadow-lg transition-all duration-200 flex items-center gap-2"
|
|
onclick="openInviteModal()">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
|
</svg>
|
|
{{ _("New Invite") }}
|
|
</button>
|
|
</div>
|
|
<!-- Dashboard Grid Layout -->
|
|
<div class="dashboard-content space-y-6">
|
|
<!-- Now Playing Section (adaptive height based on content) -->
|
|
<div id="nowPlayingSection"
|
|
class="transition-all duration-500 ease-in-out">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<svg class="w-6 h-6 text-primary"
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
fill="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path fill-rule="evenodd" d="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z" clip-rule="evenodd" />
|
|
</svg>
|
|
{{ _("Now Playing") }}
|
|
</h2>
|
|
</div>
|
|
<div id="nowPlayingContent"
|
|
class="relative z-20"
|
|
hx-get="/now-playing-cards"
|
|
hx-trigger="load, every 10s"
|
|
hx-swap="innerHTML settle:0ms">
|
|
<!-- Now playing cards container -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
|
<!-- Loading placeholder -->
|
|
<div class="col-span-full flex justify-center items-center py-12">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Widget Grid - main widgets row -->
|
|
<div id="widgetGrid"
|
|
class="grid grid-cols-1 lg:grid-cols-2 gap-6 transition-all duration-500 ease-in-out items-stretch lg:grid-rows-1">
|
|
<!-- Latest Accepted Invites Card -->
|
|
<div id="latestInvitesContainer"
|
|
class="min-h-[400px]"
|
|
hx-get="/accepted-invites-card"
|
|
hx-trigger="load, every 30s"
|
|
hx-swap="innerHTML">
|
|
<!-- Loading placeholder -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 h-full flex items-center justify-center">
|
|
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Server Health Card -->
|
|
<div id="serverHealthContainer"
|
|
class="min-h-[400px]"
|
|
hx-get="/server-health-card"
|
|
hx-trigger="load, every 45s"
|
|
hx-swap="innerHTML">
|
|
<!-- Loading placeholder -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 h-full flex items-center justify-center">
|
|
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Removed additional widgets (Library Breakdown & Recent Content) -->
|
|
</div>
|
|
<!-- Invite Modal Container -->
|
|
<div id="invite-modal"></div>
|
|
</section>
|
|
<script>
|
|
// Modal functions
|
|
function openInviteModal() {
|
|
// Load invite form into the modal container
|
|
htmx.ajax('GET', '/invite', {
|
|
target: '#invite-modal',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
|
|
function closeInviteModal() {
|
|
document.getElementById('invite-modal').innerHTML = '';
|
|
}
|
|
|
|
// Close modal on escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeInviteModal();
|
|
}
|
|
});
|
|
|
|
// Live timestamp updating
|
|
let liveTimers = [];
|
|
let nowPlayingTimers = [];
|
|
|
|
function startLiveTimers() {
|
|
// Find all live timestamp elements and start their timers
|
|
document.querySelectorAll('[data-timestamp]').forEach(function(elem) {
|
|
const timestamp = parseInt(elem.getAttribute('data-timestamp'));
|
|
if (timestamp) {
|
|
const timer = setInterval(function() {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const diff = now - timestamp;
|
|
elem.textContent = formatDuration(diff);
|
|
}, 1000);
|
|
liveTimers.push(timer);
|
|
}
|
|
});
|
|
}
|
|
|
|
function startNowPlayingTimers() {
|
|
// Clear existing timers
|
|
nowPlayingTimers.forEach(timer => clearInterval(timer));
|
|
nowPlayingTimers = [];
|
|
|
|
// Find all now playing cards and start live time updates
|
|
document.querySelectorAll('.now-playing-card[data-session-state="playing"]').forEach(function(card) {
|
|
const duration = parseInt(card.getAttribute('data-session-duration')) || 0;
|
|
const initialPosition = parseInt(card.getAttribute('data-session-position')) || 0;
|
|
const sessionId = card.getAttribute('data-session-id');
|
|
|
|
if (duration > 0 && sessionId) {
|
|
let currentPosition = initialPosition;
|
|
const startTime = Date.now();
|
|
|
|
const timer = setInterval(function() {
|
|
// Calculate elapsed time since timer started (in milliseconds)
|
|
const elapsed = Date.now() - startTime;
|
|
// Add elapsed time to initial position
|
|
const newPosition = currentPosition + elapsed;
|
|
|
|
// Don't exceed duration
|
|
if (newPosition < duration) {
|
|
updateCardTime(card, newPosition, duration);
|
|
} else {
|
|
clearInterval(timer);
|
|
}
|
|
}, 1000);
|
|
|
|
nowPlayingTimers.push(timer);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateCardTime(card, position, duration) {
|
|
const timeDisplay = card.querySelector('.session-time-display');
|
|
const progressBar = card.querySelector('.session-progress-bar');
|
|
|
|
if (timeDisplay) {
|
|
const posSeconds = Math.floor(position / 1000);
|
|
const durSeconds = Math.floor(duration / 1000);
|
|
const posMins = Math.floor(posSeconds / 60);
|
|
const durMins = Math.floor(durSeconds / 60);
|
|
const posSecsRemainder = posSeconds % 60;
|
|
const durSecsRemainder = durSeconds % 60;
|
|
|
|
timeDisplay.textContent = `${posMins}:${posSecsRemainder.toString().padStart(2, '0')} / ${durMins}:${durSecsRemainder.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
if (progressBar && duration > 0) {
|
|
const progressPercent = (position / duration) * 100;
|
|
progressBar.style.width = `${Math.min(progressPercent, 100)}%`;
|
|
}
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = seconds % 60;
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
// Re-init Flowbite popovers when HTMX swaps in new now-playing cards
|
|
function reinitFlowbitePopovers() {
|
|
if (typeof initFlowbite === 'function') {
|
|
initFlowbite(); // official helper (ES module / CDN helper)
|
|
} else if (window.Flowbite && typeof window.Flowbite.init === 'function') {
|
|
window.Flowbite.init();
|
|
}
|
|
// After Flowbite (or fallback) initialises, explicitly set the placement to bottom
|
|
document.querySelectorAll('[data-popover-target]').forEach(trigger => {
|
|
const targetId = trigger.getAttribute('data-popover-target');
|
|
const pop = document.getElementById(targetId);
|
|
if (pop) {
|
|
pop.setAttribute('data-popper-placement', 'bottom');
|
|
}
|
|
});
|
|
|
|
// Manual fallback binding when Flowbite JS is not available
|
|
if (!(typeof initFlowbite === 'function' || (window.Flowbite && typeof window.Flowbite.init === 'function'))) {
|
|
document.querySelectorAll('[data-popover-target]').forEach(trigger => {
|
|
if (trigger.dataset.popoverBound) return; // skip if already bound
|
|
const targetId = trigger.getAttribute('data-popover-target');
|
|
const pop = document.getElementById(targetId);
|
|
if (!pop) return;
|
|
const show = () => {
|
|
pop.classList.remove('invisible', 'opacity-0');
|
|
};
|
|
const hide = () => {
|
|
pop.classList.add('opacity-0');
|
|
setTimeout(() => pop.classList.add('invisible'), 300);
|
|
};
|
|
trigger.addEventListener('mouseenter', show);
|
|
trigger.addEventListener('mouseleave', hide);
|
|
pop.addEventListener('mouseenter', show);
|
|
pop.addEventListener('mouseleave', hide);
|
|
trigger.dataset.popoverBound = 'true';
|
|
});
|
|
}
|
|
}
|
|
|
|
document.addEventListener('htmx:afterSwap', function(evt) {
|
|
if (evt.target && evt.target.id === 'nowPlayingContent') {
|
|
reinitFlowbitePopovers();
|
|
// Restart live-timer so newly swapped cards animate correctly
|
|
liveTimers.forEach(timer => clearInterval(timer));
|
|
liveTimers = [];
|
|
startLiveTimers();
|
|
|
|
// Start live time updates for now playing cards
|
|
startNowPlayingTimers();
|
|
|
|
// Removed adaptive additionalWidgets logic since those widgets are no longer present
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize live timers
|
|
startLiveTimers();
|
|
|
|
// Initialize now playing timers
|
|
startNowPlayingTimers();
|
|
|
|
// Initialize Flowbite popovers
|
|
reinitFlowbitePopovers();
|
|
|
|
// Removed adaptive additionalWidgets logic since those widgets are no longer present
|
|
});
|
|
</script>
|
|
<style>
|
|
/* Ensure equal height cards in widget grid on desktop */
|
|
@media (min-width: 1024px) {
|
|
#widgetGrid {
|
|
display: grid;
|
|
grid-template-rows: 1fr;
|
|
align-items: stretch;
|
|
}
|
|
|
|
#latestInvitesContainer,
|
|
#serverHealthContainer {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Blobby hover animation for session cards */
|
|
.session-card {
|
|
transform-origin: center top;
|
|
transition: all 0.3s ease;
|
|
/* simpler transition */
|
|
overflow: visible;
|
|
}
|
|
|
|
.session-card:hover {
|
|
position: relative;
|
|
z-index: 50;
|
|
/* remove scaling/shadow animation */
|
|
}
|
|
|
|
/* Remove sibling shrink effect */
|
|
.session-card:hover~.session-card {
|
|
transform: none;
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Hover overlay animation */
|
|
.hover-overlay {
|
|
backdrop-filter: blur(8px);
|
|
border-radius: 0.5rem;
|
|
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
/* Ensure cards don't interfere with each other during hover */
|
|
.session-card:hover~.session-card {
|
|
transform: none;
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Grid container adjustments for hover effects */
|
|
#nowPlayingContent {
|
|
perspective: 1000px;
|
|
overflow: visible;
|
|
/* Allow cards to overflow outside grid */
|
|
}
|
|
|
|
/* Smooth transitions for grid items */
|
|
#nowPlayingContent>div {
|
|
transition: all 0.3s ease;
|
|
overflow: visible;
|
|
}
|
|
|
|
/* Additional hover effects for better UX */
|
|
.session-card:hover .hover-overlay {
|
|
animation: slideInUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
@keyframes slideInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px) scale(0.95);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
</style>
|