Compare commits

1 Commits

Author SHA1 Message Date
f34b81700f Dateien nach "content/tracks" hochladen 2025-12-29 12:31:47 +01:00
67 changed files with 111 additions and 367 deletions

View File

@@ -0,0 +1,21 @@
{
"permissions": {
"allow": [
"Bash(chmod:*)",
"Bash(npx tailwindcss:*)",
"Bash(pnpm add:*)",
"Bash(pnpm exec tailwindcss:*)",
"Bash(pnpm css:*)",
"Bash(hugo:*)",
"WebFetch(domain:pivoine.art)",
"Bash(curl:*)",
"WebFetch(domain:github.com)",
"WebSearch",
"WebFetch(domain:htmx.org)",
"Bash(cat:*)",
"Bash(git remote:*)"
],
"deny": [],
"ask": []
}
}

3
.gitignore vendored
View File

@@ -30,6 +30,3 @@ npm-debug.log*
# Cache # Cache
.cache/ .cache/
# Claude
.claude/

View File

@@ -4,6 +4,8 @@ date: {{ .Date }}
draft: true draft: true
description: "" description: ""
# Audio
audio: ""
duration: "" duration: ""
# Metadata # Metadata

View File

@@ -16,53 +16,6 @@ class AudioManager {
this.source = null; this.source = null;
this.frequencyData = null; this.frequencyData = null;
this.isInitialized = false; this.isInitialized = false;
this.tracks = [];
this.autoplayEnabled = true;
}
async fetchTracks() {
if (this.tracks.length > 0) return this.tracks;
try {
const response = await fetch('/tracks/index.json');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
this.tracks = data.tracks || [];
} catch (e) {
console.error('Failed to fetch tracks:', e);
this.tracks = [];
}
return this.tracks;
}
getRandomTrack(excludeUrl = null) {
const available = this.tracks.filter((t) => t.audio !== excludeUrl);
if (available.length === 0) return null;
return available[Math.floor(Math.random() * available.length)];
}
async playRandomTrack() {
await this.fetchTracks();
const currentUrl = window.Alpine?.store('audio')?.currentTrack?.url;
const nextTrack = this.getRandomTrack(currentUrl);
if (nextTrack) {
// Store pending track info for auto-play on page load
sessionStorage.setItem(
'pivoine-autoplay',
JSON.stringify({
title: nextTrack.title,
url: nextTrack.audio,
image: nextTrack.image
})
);
// Navigate to the new track page
window.location.href = nextTrack.url;
}
} }
async init() { async init() {
@@ -93,10 +46,6 @@ class AudioManager {
if (window.Alpine) { if (window.Alpine) {
Alpine.store('audio').isPlaying = false; Alpine.store('audio').isPlaying = false;
} }
// Auto-play next random track
if (this.autoplayEnabled) {
this.playRandomTrack();
}
}); });
this.audio.addEventListener('play', () => { this.audio.addEventListener('play', () => {
@@ -186,32 +135,6 @@ if (!window.__pivoine) {
logo: null logo: null
}; };
// Check for auto-play from shuffle/random track
const checkAutoplay = () => {
const autoplayData = sessionStorage.getItem('pivoine-autoplay');
if (autoplayData) {
sessionStorage.removeItem('pivoine-autoplay');
try {
const track = JSON.parse(autoplayData);
// Update Alpine store
if (window.Alpine) {
Alpine.store('audio').currentTrack = track;
}
// Start playback
audioManager.play(track.url);
} catch (e) {
console.error('Failed to auto-play track:', e);
}
}
};
// Run autoplay check after Alpine is ready
document.addEventListener('alpine:initialized', checkAutoplay);
// Fallback if Alpine is already initialized
if (window.Alpine) {
setTimeout(checkAutoplay, 100);
}
// Initialize WebGL components after DOM is ready // Initialize WebGL components after DOM is ready
const initWebGL = () => { const initWebGL = () => {
// Main visualizer (fullscreen background) // Main visualizer (fullscreen background)

View File

@@ -2,9 +2,6 @@ baseURL = "https://pivoine.art/"
languageCode = "en-us" languageCode = "en-us"
title = "Valknar's" title = "Valknar's"
[pagination]
pagerSize = 12
[permalinks] [permalinks]
tracks = "/tracks/:slug/" tracks = "/tracks/:slug/"
@@ -14,7 +11,7 @@ title = "Valknar's"
[outputs] [outputs]
home = ["HTML", "RSS"] home = ["HTML", "RSS"]
section = ["HTML", "RSS", "JSON"] section = ["HTML", "RSS"]
[sitemap] [sitemap]
changefreq = "weekly" changefreq = "weekly"

View File

@@ -2,6 +2,10 @@ description = "Valknar's Pivoine.Art"
author = "Valknar" author = "Valknar"
email = "valknar@pivoine.art" email = "valknar@pivoine.art"
[jellyfin]
baseURL = "https://jellyfin.media.pivoine.art"
# API key via environment variable: HUGO_PARAMS_JELLYFIN_APIKEY
[umami] [umami]
enabled = true enabled = true
websiteID = "" # Set your Umami website ID websiteID = "" # Set your Umami website ID
@@ -10,6 +14,3 @@ email = "valknar@pivoine.art"
[visualizer] [visualizer]
particleCount = 5000 particleCount = 5000
fftSize = 512 fftSize = 512
[images]
default = "/images/og-default.png"

View File

@@ -7,12 +7,9 @@ description: "About Valknar and his music"
Technology and sound, creating massive beats to push the boundaries of audio perception. Technology and sound, creating massive beats to push the boundaries of audio perception.
Also check my other sites: ### Equipment
- [palina.pivoine.art](https://palina.pivoine.art) Debian GNU/Linux 13
- [sexy.pivoine.art](https://sexy.pivoine.art)
- [dev.pivoine.art](https://dev.pivoine.art/valknar)
- [kit.pivoine.art](https://kit.pivoine.art)
### Contact ### Contact

View File

@@ -4,6 +4,7 @@ date: 2025-12-10
draft: false draft: false
description: "Black heart, black soul" description: "Black heart, black soul"
audio: "https://jellyfin.media.pivoine.art/Items/ac825a429c2485545b557567c704d93c/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "0:53" duration: "0:53"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -1,9 +1,10 @@
--- ---
title: "Bunker" title: "Bunker"
date: 2025-11-30 date: 2025-11-30
draft: false draft: true
description: "Djane" description: "Djane"
audio: "https://jellyfin.media.pivoine.art/Items/5e09032deaaeb222b4de117b2e0233af/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "6:46" duration: "6:46"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -1,9 +1,10 @@
--- ---
title: "Changed Her Mind Again" title: "Changed Her Mind Again"
date: 2025-09-10 date: 2025-09-10
draft: false draft: true
description: "Again..." description: "Again..."
audio: "https://jellyfin.media.pivoine.art/Items/ebd4e9f45b9dda1cada560af5e6cb7a8/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "4:07" duration: "4:07"
artist: "Valknar" artist: "Valknar"

View File

@@ -4,6 +4,7 @@ date: 2025-12-09
draft: false draft: false
description: "At the gate" description: "At the gate"
audio: "https://jellyfin.media.pivoine.art/Items/7fbd8a967c7b89c7b8aad91b056ad27a/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "5:03" duration: "5:03"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -4,6 +4,7 @@ date: 2025-08-21
draft: false draft: false
description: "Motown Madness" description: "Motown Madness"
audio: "https://jellyfin.media.pivoine.art/Items/4002348ba445a02db3b48cd9e1562936/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "4:35" duration: "4:35"
artist: "Valknar" artist: "Valknar"

View File

@@ -4,6 +4,7 @@ date: 2025-12-08
draft: false draft: false
description: "28 days later" description: "28 days later"
audio: "https://jellyfin.media.pivoine.art/Items/4f7487b2006b02a9901a1a56b3c2e516/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "3:43" duration: "3:43"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -4,6 +4,7 @@ date: 2025-08-16
draft: false draft: false
description: "Flamingo Hyperloop" description: "Flamingo Hyperloop"
audio: "https://jellyfin.media.pivoine.art/Items/2d87945379960133e08a8a1077b3bc7b/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "3:33" duration: "3:33"
artist: "Valknar" artist: "Valknar"

View File

@@ -1,9 +1,10 @@
--- ---
title: "Latex" title: "Latex"
date: 2025-11-30 date: 2025-11-30
draft: false draft: true
description: "Posing" description: "Posing"
audio: "https://jellyfin.media.pivoine.art/Items/ed71ecad292dc60ea3475cf9029974c3/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "5:57" duration: "5:57"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -4,6 +4,7 @@ date: 2025-12-07
draft: false draft: false
description: "Soul Survivor VIII" description: "Soul Survivor VIII"
audio: "https://jellyfin.media.pivoine.art/Items/9a541705578deef7d3e98870fadfaa61/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "4:50" duration: "4:50"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,16 +0,0 @@
---
title: "Listen To Your Heart"
date: 2025-12-31
draft: false
description: "Roxette"
duration: "4:23"
artist: "Valknar"
genre: "Drum'n'Bass"
tags:
- liquid
- funk
- soul
---

View File

@@ -4,6 +4,7 @@ date: 2025-12-04
draft: false draft: false
description: "गाँडकामसूत्र" description: "गाँडकामसूत्र"
audio: "https://jellyfin.media.pivoine.art/Items/cc167bb419fcfaf23d3748fd9739f1ba/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "2:19" duration: "2:19"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -1,16 +0,0 @@
---
title: "Paline I"
date: 2026-02-05
draft: false
description: "Mon amour"
duration: "5:29"
artist: "Valknar"
genre: "Techhouse"
tags:
- deep
- dub
- soul
---

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -1,16 +0,0 @@
---
title: "Paline II"
date: 2026-02-05
draft: false
description: "Mon amour"
duration: "5:56"
artist: "Valknar"
genre: "Techhouse"
tags:
- deep
- dub
- soul
---

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -1,16 +0,0 @@
---
title: "Paline III"
date: 2026-02-05
draft: false
description: "Mon amour"
duration: "5:08"
artist: "Valknar"
genre: "Techhouse"
tags:
- deep
- dub
- soul
---

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -1,16 +0,0 @@
---
title: "Paule"
date: 2025-12-31
draft: false
description: "Du Bisch"
duration: "0:31"
artist: "Valknar"
genre: "HipHop"
tags:
- patriotism
- funk
- superior
---

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -1,16 +0,0 @@
---
title: "Rome"
date: 2025-12-31
draft: false
description: "Honor Et Virtus"
duration: "3:57"
artist: "Valknar"
genre: "Metal"
tags:
- metal
- strength
- honor
---

Binary file not shown.

View File

@@ -4,6 +4,7 @@ date: 2025-11-06
draft: false draft: false
description: "In my shadow" description: "In my shadow"
audio: "https://jellyfin.media.pivoine.art/Items/aada4e4ac0320e258595976a9edb6a09/Download?api_key=53d58826a49b4026a815d13db5e38ff7"
duration: "3:26" duration: "3:26"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -1,9 +1,10 @@
--- ---
title: "The End Of All" title: "The End Of All"
date: 2025-11-16 date: 2025-11-16
draft: false draft: true
description: "The end of all is just the beginning" description: "The end of all is just the beginning"
audio: "https://jellyfin.media.pivoine.art/Items/60d39ab0aad880627e8fb85cf1ee7b40/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "4:35" duration: "4:35"
artist: "Valknar" artist: "Valknar"

View File

@@ -1,9 +1,10 @@
--- ---
title: "The Moon" title: "The Moon"
date: 2025-08-19 date: 2025-08-19
draft: false draft: true
description: "Because we are the last" description: "Because we are the last"
audio: "https://jellyfin.media.pivoine.art/Items/d91333f9c7c4d8251174c86a81588cbd/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "3:44" duration: "3:44"
artist: "Valknar" artist: "Valknar"

Binary file not shown.

View File

@@ -4,6 +4,7 @@ date: 2025-12-04
draft: false draft: false
description: "Death Tank" description: "Death Tank"
audio: "https://jellyfin.media.pivoine.art/Items/a5594979b65378601cb50fe890bd8389/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"
duration: "4:39" duration: "4:39"
artist: "Valknar" artist: "Valknar"

View File

@@ -1,33 +0,0 @@
{{ define "main" }}
<section class="min-h-screen flex flex-col items-center justify-center px-4">
<div class="text-center">
<h1 class="text-[clamp(8rem,20vw,16rem)] font-bold leading-none tracking-tight">404</h1>
<p class="mt-8 text-xl md:text-2xl text-text-secondary font-mono tracking-widest uppercase">
Signal Lost
</p>
<div class="mt-12">
<a
href="/"
class="group inline-flex items-center gap-2 px-6 py-3 border border-border hover:border-accent hover:bg-accent hover:text-surface-0 transition-all duration-300"
>
Return home
<svg
class="w-4 h-4 group-hover:translate-x-1 transition-transform duration-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</a>
</div>
</div>
</section>
{{ end }}

View File

@@ -30,10 +30,10 @@
"@type": "Person", "@type": "Person",
"name": "{{ .Params.artist | default .Site.Params.author }}" "name": "{{ .Params.artist | default .Site.Params.author }}"
} }
{{- with .Resources.GetMatch "track.*" }}, {{- if .Params.audio }},
"audio": { "audio": {
"@type": "AudioObject", "@type": "AudioObject",
"contentUrl": "{{ .Permalink }}" "contentUrl": "{{ .Params.audio }}"
} }
{{- end }} {{- end }}
{{- if .Params.genre }}, {{- if .Params.genre }},

View File

@@ -15,6 +15,9 @@
<meta name="theme-color" content="#0a0a0a"> <meta name="theme-color" content="#0a0a0a">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
{{/* Favicon */}}
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{/* RSS */}} {{/* RSS */}}
<link rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}" href="{{ "index.xml" | absURL }}"> <link rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}" href="{{ "index.xml" | absURL }}">

View File

@@ -1,19 +1,7 @@
{{- $title := .Title | default .Site.Title -}} {{- $title := .Title | default .Site.Title -}}
{{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 200 -}} {{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 200 -}}
{{- $image := .Params.image | default "/images/og-default.png" -}}
{{/* Resolve image: page resource cover.* > .Params.image > site default */}} {{- if not (strings.HasPrefix $image "http") -}}
{{- $image := "" -}}
{{- with .Resources.GetMatch "cover.*" -}}
{{- $img := .Resize "1200x webp q90" -}}
{{- $image = $img.Permalink -}}
{{- else -}}
{{- with $.Params.image -}}
{{- $image = . -}}
{{- else -}}
{{- $image = $.Site.Params.images.default | default "/images/og-default.png" -}}
{{- end -}}
{{- end -}}
{{- if and $image (not (strings.HasPrefix $image "http")) -}}
{{- $image = $image | absURL -}} {{- $image = $image | absURL -}}
{{- end -}} {{- end -}}
@@ -30,15 +18,13 @@
{{- end }} {{- end }}
{{/* Audio-specific OpenGraph tags */}} {{/* Audio-specific OpenGraph tags */}}
{{- if eq .Section "tracks" }} {{- if and (eq .Section "tracks") .Params.audio }}
{{- with .Resources.GetMatch "track.*" }} <meta property="og:audio" content="{{ .Params.audio }}">
<meta property="og:audio" content="{{ .Permalink }}">
<meta property="og:audio:type" content="audio/mpeg"> <meta property="og:audio:type" content="audio/mpeg">
{{- if .Params.duration }}
<meta property="music:duration" content="{{ .Params.duration }}">
{{- end }} {{- end }}
{{- if $.Params.duration }} {{- if .Params.artist }}
<meta property="music:duration" content="{{ $.Params.duration }}"> <meta property="music:musician" content="{{ .Params.artist }}">
{{- end }}
{{- if $.Params.artist }}
<meta property="music:musician" content="{{ $.Params.artist }}">
{{- end }} {{- end }}
{{- end }} {{- end }}

View File

@@ -1,3 +1,6 @@
{{/* Preconnect to external domains */}}
<link rel="preconnect" href="https://jellyfin.media.pivoine.art" crossorigin>
{{/* DNS prefetch */}} {{/* DNS prefetch */}}
<link rel="dns-prefetch" href="https://unpkg.com"> <link rel="dns-prefetch" href="https://unpkg.com">

View File

@@ -1,23 +1,11 @@
{{- $title := .Title | default .Site.Title -}} {{- $title := .Title | default .Site.Title -}}
{{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 200 -}} {{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 200 -}}
{{- $image := .Params.image | default "/images/og-default.png" -}}
{{/* Resolve image: page resource cover.* > .Params.image > site default */}} {{- if not (strings.HasPrefix $image "http") -}}
{{- $image := "" -}}
{{- with .Resources.GetMatch "cover.*" -}}
{{- $img := .Resize "1200x webp q90" -}}
{{- $image = $img.Permalink -}}
{{- else -}}
{{- with $.Params.image -}}
{{- $image = . -}}
{{- else -}}
{{- $image = $.Site.Params.images.default | default "/images/og-default.png" -}}
{{- end -}}
{{- end -}}
{{- if and $image (not (strings.HasPrefix $image "http")) -}}
{{- $image = $image | absURL -}} {{- $image = $image | absURL -}}
{{- end -}} {{- end -}}
{{/* Determine card type - use large image when we have one */}} {{/* Determine card type */}}
{{- $cardType := "summary" -}} {{- $cardType := "summary" -}}
{{- if $image -}} {{- if $image -}}
{{- $cardType = "summary_large_image" -}} {{- $cardType = "summary_large_image" -}}

View File

@@ -1,21 +1,5 @@
{{/* Track Card Component */}} {{/* Track Card Component */}}
{{- $hasVideo := .Resources.GetMatch "preview.*" -}} <article class="track-card rounded-lg overflow-hidden group grayscale hover:grayscale-0">
{{- $audio := "" -}}
{{- with .Resources.GetMatch "track.*" -}}
{{- $audio = .RelPermalink -}}
{{- end -}}
<article
class="track-card rounded-lg overflow-hidden group transition-all duration-300"
x-data="{
hovering: false,
get isActive() {
return this.hovering || ($store.audio.isPlaying && $store.audio.currentTrack?.url === '{{ $audio }}');
}
}"
:class="isActive ? '' : 'grayscale'"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
{{/* Cover Image/Video */}} {{/* Cover Image/Video */}}
<a href="{{ .Permalink }}" class="block relative aspect-square overflow-hidden"> <a href="{{ .Permalink }}" class="block relative aspect-square overflow-hidden">
{{- with .Resources.GetMatch "cover.*" }} {{- with .Resources.GetMatch "cover.*" }}
@@ -23,8 +7,7 @@
<img <img
src="{{ $img.RelPermalink }}" src="{{ $img.RelPermalink }}"
alt="{{ $.Title }}" alt="{{ $.Title }}"
class="track-card__cover w-full h-full object-cover" class="track-card__cover w-full h-full"
:class="{ 'opacity-0': isActive && $refs.video }"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
> >
@@ -36,25 +19,23 @@
</div> </div>
{{- end }} {{- end }}
{{/* Video preview on hover or playing */}} {{/* Video preview on hover */}}
{{- with .Resources.GetMatch "preview.*" }} {{- with .Resources.GetMatch "preview.*" }}
<video <video
x-ref="video"
src="{{ .RelPermalink }}" src="{{ .RelPermalink }}"
class="absolute inset-0 w-full h-full object-cover transition-opacity duration-300" class="absolute inset-0 w-full h-full object-cover opacity-0 group-hover:opacity-100 transition-opacity duration-300"
:class="isActive ? 'opacity-100' : 'opacity-0'"
muted muted
loop loop
playsinline playsinline
preload="metadata" onmouseenter="this.play()"
x-effect="if (isActive) { $el.play().catch(() => {}) } else { $el.pause(); $el.currentTime = 0; }" onmouseleave="this.pause(); this.currentTime=0;"
></video> ></video>
{{- end }} {{- end }}
{{/* Play overlay - only show if NO video exists */}} {{/* Play overlay - hide if video exists */}}
{{- if not $hasVideo }} {{- if not (.Resources.GetMatch "preview.*") }}
<div class="absolute inset-0 bg-surface-0/60 transition-opacity flex items-center justify-center" :class="isActive ? 'opacity-100' : 'opacity-0'"> <div class="absolute inset-0 bg-surface-0/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<div class="w-14 h-14 rounded-full bg-accent flex items-center justify-center transform transition-transform" :class="isActive ? 'scale-100' : 'scale-90'"> <div class="w-14 h-14 rounded-full bg-accent flex items-center justify-center transform scale-90 group-hover:scale-100 transition-transform">
<svg class="w-6 h-6 text-surface-0 ml-1" fill="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-surface-0 ml-1" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/> <path d="M8 5v14l11-7z"/>
</svg> </svg>

View File

@@ -6,7 +6,7 @@
<p class="text-text-secondary mt-2">{{ .Description | default "All audio experiments" }}</p> <p class="text-text-secondary mt-2">{{ .Description | default "All audio experiments" }}</p>
</header> </header>
{{- $tracks := .Paginator.Pages -}} {{- $tracks := .Pages -}}
{{- if $tracks }} {{- if $tracks }}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{{- range $tracks }} {{- range $tracks }}

View File

@@ -1,22 +0,0 @@
{{- $tracks := slice -}}
{{- range .Pages -}}
{{- $audio := "" -}}
{{- with .Resources.GetMatch "track.*" -}}
{{- $audio = .RelPermalink -}}
{{- end -}}
{{- $track := dict
"title" .Title
"url" .Permalink
"slug" .File.ContentBaseName
"audio" $audio
"duration" .Params.duration
"genre" .Params.genre
"image" ""
-}}
{{- with .Resources.GetMatch "cover.*" -}}
{{- $img := .Resize "200x webp q85" -}}
{{- $track = merge $track (dict "image" $img.RelPermalink) -}}
{{- end -}}
{{- $tracks = $tracks | append $track -}}
{{- end -}}
{{- dict "tracks" $tracks | jsonify (dict "indent" " ") -}}

View File

@@ -1,8 +1,4 @@
{{ define "main" }} {{ define "main" }}
{{- $audio := "" -}}
{{- with .Resources.GetMatch "track.*" -}}
{{- $audio = .RelPermalink -}}
{{- end -}}
<article class="py-16"> <article class="py-16">
<div class="container-wide"> <div class="container-wide">
{{/* Content area with offset */}} {{/* Content area with offset */}}
@@ -35,69 +31,52 @@
{{- $img := .Resize "400x webp q90" }} {{- $img := .Resize "400x webp q90" }}
<figure <figure
class="overflow-hidden rounded-lg group cursor-pointer relative aspect-square border border-border" class="overflow-hidden rounded-lg group cursor-pointer relative aspect-square border border-border"
x-data="{ {{- if $.Params.audio }}
hovering: false, x-data
get isActive() {
return this.hovering || ($store.audio.isPlaying && $store.audio.currentTrack?.url === '{{ $audio }}');
}
}"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
{{- if $audio }}
@click=" @click="
if ($store.audio.currentTrack?.url === '{{ $audio }}') { $store.audio.currentTrack = {
window.__pivoine?.audioManager?.toggle(); title: '{{ $.Title }}',
} else { url: '{{ $.Params.audio }}',
$store.audio.currentTrack = { image: '{{ with $.Resources.GetMatch "cover.*" }}{{ (.Resize "200x webp q85").RelPermalink }}{{ end }}'
title: '{{ $.Title }}', };
url: '{{ $audio }}', window.__pivoine?.audioManager?.play('{{ $.Params.audio }}');
image: '{{ with $.Resources.GetMatch "cover.*" }}{{ (.Resize "200x webp q85").RelPermalink }}{{ end }}' $store.audio.isPlaying = true;
};
window.__pivoine?.audioManager?.play('{{ $audio }}');
$store.audio.isPlaying = true;
}
" "
{{- end }} {{- end }}
> >
<img <img
src="{{ $img.RelPermalink }}" src="{{ $img.RelPermalink }}"
alt="{{ $.Title }}" alt="{{ $.Title }}"
class="w-full h-full object-cover transition-all duration-500" class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500"
:class="isActive ? 'grayscale-0' : 'grayscale'"
loading="eager" loading="eager"
> >
{{/* Video preview on hover or playing */}} {{/* Video preview on hover */}}
{{- with $.Resources.GetMatch "preview.*" }} {{- with $.Resources.GetMatch "preview.*" }}
<video <video
x-ref="video"
src="{{ .RelPermalink }}" src="{{ .RelPermalink }}"
class="absolute inset-0 w-full h-full object-cover transition-opacity duration-300" class="absolute inset-0 w-full h-full object-cover opacity-0 group-hover:opacity-100 transition-opacity duration-300"
:class="isActive ? 'opacity-100' : 'opacity-0'"
muted muted
loop loop
playsinline playsinline
x-effect="if (isActive) { $el.play().catch(() => {}) } else { $el.pause(); $el.currentTime = 0; }" onmouseenter="this.play()"
onmouseleave="this.pause(); this.currentTime=0;"
></video> ></video>
{{- end }} {{- end }}
</figure> </figure>
{{- end }} {{- end }}
{{/* Play Widget - 2 columns */}} {{/* Play Widget - 2 columns */}}
{{- if $audio }} {{- if .Params.audio }}
<div <div
x-data x-data
@click=" @click="
if ($store.audio.currentTrack?.url === '{{ $audio }}') { $store.audio.currentTrack = {
window.__pivoine?.audioManager?.toggle(); title: '{{ .Title }}',
} else { url: '{{ .Params.audio }}',
$store.audio.currentTrack = { image: '{{ with .Resources.GetMatch "cover.*" }}{{ (.Resize "200x webp q85").RelPermalink }}{{ end }}'
title: '{{ .Title }}', };
url: '{{ $audio }}', window.__pivoine?.audioManager?.play('{{ .Params.audio }}');
image: '{{ with .Resources.GetMatch "cover.*" }}{{ (.Resize "200x webp q85").RelPermalink }}{{ end }}' $store.audio.isPlaying = true;
};
window.__pivoine?.audioManager?.play('{{ $audio }}');
$store.audio.isPlaying = true;
}
" "
class="md:col-span-2 flex items-center gap-4 cursor-pointer group" class="md:col-span-2 flex items-center gap-4 cursor-pointer group"
> >

View File

@@ -29,12 +29,9 @@ server {
add_header Cache-Control "no-store, no-cache, must-revalidate"; add_header Cache-Control "no-store, no-cache, must-revalidate";
} }
# Custom 404 page # Clean URLs - try files, then directories, then fallback to index.html
error_page 404 /404.html;
# Clean URLs - try files, then directories, then 404
location / { location / {
try_files $uri $uri/ $uri.html =404; try_files $uri $uri/ $uri.html /index.html;
} }
# RSS feed # RSS feed

View File

@@ -1,2 +0,0 @@
onlyBuiltDependencies:
- '@parcel/watcher'

View File

@@ -907,6 +907,14 @@
} }
} }
} }
.hover\:grayscale-0 {
&:hover {
@media (hover: hover) {
--tw-grayscale: grayscale(0%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
}
}
.hover\:after\:w-full { .hover\:after\:w-full {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB