managarten/apps-archived/mukke/apps/mobile/app/playlist/[id].tsx
Till JS 7a56699d45 feat(mukke): rename LightWrite to Mukke and add music library, player, playlists
Combines LightWrite (beat/lyrics editor) and Mukke (iOS music player) into
a single web-based music workspace app. Archives the old Mukke mobile app.

- Rename: @lightwrite/* → @mukke/*, all branding, configs, Dockerfiles
- New DB schemas: songs, playlists, playlist_songs + songId FK on projects
- New backend modules: SongModule, PlaylistModule, LibraryModule
- New web: app shell with sidebar, library (songs/albums/artists/genres),
  web player (queue/shuffle/repeat/MediaSession), playlists, search,
  upload, dashboard, album/artist/genre detail pages
- Auth: add forgot-password + reset-password pages, extend auth store
- Tests: 40 backend unit tests (song, playlist, library services)
- Config: env generation, MinIO bucket, docker-compose prod, cloudflare
- Docs: update CLAUDE.md, auth guidelines with SvelteKit checklist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:55:56 +01:00

118 lines
3.2 KiB
TypeScript

import { Ionicons } from '@expo/vector-icons';
import { Stack, useLocalSearchParams } from 'expo-router';
import { useEffect, useState } from 'react';
import { View, Text, Pressable, Alert } from 'react-native';
import { EmptyState } from '~/components/EmptyState';
import { SongList } from '~/components/SongList';
import { SongPicker } from '~/components/SongPicker';
import {
getPlaylistById,
getPlaylistSongs,
addSongToPlaylist,
removeSongFromPlaylist,
} from '~/services/playlistService';
import { usePlayerStore } from '~/stores/playerStore';
import type { Playlist, Song } from '~/types';
import { useTheme } from '~/utils/themeContext';
export default function PlaylistDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { colors } = useTheme();
const playSong = usePlayerStore((s) => s.playSong);
const [playlist, setPlaylist] = useState<Playlist | null>(null);
const [songs, setSongs] = useState<Song[]>([]);
const [showPicker, setShowPicker] = useState(false);
const loadData = async () => {
if (!id) return;
const [p, s] = await Promise.all([getPlaylistById(id), getPlaylistSongs(id)]);
setPlaylist(p);
setSongs(s);
};
useEffect(() => {
loadData();
}, [id]);
const handleAddSongs = async (selected: Song[]) => {
if (!id) return;
for (const song of selected) {
await addSongToPlaylist(id, song.id);
}
await loadData();
};
const handleLongPress = (song: Song) => {
Alert.alert('Song entfernen', `"${song.title}" aus der Playlist entfernen?`, [
{ text: 'Abbrechen', style: 'cancel' },
{
text: 'Entfernen',
style: 'destructive',
onPress: async () => {
if (id) {
await removeSongFromPlaylist(id, song.id);
await loadData();
}
},
},
]);
};
return (
<View style={{ flex: 1 }}>
<Stack.Screen
options={{
title: playlist?.name || 'Playlist',
headerRight: () => (
<Pressable onPress={() => setShowPicker(true)} style={{ padding: 8 }}>
<Ionicons name="add" size={28} color={colors.primary} />
</Pressable>
),
}}
/>
{playlist && (
<View style={{ padding: 16, alignItems: 'center' }}>
<View
style={{
width: 120,
height: 120,
borderRadius: 12,
backgroundColor: colors.primary + '20',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name="musical-notes" size={48} color={colors.primary} />
</View>
<Text style={{ fontSize: 20, fontWeight: '700', color: colors.text, marginTop: 12 }}>
{playlist.name}
</Text>
{playlist.description && (
<Text style={{ fontSize: 14, color: colors.textSecondary, marginTop: 4 }}>
{playlist.description}
</Text>
)}
<Text style={{ fontSize: 13, color: colors.textTertiary, marginTop: 4 }}>
{songs.length} Songs
</Text>
</View>
)}
<SongList
songs={songs}
onSongPress={(song, index) => playSong(song, songs, index)}
emptyTitle="Playlist ist leer"
emptyMessage="Füge Songs über den + Button hinzu."
/>
<SongPicker
visible={showPicker}
onClose={() => setShowPicker(false)}
onSelect={handleAddSongs}
excludeIds={songs.map((s) => s.id)}
/>
</View>
);
}