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 LikedTracksScreen from "./src/screens/LikedTracksScreen";
import AlbumScreen from "./src/screens/AlbumScreen";
import { LibraryProvider } from "./src/contexts/LibraryContext";
const Stack = createNativeStackNavigator();
@@ -63,7 +64,9 @@ function RootLayout() {
export default function App() {
return (
<PlayerProvider>
<RootLayout />
<LibraryProvider>
<RootLayout />
</LibraryProvider>
</PlayerProvider>
);
}

View File

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

View File

@@ -1,122 +1,106 @@
import React from 'react';
import React, { useMemo } from 'react';
import { View, Text, StyleSheet, FlatList, Pressable, Image } from 'react-native';
import TrackRow from '../components/TrackRow';
import { useLibrary } from '../contexts/LibraryContext';
import { durationFormatter } from '../components/DurationFormatter';
export default function AlbumScreen({ route, navigation }) {
const { album } = route.params;
return (
<View style={styles.container}>
<Pressable onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backText}> Back</Text>
</Pressable>
<View style={styles.header}>
<Image source={album.cover} style={styles.cover} resizeMode="cover" />
<Text style={styles.title}>{album.title}</Text>
<Text style={styles.artist}>{album.artist}</Text>
<Text style={styles.meta}>{album.date} · {album.label} · {album.duration}</Text>
</View>
const { album: routeAlbum } = route.params;
const { albums, toggleLike } = useLibrary();
<FlatList
data={album.tracks}
keyExtractor={(item, index) => item.id ?? index.toString()}
renderItem={({ item }) => (
<TrackRow
title={typeof item === 'string' ? item : item.title}
artist={album.artist}
duration={item.duration}
onPress={() => { }}
showHeart={true}
liked={item.liked}
/>
)}
contentContainerStyle={styles.trackList}
/>
</View>
);
// 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 (
<View style={styles.container}>
<Pressable onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backText}> Back</Text>
</Pressable>
<View style={styles.header}>
<Image source={album.cover} style={styles.cover} resizeMode="cover" />
<Text style={styles.title}>{album.title}</Text>
<Text style={styles.artist}>{album.artist}</Text>
<Text style={styles.meta}>
{album.date} · {album.label} · {durationFormatter(album.duration)}
</Text>
</View>
<FlatList
data={album.tracks}
keyExtractor={(item, index) => item.id ?? index.toString()}
renderItem={({ item }) => (
<TrackRow
title={typeof item === 'string' ? item : item.title}
artist={album.artist}
duration={item.duration}
cover={album.cover}
onPress={() => {}}
showHeart={true}
liked={item.liked}
onToggleLike={() => toggleLike(album.id, item.id)}
/>
)}
contentContainerStyle={styles.trackList}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
paddingTop: 28,
paddingHorizontal: 16,
paddingBottom: 24,
},
header: {
alignItems: 'center',
},
cover: {
width: 180,
height: 180,
borderRadius: 8,
marginBottom: 12,
},
title: {
color: '#fff',
fontSize: 32,
fontWeight: '700',
textAlign: 'center',
marginBottom: 1,
},
artist: {
color: '#fff',
fontSize: 26,
fontWeight: '500',
textAlign: 'center',
marginTop: 1,
},
meta: {
color: '#fff',
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
marginTop: 8,
marginBottom: 24,
},
trackList: {
width: '100%',
paddingHorizontal: 16,
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: {
marginBottom: 10,
alignSelf: 'flex-start',
},
backText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
container: {
flex: 1,
backgroundColor: '#000',
paddingTop: 28,
paddingHorizontal: 16,
paddingBottom: 24,
},
header: {
alignItems: 'center',
},
cover: {
width: 180,
height: 180,
borderRadius: 8,
marginBottom: 12,
},
title: {
color: '#fff',
fontSize: 32,
fontWeight: '700',
textAlign: 'center',
marginBottom: 1,
},
artist: {
color: '#fff',
fontSize: 26,
fontWeight: '500',
textAlign: 'center',
marginTop: 1,
},
meta: {
color: '#fff',
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
marginTop: 8,
marginBottom: 24,
},
trackList: {
width: '100%',
paddingHorizontal: 16,
paddingBottom: 180,
},
backBtn: {
marginBottom: 10,
alignSelf: 'flex-start',
},
backText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});

View File

