Finish Favorites UI and jumping
This commit is contained in:
1
backend/favorites.json
Normal file
1
backend/favorites.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
112
backend/src/FavoritesStore.ts
Normal file
112
backend/src/FavoritesStore.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { PlaylistItem } from './types';
|
||||||
|
import { getLinkPreview } from 'link-preview-js';
|
||||||
|
|
||||||
|
export class FavoritesStore {
|
||||||
|
onFavoritesChanged: (favorites: PlaylistItem[]) => void = () => {};
|
||||||
|
|
||||||
|
private storePath: string;
|
||||||
|
private favorites: PlaylistItem[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.storePath = this.determineStorePath();
|
||||||
|
this.loadFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
private determineStorePath(): string {
|
||||||
|
const storeFilename = 'favorites.json';
|
||||||
|
var storePath = path.join(os.tmpdir(), 'queuecube');
|
||||||
|
|
||||||
|
// Check for explicitly set path
|
||||||
|
if (process.env.STORE_PATH) {
|
||||||
|
storePath = path.resolve(process.env.STORE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In production (in a container), use /app/data
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
storePath = path.resolve('/app/data');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdir(storePath, { recursive: true }).catch(err => {
|
||||||
|
console.error('Failed to create intermediate directory:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return path.join(storePath, storeFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFavorites() {
|
||||||
|
try {
|
||||||
|
// Ensure parent directory exists
|
||||||
|
await fs.mkdir(path.dirname(this.storePath), { recursive: true });
|
||||||
|
|
||||||
|
const data = await fs.readFile(this.storePath, 'utf-8');
|
||||||
|
this.favorites = JSON.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
// If file doesn't exist or is invalid, start with empty array
|
||||||
|
this.favorites = [];
|
||||||
|
await this.saveFavorites();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveFavorites() {
|
||||||
|
await fs.writeFile(this.storePath, JSON.stringify(this.favorites, null, 2));
|
||||||
|
this.onFavoritesChanged(this.favorites);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFavorites(): Promise<PlaylistItem[]> {
|
||||||
|
return this.favorites;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFavorite(item: PlaylistItem, fetchMetadata: boolean = true): Promise<void> {
|
||||||
|
// Check if the item already exists by filename
|
||||||
|
const exists = this.favorites.some(f => f.filename === item.filename);
|
||||||
|
if (!exists) {
|
||||||
|
this.favorites.push({
|
||||||
|
...item,
|
||||||
|
id: this.favorites.length // Generate new ID
|
||||||
|
});
|
||||||
|
await this.saveFavorites();
|
||||||
|
} else {
|
||||||
|
// Otherwise, update the item with the new metadata
|
||||||
|
const index = this.favorites.findIndex(f => f.filename === item.filename);
|
||||||
|
this.favorites[index] = {
|
||||||
|
...this.favorites[index],
|
||||||
|
...item,
|
||||||
|
};
|
||||||
|
await this.saveFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the item is missing metadata, fetch it
|
||||||
|
if (fetchMetadata && !item.metadata) {
|
||||||
|
await this.fetchMetadata(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchMetadata(item: PlaylistItem): Promise<void> {
|
||||||
|
console.log("Fetching metadata for " + item.filename);
|
||||||
|
const metadata = await getLinkPreview(item.filename);
|
||||||
|
|
||||||
|
item.metadata = {
|
||||||
|
title: (metadata as any)?.title,
|
||||||
|
description: (metadata as any)?.description,
|
||||||
|
siteName: (metadata as any)?.siteName,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Metadata fetched for " + item.filename);
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
await this.addFavorite(item, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFavorite(id: number): Promise<void> {
|
||||||
|
this.favorites = this.favorites.filter(f => f.id !== id);
|
||||||
|
await this.saveFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearFavorites(): Promise<void> {
|
||||||
|
this.favorites = [];
|
||||||
|
await this.saveFavorites();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,31 +2,18 @@ import { ChildProcess, spawn } from "child_process";
|
|||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { getLinkPreview } from "link-preview-js";
|
import { getLinkPreview } from "link-preview-js";
|
||||||
|
import { PlaylistItem, LinkMetadata } from './types';
|
||||||
|
import { FavoritesStore } from "./FavoritesStore";
|
||||||
interface PendingCommand {
|
interface PendingCommand {
|
||||||
resolve: (value: any) => void;
|
resolve: (value: any) => void;
|
||||||
reject: (reason: any) => void;
|
reject: (reason: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LinkMetadata {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
siteName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PlaylistItem {
|
|
||||||
id: number;
|
|
||||||
filename: string;
|
|
||||||
title?: string;
|
|
||||||
playing?: boolean;
|
|
||||||
current?: boolean;
|
|
||||||
metadata?: LinkMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MediaPlayer {
|
export class MediaPlayer {
|
||||||
private playerProcess: ChildProcess;
|
private playerProcess: ChildProcess;
|
||||||
private socket: Socket;
|
private socket: Socket;
|
||||||
private eventSubscribers: WebSocket[] = [];
|
private eventSubscribers: WebSocket[] = [];
|
||||||
|
private favoritesStore: FavoritesStore;
|
||||||
|
|
||||||
private pendingCommands: Map<number, PendingCommand> = new Map();
|
private pendingCommands: Map<number, PendingCommand> = new Map();
|
||||||
private requestId: number = 1;
|
private requestId: number = 1;
|
||||||
@@ -53,6 +40,11 @@ export class MediaPlayer {
|
|||||||
this.connectToSocket(socketPath);
|
this.connectToSocket(socketPath);
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.favoritesStore = new FavoritesStore();
|
||||||
|
this.favoritesStore.onFavoritesChanged = (favorites) => {
|
||||||
|
this.handleEvent("favorites_update", { favorites });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPlaylist(): Promise<PlaylistItem[]> {
|
public async getPlaylist(): Promise<PlaylistItem[]> {
|
||||||
@@ -123,14 +115,11 @@ export class MediaPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async append(url: string) {
|
public async append(url: string) {
|
||||||
const result = await this.modify(() => this.writeCommand("loadfile", [url, "append-play"]));
|
await this.loadFile(url, "append-play");
|
||||||
|
}
|
||||||
|
|
||||||
// Asynchronously fetch the metadata for this after we update the playlist
|
public async replace(url: string) {
|
||||||
this.fetchMetadataAndNotify(url).catch(error => {
|
await this.loadFile(url, "replace");
|
||||||
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async play() {
|
public async play() {
|
||||||
@@ -169,6 +158,30 @@ export class MediaPlayer {
|
|||||||
this.eventSubscribers = this.eventSubscribers.filter(subscriber => subscriber !== ws);
|
this.eventSubscribers = this.eventSubscribers.filter(subscriber => subscriber !== ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getFavorites(): Promise<PlaylistItem[]> {
|
||||||
|
return this.favoritesStore.getFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addFavorite(item: PlaylistItem) {
|
||||||
|
return this.favoritesStore.addFavorite(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeFavorite(id: number) {
|
||||||
|
return this.favoritesStore.removeFavorite(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clearFavorites() {
|
||||||
|
return this.favoritesStore.clearFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFile(url: string, mode: string) {
|
||||||
|
this.modify(() => this.writeCommand("loadfile", [url, mode]));
|
||||||
|
|
||||||
|
this.fetchMetadataAndNotify(url).catch(error => {
|
||||||
|
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async modify<T>(func: () => Promise<T>): Promise<T> {
|
private async modify<T>(func: () => Promise<T>): Promise<T> {
|
||||||
return func()
|
return func()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@@ -205,6 +218,7 @@ export class MediaPlayer {
|
|||||||
|
|
||||||
private async fetchMetadataAndNotify(url: string) {
|
private async fetchMetadataAndNotify(url: string) {
|
||||||
try {
|
try {
|
||||||
|
console.log("Fetching metadata for " + url);
|
||||||
const metadata = await getLinkPreview(url);
|
const metadata = await getLinkPreview(url);
|
||||||
this.metadata.set(url, {
|
this.metadata.set(url, {
|
||||||
title: (metadata as any)?.title,
|
title: (metadata as any)?.title,
|
||||||
@@ -212,6 +226,9 @@ export class MediaPlayer {
|
|||||||
siteName: (metadata as any)?.siteName,
|
siteName: (metadata as any)?.siteName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("Metadata fetched for " + url);
|
||||||
|
console.log(this.metadata.get(url));
|
||||||
|
|
||||||
// Notify clients that metadata has been updated
|
// Notify clients that metadata has been updated
|
||||||
this.handleEvent("metadata_update", {
|
this.handleEvent("metadata_update", {
|
||||||
url,
|
url,
|
||||||
@@ -228,7 +245,7 @@ export class MediaPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleEvent(event: string, data: any) {
|
private handleEvent(event: string, data: any) {
|
||||||
console.log("MPV Event [" + event + "]: " + JSON.stringify(data, null, 2));
|
console.log("Event [" + event + "]: " + JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
// Notify all subscribers
|
// Notify all subscribers
|
||||||
this.eventSubscribers.forEach(subscriber => {
|
this.eventSubscribers.forEach(subscriber => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import expressWs from "express-ws";
|
import expressWs from "express-ws";
|
||||||
import { MediaPlayer } from "./MediaPlayer";
|
import { MediaPlayer } from "./MediaPlayer";
|
||||||
import { searchInvidious, getInvidiousThumbnailURL, fetchThumbnail } from "./InvidiousAPI";
|
import { searchInvidious, fetchThumbnail } from "./InvidiousAPI";
|
||||||
import fetch from "node-fetch";
|
import { PlaylistItem } from './types';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -38,6 +38,12 @@ apiRouter.delete("/playlist/:index", withErrorHandling(async (req, res) => {
|
|||||||
res.send(JSON.stringify({ success: true }));
|
res.send(JSON.stringify({ success: true }));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
apiRouter.post("/playlist/replace", withErrorHandling(async (req, res) => {
|
||||||
|
const { url } = req.body as { url: string };
|
||||||
|
await mediaPlayer.replace(url);
|
||||||
|
res.send(JSON.stringify({ success: true }));
|
||||||
|
}));
|
||||||
|
|
||||||
apiRouter.post("/play", withErrorHandling(async (req, res) => {
|
apiRouter.post("/play", withErrorHandling(async (req, res) => {
|
||||||
await mediaPlayer.play();
|
await mediaPlayer.play();
|
||||||
res.send(JSON.stringify({ success: true }));
|
res.send(JSON.stringify({ success: true }));
|
||||||
@@ -128,6 +134,28 @@ apiRouter.get("/thumbnail", withErrorHandling(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
apiRouter.get("/favorites", withErrorHandling(async (req, res) => {
|
||||||
|
const favorites = await mediaPlayer.getFavorites();
|
||||||
|
res.send(JSON.stringify(favorites));
|
||||||
|
}));
|
||||||
|
|
||||||
|
apiRouter.post("/favorites", withErrorHandling(async (req, res) => {
|
||||||
|
const item = req.body as PlaylistItem;
|
||||||
|
await mediaPlayer.addFavorite(item);
|
||||||
|
res.send(JSON.stringify({ success: true }));
|
||||||
|
}));
|
||||||
|
|
||||||
|
apiRouter.delete("/favorites/:id", withErrorHandling(async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
await mediaPlayer.removeFavorite(parseInt(id));
|
||||||
|
res.send(JSON.stringify({ success: true }));
|
||||||
|
}));
|
||||||
|
|
||||||
|
apiRouter.delete("/favorites", withErrorHandling(async (req, res) => {
|
||||||
|
await mediaPlayer.clearFavorites();
|
||||||
|
res.send(JSON.stringify({ success: true }));
|
||||||
|
}));
|
||||||
|
|
||||||
// Serve static files for React app (after building)
|
// Serve static files for React app (after building)
|
||||||
app.use(express.static("dist/frontend"));
|
app.use(express.static("dist/frontend"));
|
||||||
|
|
||||||
|
|||||||
14
backend/src/types.ts
Normal file
14
backend/src/types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface LinkMetadata {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
siteName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlaylistItem {
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
title?: string;
|
||||||
|
playing?: boolean;
|
||||||
|
current?: boolean;
|
||||||
|
metadata?: LinkMetadata;
|
||||||
|
}
|
||||||
@@ -62,6 +62,16 @@ export const API = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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> {
|
async removeFromPlaylist(index: number): Promise<void> {
|
||||||
await fetch(`/api/playlist/${index}`, {
|
await fetch(`/api/playlist/${index}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -116,5 +126,38 @@ export const API = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return ws;
|
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' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,18 +18,81 @@ const EmptyContent: React.FC<{ label: string}> = ({label}) => (
|
|||||||
</div>
|
</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 App: React.FC = () => {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [nowPlayingSong, setNowPlayingSong] = useState<string | null>(null);
|
const [nowPlayingSong, setNowPlayingSong] = useState<string | null>(null);
|
||||||
const [nowPlayingFileName, setNowPlayingFileName] = useState<string | null>(null);
|
const [nowPlayingFileName, setNowPlayingFileName] = useState<string | null>(null);
|
||||||
const [volume, setVolume] = useState(100);
|
const [volume, setVolume] = useState(100);
|
||||||
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
|
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 [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.Playlist);
|
||||||
|
|
||||||
const fetchPlaylist = useCallback(async () => {
|
const fetchPlaylist = useCallback(async () => {
|
||||||
const playlist = await API.getPlaylist();
|
const playlist = await API.getPlaylist();
|
||||||
setSongs(playlist);
|
setPlaylist(playlist);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchFavorites = useCallback(async () => {
|
||||||
|
const favorites = await API.getFavorites();
|
||||||
|
setFavorites(favorites);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchNowPlaying = useCallback(async () => {
|
const fetchNowPlaying = useCallback(async () => {
|
||||||
@@ -46,8 +109,13 @@ const App: React.FC = () => {
|
|||||||
const handleAddURL = async (url: string) => {
|
const handleAddURL = async (url: string) => {
|
||||||
const urlToAdd = url.trim();
|
const urlToAdd = url.trim();
|
||||||
if (urlToAdd) {
|
if (urlToAdd) {
|
||||||
|
if (selectedTab === Tabs.Favorites) {
|
||||||
|
await API.addToFavorites(urlToAdd);
|
||||||
|
fetchFavorites();
|
||||||
|
} else {
|
||||||
await API.addToPlaylist(urlToAdd);
|
await API.addToPlaylist(urlToAdd);
|
||||||
fetchPlaylist();
|
fetchPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
await API.play();
|
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 () => {
|
const togglePlayPause = async () => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await API.pause();
|
await API.pause();
|
||||||
@@ -109,8 +157,11 @@ const App: React.FC = () => {
|
|||||||
fetchPlaylist();
|
fetchPlaylist();
|
||||||
fetchNowPlaying();
|
fetchNowPlaying();
|
||||||
break;
|
break;
|
||||||
|
case 'favorites_update':
|
||||||
|
fetchFavorites();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, [fetchPlaylist, fetchNowPlaying]);
|
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
|
||||||
|
|
||||||
// Use the hook
|
// Use the hook
|
||||||
useEventWebSocket(handleWebSocketEvent);
|
useEventWebSocket(handleWebSocketEvent);
|
||||||
@@ -130,24 +181,18 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [fetchPlaylist, fetchNowPlaying]);
|
}, [fetchPlaylist, fetchNowPlaying]);
|
||||||
|
|
||||||
|
const refreshContent = () => {
|
||||||
|
fetchPlaylist();
|
||||||
|
fetchNowPlaying();
|
||||||
|
fetchFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
// Initial data fetch
|
// Initial data fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPlaylist();
|
fetchPlaylist();
|
||||||
fetchNowPlaying();
|
fetchNowPlaying();
|
||||||
}, [fetchPlaylist, fetchNowPlaying]);
|
fetchFavorites();
|
||||||
|
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
|
||||||
const playlistContent = (
|
|
||||||
songs.length > 0 ? (
|
|
||||||
<SongTable
|
|
||||||
songs={songs}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onSkipTo={handleSkipTo}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EmptyContent label="Playlist is empty" />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
|
<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}>
|
<TabView selectedTab={selectedTab} onTabChange={setSelectedTab}>
|
||||||
<Tab label="Playlist" identifier={Tabs.Playlist} icon={<FaMusic />}>
|
<Tab label="Playlist" identifier={Tabs.Playlist} icon={<FaMusic />}>
|
||||||
{playlistContent}
|
<PlaylistContent
|
||||||
|
songs={playlist}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onNeedsRefresh={refreshContent}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab label="Favorites" identifier={Tabs.Favorites} icon={<FaHeart />}>
|
<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>
|
</Tab>
|
||||||
</TabView>
|
</TabView>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user