Compare commits

8 Commits

21 changed files with 181 additions and 25 deletions

View File

@@ -13,7 +13,8 @@
"WebSearch", "WebSearch",
"WebFetch(domain:htmx.org)", "WebFetch(domain:htmx.org)",
"Bash(cat:*)", "Bash(cat:*)",
"Bash(git remote:*)" "Bash(git remote:*)",
"Bash(ls:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

3
.gitignore vendored
View File

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

View File

@@ -16,6 +16,53 @@ 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() {
@@ -46,6 +93,10 @@ 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', () => {
@@ -135,6 +186,32 @@ 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,6 +2,9 @@ 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/"
@@ -11,7 +14,7 @@ title = "Valknar's"
[outputs] [outputs]
home = ["HTML", "RSS"] home = ["HTML", "RSS"]
section = ["HTML", "RSS"] section = ["HTML", "RSS", "JSON"]
[sitemap] [sitemap]
changefreq = "weekly" changefreq = "weekly"

View File

@@ -7,9 +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.
### Equipment ### Dedication
Debian GNU/Linux 13 The love of my life, Palina.
### Contact ### Contact

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -0,0 +1,17 @@
---
title: "Listen To Your Heart"
date: 2025-12-31
draft: false
description: "Roxette"
audio: "https://jellyfin.media.pivoine.art/Items/7665d067afb622eef70db5fa65ab8829/Download?api_key=8db4f88966fd4feb9308dfff68e9eeea"
duration: "4:23"
artist: "Valknar"
genre: "Drum'n'Bass"
tags:
- liquid
- funk
- soul
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -0,0 +1,17 @@
---
title: "Paule"
date: 2025-12-31
draft: false
description: "Du Bisch"
audio: "https://jellyfin.media.pivoine.art/Items/f1050f982226566de6ef11f4f2edbf33/Download?api_key=8db4f88966fd4feb9308dfff68e9eeea"
duration: "0:31"
artist: "Valknar"
genre: "HipHop"
tags:
- patriotism
- funk
- superior
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -0,0 +1,17 @@
---
title: "Rome"
date: 2025-12-31
draft: false
description: "Honor Et Virtus"
audio: "https://jellyfin.media.pivoine.art/Items/9f4b3b6e523b3488c850e2d50fbe157f/Download?api_key=8db4f88966fd4feb9308dfff68e9eeea"
duration: "3:57"
artist: "Valknar"
genre: "Metal"
tags:
- metal
- strength
- honor
---

View File

@@ -1,7 +1,7 @@
--- ---
title: "The End Of All" title: "The End Of All"
date: 2025-11-16 date: 2025-11-16
draft: true draft: false
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" audio: "https://jellyfin.media.pivoine.art/Items/60d39ab0aad880627e8fb85cf1ee7b40/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60"

View File

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

View File

@@ -1,5 +1,12 @@
{{/* Track Card Component */}} {{/* Track Card Component */}}
<article class="track-card rounded-lg overflow-hidden group grayscale hover:grayscale-0"> {{- $hasVideo := .Resources.GetMatch "preview.*" -}}
<article
class="track-card rounded-lg overflow-hidden group transition-all duration-300"
:class="hovering ? '' : 'grayscale'"
x-data="{ hovering: false }"
@mouseenter="hovering = true; $refs.video?.play().catch(() => {})"
@mouseleave="hovering = false; if ($refs.video) { $refs.video.pause(); $refs.video.currentTime = 0; }"
>
{{/* 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.*" }}
@@ -7,7 +14,8 @@
<img <img
src="{{ $img.RelPermalink }}" src="{{ $img.RelPermalink }}"
alt="{{ $.Title }}" alt="{{ $.Title }}"
class="track-card__cover w-full h-full" class="track-card__cover w-full h-full object-cover"
:class="{ 'opacity-0': hovering && $refs.video }"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
> >
@@ -22,18 +30,19 @@
{{/* Video preview on hover */}} {{/* 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 opacity-0 group-hover:opacity-100 transition-opacity duration-300" class="absolute inset-0 w-full h-full object-cover transition-opacity duration-300"
:class="hovering ? 'opacity-100' : 'opacity-0'"
muted muted
loop loop
playsinline playsinline
onmouseenter="this.play()" preload="metadata"
onmouseleave="this.pause(); this.currentTime=0;"
></video> ></video>
{{- end }} {{- end }}
{{/* Play overlay - hide if video exists */}} {{/* Play overlay - only show if NO video exists */}}
{{- if not (.Resources.GetMatch "preview.*") }} {{- if not $hasVideo }}
<div class="absolute inset-0 bg-surface-0/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"> <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 scale-90 group-hover:scale-100 transition-transform"> <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">

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 := .Pages -}} {{- $tracks := .Paginator.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

@@ -0,0 +1,18 @@
{{- $tracks := slice -}}
{{- range .Pages -}}
{{- $track := dict
"title" .Title
"url" .Permalink
"slug" .File.ContentBaseName
"audio" .Params.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" " ") -}}

2
pnpm-workspace.yaml Normal file
View File

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

View File

@@ -907,14 +907,6 @@
} }
} }
} }
.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) {