@@ -1,78 +1,67 @@
import React from "react";
import React, { useMemo } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
ScrollView,
Pressable,
Image,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import library from '../data/library';
import TrackRow from '../components/TrackRow';
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,
}))
);
import TrackRow from "../components/TrackRow";
import { useLibrary } from "../contexts/LibraryContext";
export default function HomeScreen() {
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 (
<View style={styles.container}>
<View style={styles.headerRow}>
<Text style={styles.homeTitle}>Home</Text>
<View style={styles.headerActions}>
<Pressable
style={styles.iconBtn}
onPress={() => {
// navigation.navigate("Login")
console.log("Login pressed");
}}
>
<Ionicons name="person-outline" size={24} color="#fff" />
</Pressable>
<Pressable
style={styles.iconBtn}
onPress={() => {
// navigation.navigate("Settings")
console.log("Settings pressed");
}}
>
<Ionicons name="settings-outline" size={24} color="#fff" />
</Pressable>
<View style={styles.headerActions}>
<Pressable
style={styles.iconBtn}
onPress={() => {
console.log("Login pressed");
}}
>
<Ionicons name="person-outline" size={24} color="#fff" />
</Pressable>
<Pressable
style={styles.iconBtn}
onPress={() => {
console.log("Settings pressed");
}}
>
<Ionicons name="settings-outline" size={24} color="#fff" />
</Pressable>
</View>
</View>
<Pressable
onPress={() => navigation.navigate("LikedTracks", { tracks: likedTracks })}
>
<Pressable onPress={() => navigation.navigate("LikedTracks")}>
<Text style={styles.sectionTitle}>Liked tracks </Text>
</Pressable>
{likedTracks.map((track) => {
const album = likedAlbums.find((a) => a.id === track.albumId);
{homeLikedTracks.map((track) => {
const album = albums.find((a) => a.id === track.albumId);
return (
<TrackRow
key={track.id}
title={track.title}
artist={album?.artist ?? "Unknown artist"}
artist={track.artist ?? "Unknown artist"}
duration={track.duration}
cover={album?.cover}
cover={track.cover}
showHeart={true}
liked={track.liked}
onToggleLike={() => toggleLike(track.albumId, track.id)}
onPress={() => album && navigation.navigate("Album", { album })}
/>
);
@@ -116,12 +105,9 @@ const styles = StyleSheet.create({
headerActions: {
flexDirection: "row",
alignItems: "center",
gap: 8, // if gap not supported on your RN version, use marginLeft in iconBtn
gap: 8,
},
settingsBtn: {
padding: 2,
},
loginBtn: {
iconBtn: {
padding: 2,
},
homeTitle: {
@@ -136,38 +122,6 @@ const styles = StyleSheet.create({
fontWeight: "700",
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: {
paddingBottom: 120,
},
@@ -176,14 +130,14 @@ const styles = StyleSheet.create({
marginBottom: 12,
},
albumItem: {
width: '48%',
width: "48%",
aspectRatio: 1,
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#222',
overflow: "hidden",
backgroundColor: "#222",
},
cover: {
width: '100%',
height: '100%',
width: "100%",
height: "100%",
},
});
});

View File

@@ -1,23 +1,51 @@
import React from "react";
import { View, Text, StyleSheet, FlatList, Pressable } from "react-native";
import library from "../data/library";
import { useMemo, useState } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
Pressable,
TextInput,
} from "react-native";
import TrackRow from "../components/TrackRow";
import { useLibrary } from "../contexts/LibraryContext";
export default function LikedTracksScreen({ navigation }) {
const likedTracks = library.flatMap((album) =>
album.tracks
.filter((track) => track.liked)
.map((track) => ({
...track,
albumId: album.id,
albumTitle: album.title,
artist: album.artist,
cover: album.cover,
}))
);
const [query, setQuery] = useState("");
const [sortDir, setSortDir] = useState("desc");
const { albums, likedTracks, toggleLike } = useLibrary();
// testing only
const tracks = likedTracks.slice(0, 2);
const [sortBy, setSortBy] = useState("dateAdded");
const [showSortMenu, setShowSortMenu] = useState(false);
const filteredAndSortedTracks = useMemo(() => {
const q = query.trim().toLowerCase();
let result = !q
? [...likedTracks]
: likedTracks.filter(
(item) =>
item.title.toLowerCase().includes(q) ||
item.artist.toLowerCase().includes(q)
);
if (sortBy === "name") {
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 (
<View style={styles.container}>
@@ -27,8 +55,52 @@ export default function LikedTracksScreen({ navigation }) {
<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
data={tracks}
data={filteredAndSortedTracks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TrackRow
@@ -38,12 +110,17 @@ export default function LikedTracksScreen({ navigation }) {
cover={item.cover}
showHeart={true}
liked={item.liked}
onToggleLike={() => toggleLike(item.albumId, item.id)}
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 });
}}
/>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>No liked tracks found.</Text>
}
keyboardShouldPersistTaps="handled"
contentContainerStyle={{ paddingBottom: 180 }}
/>
</View>
@@ -72,4 +149,69 @@ const styles = StyleSheet.create({
fontWeight: "700",
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",
},
});