Compare commits
7 Commits
9f4bc341e1
...
7773d7c3ea
Author | SHA1 | Date | |
---|---|---|---|
7773d7c3ea
|
|||
a5f00515a5
|
|||
4b48554501
|
|||
608d9af232
|
|||
057b88437c
|
|||
f8ab08d76a
|
|||
1666351268
|
@@ -33,7 +33,7 @@ export default defineConfig({
|
||||
trailingSlash: "always",
|
||||
|
||||
image: {
|
||||
responsiveStyles: true
|
||||
responsiveStyles: false
|
||||
},
|
||||
|
||||
output: "static"
|
||||
|
3300
package-lock.json
generated
3300
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev": "astro dev --host",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
@@ -19,6 +19,7 @@
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"astro": "^5.12.8",
|
||||
"astro-icon": "^1.1.5",
|
||||
"get-audio-duration": "^4.0.1",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -1,4 +1,10 @@
|
||||
---
|
||||
export interface Track {
|
||||
src: string;
|
||||
metadata: Record<string, any>;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
interface Props {
|
||||
@@ -6,6 +12,28 @@ interface Props {
|
||||
}
|
||||
|
||||
const { height = "h-28" } = Astro.props as Props;
|
||||
|
||||
import { getAudioDurationInSeconds } from "get-audio-duration";
|
||||
import { join } from "path";
|
||||
|
||||
import { getCollection } from "astro:content";
|
||||
const autoQueuedTracks = (
|
||||
await getCollection("tracks", ({ data }) => data.autoQueue)
|
||||
).sort(
|
||||
(a, b) => (a.data.autoQueue?.order || -1) - (b.data.autoQueue?.order || 1)
|
||||
);
|
||||
|
||||
const initialQueue = await Promise.all(
|
||||
autoQueuedTracks.map(async ({ data }) => {
|
||||
const fullFilePath = join(process.cwd(), "public", data.src);
|
||||
const duration = await getAudioDurationInSeconds(fullFilePath);
|
||||
return {
|
||||
src: data.src,
|
||||
metadata: data.metadata,
|
||||
duration: duration
|
||||
};
|
||||
})
|
||||
);
|
||||
---
|
||||
|
||||
<div
|
||||
@@ -32,13 +60,13 @@ const { height = "h-28" } = Astro.props as Props;
|
||||
</div>
|
||||
<div class="p-2 text-white">
|
||||
<div class="grid grid-cols-2 text-xs">
|
||||
<span id="player:time:current" class="col-span-1 text-left"></span>
|
||||
<span id="player:time:total" class="col-span-1 text-right"></span>
|
||||
<span id="player:times:current" class="col-span-1 text-left">0:00</span>
|
||||
<span id="player:times:duration" class="col-span-1 text-right">0:00</span>
|
||||
</div>
|
||||
<div class="flex h-full flex-row items-center justify-center align-middle">
|
||||
<div class="m-0 flex grow-0 flex-row space-x-2 p-0 text-3xl">
|
||||
<span>
|
||||
<Icon name="fa7-solid:backward" id="player:controls:backward" />
|
||||
<Icon name="fa7-solid:backward" id="player:controls:previous" />
|
||||
</span>
|
||||
<span>
|
||||
<Icon name="fa7-solid:play" id="player:controls:play" />
|
||||
@@ -49,7 +77,7 @@ const { height = "h-28" } = Astro.props as Props;
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<Icon name="fa7-solid:forward" id="player:controls:forward" />
|
||||
<Icon name="fa7-solid:forward" id="player:controls:next" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-grow flex-col text-sm">
|
||||
@@ -63,363 +91,260 @@ const { height = "h-28" } = Astro.props as Props;
|
||||
</div>
|
||||
<div class:list={[height]}></div>
|
||||
|
||||
<script src="/js/howler.min.js" is:inline></script>
|
||||
<script is:inline src="/js/howler.min.js"></script>
|
||||
<script is:inline define:vars={{ initialQueue }}>
|
||||
/**
|
||||
* Player class containing the state of our playlist and where we are in it.
|
||||
* Includes all methods for playing, skipping, updating the display, etc.
|
||||
* @param {Array} playlist Array of objects with playlist song details ({title, file, howl}).
|
||||
*/
|
||||
class Player {
|
||||
constructor(playlist) {
|
||||
this.playlist = window.player?.playlist ?? playlist;
|
||||
this.index = window.player?.index ?? 0;
|
||||
|
||||
<!-- Retrieve elements -->
|
||||
<script is:inline>
|
||||
// numbers
|
||||
const player_playerTimeCurrent = document.getElementById(
|
||||
"player:time:current"
|
||||
);
|
||||
const player_playerTimeTotal = document.getElementById("player:time:total");
|
||||
this.UI = {
|
||||
controls: {
|
||||
play: document.getElementById("player:controls:play"),
|
||||
pause: document.getElementById("player:controls:pause"),
|
||||
previous: document.getElementById("player:controls:previous"),
|
||||
next: document.getElementById("player:controls:next")
|
||||
},
|
||||
times: {
|
||||
current: document.getElementById("player:times:current"),
|
||||
duration: document.getElementById("player:times:duration")
|
||||
},
|
||||
metadata: {
|
||||
title: document.getElementById("player:metadata:title"),
|
||||
subtitle: document.getElementById("player:metadata:subtitle"),
|
||||
extra: document.getElementById("player:metadata:extra")
|
||||
},
|
||||
progress: {
|
||||
bar: document.getElementById("player:time:progress"),
|
||||
buffered: document.getElementById("player:time:progress:buffered"),
|
||||
played: document.getElementById("player:time:progress:played")
|
||||
}
|
||||
};
|
||||
|
||||
// progress bars
|
||||
const player_playerTimeProgress = document.getElementById(
|
||||
"player:time:progress"
|
||||
);
|
||||
const player_playerTimeProgressPlayed = document.getElementById(
|
||||
"player:time:progress:played"
|
||||
);
|
||||
const player_playerTimeProgressBuffered = document.getElementById(
|
||||
"player:time:progress:buffered"
|
||||
this.UI.metadata.title.textContent = playlist[this.index].metadata.title;
|
||||
this.UI.metadata.subtitle.textContent =
|
||||
playlist[this.index].metadata.subtitle;
|
||||
this.UI.metadata.extra.textContent = playlist[this.index].metadata.extra;
|
||||
this.UI.times.duration.textContent = this.formatTime(
|
||||
Math.round(playlist[this.index].duration)
|
||||
);
|
||||
|
||||
// metadata
|
||||
const player_playerMetadataTitle = document.getElementById(
|
||||
"player:metadata:title"
|
||||
);
|
||||
const player_playerMetadataSubtitle = document.getElementById(
|
||||
"player:metadata:subtitle"
|
||||
);
|
||||
const player_playerMetadataExtra = document.getElementById(
|
||||
"player:metadata:extra"
|
||||
);
|
||||
|
||||
// controls
|
||||
const player_playerControlsPlay = document.getElementById(
|
||||
"player:controls:play"
|
||||
);
|
||||
const player_playerControlsPause = document.getElementById(
|
||||
"player:controls:pause"
|
||||
);
|
||||
const player_playerControlsForward = document.getElementById(
|
||||
"player:controls:forward"
|
||||
);
|
||||
const player_playerControlsBackward = document.getElementById(
|
||||
"player:controls:backward"
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- Add listeners -->
|
||||
<script is:inline>
|
||||
if (player_playerTimeProgress) {
|
||||
player_playerTimeProgress.addEventListener("click", function (event) {
|
||||
const x = event.offsetX;
|
||||
const percentage =
|
||||
x / player_playerTimeProgress.getBoundingClientRect().width;
|
||||
player_seekTrack(percentage);
|
||||
this.UI.controls.play.addEventListener("click", function () {
|
||||
window.player.play();
|
||||
});
|
||||
}
|
||||
|
||||
if (player_playerControlsPlay) {
|
||||
player_playerControlsPlay.addEventListener("click", function (event) {
|
||||
player_playPause();
|
||||
this.UI.controls.pause.addEventListener("click", function () {
|
||||
window.player.pause();
|
||||
});
|
||||
}
|
||||
|
||||
if (player_playerControlsPause) {
|
||||
player_playerControlsPause.addEventListener("click", function (event) {
|
||||
player_playPause();
|
||||
this.UI.controls.previous.addEventListener("click", function () {
|
||||
window.player.skip("prev");
|
||||
});
|
||||
}
|
||||
|
||||
if (player_playerControlsForward) {
|
||||
player_playerControlsForward.addEventListener("click", function (event) {
|
||||
player_playForward();
|
||||
this.UI.controls.next.addEventListener("click", function () {
|
||||
window.player.skip("next");
|
||||
});
|
||||
}
|
||||
|
||||
if (player_playerControlsBackward) {
|
||||
player_playerControlsBackward.addEventListener("click", function (event) {
|
||||
player_playBackward();
|
||||
this.UI.progress.bar.addEventListener("click", function (event) {
|
||||
window.player.seek(event.clientX / window.innerWidth);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Playing control logic -->
|
||||
<script is:inline>
|
||||
function player_seekTrack(percentage) {
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
if (currentTrack) {
|
||||
const duration = currentTrack.howl.duration();
|
||||
const seconds = duration * percentage;
|
||||
const ID = player_getCurrentTrackID();
|
||||
currentTrack.howl.seek(seconds, ID);
|
||||
}
|
||||
window.playerIsInitialised = true;
|
||||
}
|
||||
|
||||
function player_playPause() {
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
if (!currentTrack) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Play a song in the playlist.
|
||||
* @param {Number} index Index of the song in the playlist (leave empty to play the first or current).
|
||||
*/
|
||||
play(index) {
|
||||
var sound;
|
||||
|
||||
const ID = player_getCurrentTrackID();
|
||||
index = typeof index === "number" ? index : this.index;
|
||||
var data = this.playlist[index];
|
||||
|
||||
if (currentTrack.howl.playing(ID)) {
|
||||
currentTrack.howl.pause(ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Valid ID but not playing
|
||||
if (typeof ID === "number" && ID >= 0) {
|
||||
currentTrack.howl.play(ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: No valid ID yet (first play)
|
||||
player_nextIndex++;
|
||||
player_currentID = currentTrack.howl.play();
|
||||
}
|
||||
|
||||
function player_playForward() {
|
||||
player_playNext();
|
||||
}
|
||||
|
||||
function player_playBackward() {
|
||||
player_playPrevious();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Handle UI -->
|
||||
<script is:inline>
|
||||
function player_formatTime(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
// Pad with 0s if less than 10
|
||||
const paddedMinutes = String(minutes).padStart(2, "0");
|
||||
const paddedSecs = String(secs).padStart(2, "0");
|
||||
return `${paddedMinutes}:${paddedSecs}`;
|
||||
}
|
||||
|
||||
function player_getBufferedAmount(track) {
|
||||
const audioNode = track._sounds[0]._node;
|
||||
if (audioNode && audioNode.buffered.length) {
|
||||
// Get the highest buffered point
|
||||
return audioNode.buffered.end(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function player_updateProgressAndDuration(
|
||||
current,
|
||||
total,
|
||||
progress,
|
||||
buffered
|
||||
) {
|
||||
if (player_playerTimeCurrent) {
|
||||
player_playerTimeCurrent.textContent = player_formatTime(current);
|
||||
}
|
||||
|
||||
if (player_playerTimeTotal) {
|
||||
player_playerTimeTotal.textContent = player_formatTime(total);
|
||||
}
|
||||
|
||||
if (player_playerTimeProgressPlayed) {
|
||||
player_playerTimeProgressPlayed.style.width = `${(progress * 100).toString()}%`;
|
||||
}
|
||||
|
||||
if (player_playerTimeProgressBuffered) {
|
||||
let bufferedPercentage = (total / buffered) * 100;
|
||||
|
||||
if (Math.abs(100 - bufferedPercentage) < 1) {
|
||||
bufferedPercentage = 100;
|
||||
}
|
||||
|
||||
player_playerTimeProgressBuffered.style.width = `${bufferedPercentage.toString()}%`;
|
||||
}
|
||||
}
|
||||
|
||||
function player_updateMetadata(title, subtitle, extra, artist) {
|
||||
if (player_playerMetadataTitle) {
|
||||
player_playerMetadataTitle.textContent = title;
|
||||
}
|
||||
|
||||
if (player_playerMetadataSubtitle) {
|
||||
player_playerMetadataSubtitle.textContent = subtitle;
|
||||
}
|
||||
|
||||
if (player_playerMetadataExtra) {
|
||||
player_playerMetadataExtra.textContent = extra;
|
||||
}
|
||||
}
|
||||
|
||||
function player_updatePlayPause() {
|
||||
if (player_playerControlsPlay && player_playerControlsPause) {
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
|
||||
if (currentTrack) {
|
||||
if (currentTrack.howl.playing()) {
|
||||
player_playerControlsPause.classList.remove("hidden");
|
||||
player_playerControlsPlay.classList.add("hidden");
|
||||
// If we already loaded this track, use the current one.
|
||||
if (data.howl) {
|
||||
sound = data.howl;
|
||||
} else {
|
||||
player_playerControlsPause.classList.add("hidden");
|
||||
player_playerControlsPlay.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
sound = data.howl = new Howl({
|
||||
src: [data.src],
|
||||
autoplay: false,
|
||||
html5: true, // Force to HTML5 so that the audio can stream in (best for large files).
|
||||
onplay: () => {
|
||||
// Display the duration.
|
||||
this.UI.times.duration.textContent = this.formatTime(
|
||||
Math.round(sound.duration())
|
||||
);
|
||||
|
||||
// Start updating the progress of the track.
|
||||
requestAnimationFrame(this.step.bind(this));
|
||||
|
||||
this.UI.controls.pause.classList.remove("hidden");
|
||||
this.UI.controls.play.classList.add("hidden");
|
||||
},
|
||||
// onload: () => {
|
||||
// loading.style.display = "none";
|
||||
// },
|
||||
onend: () => {
|
||||
this.skip("next");
|
||||
},
|
||||
onpause: () => {
|
||||
this.UI.controls.pause.classList.add("hidden");
|
||||
this.UI.controls.play.classList.remove("hidden");
|
||||
},
|
||||
// onstop: () => {
|
||||
// },
|
||||
onseek: () => {
|
||||
// Start updating the progress of the track.
|
||||
requestAnimationFrame(this.step.bind(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function player_updateUIElements() {
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
if (currentTrack) {
|
||||
// progress bar and times
|
||||
const current = currentTrack.howl.seek();
|
||||
const duration = currentTrack.howl.duration();
|
||||
const progress = current / duration;
|
||||
const buffered = player_getBufferedAmount(currentTrack.howl);
|
||||
// Begin playing the sound.
|
||||
sound.play();
|
||||
|
||||
player_updateProgressAndDuration(current, duration, progress, buffered);
|
||||
// Update the track display.
|
||||
this.UI.metadata.title.textContent = data.metadata.title;
|
||||
this.UI.metadata.subtitle.textContent = data.metadata.subtitle;
|
||||
this.UI.metadata.extra.textContent = data.metadata.extra;
|
||||
this.UI.times.duration.textContent = this.formatTime(
|
||||
Math.round(data.duration)
|
||||
);
|
||||
|
||||
// metadata
|
||||
const title = currentTrack.metadata.title;
|
||||
const subtitle = currentTrack.metadata.subtitle || "";
|
||||
const extra = currentTrack.metadata.extra || "";
|
||||
const artist = currentTrack.metadata.artist || "";
|
||||
|
||||
player_updateMetadata(title, subtitle, extra, artist);
|
||||
|
||||
player_updatePlayPause();
|
||||
// Keep track of the index we are currently playing.
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
requestAnimationFrame(player_updateUIElements);
|
||||
/**
|
||||
* Pause the currently playing track.
|
||||
*/
|
||||
pause() {
|
||||
// Get the Howl we want to manipulate.
|
||||
var sound = this.playlist[this.index].howl;
|
||||
|
||||
sound.pause();
|
||||
|
||||
this.UI.controls.pause.classList.add("hidden");
|
||||
this.UI.controls.play.classList.remove("hidden");
|
||||
}
|
||||
|
||||
requestAnimationFrame(player_updateUIElements);
|
||||
</script>
|
||||
/**
|
||||
* Skip to the next or previous track.
|
||||
* @param {String} direction 'next' or 'prev'.
|
||||
*/
|
||||
skip(direction) {
|
||||
var current = this.playlist[this.index];
|
||||
var sound = current.howl;
|
||||
var currentlyPlaying = sound && sound.playing();
|
||||
|
||||
<!-- Main logic -->
|
||||
<script is:inline>
|
||||
const player_playlist = [];
|
||||
let player_currentID = -999;
|
||||
let player_nextIndex = 0;
|
||||
|
||||
function player_getCurrentTrack() {
|
||||
if (player_getPlaylistSize() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return player_playlist[player_getCurrentTrackIndex()];
|
||||
}
|
||||
|
||||
function player_getCurrentTrackNumber() {
|
||||
return player_nextIndex;
|
||||
}
|
||||
|
||||
function player_getCurrentTrackIndex() {
|
||||
if (player_nextIndex === 0) {
|
||||
return player_nextIndex;
|
||||
}
|
||||
return player_nextIndex - 1;
|
||||
}
|
||||
|
||||
function player_getCurrentTrackID() {
|
||||
return player_currentID;
|
||||
}
|
||||
|
||||
function player_getPlaylistSize() {
|
||||
return player_playlist.length;
|
||||
}
|
||||
|
||||
function player_playNext() {
|
||||
if (player_getPlaylistSize() === 0) {
|
||||
// the playlist is empty, just exit
|
||||
if (direction === "prev" && currentlyPlaying && sound.seek() > 3) {
|
||||
sound.seek(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
const currentID = player_getCurrentTrackID();
|
||||
|
||||
if (currentTrack && currentTrack.howl.playing()) {
|
||||
currentTrack.howl.stop(currentID);
|
||||
currentTrack.howl.seek(0, currentID);
|
||||
let index;
|
||||
if (direction === "prev") {
|
||||
index = (this.index - 1 + this.playlist.length) % this.playlist.length;
|
||||
} else {
|
||||
index = (this.index + 1) % this.playlist.length;
|
||||
}
|
||||
|
||||
if (player_nextIndex >= player_getPlaylistSize()) {
|
||||
// wrap around
|
||||
player_nextIndex = 0;
|
||||
this.skipTo(index);
|
||||
}
|
||||
|
||||
const newTrack = player_playlist[player_nextIndex];
|
||||
player_nextIndex++;
|
||||
player_currentID = newTrack.howl.play();
|
||||
/**
|
||||
* Skip to a specific track based on its playlist index.
|
||||
* @param {Number} index Index in the playlist.
|
||||
*/
|
||||
skipTo(index) {
|
||||
// Stop the current track.
|
||||
if (this.playlist[this.index].howl) {
|
||||
this.playlist[this.index].howl.stop();
|
||||
}
|
||||
|
||||
function player_playPrevious() {
|
||||
if (player_getPlaylistSize() === 0) {
|
||||
// the playlist is empty, just exit
|
||||
return;
|
||||
// Reset progress.
|
||||
this.UI.progress.played.style.width = "0%";
|
||||
this.UI.times.current.textContent = this.formatTime(0);
|
||||
|
||||
// Play the new track.
|
||||
this.play(index);
|
||||
}
|
||||
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
const ID = player_getCurrentTrackID();
|
||||
/**
|
||||
* Seek to a new position in the currently playing track.
|
||||
* @param {Number} per Percentage through the song to skip.
|
||||
*/
|
||||
seek(per) {
|
||||
// Get the Howl we want to manipulate.
|
||||
var sound = this.playlist[this.index].howl;
|
||||
|
||||
if (sound.playing()) {
|
||||
// Convert the percent into a seek position.
|
||||
sound.seek(sound.duration() * per);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The step called within requestAnimationFrame to update the playback position.
|
||||
*/
|
||||
step() {
|
||||
// Get the Howl we want to manipulate.
|
||||
var sound = this.playlist[this.index].howl;
|
||||
|
||||
var seek = sound.seek() || 0;
|
||||
this.UI.times.current.textContent = this.formatTime(Math.round(seek));
|
||||
this.UI.progress.played.style.width =
|
||||
((seek / sound.duration()) * 100 || 0) + "%";
|
||||
|
||||
// If the sound is still playing, continue stepping.
|
||||
if (sound.playing()) {
|
||||
requestAnimationFrame(this.step.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time from seconds to M:SS.
|
||||
* @param {Number} secs Seconds to format.
|
||||
* @return {String} Formatted time.
|
||||
*/
|
||||
formatTime(secs) {
|
||||
var minutes = Math.floor(secs / 60) || 0;
|
||||
var seconds = secs - minutes * 60 || 0;
|
||||
|
||||
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||
}
|
||||
|
||||
queue(track, immediatePlay = false) {
|
||||
this.playlist.splice(this.index + 1, 0, track);
|
||||
|
||||
if (immediatePlay) {
|
||||
this.skipTo(this.index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
addFromTrackCard(track) {
|
||||
var current = this.playlist[this.index];
|
||||
|
||||
if (
|
||||
currentTrack &&
|
||||
currentTrack.howl.playing() &&
|
||||
currentTrack.howl.seek() >= 2
|
||||
track.metadata.title === current.metadata.title &&
|
||||
track.metadata.subtitle === current.metadata.subtitle &&
|
||||
track.metadata.extra === current.metadata.extra
|
||||
) {
|
||||
// there's currently a track playing and we're more than two seconds into it
|
||||
// play it from the start rather than going back to the previous
|
||||
currentTrack.howl.seek(0, ID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTrack && currentTrack.howl.playing(ID)) {
|
||||
currentTrack.howl.stop(ID);
|
||||
}
|
||||
|
||||
// Rewind index
|
||||
player_nextIndex -= 2;
|
||||
if (player_nextIndex < 0) {
|
||||
player_nextIndex = player_getPlaylistSize() - 1;
|
||||
}
|
||||
|
||||
const newTrack = player_playlist[player_nextIndex];
|
||||
player_nextIndex++;
|
||||
player_currentID = newTrack.howl.play();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Player utilities -->
|
||||
<script is:inline>
|
||||
function player_constructTrack(trackData) {
|
||||
trackData.howl = new Howl({
|
||||
src: [`/audio/${trackData.src}`],
|
||||
html5: true,
|
||||
preload: "metadata",
|
||||
volume: 1,
|
||||
onend: player_playNext
|
||||
});
|
||||
return trackData;
|
||||
}
|
||||
|
||||
function player_addToPlaylist(track, autoPlay = true) {
|
||||
const currentTrack = player_getCurrentTrack();
|
||||
if (
|
||||
currentTrack &&
|
||||
currentTrack.metadata.title === track.metadata.title &&
|
||||
currentTrack.metadata.subtitle === track.metadata.subtitle
|
||||
) {
|
||||
player_playPause();
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
player_playlist.splice(player_nextIndex, 0, track);
|
||||
player_playNext();
|
||||
if (current.howl && current.howl.playing()) {
|
||||
this.pause();
|
||||
} else {
|
||||
player_playlist.push(track);
|
||||
this.play();
|
||||
}
|
||||
} else {
|
||||
this.queue(track, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.playerIsInitialised) {
|
||||
window.player = new Player(initialQueue);
|
||||
}
|
||||
</script>
|
||||
|
@@ -164,7 +164,7 @@ const projectHasBody = project.body && project.body.trim().length > 0;
|
||||
<Image
|
||||
id={`hero-image-${index}`}
|
||||
class:list={[
|
||||
"animate-floaty absolute h-48 w-auto origin-center transform rounded object-cover shadow-lg/50 transition duration-300 ease-in-out group-hover:z-30 group-hover:scale-130 hover:z-40",
|
||||
"animate-floaty absolute h-48 w-auto origin-center transform rounded object-cover shadow-lg/50 transition duration-300 ease-in-out md:group-hover:z-30 md:group-hover:scale-130 md:hover:z-40",
|
||||
translateXOptions[
|
||||
Math.floor(Math.random() * translateXOptions.length)
|
||||
],
|
||||
|
@@ -39,8 +39,8 @@ import site from "@data/site";
|
||||
/>
|
||||
<meta property="og:image:alt" content={image?.alt || site.image.alt} />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:width" content="1920" />
|
||||
<meta property="og:image:height" content="1080" />
|
||||
<meta property="og:image:width" content={image?.width.toString() || "1920"} />
|
||||
<meta property="og:image:height" content={image?.height.toString() || "1080"} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:site" content="@nathancummins.au" />
|
||||
|
@@ -9,6 +9,11 @@ interface Props {
|
||||
const { track, index } = Astro.props as Props;
|
||||
|
||||
import Image from "astro/components/Image.astro";
|
||||
|
||||
import { getAudioDurationInSeconds } from "get-audio-duration";
|
||||
import { join } from "path";
|
||||
const fullFilePath = join(process.cwd(), "public", track.data.src);
|
||||
const duration = await getAudioDurationInSeconds(fullFilePath);
|
||||
---
|
||||
|
||||
<div
|
||||
@@ -16,7 +21,7 @@ import Image from "astro/components/Image.astro";
|
||||
"relative flex min-h-42 flex-auto hover:cursor-pointer",
|
||||
index < 2 ? "lg:w-1/2" : "sm:w-1/2 md:w-1/3 lg:w-1/4"
|
||||
]}
|
||||
onclick=`player_addToPlaylist(player_constructTrack(${JSON.stringify(track.data)}))`
|
||||
onclick=`window.player.addFromTrackCard({ src: "${track.data.src}", "metadata": ${JSON.stringify(track.data.metadata)}, "duration": "${duration}" })`
|
||||
>
|
||||
<Image
|
||||
src={track.data.card.image.src}
|
||||
|
@@ -51,7 +51,7 @@ const tracks = defineCollection({
|
||||
loader: glob({ pattern: "**/*.json", base: "./src/assets/tracks" }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
src: z.string(),
|
||||
src: z.preprocess((val) => `/audio/${val}`, z.string()),
|
||||
metadata: z.object({
|
||||
title: z.string(),
|
||||
subtitle: z.string().optional(),
|
||||
|
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@@ -9,4 +9,6 @@ interface Window {
|
||||
getDefaultTheme: () => "auto" | "dark" | "light";
|
||||
};
|
||||
navbarDisplay: "normal" | "transparent";
|
||||
player: any;
|
||||
playerIsInitialised: boolean;
|
||||
}
|
||||
|
@@ -7,14 +7,6 @@ interface Props {
|
||||
image?: SiteImage;
|
||||
}
|
||||
|
||||
import "@/styles/global.css";
|
||||
|
||||
import { getCollection } from "astro:content";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Navbar from "@components/Navbar.astro";
|
||||
import Player from "@components/Player.astro";
|
||||
import MainHead from "@layouts/MainHead.astro";
|
||||
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
@@ -23,11 +15,12 @@ const {
|
||||
image
|
||||
} = Astro.props;
|
||||
|
||||
const autoQueuedTracks = (
|
||||
await getCollection("tracks", ({ data }) => data.autoQueue)
|
||||
).sort(
|
||||
(a, b) => (a.data.autoQueue?.order || -1) - (b.data.autoQueue?.order || 1)
|
||||
);
|
||||
import "@/styles/global.css";
|
||||
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Navbar from "@components/Navbar.astro";
|
||||
import Player from "@components/Player.astro";
|
||||
import MainHead from "@layouts/MainHead.astro";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -42,9 +35,3 @@ const autoQueuedTracks = (
|
||||
<Player />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script define:vars={{ autoQueuedTracks }}>
|
||||
autoQueuedTracks.map((track, index) => {
|
||||
player_addToPlaylist(player_constructTrack(track.data), false);
|
||||
});
|
||||
</script>
|
||||
|
@@ -17,15 +17,17 @@ import MainLayout from "../layouts/MainLayout.astro";
|
||||
<Image
|
||||
src={aboutImage}
|
||||
alt="A photo of Nathan conducting the Woodville Concert Band"
|
||||
class="mx-auto mt-8 size-96 rounded-full object-cover drop-shadow-lg/75 transition ease-in-out hover:scale-150 hover:shadow-xl/50"
|
||||
class="mx-auto mt-8 size-96 rounded-full object-cover drop-shadow-lg/75 transition ease-in-out md:hover:scale-150 md:hover:shadow-xl/50"
|
||||
transition:name="aboutImage"
|
||||
/>
|
||||
<a
|
||||
<span class="block w-full text-center"
|
||||
><a
|
||||
href="/contact/"
|
||||
class="bg-primary text-md font-header hover:text-primary repeat hover:ring-primary mx-auto mt-8 inline-block rounded object-center px-6 py-3 font-light text-white uppercase drop-shadow-lg/75 transition hover:bg-white hover:ring-2"
|
||||
>
|
||||
Get in touch!
|
||||
</a>
|
||||
</a></span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
@@ -1,25 +1,22 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
import ProjectCard from "@/components/ProjectCard.astro";
|
||||
import TrackCard from "@/components/TrackCard.astro";
|
||||
import { Content } from "@assets/bios/short.mdx";
|
||||
import aboutImage from "@assets/img/about.jpg";
|
||||
import AwardCard from "@components/AwardCard.astro";
|
||||
import ImageCarousel from "@components/ImageCarousel.astro";
|
||||
import Link from "@components/Link.astro";
|
||||
import Paragraph from "@components/Paragraph.astro";
|
||||
import SectionTitle from "@components/SectionTitle.astro";
|
||||
import person from "@data/person";
|
||||
import site from "@data/site";
|
||||
import MainLayout from "@layouts/MainLayout.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import {
|
||||
Content as AboutContent,
|
||||
components as aboutComponents
|
||||
} from "../assets/bios/short.mdx";
|
||||
import aboutImage from "../assets/img/about.jpg";
|
||||
import AwardCard from "../components/AwardCard.astro";
|
||||
import ImageCarousel from "../components/ImageCarousel.astro";
|
||||
import Link from "../components/Link.astro";
|
||||
import Paragraph from "../components/Paragraph.astro";
|
||||
import ProjectCard from "../components/ProjectCard.astro";
|
||||
import SectionTitle from "../components/SectionTitle.astro";
|
||||
import person from "../data/person";
|
||||
import site from "../data/site";
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
import TrackCard from "@/components/TrackCard.astro";
|
||||
import { convertEagerImagesImportGlobToArray } from "@lib/utils";
|
||||
|
||||
const heroImages = import.meta.glob<{ default: ImageMetadata; eager: true }>(
|
||||
@@ -56,13 +53,17 @@ const tracks = (
|
||||
---
|
||||
|
||||
<MainLayout navbarDisplay="transparent">
|
||||
<div class="flex h-screen w-full items-center justify-center">
|
||||
<div class="h-screen w-full flex-row">
|
||||
<div class="absolute inset-0 h-full w-full bg-black">
|
||||
<ImageCarousel
|
||||
images={heroImagesArray}
|
||||
className="absolute z-10 h-full w-full"
|
||||
className="h-full w-full"
|
||||
foreground={true}
|
||||
/>
|
||||
<div class="z-20 px-8 py-36 text-center">
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex h-full w-full flex-col justify-center px-8 py-36 text-center"
|
||||
>
|
||||
<h1
|
||||
class="font-header pb-8 text-5xl font-medium text-white uppercase text-shadow-lg/75 md:text-7xl lg:text-9xl"
|
||||
>
|
||||
@@ -87,13 +88,11 @@ const tracks = (
|
||||
<section id="about" class="bg-white dark:bg-gray-950">
|
||||
<div class="mx-auto max-w-4xl px-8 py-16 text-justify sm:text-center">
|
||||
<SectionTitle>About</SectionTitle>
|
||||
<AboutContent
|
||||
components={{ ...aboutComponents, p: Paragraph, a: Link }}
|
||||
/>
|
||||
<Content components={{ p: Paragraph, a: Link }} />
|
||||
<Image
|
||||
src={aboutImage}
|
||||
alt="Nathan conducting the Woodville Concert Band"
|
||||
class="mx-auto mt-8 size-96 rounded-full object-cover drop-shadow-lg/75 transition ease-in-out hover:scale-150 hover:shadow-xl/50"
|
||||
class="mx-auto mt-8 size-96 rounded-full object-cover drop-shadow-lg/75 transition ease-in-out md:hover:scale-150 md:hover:shadow-xl/50"
|
||||
transition:name="aboutImage"
|
||||
/>
|
||||
<span class="block w-full text-center"
|
||||
@@ -128,7 +127,7 @@ const tracks = (
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section id="projects" class="bg-white dark:bg-gray-950">
|
||||
<section id="projects" class="overflow-hidden bg-white dark:bg-gray-950">
|
||||
<div class="mx-auto max-w-4xl px-8 py-16 text-center">
|
||||
<SectionTitle>What I've been working on</SectionTitle>
|
||||
<Paragraph>
|
||||
@@ -156,7 +155,7 @@ const tracks = (
|
||||
</div>
|
||||
</section>
|
||||
<section id="listen" class="bg-primary text-white dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<div class="w-full overflow-hidden">
|
||||
<div class="pt-16 text-center">
|
||||
<SectionTitle lineColour="text-white">Listen</SectionTitle>
|
||||
|
||||
|
@@ -12,7 +12,10 @@ import {
|
||||
} from "@/lib/utils";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const projects = await getCollection("projects");
|
||||
const projects = await getCollection("projects", ({ body }) => {
|
||||
return body && body.trim().length > 0;
|
||||
});
|
||||
|
||||
return projects.map((project) => ({
|
||||
params: { slug: project.id },
|
||||
props: { project }
|
||||
@@ -28,7 +31,9 @@ const hero = getProjectHero(project);
|
||||
const seoImage: SiteImage = {
|
||||
externalURL: await getFullExternalURLOfImage(hero),
|
||||
src: hero.src,
|
||||
alt: project.data.images.hero.alt
|
||||
alt: project.data.images.hero.alt,
|
||||
width: hero.width,
|
||||
height: hero.height
|
||||
};
|
||||
---
|
||||
|
@@ -2,4 +2,6 @@ type SiteImage = {
|
||||
src: string;
|
||||
alt: string;
|
||||
externalURL?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
Reference in New Issue
Block a user