Added sorting and searching of liked tracks, like/unlike button working with logic in its own context
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
6
jukebox/package-lock.json
generated
6
jukebox/package-lock.json
generated
@@ -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",
|
||||
|
||||
13
jukebox/src/components/DurationFormatter.js
Normal file
13
jukebox/src/components/DurationFormatter.js
Normal 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")}`;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
51
jukebox/src/contexts/LibraryContext.js
Normal file
51
jukebox/src/contexts/LibraryContext.js
Normal 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;
|
||||
}
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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%",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user