Finish Favorites UI and jumping

This commit is contained in:
2025-02-23 13:46:31 -08:00
parent d6a375fff3
commit fe05a27b51
7 changed files with 336 additions and 68 deletions

View File

@@ -61,6 +61,16 @@ export const API = {
body: JSON.stringify({ url }),
});
},
async replaceCurrentFile(url: string): Promise<void> {
await fetch('/api/playlist/replace', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url }),
});
},
async removeFromPlaylist(index: number): Promise<void> {
await fetch(`/api/playlist/${index}`, {
@@ -116,5 +126,38 @@ export const API = {
};
return ws;
},
async getFavorites(): Promise<PlaylistItem[]> {
const response = await fetch('/api/favorites');
return response.json();
},
async addToFavorites(url: string): Promise<void> {
// Maybe a little weird to make an empty PlaylistItem here, but we do want to support adding
// known PlaylistItems to favorites in the future.
const playlistItem: PlaylistItem = {
filename: url,
title: null,
id: 0,
playing: null,
metadata: undefined,
};
await fetch('/api/favorites', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(playlistItem),
});
},
async removeFromFavorites(id: number): Promise<void> {
await fetch(`/api/favorites/${id}`, { method: 'DELETE' });
},
async clearFavorites(): Promise<void> {
await fetch('/api/favorites', { method: 'DELETE' });
}
};

View File

@@ -18,18 +18,81 @@ const EmptyContent: React.FC<{ label: string}> = ({label}) => (
</div>
);
interface SonglistContentProps {
songs: PlaylistItem[];
isPlaying: boolean;
onNeedsRefresh: () => void;
}
const PlaylistContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, onNeedsRefresh }) => {
const handleDelete = (index: number) => {
API.removeFromPlaylist(index);
onNeedsRefresh();
};
const handleSkipTo = (index: number) => {
API.skipTo(index);
onNeedsRefresh();
};
return (
songs.length > 0 ? (
<SongTable
songs={songs}
isPlaying={isPlaying}
onDelete={handleDelete}
onSkipTo={handleSkipTo}
/>
) : (
<EmptyContent label="Playlist is empty" />
)
);
};
const FavoritesContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, onNeedsRefresh }) => {
const handleDelete = (index: number) => {
API.removeFromFavorites(index);
onNeedsRefresh();
};
const handleSkipTo = (index: number) => {
API.replaceCurrentFile(songs[index].filename);
API.play();
onNeedsRefresh();
};
return (
songs.length > 0 ? (
<SongTable
songs={songs}
isPlaying={isPlaying}
onDelete={handleDelete}
onSkipTo={handleSkipTo}
/>
) : (
<EmptyContent label="Favorites are empty" />
)
);
};
const App: React.FC = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [nowPlayingSong, setNowPlayingSong] = useState<string | null>(null);
const [nowPlayingFileName, setNowPlayingFileName] = useState<string | null>(null);
const [volume, setVolume] = useState(100);
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
const [songs, setSongs] = useState<PlaylistItem[]>([]);
const [playlist, setPlaylist] = useState<PlaylistItem[]>([]);
const [favorites, setFavorites] = useState<PlaylistItem[]>([]);
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.Playlist);
const fetchPlaylist = useCallback(async () => {
const playlist = await API.getPlaylist();
setSongs(playlist);
setPlaylist(playlist);
}, []);
const fetchFavorites = useCallback(async () => {
const favorites = await API.getFavorites();
setFavorites(favorites);
}, []);
const fetchNowPlaying = useCallback(async () => {
@@ -46,8 +109,13 @@ const App: React.FC = () => {
const handleAddURL = async (url: string) => {
const urlToAdd = url.trim();
if (urlToAdd) {
await API.addToPlaylist(urlToAdd);
fetchPlaylist();
if (selectedTab === Tabs.Favorites) {
await API.addToFavorites(urlToAdd);
fetchFavorites();
} else {
await API.addToPlaylist(urlToAdd);
fetchPlaylist();
}
if (!isPlaying) {
await API.play();
@@ -55,26 +123,6 @@ const App: React.FC = () => {
}
};
const handleDelete = (index: number) => {
setSongs(songs.filter((_, i) => i !== index));
API.removeFromPlaylist(index);
fetchPlaylist();
fetchNowPlaying();
};
const handleSkipTo = async (index: number) => {
const song = songs[index];
if (song.playing) {
togglePlayPause();
} else {
await API.skipTo(index);
await API.play();
}
fetchNowPlaying();
fetchPlaylist();
};
const togglePlayPause = async () => {
if (isPlaying) {
await API.pause();
@@ -109,8 +157,11 @@ const App: React.FC = () => {
fetchPlaylist();
fetchNowPlaying();
break;
case 'favorites_update':
fetchFavorites();
break;
}
}, [fetchPlaylist, fetchNowPlaying]);
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
// Use the hook
useEventWebSocket(handleWebSocketEvent);
@@ -130,24 +181,18 @@ const App: React.FC = () => {
};
}, [fetchPlaylist, fetchNowPlaying]);
const refreshContent = () => {
fetchPlaylist();
fetchNowPlaying();
fetchFavorites();
}
// Initial data fetch
useEffect(() => {
fetchPlaylist();
fetchNowPlaying();
}, [fetchPlaylist, fetchNowPlaying]);
const playlistContent = (
songs.length > 0 ? (
<SongTable
songs={songs}
isPlaying={isPlaying}
onDelete={handleDelete}
onSkipTo={handleSkipTo}
/>
) : (
<EmptyContent label="Playlist is empty" />
)
);
fetchFavorites();
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
return (
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
@@ -168,10 +213,18 @@ const App: React.FC = () => {
<TabView selectedTab={selectedTab} onTabChange={setSelectedTab}>
<Tab label="Playlist" identifier={Tabs.Playlist} icon={<FaMusic />}>
{playlistContent}
<PlaylistContent
songs={playlist}
isPlaying={isPlaying}
onNeedsRefresh={refreshContent}
/>
</Tab>
<Tab label="Favorites" identifier={Tabs.Favorites} icon={<FaHeart />}>
<EmptyContent label="Favorites are not implemented yet" />
<FavoritesContent
songs={favorites.map(f => ({ ...f, playing: f.filename === nowPlayingFileName }))}
isPlaying={isPlaying}
onNeedsRefresh={refreshContent}
/>
</Tab>
</TabView>