139 lines
4.0 KiB
HTML
139 lines
4.0 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="{{ .Site.LanguageCode | default "en" }}" class="dark">
|
||
|
|
<head>
|
||
|
|
{{- partial "head/meta.html" . -}}
|
||
|
|
{{- partial "head/opengraph.html" . -}}
|
||
|
|
{{- partial "head/twitter.html" . -}}
|
||
|
|
{{- partial "head/json-ld.html" . -}}
|
||
|
|
{{- partial "head/preload.html" . -}}
|
||
|
|
{{- partial "head/favicon.html" . -}}
|
||
|
|
|
||
|
|
{{/* CSS - built by Tailwind CLI to static folder */}}
|
||
|
|
<link rel="stylesheet" href="/css/main.css">
|
||
|
|
</head>
|
||
|
|
<body
|
||
|
|
x-data
|
||
|
|
hx-boost="true"
|
||
|
|
hx-target="#main-content"
|
||
|
|
hx-select="#main-content"
|
||
|
|
hx-swap="innerHTML show:top"
|
||
|
|
hx-push-url="true"
|
||
|
|
class="text-text-primary min-h-screen flex flex-col"
|
||
|
|
>
|
||
|
|
{{/* WebGL Background Canvas (preserved across navigation) */}}
|
||
|
|
<canvas
|
||
|
|
id="webgl-bg"
|
||
|
|
hx-preserve="true"
|
||
|
|
class="fixed inset-0 -z-10 pointer-events-none"
|
||
|
|
aria-hidden="true"
|
||
|
|
></canvas>
|
||
|
|
|
||
|
|
{{- partial "header.html" . -}}
|
||
|
|
|
||
|
|
<main id="main-content" class="flex-1">
|
||
|
|
{{- block "main" . }}{{- end -}}
|
||
|
|
</main>
|
||
|
|
|
||
|
|
{{- partial "footer.html" . -}}
|
||
|
|
|
||
|
|
{{/* Persistent Audio Player (preserved across navigation) */}}
|
||
|
|
<div id="audio-player-container" hx-preserve="true" class="fixed bottom-0 left-0 right-0 z-player">
|
||
|
|
{{- partial "player.html" . -}}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{/* WebGL Visualizer Canvas (preserved) */}}
|
||
|
|
<canvas
|
||
|
|
id="visualizer"
|
||
|
|
hx-preserve="true"
|
||
|
|
class="fixed inset-0 pointer-events-none z-visualizer"
|
||
|
|
aria-hidden="true"
|
||
|
|
></canvas>
|
||
|
|
|
||
|
|
{{/* Alpine.js - data and stores defined before CDN loads */}}
|
||
|
|
<script>
|
||
|
|
// Define Alpine stores and components BEFORE Alpine loads
|
||
|
|
document.addEventListener('alpine:init', () => {
|
||
|
|
// Global audio store
|
||
|
|
Alpine.store('audio', {
|
||
|
|
currentTrack: null,
|
||
|
|
isPlaying: false,
|
||
|
|
progress: 0,
|
||
|
|
duration: 0,
|
||
|
|
volume: 0.8
|
||
|
|
});
|
||
|
|
|
||
|
|
// Player UI component
|
||
|
|
Alpine.data('playerUI', () => ({
|
||
|
|
togglePlay() {
|
||
|
|
window.__pivoine?.audioManager?.toggle();
|
||
|
|
},
|
||
|
|
seek(time) {
|
||
|
|
window.__pivoine?.audioManager?.seek(parseFloat(time));
|
||
|
|
},
|
||
|
|
setVolume(v) {
|
||
|
|
const volume = parseFloat(v);
|
||
|
|
Alpine.store('audio').volume = volume;
|
||
|
|
window.__pivoine?.audioManager?.setVolume(volume);
|
||
|
|
localStorage.setItem('pivoine-volume', volume);
|
||
|
|
},
|
||
|
|
toggleMute() {
|
||
|
|
const store = Alpine.store('audio');
|
||
|
|
if (store.volume > 0) {
|
||
|
|
this._previousVolume = store.volume;
|
||
|
|
this.setVolume(0);
|
||
|
|
} else {
|
||
|
|
this.setVolume(this._previousVolume || 0.8);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
formatTime(seconds) {
|
||
|
|
if (!seconds || isNaN(seconds)) return '0:00';
|
||
|
|
const mins = Math.floor(seconds / 60);
|
||
|
|
const secs = Math.floor(seconds % 60);
|
||
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||
|
|
},
|
||
|
|
_previousVolume: 0.8
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
{{/* htmx */}}
|
||
|
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||
|
|
|
||
|
|
{{/* Alpine.js */}}
|
||
|
|
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
||
|
|
|
||
|
|
{{/* Main JS - audio manager and visualizer */}}
|
||
|
|
{{- $js := resources.Get "js/main.js" -}}
|
||
|
|
{{- if $js -}}
|
||
|
|
{{- $jsOpts := dict "format" "esm" -}}
|
||
|
|
{{- if hugo.IsProduction -}}
|
||
|
|
{{- $jsOpts = merge $jsOpts (dict "minify" true) -}}
|
||
|
|
{{- end -}}
|
||
|
|
{{- $js = $js | js.Build $jsOpts -}}
|
||
|
|
{{- if hugo.IsProduction -}}
|
||
|
|
{{- $js = $js | fingerprint -}}
|
||
|
|
{{- end -}}
|
||
|
|
<script type="module" src="{{ $js.RelPermalink }}"></script>
|
||
|
|
{{- end -}}
|
||
|
|
|
||
|
|
{{/* Analytics */}}
|
||
|
|
{{- if and .Site.Params.umami.enabled hugo.IsProduction -}}
|
||
|
|
{{- partial "analytics.html" . -}}
|
||
|
|
{{- end -}}
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// htmx config
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
if (typeof htmx !== 'undefined') {
|
||
|
|
htmx.config.globalViewTransitions = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Re-init components after htmx swap
|
||
|
|
document.body.addEventListener('htmx:afterSwap', function() {
|
||
|
|
window.dispatchEvent(new CustomEvent('page:loaded'));
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|