Fixes to strange bars on mobile, and addition of server which currently powers a new Last.FM-based music tracker.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m20s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m20s
This commit is contained in:
parent
c65b8465b5
commit
be5a61185b
8 changed files with 662 additions and 1251 deletions
150
src/client.ts
Normal file
150
src/client.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { treaty } from '@elysiajs/eden';
|
||||
import type { App } from './server';
|
||||
|
||||
// Create Eden Treaty client using current page origin
|
||||
const client = treaty<App>(window.location.origin);
|
||||
|
||||
interface LastSongImage {
|
||||
size: 'small' | 'medium' | 'large' | 'extralarge';
|
||||
'#text': string;
|
||||
}
|
||||
|
||||
interface LastSong {
|
||||
name: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
url: string;
|
||||
nowPlaying: boolean;
|
||||
image: LastSongImage[];
|
||||
}
|
||||
|
||||
// Function to fetch and display last song
|
||||
async function fetchLastSong(): Promise<void> {
|
||||
try {
|
||||
const { data, error } = await client.api['last-song'].get();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching last song:', error);
|
||||
updateLastSongDisplay(null, 'Failed to load song data');
|
||||
return;
|
||||
}
|
||||
|
||||
const song = data as LastSong;
|
||||
updateLastSongDisplay(song);
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
updateLastSongDisplay(null, 'Network error occurred');
|
||||
}
|
||||
}
|
||||
|
||||
// Function to select appropriate image size based on screen width
|
||||
function getOptimalImageUrl(images: LastSongImage[]): string {
|
||||
if (!images || images.length === 0) return '';
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
|
||||
// Select image size based on screen width and device pixel ratio
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const effectiveWidth = screenWidth * pixelRatio;
|
||||
|
||||
let targetSize: string;
|
||||
|
||||
if (effectiveWidth <= 400) {
|
||||
targetSize = 'small'; // 34px
|
||||
} else if (effectiveWidth <= 800) {
|
||||
targetSize = 'medium'; // 64px
|
||||
} else if (effectiveWidth <= 1400) {
|
||||
targetSize = 'large'; // 174px
|
||||
} else {
|
||||
targetSize = 'extralarge'; // 300px
|
||||
}
|
||||
|
||||
// Find the target size, fallback to largest available
|
||||
const targetImage = images.find(img => img.size === targetSize);
|
||||
if (targetImage) return targetImage['#text'];
|
||||
|
||||
// Fallback: return the largest available image
|
||||
const fallbackOrder = ['extralarge', 'large', 'medium', 'small'];
|
||||
for (const size of fallbackOrder) {
|
||||
const image = images.find(img => img.size === size);
|
||||
if (image) return image['#text'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Function to update the UI with song information
|
||||
function updateLastSongDisplay(song: LastSong | null, errorMessage?: string): void {
|
||||
const containers = [
|
||||
document.getElementById('last-song-container'),
|
||||
document.getElementById('last-song-container-mobile')
|
||||
];
|
||||
|
||||
containers.forEach(container => {
|
||||
if (!container) return;
|
||||
|
||||
if (errorMessage || !song) {
|
||||
container.innerHTML = `
|
||||
<div class="text-red-400 text-sm">
|
||||
${errorMessage || 'No song data available'}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const statusText = song.nowPlaying ? 'Currently listening to' : 'Last listened to';
|
||||
const imageUrl = getOptimalImageUrl(song.image);
|
||||
|
||||
const imageElement = imageUrl
|
||||
? `<img src="${imageUrl}" alt="${song.name}" class="w-24 h-24 rounded-lg object-cover mx-auto border border-white/20" loading="lazy">`
|
||||
: `<div class="w-16 h-16 rounded-lg bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center mx-auto border border-white/20">
|
||||
<svg class="w-8 h-8 text-cyan-300" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 3a1 1 0 00-1.447-.894L8.763 6H5a3 3 0 000 6h.28l1.771 5.316A1 1 0 008 18a1 1 0 001-1v-4.382l6.553 3.276A1 1 0 0017 15V3z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="flex flex-col items-center space-y-3">
|
||||
${imageElement}
|
||||
<div class="text-center w-full">
|
||||
<p class="text-sm text-cyan-300 font-medium">${statusText}</p>
|
||||
<a href="${song.url}" target="_blank" class="block hover:text-cyan-200 transition-colors duration-200">
|
||||
<p class="font-semibold text-white">${song.name}</p>
|
||||
<p class="text-sm text-gray-300">by ${song.artist}</p>
|
||||
${song.album ? `<p class="text-xs text-white-400">from ${song.album}</p>` : ''}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-refresh functionality
|
||||
let refreshInterval: number;
|
||||
|
||||
function startAutoRefresh(): void {
|
||||
// Fetch immediately
|
||||
fetchLastSong();
|
||||
|
||||
// Then fetch every 30 seconds
|
||||
refreshInterval = window.setInterval(fetchLastSong, 30000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh(): void {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// Clean up on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
stopAutoRefresh();
|
||||
});
|
||||
|
||||
// Export functions for manual control if needed
|
||||
export { fetchLastSong, startAutoRefresh, stopAutoRefresh };
|
48
src/server.ts
Normal file
48
src/server.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Elysia, t } from 'elysia';
|
||||
import { staticPlugin } from '@elysiajs/static';
|
||||
|
||||
const app = new Elysia()
|
||||
.use(staticPlugin({
|
||||
assets: 'dist',
|
||||
prefix: ''
|
||||
}))
|
||||
.get('/api/last-song', async () => {
|
||||
const apiKey = Bun.env.LASTFM_API_KEY;
|
||||
const username = Bun.env.LASTFM_USERNAME;
|
||||
|
||||
if (!apiKey || !username) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Last.fm API key or username not configured' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
const track = data.recenttracks.track[0];
|
||||
return {
|
||||
name: track.name,
|
||||
artist: track.artist['#text'],
|
||||
album: track.album['#text'],
|
||||
url: track.url,
|
||||
nowPlaying: track['@attr']?.nowplaying === 'true',
|
||||
image: track.image || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching from Last.fm:', error);
|
||||
return new Response(JSON.stringify({ error: 'Failed to fetch last song' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
})
|
||||
.listen(3000);
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
|
||||
export type App = typeof app;
|
|
@ -2,11 +2,18 @@
|
|||
@import "@fontsource-variable/exo-2";
|
||||
|
||||
/* Custom styles for the glassmorphism effect and background */
|
||||
body {
|
||||
html {
|
||||
font-family: 'Exo 2 Variable', sans-serif;
|
||||
/* Deep blue theme */
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #4ca1af 100%);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* The main card with the glass effect */
|
||||
|
@ -25,14 +32,16 @@ body {
|
|||
filter: blur(100px);
|
||||
z-index: -1;
|
||||
animation: float 20s infinite ease-in-out alternate;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: rgba(76, 161, 175, 0.4);
|
||||
top: -50px;
|
||||
left: -50px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(-25%, -25%);
|
||||
animation-duration: 25s;
|
||||
}
|
||||
|
||||
|
@ -40,8 +49,9 @@ body {
|
|||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(108, 99, 255, 0.3);
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(25%, 25%);
|
||||
animation-duration: 20s;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue