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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
6
jukebox/package-lock.json
generated
6
jukebox/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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',
|
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 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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}
|
||||||
onPress={() => { }}
|
cover={album.cover}
|
||||||
|
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',
|
||||||
|
|||||||
@@ -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%",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user