add favorites, add tinder playlists
This commit is contained in:
@@ -21,6 +21,7 @@ import DiscoverPage from "./pages/Discover/DiscoverPage";
|
||||
import HomePage from "./pages/Home/HomePage";
|
||||
import LibraryPage from "./pages/Library/LibraryPage";
|
||||
import PlaylistsPage from "./pages/Playlists/PlaylistsPage";
|
||||
import PlaylistDetailPage from "./pages/Playlists/PlaylistDetailPage";
|
||||
import SettingsPage from "./pages/Settings/SettingsPage";
|
||||
import SettingsDetailPage from "./pages/Settings/SettingsDetailPage";
|
||||
|
||||
@@ -36,6 +37,11 @@ export default function App() {
|
||||
<Route exact path="/home" component={HomePage} />
|
||||
<Route exact path="/library" component={LibraryPage} />
|
||||
<Route exact path="/playlists" component={PlaylistsPage} />
|
||||
<Route
|
||||
exact
|
||||
path="/playlists/:playlistId"
|
||||
component={PlaylistDetailPage}
|
||||
/>
|
||||
<Route exact path="/discover" component={DiscoverPage} />
|
||||
<Route exact path="/settings" component={SettingsPage} />
|
||||
<Route
|
||||
|
||||
@@ -15,29 +15,12 @@ import {
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from "@ionic/react";
|
||||
import { swapVerticalOutline } from "ionicons/icons";
|
||||
import { heart, heartOutline, swapVerticalOutline } from "ionicons/icons";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { db } from "../../services/Database";
|
||||
import { db, type Game } from "../../services/Database";
|
||||
|
||||
import "./LibraryPage.css";
|
||||
|
||||
type SteamGame = {
|
||||
id: string;
|
||||
title: string;
|
||||
platform?: string;
|
||||
lastPlayed?: string | null;
|
||||
playtimeHours?: number;
|
||||
url?: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
type SourceConfig = {
|
||||
name: string;
|
||||
label: string;
|
||||
platform: string;
|
||||
file: string;
|
||||
};
|
||||
|
||||
const formatDate = (value?: string | null) => {
|
||||
if (!value) return "-";
|
||||
return new Date(value).toLocaleDateString("de");
|
||||
@@ -46,8 +29,8 @@ const formatDate = (value?: string | null) => {
|
||||
const normalizeTitle = (title: string) =>
|
||||
title.toLowerCase().replace(/[:\-]/g, " ").replace(/\s+/g, " ").trim();
|
||||
|
||||
const mergeGames = (allGames: SteamGame[]) => {
|
||||
const map = new Map<string, SteamGame>();
|
||||
const mergeGames = (allGames: Game[]) => {
|
||||
const map = new Map<string, Game>();
|
||||
|
||||
allGames.forEach((game) => {
|
||||
const key = normalizeTitle(game.title);
|
||||
@@ -72,7 +55,8 @@ const mergeGames = (allGames: SteamGame[]) => {
|
||||
};
|
||||
|
||||
export default function LibraryPage() {
|
||||
const [games, setGames] = useState<SteamGame[]>([]);
|
||||
const [games, setGames] = useState<Game[]>([]);
|
||||
const [favoriteIds, setFavoriteIds] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
@@ -92,11 +76,14 @@ export default function LibraryPage() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Lade Spiele aus IndexedDB
|
||||
const dbGames = await db.getGames();
|
||||
const [dbGames, favPlaylist] = await Promise.all([
|
||||
db.getGames(),
|
||||
db.getPlaylist("favorites"),
|
||||
]);
|
||||
|
||||
if (active) {
|
||||
setGames(dbGames);
|
||||
setFavoriteIds(new Set(favPlaylist?.gameIds ?? []));
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -118,7 +105,7 @@ export default function LibraryPage() {
|
||||
|
||||
const totalPlaytime = useMemo(() => {
|
||||
return games.reduce(
|
||||
(sum: number, game: SteamGame) => sum + (game.playtimeHours ?? 0),
|
||||
(sum: number, game: Game) => sum + (game.playtimeHours ?? 0),
|
||||
0,
|
||||
);
|
||||
}, [games]);
|
||||
@@ -180,6 +167,20 @@ export default function LibraryPage() {
|
||||
return filteredAndSortedGames.slice(0, displayCount);
|
||||
}, [filteredAndSortedGames, displayCount]);
|
||||
|
||||
const handleToggleFavorite = async (gameId: string) => {
|
||||
if (favoriteIds.has(gameId)) {
|
||||
await db.removeGameFromPlaylist("favorites", gameId);
|
||||
setFavoriteIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(gameId);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
await db.addGameToPlaylist("favorites", gameId);
|
||||
setFavoriteIds((prev) => new Set(prev).add(gameId));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader translucent>
|
||||
@@ -277,6 +278,17 @@ export default function LibraryPage() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<IonIcon
|
||||
icon={favoriteIds.has(game.id) ? heart : heartOutline}
|
||||
slot="start"
|
||||
color={favoriteIds.has(game.id) ? "danger" : "medium"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleToggleFavorite(game.id);
|
||||
}}
|
||||
style={{ cursor: "pointer", fontSize: "22px" }}
|
||||
/>
|
||||
<IonLabel>
|
||||
<h2>{game.title}</h2>
|
||||
<p>Zuletzt gespielt: {formatDate(game.lastPlayed)}</p>
|
||||
|
||||
29
src/pages/Playlists/PlaylistDetailPage.css
Normal file
29
src/pages/Playlists/PlaylistDetailPage.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.detail-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-name-edit {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.detail-name-edit .name-input {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-search {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
cursor: pointer;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
281
src/pages/Playlists/PlaylistDetailPage.tsx
Normal file
281
src/pages/Playlists/PlaylistDetailPage.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonBadge,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonInput,
|
||||
IonItem,
|
||||
IonItemOption,
|
||||
IonItemOptions,
|
||||
IonItemSliding,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonPage,
|
||||
IonSearchbar,
|
||||
IonSpinner,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from "@ionic/react";
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
addCircleOutline,
|
||||
heart,
|
||||
heartOutline,
|
||||
removeCircleOutline,
|
||||
} from "ionicons/icons";
|
||||
import { db, type Playlist, type Game } from "../../services/Database";
|
||||
|
||||
import "./PlaylistDetailPage.css";
|
||||
|
||||
export default function PlaylistDetailPage() {
|
||||
const { playlistId } = useParams<{ playlistId: string }>();
|
||||
const [playlist, setPlaylist] = useState<Playlist | null>(null);
|
||||
const [games, setGames] = useState<Game[]>([]);
|
||||
const [favoriteIds, setFavoriteIds] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [editName, setEditName] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [pl, allGames, favPlaylist] = await Promise.all([
|
||||
db.getPlaylist(playlistId),
|
||||
db.getGames(),
|
||||
db.getPlaylist("favorites"),
|
||||
]);
|
||||
setPlaylist(pl);
|
||||
setGames(allGames);
|
||||
setFavoriteIds(new Set(favPlaylist?.gameIds ?? []));
|
||||
if (pl) setEditName(pl.name);
|
||||
setLoading(false);
|
||||
}, [playlistId]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const playlistGames = useMemo(() => {
|
||||
if (!playlist) return [];
|
||||
return playlist.gameIds
|
||||
.map((id) => games.find((g) => g.id === id))
|
||||
.filter(Boolean) as Game[];
|
||||
}, [playlist, games]);
|
||||
|
||||
const searchResults = useMemo(() => {
|
||||
if (!searchText.trim() || !playlist) return [];
|
||||
const term = searchText.toLowerCase();
|
||||
const inPlaylist = new Set(playlist.gameIds);
|
||||
return games
|
||||
.filter(
|
||||
(g) => g.title.toLowerCase().includes(term) && !inPlaylist.has(g.id),
|
||||
)
|
||||
.slice(0, 20);
|
||||
}, [searchText, games, playlist]);
|
||||
|
||||
const handleAddGame = async (gameId: string) => {
|
||||
await db.addGameToPlaylist(playlistId, gameId);
|
||||
const updated = await db.getPlaylist(playlistId);
|
||||
setPlaylist(updated);
|
||||
};
|
||||
|
||||
const handleRemoveGame = async (gameId: string) => {
|
||||
await db.removeGameFromPlaylist(playlistId, gameId);
|
||||
const updated = await db.getPlaylist(playlistId);
|
||||
setPlaylist(updated);
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async (gameId: string) => {
|
||||
if (favoriteIds.has(gameId)) {
|
||||
await db.removeGameFromPlaylist("favorites", gameId);
|
||||
setFavoriteIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(gameId);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
await db.addGameToPlaylist("favorites", gameId);
|
||||
setFavoriteIds((prev) => new Set(prev).add(gameId));
|
||||
}
|
||||
// If we're viewing the favorites playlist, reload it
|
||||
if (playlistId === "favorites") {
|
||||
const updated = await db.getPlaylist("favorites");
|
||||
setPlaylist(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameBlur = async () => {
|
||||
if (!playlist || playlist.isStatic) return;
|
||||
const trimmed = editName.trim() || "Neue Liste";
|
||||
if (trimmed !== playlist.name) {
|
||||
await db.updatePlaylist(playlistId, { name: trimmed });
|
||||
setPlaylist((prev) => (prev ? { ...prev, name: trimmed } : prev));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader translucent>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref="/playlists" />
|
||||
</IonButtons>
|
||||
<IonTitle>Playlist</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<div className="detail-loading">
|
||||
<IonSpinner name="crescent" />
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (!playlist) {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader translucent>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref="/playlists" />
|
||||
</IonButtons>
|
||||
<IonTitle>Nicht gefunden</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<p className="ion-padding ion-text-center">
|
||||
Playlist nicht gefunden.
|
||||
</p>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader translucent>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref="/playlists" />
|
||||
</IonButtons>
|
||||
<IonTitle>{playlist.name}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">{playlist.name}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
{/* Editable name for custom playlists */}
|
||||
{!playlist.isStatic && (
|
||||
<div className="detail-name-edit">
|
||||
<IonInput
|
||||
value={editName}
|
||||
placeholder="Playlist-Name"
|
||||
onIonInput={(e) => setEditName(e.detail.value ?? "")}
|
||||
onIonBlur={handleNameBlur}
|
||||
className="name-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search to add games */}
|
||||
<div className="detail-search">
|
||||
<IonSearchbar
|
||||
placeholder="Spiel hinzufügen..."
|
||||
value={searchText}
|
||||
onIonInput={(e) => setSearchText(e.detail.value ?? "")}
|
||||
debounce={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<IonList inset>
|
||||
<IonListHeader>
|
||||
<IonLabel>Suchergebnisse</IonLabel>
|
||||
</IonListHeader>
|
||||
{searchResults.map((game) => (
|
||||
<IonItem key={game.id}>
|
||||
<IonLabel>
|
||||
<h2>{game.title}</h2>
|
||||
{game.source && (
|
||||
<p>
|
||||
<IonBadge color="medium" className="source-badge">
|
||||
{game.source}
|
||||
</IonBadge>
|
||||
</p>
|
||||
)}
|
||||
</IonLabel>
|
||||
<IonIcon
|
||||
icon={addCircleOutline}
|
||||
slot="end"
|
||||
color="primary"
|
||||
onClick={() => handleAddGame(game.id)}
|
||||
className="action-icon"
|
||||
/>
|
||||
</IonItem>
|
||||
))}
|
||||
</IonList>
|
||||
)}
|
||||
|
||||
{/* Playlist games */}
|
||||
<IonList inset>
|
||||
<IonListHeader>
|
||||
<IonLabel>
|
||||
{playlist.gameIds.length}{" "}
|
||||
{playlist.gameIds.length === 1 ? "Spiel" : "Spiele"}
|
||||
</IonLabel>
|
||||
</IonListHeader>
|
||||
{playlistGames.length === 0 ? (
|
||||
<IonItem>
|
||||
<IonLabel color="medium" className="ion-text-center">
|
||||
Keine Spiele in dieser Playlist
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
) : (
|
||||
playlistGames.map((game) => (
|
||||
<IonItemSliding key={game.id}>
|
||||
<IonItem>
|
||||
<IonIcon
|
||||
icon={favoriteIds.has(game.id) ? heart : heartOutline}
|
||||
slot="start"
|
||||
color={favoriteIds.has(game.id) ? "danger" : "medium"}
|
||||
onClick={() => handleToggleFavorite(game.id)}
|
||||
className="action-icon"
|
||||
/>
|
||||
<IonLabel>
|
||||
<h2>{game.title}</h2>
|
||||
{game.source && (
|
||||
<p>
|
||||
<IonBadge color="medium" className="source-badge">
|
||||
{game.source}
|
||||
</IonBadge>
|
||||
</p>
|
||||
)}
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
<IonItemOptions side="end">
|
||||
<IonItemOption
|
||||
color="danger"
|
||||
onClick={() => handleRemoveGame(game.id)}
|
||||
>
|
||||
<IonIcon
|
||||
slot="icon-only"
|
||||
icon={removeCircleOutline}
|
||||
/>
|
||||
</IonItemOption>
|
||||
</IonItemOptions>
|
||||
</IonItemSliding>
|
||||
))
|
||||
)}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,11 @@
|
||||
.playlists-content {
|
||||
--padding-top: 16px;
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
}
|
||||
|
||||
.playlists-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.playlists-container {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.playlists-empty {
|
||||
color: #8e8e93;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
ion-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
.chevron-icon {
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@@ -1,65 +1,95 @@
|
||||
import {
|
||||
IonBadge,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonItemOption,
|
||||
IonItemOptions,
|
||||
IonItemSliding,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonPage,
|
||||
IonSpinner,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonCard,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
IonCardContent,
|
||||
IonBadge,
|
||||
IonIcon,
|
||||
IonSpinner,
|
||||
} from "@ionic/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { gameControllerOutline, closeCircleOutline } from "ionicons/icons";
|
||||
import { db, type Playlist, type Game } from "../../services/Database";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
addOutline,
|
||||
chevronForward,
|
||||
heartOutline,
|
||||
gameControllerOutline,
|
||||
thumbsDownOutline,
|
||||
listOutline,
|
||||
trashOutline,
|
||||
} from "ionicons/icons";
|
||||
import { db, type Playlist } from "../../services/Database";
|
||||
|
||||
import "./PlaylistsPage.css";
|
||||
|
||||
const STATIC_ORDER = ["favorites", "want-to-play", "not-interesting"];
|
||||
const STATIC_ICONS: Record<string, string> = {
|
||||
favorites: heartOutline,
|
||||
"want-to-play": gameControllerOutline,
|
||||
"not-interesting": thumbsDownOutline,
|
||||
};
|
||||
|
||||
export default function PlaylistsPage() {
|
||||
const [playlists, setPlaylists] = useState<Playlist[]>([]);
|
||||
const [games, setGames] = useState<Game[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [dbPlaylists, dbGames] = await Promise.all([
|
||||
db.getPlaylists(),
|
||||
db.getGames(),
|
||||
]);
|
||||
|
||||
if (active) {
|
||||
setPlaylists(dbPlaylists);
|
||||
setGames(dbGames);
|
||||
}
|
||||
} finally {
|
||||
if (active) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
const load = useCallback(async () => {
|
||||
const all = await db.getPlaylists();
|
||||
setPlaylists(all);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const getGameById = (gameId: string) => {
|
||||
return games.find((g) => g.id === gameId);
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
// Reload when returning to this page (ionViewWillEnter equivalent)
|
||||
useEffect(() => {
|
||||
const unlisten = history.listen((location) => {
|
||||
if (location.pathname === "/playlists") {
|
||||
load();
|
||||
}
|
||||
});
|
||||
return unlisten;
|
||||
}, [history, load]);
|
||||
|
||||
const staticPlaylists = STATIC_ORDER.map((id) =>
|
||||
playlists.find((p) => p.id === id),
|
||||
).filter(Boolean) as Playlist[];
|
||||
|
||||
const customPlaylists = playlists
|
||||
.filter((p) => !p.isStatic)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const id = `custom-${Date.now()}`;
|
||||
await db.createPlaylist({
|
||||
id,
|
||||
name: "Neue Liste",
|
||||
gameIds: [],
|
||||
isStatic: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
history.push(`/playlists/${id}`);
|
||||
};
|
||||
|
||||
const handleRemoveGame = async (playlistId: string, gameId: string) => {
|
||||
await db.removeGameFromPlaylist(playlistId, gameId);
|
||||
const updatedPlaylists = await db.getPlaylists();
|
||||
setPlaylists(updatedPlaylists);
|
||||
const handleDelete = async (id: string) => {
|
||||
await db.deletePlaylist(id);
|
||||
await load();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -67,9 +97,14 @@ export default function PlaylistsPage() {
|
||||
<IonHeader translucent>
|
||||
<IonToolbar>
|
||||
<IonTitle>Playlists</IonTitle>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={handleAdd}>
|
||||
<IonIcon slot="icon-only" icon={addOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen className="playlists-content">
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Playlists</IonTitle>
|
||||
@@ -81,65 +116,79 @@ export default function PlaylistsPage() {
|
||||
<IonSpinner name="crescent" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="playlists-container">
|
||||
{playlists.map((playlist) => (
|
||||
<IonCard key={playlist.id}>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>
|
||||
{playlist.name}
|
||||
<IonBadge color="primary" style={{ marginLeft: "8px" }}>
|
||||
{playlist.gameIds.length}
|
||||
</IonBadge>
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
{playlist.gameIds.length === 0 ? (
|
||||
<p className="playlists-empty">
|
||||
Keine Spiele in dieser Playlist
|
||||
</p>
|
||||
) : (
|
||||
<IonList>
|
||||
{playlist.gameIds.map((gameId) => {
|
||||
const game = getGameById(gameId);
|
||||
if (!game) return null;
|
||||
return (
|
||||
<IonItem key={gameId}>
|
||||
<IonIcon
|
||||
icon={gameControllerOutline}
|
||||
slot="start"
|
||||
color="primary"
|
||||
/>
|
||||
<IonLabel>
|
||||
<h2>{game.title}</h2>
|
||||
{game.source && (
|
||||
<p>
|
||||
<IonBadge
|
||||
color="medium"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{game.source}
|
||||
</IonBadge>
|
||||
</p>
|
||||
)}
|
||||
</IonLabel>
|
||||
<IonIcon
|
||||
icon={closeCircleOutline}
|
||||
slot="end"
|
||||
color="medium"
|
||||
onClick={() =>
|
||||
handleRemoveGame(playlist.id, gameId)
|
||||
}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</IonItem>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
)}
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<IonList inset>
|
||||
{staticPlaylists.map((playlist) => (
|
||||
<IonItem
|
||||
key={playlist.id}
|
||||
routerLink={`/playlists/${playlist.id}`}
|
||||
detail={false}
|
||||
>
|
||||
<IonIcon
|
||||
icon={STATIC_ICONS[playlist.id]}
|
||||
slot="start"
|
||||
color="primary"
|
||||
/>
|
||||
<IonLabel>{playlist.name}</IonLabel>
|
||||
<IonBadge color="medium" slot="end">
|
||||
{playlist.gameIds.length}
|
||||
</IonBadge>
|
||||
<IonIcon
|
||||
icon={chevronForward}
|
||||
slot="end"
|
||||
color="medium"
|
||||
className="chevron-icon"
|
||||
/>
|
||||
</IonItem>
|
||||
))}
|
||||
</IonList>
|
||||
|
||||
<IonList inset>
|
||||
<IonListHeader>
|
||||
<IonLabel>Eigene Playlists</IonLabel>
|
||||
</IonListHeader>
|
||||
{customPlaylists.length === 0 ? (
|
||||
<IonItem>
|
||||
<IonLabel color="medium" className="ion-text-center">
|
||||
Keine eigenen Playlists
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
) : (
|
||||
customPlaylists.map((playlist) => (
|
||||
<IonItemSliding key={playlist.id}>
|
||||
<IonItem
|
||||
routerLink={`/playlists/${playlist.id}`}
|
||||
detail={false}
|
||||
>
|
||||
<IonIcon
|
||||
icon={listOutline}
|
||||
slot="start"
|
||||
color="primary"
|
||||
/>
|
||||
<IonLabel>{playlist.name}</IonLabel>
|
||||
<IonBadge color="medium" slot="end">
|
||||
{playlist.gameIds.length}
|
||||
</IonBadge>
|
||||
<IonIcon
|
||||
icon={chevronForward}
|
||||
slot="end"
|
||||
color="medium"
|
||||
className="chevron-icon"
|
||||
/>
|
||||
</IonItem>
|
||||
<IonItemOptions side="end">
|
||||
<IonItemOption
|
||||
color="danger"
|
||||
onClick={() => handleDelete(playlist.id)}
|
||||
>
|
||||
<IonIcon slot="icon-only" icon={trashOutline} />
|
||||
</IonItemOption>
|
||||
</IonItemOptions>
|
||||
</IonItemSliding>
|
||||
))
|
||||
)}
|
||||
</IonList>
|
||||
</>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
|
||||
@@ -229,27 +229,23 @@ class Database {
|
||||
|
||||
private async initStaticPlaylists(): Promise<void> {
|
||||
const playlists = await this.getPlaylists();
|
||||
const hasWantToPlay = playlists.some((p) => p.id === "want-to-play");
|
||||
const hasNotInteresting = playlists.some((p) => p.id === "not-interesting");
|
||||
|
||||
if (!hasWantToPlay) {
|
||||
await this.createPlaylist({
|
||||
id: "want-to-play",
|
||||
name: "Want to Play",
|
||||
gameIds: [],
|
||||
isStatic: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
const statics: Array<{ id: string; name: string }> = [
|
||||
{ id: "favorites", name: "Favoriten" },
|
||||
{ id: "want-to-play", name: "Want to Play" },
|
||||
{ id: "not-interesting", name: "Not Interesting" },
|
||||
];
|
||||
|
||||
if (!hasNotInteresting) {
|
||||
await this.createPlaylist({
|
||||
id: "not-interesting",
|
||||
name: "Not Interesting",
|
||||
gameIds: [],
|
||||
isStatic: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
for (const s of statics) {
|
||||
if (!playlists.some((p) => p.id === s.id)) {
|
||||
await this.createPlaylist({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
gameIds: [],
|
||||
isStatic: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +308,34 @@ class Database {
|
||||
playlist.gameIds = playlist.gameIds.filter((id) => id !== gameId);
|
||||
await this.createPlaylist(playlist);
|
||||
}
|
||||
|
||||
async updatePlaylist(
|
||||
id: string,
|
||||
updates: Partial<Pick<Playlist, "name">>,
|
||||
): Promise<void> {
|
||||
const playlist = await this.getPlaylist(id);
|
||||
if (!playlist) throw new Error(`Playlist ${id} not found`);
|
||||
|
||||
Object.assign(playlist, updates);
|
||||
await this.createPlaylist(playlist);
|
||||
}
|
||||
|
||||
async deletePlaylist(id: string): Promise<void> {
|
||||
const playlist = await this.getPlaylist(id);
|
||||
if (!playlist) return;
|
||||
if (playlist.isStatic) throw new Error("Cannot delete static playlist");
|
||||
|
||||
if (!this.db) await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db!.transaction("playlists", "readwrite");
|
||||
const store = tx.objectStore("playlists");
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
|
||||
Reference in New Issue
Block a user