Added styling for liked tracks, added album screen

This commit is contained in:
2026-03-03 11:18:24 +01:00
parent 17860d3076
commit 9e2c6e02b3
12 changed files with 496 additions and 93 deletions

View File

@@ -6,6 +6,7 @@ import MediaPlayer from "./src/components/MediaPlayer";
import HomeScreen from "./src/screens/HomeScreen";
import { createContext, useState, useContext } from "react";
import LikedTracksScreen from "./src/screens/LikedTracksScreen";
import AlbumScreen from "./src/screens/AlbumScreen";
const Stack = createNativeStackNavigator();
@@ -37,6 +38,7 @@ function AppNavigator() {
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="LikedTracks" component={LikedTracksScreen} />
<Stack.Screen name="Album" component={AlbumScreen} />
</Stack.Navigator>
</NavigationContainer>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -64,6 +64,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1344,7 +1345,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
"integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1434,6 +1434,7 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -3000,9 +3001,9 @@
}
},
"node_modules/@react-native/codegen/node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -3170,6 +3171,7 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.31.tgz",
"integrity": "sha512-+YCUwtfDgsux59Q0LDHc3Zid9ih93ecUCFWZOH6/+eNoUGnWx77wjS6ZfvBO/7E+EiIup11IVShDzCHR4of8hw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^7.15.1",
"escape-string-regexp": "^4.0.0",
@@ -4005,6 +4007,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4677,6 +4680,7 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.23",
@@ -4729,6 +4733,7 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
"license": "MIT",
"peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -5382,9 +5387,9 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
@@ -6992,12 +6997,12 @@
}
},
"node_modules/minimatch": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^5.0.2"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -7006,6 +7011,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimatch/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/minimatch/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
@@ -7668,6 +7688,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7687,6 +7708,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -7717,6 +7739,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
@@ -7774,6 +7797,7 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@@ -7827,6 +7851,7 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -7837,6 +7862,7 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1",
@@ -7939,7 +7965,6 @@
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.4.tgz",
"integrity": "sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-class-properties": "7.27.1",
@@ -7964,7 +7989,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
"integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1"
@@ -7981,7 +8005,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
"integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.2",
@@ -8002,7 +8025,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
"integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -8018,7 +8040,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
"integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
@@ -8035,7 +8056,6 @@
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
"integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
@@ -8055,7 +8075,6 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -8133,9 +8152,9 @@
}
},
"node_modules/react-native/node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -8158,6 +8177,7 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8384,9 +8404,9 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -9044,9 +9064,9 @@
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -9120,6 +9140,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

@@ -10,9 +10,10 @@ export default function MediaPlayer({ title, onPress }) {
return (
<LinearGradient
colors={[
"rgba(70,70,70,0.92)", // top
"rgba(35,35,35,0.95)", // middle
"rgba(15,15,15,0.98)", // bottom
"rgba(25,25,25,0.95)", // top
"rgba(15,15,15,0.98)", // middle-top
"rgba(5,5,5,1)", // middle-bottom
"rgba(5,5,5,1)", // bottom
]}
locations={[0, 0.55, 1]}
start={{ x: 0.5, y: 0 }}

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { Pressable, View, Text, StyleSheet, Image } from 'react-native';
export default function TrackRow({
title,
artist,
duration,
cover,
onPress,
showHeart = true,
liked = false, // new prop
}) {
return (
<Pressable style={styles.trackItem} onPress={onPress}>
{cover ? <Image source={cover} style={styles.trackCover} resizeMode="cover" /> : null}
<View style={styles.trackTextBlock}>
<Text style={styles.trackTitle} numberOfLines={1}>
{title}
</Text>
<Text style={styles.trackArtist} numberOfLines={1}>
{artist}
</Text>
</View>
{duration ? <Text style={styles.trackDuration}>{duration}</Text> : null}
{showHeart ? (
<Text style={[styles.heart, liked && styles.heartLiked]}>
{liked ? '♥' : '♡'}
</Text>
) : null}
</Pressable>
);
}
const styles = StyleSheet.create({
trackItem: {
width: '100%',
minHeight: 62,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
trackCover: {
width: 46,
height: 46,
borderRadius: 6,
marginRight: 10,
},
trackTextBlock: {
flex: 1,
},
trackTitle: {
color: '#ffffff',
fontSize: 15,
fontWeight: '700',
},
trackArtist: {
color: '#ffffff',
fontSize: 15,
marginTop: 2,
},
trackDuration: {
color: '#ffffff',
fontSize: 15,
paddingHorizontal: 18,
},
heart: {
fontSize: 22,
color: '#fff',
paddingHorizontal: 10,
},
heartLiked: {
fontSize: 22,
color: '#ff4d6d',
paddingHorizontal: 10,
},
});

View File

@@ -0,0 +1,66 @@
const library = [
{
id: 'album1',
title: 'Soundtracks For The Blind',
artist: 'Swans',
date: '1996-11-29',
label: 'Young God Records',
duration: '2:21:25',
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: 'album2',
title: 'Discovery',
artist: 'Daft Punk',
date: '2001-03-12',
label: 'Virgin Records',
duration: '1:00:50',
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 },
],
},
];
export default library;

View File

@@ -0,0 +1,122 @@
import React from 'react';
import { View, Text, StyleSheet, FlatList, Pressable, Image } from 'react-native';
import TrackRow from '../components/TrackRow';
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>
<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>
);
}
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',
},
});

View File

@@ -6,56 +6,94 @@ import {
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 likedTracks = [
"Track 1",
"Track 2",
"Track 3",
"Track 4",
"Track 5",
"Track 6",
];
const likedAlbums = library;
const likedAlbums = [
"Album 1",
"Album 2",
"Album 3",
"Album 4",
"Album 5",
"Album 6",
"Album 7",
"Album 8",
];
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() {
const navigation = useNavigation();
return (
<View style={styles.container}>
<View style={styles.headerRow}>
<Text style={styles.homeTitle}>Home</Text>
<View style={styles.headerActions}>
<Pressable
onPress={() =>
navigation.navigate("LikedTracks", { tracks: likedTracks })
}
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>
</View>
<Pressable
onPress={() => navigation.navigate("LikedTracks", { tracks: likedTracks })}
>
<Text style={styles.sectionTitle}>Liked tracks </Text>
</Pressable>
{likedTracks.map((item, index) => (
<View key={index} style={styles.trackItem} />
))}
{likedTracks.map((track) => {
const album = likedAlbums.find((a) => a.id === track.albumId);
<Text style={[styles.sectionTitle, { marginTop: 24 }]}>
Liked albums
</Text>
return (
<TrackRow
key={track.id}
title={track.title}
artist={album?.artist ?? "Unknown artist"}
duration={track.duration}
cover={album?.cover}
showHeart={true}
liked={track.liked}
onPress={() => album && navigation.navigate("Album", { album })}
/>
);
})}
<Text style={[styles.sectionTitle, { marginTop: 24 }]}>Liked albums</Text>
<FlatList
data={likedAlbums}
keyExtractor={(_, index) => index.toString()}
keyExtractor={(item) => item.id}
numColumns={2}
columnWrapperStyle={styles.albumRow}
contentContainerStyle={styles.albumList}
renderItem={() => <View style={styles.albumItem} />}
renderItem={({ item }) => (
<Pressable
style={styles.albumItem}
onPress={() => navigation.navigate("Album", { album: item })}
>
<Image source={item.cover} style={styles.cover} resizeMode="cover" />
</Pressable>
)}
showsVerticalScrollIndicator={false}
/>
</View>
@@ -69,34 +107,83 @@ const styles = StyleSheet.create({
paddingTop: 24,
paddingHorizontal: 16,
},
headerRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
},
headerActions: {
flexDirection: "row",
alignItems: "center",
gap: 8, // if gap not supported on your RN version, use marginLeft in iconBtn
},
settingsBtn: {
padding: 2,
},
loginBtn: {
padding: 2,
},
homeTitle: {
color: "#ffffff",
fontSize: 32,
fontWeight: "700",
marginBottom: 12,
},
sectionTitle: {
color: "#fff",
fontSize: 34,
color: "#ffffff",
fontSize: 24,
fontWeight: "700",
marginBottom: 12,
},
trackItem: {
height: 42,
backgroundColor: "#ffd9d9",
borderWidth: 2,
borderColor: "#ff4d4d",
borderRadius: 8,
marginBottom: 10,
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, // about 5 track rows
maxHeight: 260,
},
albumList: {
paddingBottom: 120, // keep room for your media player at bottom
paddingBottom: 120,
},
albumRow: {
justifyContent: "space-between",
marginBottom: 12,
},
albumItem: {
width: "48%",
aspectRatio: 1, // square
backgroundColor: "#f7d7a6",
borderRadius: 18,
width: '48%',
aspectRatio: 1,
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#222',
},
cover: {
width: '100%',
height: '100%',
},
});

View File

@@ -1,8 +1,23 @@
import React from "react";
import { View, Text, StyleSheet, FlatList, Pressable } from "react-native";
import library from "../data/library";
import TrackRow from "../components/TrackRow";
export default function LikedTracksScreen({ route, navigation }) {
const tracks = route.params?.tracks ?? [];
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,
}))
);
// testing only
const tracks = likedTracks.slice(0, 2);
return (
<View style={styles.container}>
@@ -14,13 +29,22 @@ export default function LikedTracksScreen({ route, navigation }) {
<FlatList
data={tracks}
keyExtractor={(_, index) => index.toString()}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.trackItem}>
<Text style={styles.trackText}>{item}</Text>
</View>
<TrackRow
title={item.title}
artist={item.artist}
duration={item.duration}
cover={item.cover}
showHeart={true}
liked={item.liked}
onPress={() => {
const album = library.find((a) => a.id === item.albumId);
if (album) navigation.navigate("Album", { album });
}}
/>
)}
contentContainerStyle={{ paddingBottom: 24 }}
contentContainerStyle={{ paddingBottom: 180 }}
/>
</View>
);
@@ -33,18 +57,19 @@ const styles = StyleSheet.create({
paddingTop: 24,
paddingHorizontal: 16,
},
backBtn: { marginBottom: 10 },
backText: { color: "#fff", fontSize: 16 },
title: { color: "#fff", fontSize: 30, fontWeight: "700", marginBottom: 12 },
trackItem: {
height: 50,
justifyContent: "center",
paddingHorizontal: 12,
backgroundColor: "#ffd9d9",
borderWidth: 2,
borderColor: "#ff4d4d",
borderRadius: 8,
backBtn: {
marginBottom: 10,
alignSelf: "flex-start",
},
backText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
title: {
color: "#fff",
fontSize: 30,
fontWeight: "700",
marginBottom: 12,
},
trackText: { color: "#111", fontWeight: "600" },
});