Added sorting and searching of liked tracks, like/unlike button working with logic in its own context

This commit is contained in:
2026-03-05 10:27:22 +01:00
parent 9e2c6e02b3
commit e7d5d1835f
10 changed files with 419 additions and 271 deletions

View File

@@ -7,6 +7,7 @@ import HomeScreen from "./src/screens/HomeScreen";
import { createContext, useState, useContext } from "react"; import { createContext, useState, useContext } from "react";
import LikedTracksScreen from "./src/screens/LikedTracksScreen"; import LikedTracksScreen from "./src/screens/LikedTracksScreen";
import AlbumScreen from "./src/screens/AlbumScreen"; import AlbumScreen from "./src/screens/AlbumScreen";
import { LibraryProvider } from "./src/contexts/LibraryContext";
const Stack = createNativeStackNavigator(); const Stack = createNativeStackNavigator();
@@ -63,7 +64,9 @@ function RootLayout() {
export default function App() { export default function App() {
return ( return (
<PlayerProvider> <PlayerProvider>
<LibraryProvider>
<RootLayout /> <RootLayout />
</LibraryProvider>
</PlayerProvider> </PlayerProvider>
); );
} }

View File

@@ -8939,9 +8939,9 @@
} }
}, },
"node_modules/tar": { "node_modules/tar": {
"version": "7.5.9", "version": "7.5.10",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/fs-minipass": "^4.0.0", "@isaacs/fs-minipass": "^4.0.0",

View File

@@ -0,0 +1,13 @@
export const durationFormatter = (totalSeconds = 0) => {
const s = Math.max(0, Math.floor(totalSeconds));
const hours = Math.floor(s / 3600);
const minutes = Math.floor((s % 3600) / 60);
const seconds = s % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
return `${minutes}:${String(seconds).padStart(2, "0")}`;
};

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Pressable, View, Text, StyleSheet, Image } from 'react-native'; import { Pressable, View, Text, StyleSheet, Image } from 'react-native';
import { durationFormatter } from './DurationFormatter';
export default function TrackRow({ export default function TrackRow({
title, title,
@@ -7,8 +8,8 @@ export default function TrackRow({
duration, duration,
cover, cover,
onPress, onPress,
showHeart = true, liked = false,
liked = false, // new prop onToggleLike,
}) { }) {
return ( return (
<Pressable style={styles.trackItem} onPress={onPress}> <Pressable style={styles.trackItem} onPress={onPress}>
@@ -23,13 +24,13 @@ export default function TrackRow({
</Text> </Text>
</View> </View>
{duration ? <Text style={styles.trackDuration}>{duration}</Text> : null} {duration ? <Text style={styles.trackDuration}>{durationFormatter(duration)}</Text> : "0"}
{showHeart ? ( <Pressable onPress={onToggleLike} hitSlop={10}>
<Text style={[styles.heart, liked && styles.heartLiked]}> <Text style={[styles.heart, liked && styles.heartLiked]}>
{liked ? '♥' : '♡'} {liked ? '♥' : '♡'}
</Text> </Text>
) : null} </Pressable>
</Pressable> </Pressable>
); );
} }

View File

@@ -0,0 +1,51 @@
import React, { createContext, useContext, useMemo, useState } from "react";
import initialLibrary from "../data/library";
const LibraryContext = createContext(null);
export function LibraryProvider({ children }) {
const [albums, setAlbums] = useState(initialLibrary);
const toggleLike = (albumId, trackId) => {
setAlbums((prev) =>
prev.map((album) =>
album.id !== albumId
? album
: {
...album,
tracks: album.tracks.map((t) =>
t.id === trackId ? { ...t, liked: !t.liked } : t
),
}
)
);
};
const likedTracks = useMemo(
() =>
albums.flatMap((album) =>
album.tracks
.filter((track) => track.liked)
.map((track) => ({
...track,
albumId: album.id,
albumTitle: album.title,
artist: album.artist,
cover: album.cover,
}))
),
[albums]
);
return (
<LibraryContext.Provider value={{ albums, likedTracks, toggleLike }}>
{children}
</LibraryContext.Provider>
);
}
export function useLibrary() {
const ctx = useContext(LibraryContext);
if (!ctx) throw new Error("useLibrary must be used inside LibraryProvider");
return ctx;
}

View File

@@ -5,35 +5,35 @@ const library = [
artist: 'Swans', artist: 'Swans',
date: '1996-11-29', date: '1996-11-29',
label: 'Young God Records', label: 'Young God Records',
duration: '2:21:25', duration: 8485,
cover: require('../../assets/covers/soundtracksfortheblind.jpg'), cover: require('../../assets/covers/soundtracksfortheblind.jpg'),
tracks: [ tracks: [
{ id: 'a1t1', title: 'Red Velvet Corridor', duration: '3:03', liked: true }, { id: 'a1t1', title: 'Red Velvet Corridor', duration: 183, liked: true },
{ id: 'a1t2', title: 'I Was a Prisoner in Your Skull', duration: '6:39', liked: false }, { id: 'a1t2', title: 'I Was a Prisoner in Your Skull', duration: 399, liked: false },
{ id: 'a1t3', title: 'Helpless Child', duration: '15:47', liked: true }, { id: 'a1t3', title: 'Helpless Child', duration: 947, liked: true },
{ id: 'a1t4', title: 'Live Through Me', duration: '2:19', liked: false }, { id: 'a1t4', title: 'Live Through Me', duration: 139, liked: false },
{ id: 'a1t5', title: 'Yum-Yab Killers', duration: '5:07', liked: false }, { id: 'a1t5', title: 'Yum-Yab Killers', duration: 307, liked: false },
{ id: 'a1t6', title: 'The Beautiful Days', duration: '1:50', liked: false }, { id: 'a1t6', title: 'The Beautiful Days', duration: 110, liked: false },
{ id: 'a1t7', title: 'Volcano', duration: '5:18', liked: false }, { id: 'a1t7', title: 'Volcano', duration: 318, liked: false },
{ id: 'a1t8', title: 'Mellothumb', duration: '2:45', liked: false }, { id: 'a1t8', title: 'Mellothumb', duration: 165, liked: false },
{ id: 'a1t9', title: 'All Lined Up', duration: '4:48', liked: false }, { id: 'a1t9', title: 'All Lined Up', duration: 288, liked: false },
{ id: 'a1t10', title: 'Surrogate Drone', duration: '2:03', liked: false }, { id: 'a1t10', title: 'Surrogate Drone', duration: 123, liked: false },
{ id: 'a1t11', title: 'How They Suffer', duration: '5:52', liked: false }, { id: 'a1t11', title: 'How They Suffer', duration: 352, liked: false },
{ id: 'a1t12', title: 'Animus', duration: '10:43', liked: false }, { id: 'a1t12', title: 'Animus', duration: 643, liked: false },
{ id: 'a1t13', title: 'Red Velvet Wound', duration: '1:01', liked: false }, { id: 'a1t13', title: 'Red Velvet Wound', duration: 61, liked: false },
{ id: 'a1t14', title: 'The Sound', duration: '13:11', liked: true }, { id: 'a1t14', title: 'The Sound', duration: 791, liked: true },
{ id: 'a1t15', title: 'Her Mouth Is Filled with Honey', duration: '3:18', liked: false }, { id: 'a1t15', title: 'Her Mouth Is Filled with Honey', duration: 198, liked: false },
{ id: 'a1t16', title: 'Blood Section', duration: '2:45', liked: false }, { id: 'a1t16', title: 'Blood Section', duration: 165, liked: false },
{ id: 'a1t17', title: 'Hypogirl', duration: '3:42', liked: false }, { id: 'a1t17', title: 'Hypogirl', duration: 222, liked: false },
{ id: 'a1t18', title: 'Minus Something', duration: '4:14', liked: false }, { id: 'a1t18', title: 'Minus Something', duration: 254, liked: false },
{ id: 'a1t19', title: 'Empathy', duration: '6:45', liked: false }, { id: 'a1t19', title: 'Empathy', duration: 405, liked: false },
{ id: 'a1t20', title: 'I Love You This Much', duration: '7:23', liked: false }, { id: 'a1t20', title: 'I Love You This Much', duration: 443, liked: false },
{ id: 'a1t21', title: "YRP", duration: '7:58', liked: false }, { id: 'a1t21', title: "YRP", duration: 478, liked: false },
{ id: 'a1t22', title: "Fan's Lament", duration: '1:47', liked: false }, { id: 'a1t22', title: "Fan's Lament", duration: 107, liked: false },
{ id: 'a1t23', title: 'Secret Friends', duration: '3:08', liked: false }, { id: 'a1t23', title: 'Secret Friends', duration: 188, liked: false },
{ id: 'a1t24', title: 'The Final Sacrifice', duration: '9:51', liked: false }, { id: 'a1t24', title: 'The Final Sacrifice', duration: 591, liked: false },
{ id: 'a1t25', title: 'YRP 2', duration: '2:09', liked: false }, { id: 'a1t25', title: 'YRP 2', duration: 129, liked: false },
{ id: 'a1t26', title: 'Surrogate 2', duration: '1:55', liked: false }, { id: 'a1t26', title: 'Surrogate 2', duration: 115, liked: false },
], ],
}, },
{ {
@@ -42,23 +42,23 @@ const library = [
artist: 'Daft Punk', artist: 'Daft Punk',
date: '2001-03-12', date: '2001-03-12',
label: 'Virgin Records', label: 'Virgin Records',
duration: '1:00:50', duration: 3650,
cover: require('../../assets/covers/discovery.jpg'), cover: require('../../assets/covers/discovery.jpg'),
tracks: [ tracks: [
{ id: 'a2t1', title: 'One More Time', duration: '5:20', liked: true }, { id: 'a2t1', title: 'One More Time', duration: 320, liked: true },
{ id: 'a2t2', title: 'Aerodynamic', duration: '3:27', liked: false }, { id: 'a2t2', title: 'Aerodynamic', duration: 207, liked: false },
{ id: 'a2t3', title: 'Digital Love', duration: '4:58', liked: false }, { id: 'a2t3', title: 'Digital Love', duration: 298, liked: false },
{ id: 'a2t4', title: 'Harder, Better, Faster, Stronger', duration: '3:45', liked: false }, { id: 'a2t4', title: 'Harder, Better, Faster, Stronger', duration: 225, liked: false },
{ id: 'a2t5', title: 'Crescendolls', duration: '3:31', liked: false }, { id: 'a2t5', title: 'Crescendolls', duration: 211, liked: false },
{ id: 'a2t6', title: 'Nightvision', duration: '1:44', liked: false }, { id: 'a2t6', title: 'Nightvision', duration: 104, liked: false },
{ id: 'a2t7', title: 'Superheroes', duration: '3:57', liked: false }, { id: 'a2t7', title: 'Superheroes', duration: 237, liked: false },
{ id: 'a2t8', title: 'High Life', duration: '3:21', liked: false }, { id: 'a2t8', title: 'High Life', duration: 201, liked: false },
{ id: 'a2t9', title: 'Something About Us', duration: '3:51', liked: false }, { id: 'a2t9', title: 'Something About Us', duration: 231, liked: false },
{ id: 'a2t10', title: 'Voyager', duration: '3:47', liked: false }, { id: 'a2t10', title: 'Voyager', duration: 227, liked: false },
{ id: 'a2t11', title: 'Veridis Quo', duration: '5:44', liked: false }, { id: 'a2t11', title: 'Veridis Quo', duration: 344, liked: false },
{ id: 'a2t12', title: 'Short Circuit', duration: '3:26', liked: false }, { id: 'a2t12', title: 'Short Circuit', duration: 206, liked: false },
{ id: 'a2t13', title: 'Face to Face', duration: '4:00', liked: false }, { id: 'a2t13', title: 'Face to Face', duration: 240, liked: false },
{ id: 'a2t14', title: 'Too Long', duration: '10:00', liked: false }, { id: 'a2t14', title: 'Too Long', duration: 600, liked: false },
], ],
}, },
]; ];

View File

@@ -1,19 +1,32 @@
import React from 'react'; import React, { useMemo } from 'react';
import { View, Text, StyleSheet, FlatList, Pressable, Image } from 'react-native'; import { View, Text, StyleSheet, FlatList, Pressable, Image } from 'react-native';
import TrackRow from '../components/TrackRow'; import TrackRow from '../components/TrackRow';
import { useLibrary } from '../contexts/LibraryContext';
import { durationFormatter } from '../components/DurationFormatter';
export default function AlbumScreen({ route, navigation }) { export default function AlbumScreen({ route, navigation }) {
const { album } = route.params; const { album: routeAlbum } = route.params;
const { albums, toggleLike } = useLibrary();
// Always use latest album from context (so likes stay in sync)
const album = useMemo(
() => albums.find((a) => a.id === routeAlbum.id) ?? routeAlbum,
[albums, routeAlbum]
);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Pressable onPress={() => navigation.goBack()} style={styles.backBtn}> <Pressable onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backText}> Back</Text> <Text style={styles.backText}> Back</Text>
</Pressable> </Pressable>
<View style={styles.header}> <View style={styles.header}>
<Image source={album.cover} style={styles.cover} resizeMode="cover" /> <Image source={album.cover} style={styles.cover} resizeMode="cover" />
<Text style={styles.title}>{album.title}</Text> <Text style={styles.title}>{album.title}</Text>
<Text style={styles.artist}>{album.artist}</Text> <Text style={styles.artist}>{album.artist}</Text>
<Text style={styles.meta}>{album.date} · {album.label} · {album.duration}</Text> <Text style={styles.meta}>
{album.date} · {album.label} · {durationFormatter(album.duration)}
</Text>
</View> </View>
<FlatList <FlatList
@@ -24,9 +37,11 @@ export default function AlbumScreen({ route, navigation }) {
title={typeof item === 'string' ? item : item.title} title={typeof item === 'string' ? item : item.title}
artist={album.artist} artist={album.artist}
duration={item.duration} duration={item.duration}
cover={album.cover}
onPress={() => {}} onPress={() => {}}
showHeart={true} showHeart={true}
liked={item.liked} liked={item.liked}
onToggleLike={() => toggleLike(album.id, item.id)}
/> />
)} )}
contentContainerStyle={styles.trackList} contentContainerStyle={styles.trackList}
@@ -79,37 +94,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingBottom: 180, paddingBottom: 180,
}, },
trackItem: {
width: '100%',
minHeight: 62,
padding: 0,
flexDirection: 'row',
},
trackTextBlock: {
flex: 1,
},
trackTitle: {
color: '#ffffff',
fontSize: 15,
fontWeight: '700',
},
trackArtist: {
color: '#ffffff',
fontSize: 15,
marginTop: 2,
},
trackDuration: {
color: '#ffffff',
fontSize: 15,
paddingHorizontal: 18,
marginTop: 10,
},
heart: {
fontSize: 22,
color: '#fff',
paddingHorizontal: 10,
marginTop: 5.5,
},
backBtn: { backBtn: {
marginBottom: 10, marginBottom: 10,
alignSelf: 'flex-start', alignSelf: 'flex-start',

View File

@@ -1,34 +1,27 @@
import React from "react"; import React, { useMemo } from "react";
import { import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
FlatList, FlatList,
ScrollView,
Pressable, Pressable,
Image, Image,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import library from '../data/library'; import TrackRow from "../components/TrackRow";
import TrackRow from '../components/TrackRow'; import { useLibrary } from "../contexts/LibraryContext";
const likedAlbums = library;
const likedTracks = library.flatMap((album) =>
album.tracks
.filter((t) => t.liked)
.map((t) => ({
...t,
albumId: album.id,
albumTitle: album.title,
artist: album.artist,
cover: album.cover,
}))
);
export default function HomeScreen() { export default function HomeScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const { albums, likedTracks, toggleLike } = useLibrary();
// Same behavior as before: show all albums in "Liked albums" section
const likedAlbums = albums;
// Optional: limit shown liked tracks on Home
const homeLikedTracks = useMemo(() => likedTracks.slice(0, 5), [likedTracks]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.headerRow}> <View style={styles.headerRow}>
@@ -37,17 +30,14 @@ export default function HomeScreen() {
<Pressable <Pressable
style={styles.iconBtn} style={styles.iconBtn}
onPress={() => { onPress={() => {
// navigation.navigate("Login")
console.log("Login pressed"); console.log("Login pressed");
}} }}
> >
<Ionicons name="person-outline" size={24} color="#fff" /> <Ionicons name="person-outline" size={24} color="#fff" />
</Pressable> </Pressable>
<Pressable <Pressable
style={styles.iconBtn} style={styles.iconBtn}
onPress={() => { onPress={() => {
// navigation.navigate("Settings")
console.log("Settings pressed"); console.log("Settings pressed");
}} }}
> >
@@ -55,24 +45,23 @@ export default function HomeScreen() {
</Pressable> </Pressable>
</View> </View>
</View> </View>
<Pressable
onPress={() => navigation.navigate("LikedTracks", { tracks: likedTracks })} <Pressable onPress={() => navigation.navigate("LikedTracks")}>
>
<Text style={styles.sectionTitle}>Liked tracks </Text> <Text style={styles.sectionTitle}>Liked tracks </Text>
</Pressable> </Pressable>
{likedTracks.map((track) => { {homeLikedTracks.map((track) => {
const album = likedAlbums.find((a) => a.id === track.albumId); const album = albums.find((a) => a.id === track.albumId);
return ( return (
<TrackRow <TrackRow
key={track.id} key={track.id}
title={track.title} title={track.title}
artist={album?.artist ?? "Unknown artist"} artist={track.artist ?? "Unknown artist"}
duration={track.duration} duration={track.duration}
cover={album?.cover} cover={track.cover}
showHeart={true} showHeart={true}
liked={track.liked} liked={track.liked}
onToggleLike={() => toggleLike(track.albumId, track.id)}
onPress={() => album && navigation.navigate("Album", { album })} onPress={() => album && navigation.navigate("Album", { album })}
/> />
); );
@@ -116,12 +105,9 @@ const styles = StyleSheet.create({
headerActions: { headerActions: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 8, // if gap not supported on your RN version, use marginLeft in iconBtn gap: 8,
}, },
settingsBtn: { iconBtn: {
padding: 2,
},
loginBtn: {
padding: 2, padding: 2,
}, },
homeTitle: { homeTitle: {
@@ -136,38 +122,6 @@ const styles = StyleSheet.create({
fontWeight: "700", fontWeight: "700",
marginBottom: 12, marginBottom: 12,
}, },
trackItem: {
minHeight: 62,
flexDirection: 'row',
alignItems: 'center',
},
trackCover: {
width: 46,
height: 46,
borderRadius: 6,
marginRight: 10,
},
trackTextBlock: {
flex: 1,
justifyContent: 'center',
},
trackTitle: {
color: '#ffffff',
fontSize: 15,
fontWeight: '700',
},
trackArtist: {
color: '#ffffff',
fontSize: 13,
marginTop: 2,
},
tracksBox: {
maxHeight: 260,
},
albumList: { albumList: {
paddingBottom: 120, paddingBottom: 120,
}, },
@@ -176,14 +130,14 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
albumItem: { albumItem: {
width: '48%', width: "48%",
aspectRatio: 1, aspectRatio: 1,
borderRadius: 8, borderRadius: 8,
overflow: 'hidden', overflow: "hidden",
backgroundColor: '#222', backgroundColor: "#222",
}, },
cover: { cover: {
width: '100%', width: "100%",
height: '100%', height: "100%",
}, },
}); });

View File

@@ -1,23 +1,51 @@
import React from "react"; import { useMemo, useState } from "react";
import { View, Text, StyleSheet, FlatList, Pressable } from "react-native"; import {
import library from "../data/library"; View,
Text,
StyleSheet,
FlatList,
Pressable,
TextInput,
} from "react-native";
import TrackRow from "../components/TrackRow"; import TrackRow from "../components/TrackRow";
import { useLibrary } from "../contexts/LibraryContext";
export default function LikedTracksScreen({ navigation }) { export default function LikedTracksScreen({ navigation }) {
const likedTracks = library.flatMap((album) => const [query, setQuery] = useState("");
album.tracks const [sortDir, setSortDir] = useState("desc");
.filter((track) => track.liked) const { albums, likedTracks, toggleLike } = useLibrary();
.map((track) => ({
...track, const [sortBy, setSortBy] = useState("dateAdded");
albumId: album.id, const [showSortMenu, setShowSortMenu] = useState(false);
albumTitle: album.title,
artist: album.artist, const filteredAndSortedTracks = useMemo(() => {
cover: album.cover, const q = query.trim().toLowerCase();
}))
let result = !q
? [...likedTracks]
: likedTracks.filter(
(item) =>
item.title.toLowerCase().includes(q) ||
item.artist.toLowerCase().includes(q)
); );
// testing only if (sortBy === "name") {
const tracks = likedTracks.slice(0, 2); result.sort((a, b) => a.title.localeCompare(b.title));
} else if (sortBy === "length") {
result.sort((a, b) => a.duration - b.duration);
} else {
result.sort((a, b) => {
if (a.dateAdded && b.dateAdded) {
return new Date(b.dateAdded) - new Date(a.dateAdded);
}
return 0;
});
}
if (sortDir === "desc") result.reverse();
return result;
}, [likedTracks, query, sortBy, sortDir]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
@@ -27,8 +55,52 @@ export default function LikedTracksScreen({ navigation }) {
<Text style={styles.title}>Your liked tracks</Text> <Text style={styles.title}>Your liked tracks</Text>
<TextInput
value={query}
onChangeText={setQuery}
placeholder="Search liked tracks"
placeholderTextColor="#9ca3af"
style={styles.searchInput}
autoCapitalize="none"
autoCorrect={false}
/>
<View style={styles.sortRow}>
<Text style={styles.sortByText}>Sort by:</Text>
<Pressable
style={styles.sortButton}
onPress={() => setShowSortMenu((v) => !v)}
>
<Text style={styles.sortButtonText}>
{sortBy === "dateAdded" ? "Date added" : sortBy === "name" ? "Name" : "Length"}
</Text>
</Pressable>
<Pressable
style={styles.sortDirButton}
onPress={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
>
<Text style={styles.sortDirButtonText}>
{sortDir === "asc" ? "ASC ↑" : "DESC ↓"}
</Text>
</Pressable>
</View>
{showSortMenu && (
<View style={styles.sortMenu}>
<Pressable style={styles.sortOption} onPress={() => { setSortBy("dateAdded"); setShowSortMenu(false); }}>
<Text style={styles.sortOptionText}>Date added</Text>
</Pressable>
<Pressable style={styles.sortOption} onPress={() => { setSortBy("name"); setShowSortMenu(false); }}>
<Text style={styles.sortOptionText}>Name</Text>
</Pressable>
<Pressable style={styles.sortOption} onPress={() => { setSortBy("length"); setShowSortMenu(false); }}>
<Text style={styles.sortOptionText}>Length</Text>
</Pressable>
</View>
)}
<FlatList <FlatList
data={tracks} data={filteredAndSortedTracks}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<TrackRow <TrackRow
@@ -38,12 +110,17 @@ export default function LikedTracksScreen({ navigation }) {
cover={item.cover} cover={item.cover}
showHeart={true} showHeart={true}
liked={item.liked} liked={item.liked}
onToggleLike={() => toggleLike(item.albumId, item.id)}
onPress={() => { onPress={() => {
const album = library.find((a) => a.id === item.albumId); const album = albums.find((a) => a.id === item.albumId);
if (album) navigation.navigate("Album", { album }); if (album) navigation.navigate("Album", { album });
}} }}
/> />
)} )}
ListEmptyComponent={
<Text style={styles.emptyText}>No liked tracks found.</Text>
}
keyboardShouldPersistTaps="handled"
contentContainerStyle={{ paddingBottom: 180 }} contentContainerStyle={{ paddingBottom: 180 }}
/> />
</View> </View>
@@ -72,4 +149,69 @@ const styles = StyleSheet.create({
fontWeight: "700", fontWeight: "700",
marginBottom: 12, marginBottom: 12,
}, },
searchInput: {
backgroundColor: "#1f2937",
color: "#fff",
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
marginBottom: 12,
},
emptyText: {
color: "#9ca3af",
fontSize: 15,
marginTop: 16,
},
sortRow: {
flexDirection: "row",
alignItems: "center",
marginBottom: 8,
},
sortByText: {
color: "#9ca3af",
marginRight: 8,
fontSize: 14,
},
sortButton: {
backgroundColor: "#111827",
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 6,
},
sortButtonText: {
color: "#fff",
fontSize: 14,
fontWeight: "600",
},
sortMenu: {
backgroundColor: "#111827",
borderRadius: 10,
marginBottom: 10,
overflow: "hidden",
borderWidth: 1,
borderColor: "#1f2937",
},
sortOption: {
paddingHorizontal: 12,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: "#1f2937",
},
sortOptionText: {
color: "#fff",
fontSize: 15,
},
sortDirButton: {
backgroundColor: "#111827",
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 6,
marginLeft: 8,
},
sortDirButtonText: {
color: "#fff",
fontSize: 14,
fontWeight: "600",
},
}); });