feat: add PWA shortcuts for music playback actions and update loading skeletons
This commit is contained in:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_COMMIT_SHA=b668c1b
|
NEXT_PUBLIC_COMMIT_SHA=a00bf3e
|
||||||
|
|||||||
@@ -70,5 +70,46 @@ export default function manifest(): MetadataRoute.Manifest {
|
|||||||
form_factor: 'wide'
|
form_factor: 'wide'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
name: 'Resume Song',
|
||||||
|
short_name: 'Resume',
|
||||||
|
description: 'Resume the last played song',
|
||||||
|
url: '/?action=resume',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icon-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Play Recent Albums',
|
||||||
|
short_name: 'Recent',
|
||||||
|
description: 'Play from recently added albums',
|
||||||
|
url: '/?action=recent',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icon-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Shuffle Favorites',
|
||||||
|
short_name: 'Shuffle',
|
||||||
|
description: 'Shuffle songs from favorite artists',
|
||||||
|
url: '/?action=shuffle-favorites',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icon-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
117
app/page.tsx
117
app/page.tsx
@@ -8,14 +8,19 @@ import { useNavidrome } from './components/NavidromeContext';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Album } from '@/lib/navidrome';
|
import { Album } from '@/lib/navidrome';
|
||||||
import { useNavidromeConfig } from './components/NavidromeConfigContext';
|
import { useNavidromeConfig } from './components/NavidromeConfigContext';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useAudioPlayer } from './components/AudioPlayerContext';
|
||||||
|
|
||||||
type TimeOfDay = 'morning' | 'afternoon' | 'evening';
|
type TimeOfDay = 'morning' | 'afternoon' | 'evening';
|
||||||
export default function MusicPage() {
|
export default function MusicPage() {
|
||||||
const { albums, isLoading, api, isConnected } = useNavidrome();
|
const { albums, isLoading, api, isConnected } = useNavidrome();
|
||||||
|
const { playAlbum, playTrack, shuffle, toggleShuffle, addToQueue } = useAudioPlayer();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [recentAlbums, setRecentAlbums] = useState<Album[]>([]);
|
const [recentAlbums, setRecentAlbums] = useState<Album[]>([]);
|
||||||
const [newestAlbums, setNewestAlbums] = useState<Album[]>([]);
|
const [newestAlbums, setNewestAlbums] = useState<Album[]>([]);
|
||||||
const [favoriteAlbums, setFavoriteAlbums] = useState<Album[]>([]);
|
const [favoriteAlbums, setFavoriteAlbums] = useState<Album[]>([]);
|
||||||
const [favoritesLoading, setFavoritesLoading] = useState(true);
|
const [favoritesLoading, setFavoritesLoading] = useState(true);
|
||||||
|
const [shortcutProcessed, setShortcutProcessed] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (albums.length > 0) {
|
if (albums.length > 0) {
|
||||||
@@ -45,6 +50,114 @@ export default function MusicPage() {
|
|||||||
loadFavoriteAlbums();
|
loadFavoriteAlbums();
|
||||||
}, [api, isConnected]);
|
}, [api, isConnected]);
|
||||||
|
|
||||||
|
// Handle PWA shortcuts
|
||||||
|
useEffect(() => {
|
||||||
|
const action = searchParams.get('action');
|
||||||
|
if (!action || shortcutProcessed || !api || !isConnected) return;
|
||||||
|
|
||||||
|
const handleShortcuts = async () => {
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case 'resume':
|
||||||
|
// Try to resume from localStorage or play a recent track
|
||||||
|
const lastTrack = localStorage.getItem('lastPlayedTrack');
|
||||||
|
if (lastTrack) {
|
||||||
|
const trackData = JSON.parse(lastTrack);
|
||||||
|
await playTrack(trackData);
|
||||||
|
} else if (recentAlbums.length > 0) {
|
||||||
|
// Fallback: play first track from most recent album
|
||||||
|
await playAlbum(recentAlbums[0].id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'recent':
|
||||||
|
if (recentAlbums.length > 0) {
|
||||||
|
// Get the 10 most recent albums and shuffle them
|
||||||
|
const tenRecentAlbums = recentAlbums.slice(0, 10);
|
||||||
|
const shuffledAlbums = [...tenRecentAlbums].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
// Enable shuffle if not already on
|
||||||
|
if (!shuffle) {
|
||||||
|
toggleShuffle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play first album and add remaining albums to queue
|
||||||
|
await playAlbum(shuffledAlbums[0].id);
|
||||||
|
|
||||||
|
// Add remaining albums to queue
|
||||||
|
for (let i = 1; i < shuffledAlbums.length; i++) {
|
||||||
|
try {
|
||||||
|
const albumSongs = await api.getAlbumSongs(shuffledAlbums[i].id);
|
||||||
|
albumSongs.forEach(song => {
|
||||||
|
addToQueue({
|
||||||
|
id: song.id,
|
||||||
|
name: song.title,
|
||||||
|
url: api.getStreamUrl(song.id),
|
||||||
|
artist: song.artist || 'Unknown Artist',
|
||||||
|
artistId: song.artistId || '',
|
||||||
|
album: song.album || 'Unknown Album',
|
||||||
|
albumId: song.parent,
|
||||||
|
duration: song.duration || 0,
|
||||||
|
coverArt: song.coverArt,
|
||||||
|
starred: !!song.starred
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load album tracks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'shuffle-favorites':
|
||||||
|
if (favoriteAlbums.length > 0) {
|
||||||
|
// Shuffle all favorite albums
|
||||||
|
const shuffledFavorites = [...favoriteAlbums].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
// Enable shuffle if not already on
|
||||||
|
if (!shuffle) {
|
||||||
|
toggleShuffle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play first album and add remaining albums to queue
|
||||||
|
await playAlbum(shuffledFavorites[0].id);
|
||||||
|
|
||||||
|
// Add remaining albums to queue
|
||||||
|
for (let i = 1; i < shuffledFavorites.length; i++) {
|
||||||
|
try {
|
||||||
|
const albumSongs = await api.getAlbumSongs(shuffledFavorites[i].id);
|
||||||
|
albumSongs.forEach(song => {
|
||||||
|
addToQueue({
|
||||||
|
id: song.id,
|
||||||
|
name: song.title,
|
||||||
|
url: api.getStreamUrl(song.id),
|
||||||
|
artist: song.artist || 'Unknown Artist',
|
||||||
|
artistId: song.artistId || '',
|
||||||
|
album: song.album || 'Unknown Album',
|
||||||
|
albumId: song.parent,
|
||||||
|
duration: song.duration || 0,
|
||||||
|
coverArt: song.coverArt,
|
||||||
|
starred: !!song.starred
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load album tracks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setShortcutProcessed(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to handle PWA shortcut:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delay to ensure data is loaded
|
||||||
|
const timeout = setTimeout(handleShortcuts, 1000);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [searchParams, api, isConnected, recentAlbums, favoriteAlbums, shortcutProcessed, playAlbum, playTrack, shuffle, toggleShuffle, addToQueue]);
|
||||||
|
|
||||||
// Get greeting and time of day
|
// Get greeting and time of day
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
|
const greeting = hour < 12 ? 'Good morning' : 'Good afternoon';
|
||||||
@@ -107,7 +220,7 @@ export default function MusicPage() {
|
|||||||
<div className="flex space-x-4 pb-4">
|
<div className="flex space-x-4 pb-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
// Loading skeletons
|
// Loading skeletons
|
||||||
Array.from({ length: 6 }).map((_, i) => (
|
Array.from({ length: 10 }).map((_, i) => (
|
||||||
<div key={i} className="w-[220px] h-[320px] bg-muted animate-pulse rounded-md flex-shrink-0" />
|
<div key={i} className="w-[220px] h-[320px] bg-muted animate-pulse rounded-md flex-shrink-0" />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@@ -144,7 +257,7 @@ export default function MusicPage() {
|
|||||||
<div className="flex space-x-4 pb-4">
|
<div className="flex space-x-4 pb-4">
|
||||||
{favoritesLoading ? (
|
{favoritesLoading ? (
|
||||||
// Loading skeletons
|
// Loading skeletons
|
||||||
Array.from({ length: 6 }).map((_, i) => (
|
Array.from({ length: 10 }).map((_, i) => (
|
||||||
<div key={i} className="w-[220px] h-[320px] bg-muted animate-pulse rounded-md flex-shrink-0" />
|
<div key={i} className="w-[220px] h-[320px] bg-muted animate-pulse rounded-md flex-shrink-0" />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user