Add a new ServiceSpotify, improved the structure and added new routes to modify user information and favorite music
continuous-integration/drone/push Build is passing Details

pull/19/head
Emre KARTAL 1 year ago
parent 0ce68c7c26
commit 1a0e02bdef

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

@ -57,7 +57,7 @@ class SpotifyController implements IController {
"error": "Parameter refresh_token missing" "error": "Parameter refresh_token missing"
}); });
} }
let authOptions = { const authOptions = {
method: 'POST', method: 'POST',
url: 'https://accounts.spotify.com/api/token', url: 'https://accounts.spotify.com/api/token',
data: qs.stringify({ data: qs.stringify({
@ -79,7 +79,7 @@ class SpotifyController implements IController {
"refresh_token": session.data.refresh_token, "refresh_token": session.data.refresh_token,
"expires_in": session.data.expires_in "expires_in": session.data.expires_in
}); });
} }
}) })
.catch(error => { .catch(error => {
res.status(400).send("Cannot get a new refresh token"); res.status(400).send("Cannot get a new refresh token");
@ -108,7 +108,7 @@ class SpotifyController implements IController {
json: true json: true
}; };
try { try {
var resp = await axios(authOptions); const resp = await axios(authOptions);
if (resp.status === 200) { if (resp.status === 200) {
let access_token = resp.data.access_token; let access_token = resp.data.access_token;
let expiration = resp.data.expires_in; let expiration = resp.data.expires_in;

File diff suppressed because one or more lines are too long

@ -31,6 +31,14 @@ const userSchema = new Schema({
image: { image: {
type: String, type: String,
required: true required: true
},
musics_likes: {
type: [{
idMusic: String,
idUser: String,
date: Date
}],
default: []
} }
}, },
{ timestamps: true } { timestamps: true }

@ -1,7 +1,7 @@
import Joi from 'joi'; import Joi from 'joi';
const register = Joi.object({ const register = Joi.object({
name: Joi.string().max(30).required().regex(/^[a-zA-Z0-9_]+$/) name: Joi.string().max(30).required().regex(/^\w+$/)
.message("Name should only contain alphanumeric characters (letters, numbers, and underscores)"), .message("Name should only contain alphanumeric characters (letters, numbers, and underscores)"),
email: Joi.string().email().required(), email: Joi.string().email().required(),
password: Joi.string().min(6).required(), password: Joi.string().min(6).required(),

@ -0,0 +1,5 @@
export interface IMusic {
idMusic: string;
idUser: string;
date: Date;
}

@ -1,10 +1,13 @@
import { Document } from 'mongoose'; import { Document } from 'mongoose';
import { IMusic } from './Music';
export default interface User extends Document { export default interface User extends Document {
email: string; idSpotify: string;
tokenSpotify: string;
name: string; name: string;
email: string;
password: string; password: string;
idFlad: string;
idSpotify: string;
isValidPassword(password: string): Promise<Error | boolean>; isValidPassword(password: string): Promise<Error | boolean>;
image: string;
musics_likes: IMusic[];
} }

@ -35,10 +35,10 @@ class LocationService {
return 0; return 0;
} }
else { else {
var radlat1 = Math.PI * lat1 / 180; const radlat1 = Math.PI * lat1 / 180;
var radlat2 = Math.PI * lat2 / 180; const radlat2 = Math.PI * lat2 / 180;
var theta = lon1 - lon2; const theta = lon1 - lon2;
var radtheta = Math.PI * theta / 180; const radtheta = Math.PI * theta / 180;
var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
if (dist > 1) { if (dist > 1) {

@ -1,3 +1,4 @@
import { IMusic } from "../models/Music";
import LocationSchema from "../database/LocationSchema"; import LocationSchema from "../database/LocationSchema";
import UserSchema from "../database/UserSchema"; import UserSchema from "../database/UserSchema";
import token from "./TokenService"; import token from "./TokenService";
@ -46,15 +47,73 @@ class UserService {
public async delete( public async delete(
id: string id: string
): Promise<string | Error> { ): Promise<void | Error> {
try { try {
await this.user.findByIdAndRemove(id); await this.user.findByIdAndRemove(id);
await this.location.findByIdAndRemove(id); await this.location.findByIdAndRemove(id);
return;
} catch (error: any) { } catch (error: any) {
throw new Error(error.message); throw new Error(error.message);
} }
} }
public async addMusic(userId: string, music: IMusic): Promise<string | Error> {
try {
return await this.user.findByIdAndUpdate(userId, {
$push: { musics_likes: music },
});
} catch (error: any) {
throw new Error(error.message);
}
}
public async deleteMusic(userId: string, musicId: string): Promise<boolean | Error> {
try {
const userOld = await this.user.findById(userId);
const userNew = await this.user.findByIdAndUpdate(userId, {
$pull: { musics_likes: { _id: musicId } },
}, { new: true });
if (userOld.musics_likes.length === userNew.musics_likes.length) {
return false;
}
return true;
} catch (error) {
throw new Error(error.message);
}
}
public async getMusics(userId: string): Promise<IMusic[] | Error> {
try {
const user = await this.user.findById(userId);
return user?.musics_likes || [];
} catch (error) {
throw new Error(error.message);
}
}
public async setName(userId: string, newName: string): Promise<void | Error> {
try {
await this.user.findByIdAndUpdate(
userId,
{ name: newName },
{ new: true }
);
} catch (error) {
throw new Error(error.message);
}
}
public async setEmail(userId: string, newEmail: string): Promise<void | Error> {
try {
await this.user.findByIdAndUpdate(
userId,
{ email: newEmail },
{ new: true }
);
} catch (error) {
throw new Error(error.message);
}
}
} }
export default UserService; export default UserService;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -1,8 +0,0 @@
export default function AdjustSize(Text: string) {
const titleLength = Text.length;
const minFontSize = 23;
const maxFontSize = 48;
const fontRatio = 1.1;
const fontSize = Math.max(minFontSize, maxFontSize - (titleLength * fontRatio));
return fontSize;
}

@ -1,44 +0,0 @@
import { View } from "react-native";
import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle } from "react-native-reanimated";
const halfPi = Math.PI / 2;
export default function AnimatedParalax() {
const sensor = useAnimatedSensor(SensorType.ROTATION);
const styleAniamatedImage = useAnimatedStyle(() => {
const { pitch, roll } = sensor.sensor.value;
const verticalAxis = interpolate(
pitch,
[-halfPi, halfPi],
[-25, 25]
)
const horizontalAxis = interpolate(
roll,
[-halfPi * 2, halfPi * 2],
[-35, 35]
)
return {
top: verticalAxis,
left: horizontalAxis,
};
})
return (
<View style={{ flex: 1, justifyContent: 'flex-start', alignItems: 'center' }}>
<Animated.Image
source={{
uri: spot.sourceUrl,
}}
style={[
{
width: 370,
height: 370,
borderRadius: 24,
resizeMode: 'stretch',
}, styleAniamatedImage
]}
/>
</View>
);
};

@ -1,71 +0,0 @@
import { View, StyleSheet, Dimensions, Image, TouchableOpacity } from "react-native";
import Animated, {
Layout,
ZoomIn,
ZoomOut,
} from "react-native-reanimated";
import { Feather as Icon } from "@expo/vector-icons";
import Music from "../models/Music";
import { useState } from "react";
const { width } = Dimensions.get("window");
const SIZE = width / 3;
interface ArtistProps {
artist: Music;
onPress: () => void;
}
export default function Artist({ artist, onPress }: ArtistProps) {
const source = typeof artist.image === 'string' ? { uri: artist.image } : artist.image;
const [selected, setSeleted] = useState(false);
const onS = () => {
setSeleted(!selected);
onPress();
};
return (
<TouchableOpacity onPress={onS}>
<Animated.View
style={styles.container}
entering={ZoomIn}
exiting={ZoomOut}
layout={Layout.delay(200)}
>
<View style={styles.card}>
<Image source={source} style={styles.image} />
{selected && (
<View style={styles.cheked}>
<Icon name="check-circle" color="black" size={24} />
</View>
)
}
</View>
</Animated.View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
width: SIZE,
height: SIZE,
padding: 8,
},
card: {
flex: 1,
padding: 8,
alignItems: "flex-end",
},
image: {
borderRadius: 8,
...StyleSheet.absoluteFillObject,
width: undefined,
height: undefined,
},
cheked: {
backgroundColor: "white",
borderRadius: 100,
alignItems: "center",
}
});

@ -1,38 +0,0 @@
import { useCallback } from 'react';
import { View, Text, Image, Pressable, Linking, Alert } from 'react-native'
import Artist from '../models/Artist';
interface ArtistChipProps {
backgroundColor: string;
artist: Artist;
}
export default function ArtistChip({ artist }: ArtistChipProps) {
const handlePress = useCallback(async () => {
const supported = await Linking.canOpenURL(artist.url);
if (supported) {
await Linking.openURL(artist.url);
} else {
Alert.alert(`Don't know how to open this URL: ${artist.url}`);
}
}, [artist.url]);
return (
<View>
<Pressable onPress={handlePress}>
<View>
<Image
source={{
uri: artist.image,
}}
></Image>
</View>
<View>
<Text>ii</Text>
</View>
</Pressable>
</View>
);
};

@ -26,8 +26,8 @@ export default function CardMusic(props: CardMusicProps) {
marginBottom: 15 marginBottom: 15
}, },
imageContainer: { imageContainer: {
width: normalize(92), width: normalize(82),
height: normalize(92), height: normalize(82),
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 20, marginRight: 20,

@ -1,34 +0,0 @@
import { View, StyleSheet } from 'react-native'
import Animated, { lessThan, multiply } from 'react-native-reanimated';
import HalfCirlce from './HalfCircle';
interface CircularProps {
background: string,
foreground: string,
progress: Animated.Value<number>,
radius: number;
}
const PI = Math.PI;
export default function FladInput({ background, foreground, progress }: CircularProps) {
const theta = multiply(progress, 2 * PI);
const opacity = lessThan(theta, PI);
return (
<>
<View style={{ zIndex: 1 }}>
<HalfCirlce backgroundColor={background} />
<Animated.View style={{ ...StyleSheet.absoluteFillObject, transform: [{ rotate: '180%' }], opacity }}>
<HalfCirlce backgroundColor={background} />
</Animated.View>
</View>
<View style={{ transform: [{ rotate: '180%' }] }}>
<HalfCirlce backgroundColor={background} />
<Animated.View>
<HalfCirlce backgroundColor={background} />
</Animated.View>
</View>
</>
);
};

@ -61,8 +61,8 @@ export default function Friend(props: FriendProps) {
justifyContent: 'center', justifyContent: 'center',
}, },
button: { button: {
width: normalize(13), width: normalize(9),
height: normalize(13), height: normalize(15),
marginRight: 42 marginRight: 42
} }
}) })

@ -1,20 +0,0 @@
import { View } from 'react-native'
interface HalfCirlceProps {
backgroundColor: string;
}
export default function HalfCirlce({ backgroundColor }: HalfCirlceProps) {
return (
<View style={{
width: RADIUS * 2,
height: RADIUS * 2,
overflow: "hidden",
}}>
<View style={{ backgroundColor: backgroundColor, width: RADIUS * 2, height: RADIUS * 2, borderRadius: RADIUS, }}>
</View>
</View>
);
};

@ -7,8 +7,8 @@ interface HorizontalFlatListProps {
title: string; title: string;
data: any[]; data: any[];
} }
export const HorizontalFlatList = ({ title, data, children: RenderCell }: HorizontalFlatListProps) => {
export default function HorizontalFlatList({ title, data, children: RenderCell }: HorizontalFlatListProps) {
return ( return (
<View style={styles.similarSection}> <View style={styles.similarSection}>
<Text style={styles.similarTitle} >{title}</Text> <Text style={styles.similarTitle} >{title}</Text>

@ -26,7 +26,7 @@ export default function Loading() {
borderWidth: size / 10, borderWidth: size / 10,
borderColor: "#F80404", borderColor: "#F80404",
shadowColor: "#F40C1C", shadowColor: "#F40C1C",
shadowOffset: { width: 0, height: 0 }, //shadowOffset: { width: 0, height: 0 },
shadowOpacity: 1, shadowOpacity: 1,
shadowRadius: 10, shadowRadius: 10,
}; };

@ -8,7 +8,7 @@ export default function Paginator({ data, scrollX }) {
return ( return (
<View style={{ flexDirection: 'row', height: 64, marginBottom: normalize(50) }}> <View style={{ flexDirection: 'row', height: 64, marginBottom: normalize(50) }}>
{data.map((_, i) => { {data.map((_ : any, i : any) => {
const inputRange = [(i - 1) * width, i * width, (i + 1) * width]; const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
const dotWidth = scrollX.interpolate({ const dotWidth = scrollX.interpolate({

@ -0,0 +1,31 @@
import { View, Text, StyleSheet, Image } from 'react-native';
export interface RenderCellProps {
data : any;
}
export const LittleCard = (props: RenderCellProps) => {
return (
<View style={styles.similarContainer}>
<Image source={{ uri: props.data.cover }} style={styles.similarPoster}></Image>
<Text numberOfLines={2} style={styles.similarTitleFilm}>{props.data.name}
</Text>
</View>
)
}
const styles = StyleSheet.create({
similarContainer: {
marginHorizontal: 7
},
similarTitleFilm: {
color: "#DADADA",
paddingTop: 5,
fontWeight: "300"
},
similarPoster: {
height: 160,
width: 160,
borderRadius: 16
}
})

@ -1,54 +0,0 @@
import Music from "../models/Music";
import { Spot } from "../models/Spot";
export const spotsData: Spot[] = [
new Spot("1", new Music("6KNw3UKRp3QRsO7Cf4ASVE",
"MOLLY - A COLORS SHOW",
"Tame Impala",
"https://i.scdn.co/image/ab67616d0000b2734299eb40408fc73ce8bf490a",
"https://p.scdn.co/mp3-preview/4faf99856f15e03a09d50b91006efd3205606866?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("2", new Music("5yHoANSze7sGzhn9MUarH3",
"Passat",
"Silk Sonic, Bruno Mars, Anderson .Paak",
"https://i.scdn.co/image/ab67616d0000b273e9df9b5a7df491536c51c922",
"https://p.scdn.co/mp3-preview/0bb7472026a00790950fc231fe61963ef7cc867b?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("3", new Music("7suNqxRED5CrwyZSzYC0nT",
"Extendo",
"Kali Uchis",
"https://i.scdn.co/image/ab67616d0000b273b856464c40a062d1723a21f2",
"https://p.scdn.co/mp3-preview/5398121f6295965e3c7cad8a6dca5667ba7f4713?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("4", new Music("07JqNLmPUJSlcouGQoJlzq",
"Addiction",
"Harry Styles",
"https://i.scdn.co/image/ab67616d0000b2739297f238f237431d56c67460",
"https://p.scdn.co/mp3-preview/33d12e9e5a3dd3394b1649d515912260b01579dd?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("5", new Music("5Ylp75kdffyJSwISRPqEiL",
"La Vidéo",
"Harry Styles",
"https://i.scdn.co/image/ab67616d0000b2738900d48677696015bf325b8b",
"https://p.scdn.co/mp3-preview/4fff3f8d76a422f42cea39f001836a3d54937fc4?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("6", new Music("30d0q6kt1BIfwAQUCAfxVQ",
"Calme toi",
"Kerchack",
"https://i.scdn.co/image/ab67616d0000b273b4f73fb5c5ea299c7ebfbf60",
"https://p.scdn.co/mp3-preview/5de1103b9528c1e47e03d32b0aa5dbfe797191a2?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("7", new Music("7IXQrRgmHxWYWitSlyFY7z",
"Peur (feat. Ziak)",
"Ziak",
"https://i.scdn.co/image/ab67616d0000b273b533a3a5625bc6ce1be56a2e",
"https://p.scdn.co/mp3-preview/2c613f31b11375980aba80a5b535bf87ddb6211b?cid=774b29d4f13844c495f206cafdad9c86")
),
new Spot("8", new Music("7wBoSW48q4ZFe8qSdozqqi",
"Blue",
"Kerchack",
"https://i.scdn.co/image/ab67616d0000b273cc4e66af40292c9d92146909",
"https://p.scdn.co/mp3-preview/401b51374dd3f2a15466a0b415a9ac7d2114a54b?cid=774b29d4f13844c495f206cafdad9c86")
)
];

@ -1,7 +0,0 @@
Object.defineProperty(exports, '__esModule', {value: true});
require('./mqttLib');
const storage = require('./storage');
function initialize() {
global.localStorage = storage;
}
exports.default = initialize;

File diff suppressed because it is too large Load Diff

@ -1,10 +0,0 @@
const storage = {
setItem: (key, item) => {
storage[key] = item;
},
getItem: key => storage[key],
removeItem: key => {
delete storage[key];
},
};
export default storage;

@ -1,15 +1,45 @@
export default class Artist { export default class Artist {
private id: string; private _id: string;
private name: string; private _name: string;
private _image: string;
private _url: string; private _url: string;
constructor(id: string, name: string, url: string) { constructor(id: string, name: string, image: string, url: string) {
this.id = id; this._id = id;
this.name = name; this._name = name;
this._image = image;
this._url = url; this._url = url;
} }
get id(): string {
return this._id;
}
set id(value: string) {
this._id = value;
}
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
get image(): string {
return this._image;
}
set image(value: string) {
this._image = value;
}
get url(): string { get url(): string {
return this.url; return this._url;
}
set url(value: string) {
this._url = value;
} }
} }

@ -1,15 +1,35 @@
import Artist from "./Artist";
export default class Music { export default class Music {
private _id: string; private _id: string;
private _title: string; private _name: string;
private _bio: string; private _url: string;
private _image: string; private _artists: Artist[];
private _cover: string;
private _date: number;
private _duration: number;
private _explicit: boolean = false;
private _trackPreviewUrl: string; private _trackPreviewUrl: string;
constructor(id: string, title: string, bio: string, image: string, trackPreviewUrl: string) { constructor(
this._title = title; id: string,
this._bio = bio; name: string,
this._image = image; url: string,
artists: Artist[],
cover: string,
date: number,
duration: number,
explicit: boolean,
trackPreviewUrl: string
) {
this._id = id; this._id = id;
this._name = name;
this._url = url;
this._artists = artists;
this._cover = cover;
this._date = date;
this._duration = duration;
this._explicit = explicit;
this._trackPreviewUrl = trackPreviewUrl; this._trackPreviewUrl = trackPreviewUrl;
} }
@ -21,28 +41,60 @@ export default class Music {
this._id = value; this._id = value;
} }
get title(): string { get name(): string {
return this._title; return this._name;
}
set name(value: string) {
this._name = value;
}
get url(): string {
return this._url;
}
set url(value: string) {
this._url = value;
}
get artists(): Artist[] {
return this._artists;
} }
set title(value: string) { set artists(value: Artist[]) {
this._title = value; this._artists = value;
} }
get bio(): string { get cover(): string {
return this._bio; return this._cover;
} }
set bio(value: string) { set cover(value: string) {
this._bio = value; this._cover = value;
} }
get image(): string { get date(): number {
return this._image; return this._date;
} }
set image(value: string) { set date(value: number) {
this._image = value; this._date = value;
}
get duration(): number {
return this._duration;
}
set duration(value: number) {
this._duration = value;
}
get explicit(): boolean {
return this._explicit;
}
set explicit(value: boolean) {
this._explicit = value;
} }
get trackPreviewUrl(): string { get trackPreviewUrl(): string {
@ -53,3 +105,4 @@ export default class Music {
this._trackPreviewUrl = value; this._trackPreviewUrl = value;
} }
} }

@ -0,0 +1,15 @@
import EmptyMusicService from "../services/EmptyMusicService";
import IMusicService from "../services/musics/interfaces/IMusicService";
import SpotifyService from "../services/musics/spotify/SpotifyService";
export class MusicServiceProvider {
static musicService: IMusicService;
static initSpotify(refreshToken: string, idSpotify: string) {
this.musicService = new SpotifyService(refreshToken, idSpotify);
}
static resetService() {
this.musicService = new EmptyMusicService();
}
}

@ -0,0 +1,19 @@
export class Person {
private _id: string;
private _name: string;
public image: string;
constructor(id: string, idSpotify: string, name: string, email: string, creationDate: Date, image: string) {
this._id = id;
this._name = name;
this.image = image;
}
get id(): string {
return this._id;
}
get name(): string {
return this._name;
}
}

@ -1,20 +1,23 @@
import Music from "./Music"; import Music from "./Music";
import { Person } from "./Person";
export class Spot { export class Spot {
private _userId: string; private _user: string;
private _music: Music; private _music: Music;
private _date: Date;
constructor(userId: string, music: Music) { constructor(userId: string, music: Music, date: Date) {
this._userId = userId; this._user = userId;
this._music = music; this._music = music;
this._date = date;
} }
get userSpotifyId(): string { get userSpotifyId(): string {
return this._userId; return this._user;
} }
set userSpotifyId(value: string) { set userSpotifyId(value: string) {
this._userId = value; this._user = value;
} }
get music(): Music { get music(): Music {
@ -24,4 +27,12 @@ export class Spot {
set music(value: Music) { set music(value: Music) {
this._music = value; this._music = value;
} }
get date(): Date {
return this._date;
}
set date(value: Date) {
this._date = value;
}
} }

@ -0,0 +1,7 @@
import Artist from "../Artist";
export default class ArtistMapper {
static toModel(artist: any): Artist {
return new Artist(artist.id, artist.name, (artist?.images?.[0]?.url ?? ""), artist.external_urls.spotify);
}
}

@ -1,12 +1,18 @@
import Music from "../Music"; import Music from "../Music";
import ArtistMapper from "./ArtistMapper";
export default class MusicMapper { export default class MusicMapper {
static toModel(music: any): Music { static toModel(music: any): Music {
const artists = music.artists.map((artist: any) => ArtistMapper.toModel(artist));
return new Music( return new Music(
music.id, music.id,
music.name, music.name,
music.artists[0].name, music.external_urls.spotify,
artists,
music.album.images[0].url, music.album.images[0].url,
music.album.release_date.split('-')[0],
music.duration_ms / 1000,
music.explicit,
music.preview_url music.preview_url
); );
} }

@ -0,0 +1,7 @@
import { Spot } from "../Spot";
export class SpotMapper {
public static toModel(spot: any): Spot {
return new Spot(spot.idUser, spot.music, new Date(spot.date));
}
}

@ -16,17 +16,17 @@ export default function AuthNavigation() {
const [appIsReady, setAppIsReady] = useState(false); const [appIsReady, setAppIsReady] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
async function prepare() {
//@ts-ignore
await dispatch(getRefreshToken())
}
async function check() { async function check() {
if (tokenProcessed && appIsReady) { if (tokenProcessed && appIsReady) {
await SplashScreen.hideAsync(); await SplashScreen.hideAsync();
} }
} }
async function prepare() {
//@ts-ignore
dispatch(getRefreshToken())
}
async function initDarkMode() { async function initDarkMode() {
const currentValue: string | null = await AsyncStorage.getItem('dark'); const currentValue: string | null = await AsyncStorage.getItem('dark');
if (currentValue) { if (currentValue) {

@ -6,16 +6,16 @@ import { createSharedElementStackNavigator } from 'react-navigation-shared-eleme
const Stack = createSharedElementStackNavigator(); const Stack = createSharedElementStackNavigator();
export default function MusicNavigation() { export default function MusicNavigation() {
return ( return (
<Stack.Navigator initialRouteName="Favorite" screenOptions={{ gestureEnabled: true, headerShown: false, cardOverlayEnabled: true, cardStyle: { backgroundColor: "transparent" } }} > <Stack.Navigator initialRouteName="Favorite">
<Stack.Screen <Stack.Screen
name="Favorite" name="Favorite"
component={Favorite} component={Favorite}
options={{ headerShown: false }}
/> />
<Stack.Screen <Stack.Screen
name="Detail" name="Detail"
component={DetailScreen} component={DetailScreen}
sharedElements={(route) => { return [route.params.music.id] }} options={{ headerShown: false }}
/> />
</Stack.Navigator> </Stack.Navigator>
) )

@ -1,10 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { View, StyleSheet, Platform } from 'react-native'; import { View, StyleSheet, Platform, Alert } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import FavoriteNavigation from './FavoriteNavigation'; import FavoriteNavigation from './FavoriteNavigation';
import SettingNavigation from './SettingNavigation'; import SettingNavigation from './SettingNavigation';
import normalize from '../components/Normalize'; import normalize from '../components/Normalize';
// @ts-ignore // @ts-ignore
import FontAwesome from 'react-native-vector-icons/FontAwesome'; import FontAwesome from 'react-native-vector-icons/FontAwesome';
@ -13,89 +12,18 @@ import MessagingNavigation from './MessagingNavigation';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { colorsDark } from '../constants/colorsDark'; import { colorsDark } from '../constants/colorsDark';
import { colorsLight } from '../constants/colorsLight'; import { colorsLight } from '../constants/colorsLight';
import { getCurrentUserMusic, getSpotList } from '../redux/thunk/spotThunk'; import { getUserCurrentMusic } from '../redux/thunk/appThunk';
import SpotifyService from '../services/spotify/spotify.service'; import { logout } from '../redux/thunk/authThunk';
import * as SecureStore from 'expo-secure-store'; import { setAccessError, setErrorEmptyMusic } from '../redux/actions/userActions';
import * as Location from 'expo-location';
import axios from 'axios';
import qs from 'qs';
const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKeySpotify';
export default function HomeNavigation() { export default function HomeNavigation() {
const [setErrorMsg] = useState('');
//@ts-ignore //@ts-ignore
const tokenSend: string = useSelector(state => state.userReducer.userFladToken); const favoritesMusicLength = useSelector(state => state.appReducer.nbAddedFavoritesMusic);
//@ts-ignore //@ts-ignore
const currentMusic: Music = useSelector(state => state.appReducer.userCurrentMusic); const accessError = useSelector(state => state.userReducer.accessError);
//@ts-ignore
const dispatch = useDispatch(); const errorEmptyMusic = useSelector(state => state.userReducer.errorEmptyMusic);
const requestLocationPermission = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
console.log('Permission to access location was denied');
} else {
console.log('Permission to access location was granted');
}
}
useEffect(() => {
requestLocationPermission();
const sendLocationUpdate = async () => {
try {
let tmpKey: string = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY);
//@ts-ignore
dispatch(getCurrentUserMusic(new SpotifyService(tmpKey)))
let { status } = await Location.requestForegroundPermissionsAsync();
if (status == 'granted') {
// should app is ready
const locationresp = await Location.getCurrentPositionAsync({});
// send location to server
if (currentMusic) {
const body: Record<string, string | boolean | number | (string | boolean | number)[]> = {
longitude: locationresp.coords.longitude,
latitude: locationresp.coords.latitude,
currentMusic: currentMusic.id
}
const resp = await axios({
url: 'https://flad-api-production.up.railway.app/api/users/nextTo?' + qs.stringify(body),
method: 'GET',
headers: {
Authorization: `Bearer ${tokenSend}`,
},
});
const datat: Record<string, string> = resp.data.listUser2;
//@ts-ignore
dispatch(getSpotList(datat, new SpotifyService(tmpKey)))
}
else {
return;
}
}
else {
//@ts-ignore
let { status } = Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
return;
}
} catch (error) {
console.log(error);
}
};
const interval = setInterval(sendLocationUpdate, 30000);
return () => {
clearInterval(interval);
};
}, [currentMusic]);
// @ts-ignore // @ts-ignore
const isDark = useSelector(state => state.userReducer.dark); const isDark = useSelector(state => state.userReducer.dark);
const style = isDark ? colorsDark : colorsLight; const style = isDark ? colorsDark : colorsLight;
@ -109,8 +37,51 @@ export default function HomeNavigation() {
text: 'rgb(138, 138, 138)', text: 'rgb(138, 138, 138)',
} }
}; };
//@ts-ignore
const favoritesMusicLength: number = useSelector(state => state.appReducer.favoriteMusic.length); const dispatch = useDispatch();
useEffect(() => {
//@ts-ignore
dispatch(getUserCurrentMusic());
}, []);
useEffect(() => {
if (accessError) {
Alert.alert(
"Problème lié à votre compte",
"Votre compte ne fait plus partie des utilisateurs ayant accès à l'application. Pour plus d'informations, veuillez contacter l'équipe de support à l'adresse suivante : fladdevpro@gmail.com.",
[
{
text: 'Réessayer plus tard',
onPress: () => {
dispatch(setAccessError(false))
//@ts-ignore
dispatch(logout());
},
},
],
{ cancelable: false }
);
}
}, [accessError]);
useEffect(() => {
if (errorEmptyMusic) {
Alert.alert(
"Bienvenue sur FLAD 🎵",
"Votre compte Spotify semble tout neuf, donc pour le moment, vous ne pouvez pas encore partager de musique.\n\n" +
"Pas encore de playlist secrète ? Aucun morceau honteux ? Nous attendons impatiemment vos découvertes musicales !",
[
{
text: "D'accord",
onPress: () => dispatch(setErrorEmptyMusic(false)),
}
]
);
}
}, [errorEmptyMusic]);
return ( return (
// @ts-ignore // @ts-ignore
<NavigationContainer theme={MyTheme}> <NavigationContainer theme={MyTheme}>

@ -1,23 +1,21 @@
import React from 'react'; import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import ConversationScreen from '../screens/ConversationScreen' import ConversationScreen from '../screens/ConversationScreen'
import ChatScreen from '../screens/ChatScreen'; import ChatScreen from '../screens/ChatScreen';
import { createStackNavigator } from '@react-navigation/stack';
export default function MessagingNavigation() { export default function MessagingNavigation() {
const Stack = createStackNavigator(); const Stack = createStackNavigator();
return ( return (
<Stack.Navigator initialRouteName="Conversation" screenOptions={{ gestureEnabled: true, headerShown: false, cardOverlayEnabled: true, cardStyle: { backgroundColor: "transparent" } }}> <Stack.Navigator initialRouteName="Conversation" >
<Stack.Screen <Stack.Screen
name="Conversation" name="Conversation"
component={ConversationScreen} component={ConversationScreen}
options={{ headerShown: false }}
/> />
<Stack.Screen <Stack.Screen
name="Chat" name="Chat"
component={ChatScreen} component={ChatScreen}
options={{ options={{ headerShown: false }}
headerShown: true
}}
/> />
</Stack.Navigator> </Stack.Navigator>
) )

@ -1,27 +1,21 @@
import React from 'react'; import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import SpotScreen from '../screens/SpotScreen' import SpotScreen from '../screens/SpotScreen'
import DetailScreen from '../screens/DetailScreen'; import DetailScreen from '../screens/DetailScreen';
import { createStackNavigator } from '@react-navigation/stack';
export default function SpotNavigation() { export default function SpotNavigation() {
const Stack = createStackNavigator(); const Stack = createStackNavigator();
return ( return (
<Stack.Navigator screenOptions={{ <Stack.Navigator>
gestureEnabled: false,
headerShown: false,
cardOverlayEnabled: true,
}}
>
<Stack.Screen <Stack.Screen
name="Spot" name="Spot"
component={SpotScreen} component={SpotScreen}
options={{ headerShown: false }}
/> />
<Stack.Screen <Stack.Screen
name="Detail" name="Detail"
component={DetailScreen} component={DetailScreen}
options={{ headerShown: false }}
/> />
</Stack.Navigator> </Stack.Navigator>
) )

@ -36,7 +36,6 @@
"react-native-reanimated": "~2.12.0", "react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1", "react-native-safe-area-context": "4.4.1",
"react-native-screens": "~3.18.0", "react-native-screens": "~3.18.0",
"react-native-shared-element": "0.8.4",
"react-native-svg": "13.4.0", "react-native-svg": "13.4.0",
"react-native-vector-icons": "^9.2.0", "react-native-vector-icons": "^9.2.0",
"react-navigation-shared-element": "^3.1.3", "react-navigation-shared-element": "^3.1.3",

@ -1,13 +1,22 @@
import Music from "../../models/Music"; import Music from "../../models/Music";
import { Spot } from "../../models/Spot";
import { favoritesTypes } from "../types/favoritesTypes"; import { favoritesTypes } from "../types/favoritesTypes";
import { spotifyTypes } from "../types/spotifyTypes";
export const getFavoritesMusic = (music: Music[]) => { export const setUserCurrentMusic = (music: Music | null) => {
return { return {
type: favoritesTypes.GET_FAVORITE_MUSICS, type: spotifyTypes.GET_USER_CURRENT_MUSIC,
payload: music, payload: music,
}; };
} }
export const setFavoriteMusic = (spots: Spot[]) => {
return {
type: favoritesTypes.GET_FAVORITE_MUSICS,
payload: spots,
};
}
export const addFavoritesMusic = (music: Music) => { export const addFavoritesMusic = (music: Music) => {
return { return {
type: favoritesTypes.ADD_FAVORITE_MUSICS, type: favoritesTypes.ADD_FAVORITE_MUSICS,

@ -1,6 +1,5 @@
import Music from "../../models/Music"; import Music from "../../models/Music";
import { Spot } from "../../models/Spot"; import { Spot } from "../../models/Spot";
import { spotifyTypes } from "../types/spotifyTypes";
import { spotTypes } from "../types/spotTypes"; import { spotTypes } from "../types/spotTypes";
export const setSpotList = (spotList: Spot[]) => { export const setSpotList = (spotList: Spot[]) => {
@ -9,6 +8,7 @@ export const setSpotList = (spotList: Spot[]) => {
payload: spotList, payload: spotList,
}; };
} }
export const removeFromSpotList = (spot: Spot) => { export const removeFromSpotList = (spot: Spot) => {
return { return {
type: spotTypes.REMOVE_SPOT, type: spotTypes.REMOVE_SPOT,
@ -21,11 +21,4 @@ export const addSpotListMock = (spotList: Spot[]) => {
type: spotTypes.ADD_SPOT_MOCK, type: spotTypes.ADD_SPOT_MOCK,
payload: spotList, payload: spotList,
}; };
}
export const setUserCurrentMusic = (currentMusic: Music) => {
return {
type: spotifyTypes.GET_USER_CURRENT_MUSIC,
payload: currentMusic,
};
} }

@ -26,13 +26,6 @@ export const restoreToken = () => {
}; };
} }
export const userSignUp = (user: User) => {
return {
type: userTypes.SIGNUP,
payload: user
};
}
export const userLogout = () => { export const userLogout = () => {
return { return {
type: userTypes.USER_LOGOUT, type: userTypes.USER_LOGOUT,
@ -41,28 +34,56 @@ export const userLogout = () => {
export const setDarkMode = (value: boolean) => { export const setDarkMode = (value: boolean) => {
return { return {
type: userTypes.DARK_MODE, type: userTypes.SET_DARK_MODE,
payload: value payload: value
}; };
} }
export const setErrorLogin = (value: boolean) => { export const setErrorLogin = (value: boolean) => {
return { return {
type: userTypes.ERROR_LOGIN, type: userTypes.SET_ERROR_LOGIN,
payload: value payload: value
}; };
} }
export const setErrorSignup = (value: string) => { export const setErrorSignup = (value: string) => {
return { return {
type: userTypes.ERROR_SIGNUP, type: userTypes.SET_ERROR_SIGNUP,
payload: value payload: value
}; };
} }
export const setErrorNetwork = (value: boolean) => { export const setErrorNetwork = (value: boolean) => {
return { return {
type: userTypes.ERROR_NETWORK, type: userTypes.SET_ERROR_NETWORK,
payload: value
};
}
export const setErrorEmptyMusic = (value: boolean) => {
return {
type: userTypes.SET_ERROR_EMPTY_MUSIC,
payload: value
};
}
export const setAccessError = (value: boolean) => {
return {
type: userTypes.SET_ERROR_ACCESS,
payload: value
};
}
export const setErrorUpdate = (value: boolean) => {
return {
type: userTypes.SET_ERROR_UPDATE,
payload: value
};
}
export const setErrorUpdateMessage = (value: string) => {
return {
type: userTypes.SET_ERROR_UPDATE_MESSAGE,
payload: value payload: value
}; };
} }

@ -1,21 +1,19 @@
import { spotsData } from "../../data/data"; import { Spot } from "../../models/Spot";
import { discoveriesTypes } from "../types/discoverieTypes";
import { favoritesTypes } from "../types/favoritesTypes"; import { favoritesTypes } from "../types/favoritesTypes";
import { spotifyTypes } from "../types/spotifyTypes"; import { spotifyTypes } from "../types/spotifyTypes";
import { spotTypes } from "../types/spotTypes"; import { spotTypes } from "../types/spotTypes";
const initialState = { const initialState = {
spot: spotsData, spot: [] as Spot[],
favoriteMusic: [], favoriteMusic: [] as Spot[],
userCurrentMusic: null userCurrentMusic: null,
nbAddedFavoritesMusic: 0
} }
const appReducer = (state = initialState, action: any) => { const appReducer = (state = initialState, action: any) => {
switch (action.type) { switch (action.type) {
case favoritesTypes.GET_FAVORITE_MUSICS: case favoritesTypes.GET_FAVORITE_MUSICS:
return { ...state, favoriteMusic: action.payload }; return { ...state, favoriteMusic: action.payload };
case favoritesTypes.ADD_FAVORITE_MUSICS:
return { ...state, favoriteMusic: [action.payload, ...state.favoriteMusic] };
case favoritesTypes.REMOVE_FAVORITE_MUSICS: case favoritesTypes.REMOVE_FAVORITE_MUSICS:
return { ...state, favoriteMusic: state.favoriteMusic }; return { ...state, favoriteMusic: state.favoriteMusic };
case spotTypes.FETCH_SPOT: case spotTypes.FETCH_SPOT:
@ -26,8 +24,6 @@ const appReducer = (state = initialState, action: any) => {
return { ...state, spot: updatedSpotList }; return { ...state, spot: updatedSpotList };
case spotTypes.REMOVE_SPOT: case spotTypes.REMOVE_SPOT:
return { ...state, spot: state.spot.filter((spot) => spot.userSpotifyId !== action.payload.userSpotifyId && spot.music.id !== action.payload.music.id) }; return { ...state, spot: state.spot.filter((spot) => spot.userSpotifyId !== action.payload.userSpotifyId && spot.music.id !== action.payload.music.id) };
case discoveriesTypes.FETCH_DISCOVERIES:
return;
case spotifyTypes.GET_USER_CURRENT_MUSIC: case spotifyTypes.GET_USER_CURRENT_MUSIC:
return { ...state, userCurrentMusic: action.payload }; return { ...state, userCurrentMusic: action.payload };
default: default:

@ -10,7 +10,11 @@ const initialState = {
failedSignup: false, failedSignup: false,
errorMessage: null, errorMessage: null,
errorNetwork: false, errorNetwork: false,
dark: null dark: null,
errorEmptyMusic: false,
accessError: false,
errorUpdateMessage: null,
errorUpdate: false,
} }
const userReducer = (state = initialState, action: any) => { const userReducer = (state = initialState, action: any) => {
@ -29,15 +33,6 @@ const userReducer = (state = initialState, action: any) => {
failedSignup: false, failedSignup: false,
errorNetwork: false errorNetwork: false
}; };
case userTypes.SIGNUP:
return {
...state,
user: action.payload,
isLogedIn: true,
failedLogin: false,
failedSignup: false,
errorNetwork: false
};
case userTypes.USER_LOGOUT: case userTypes.USER_LOGOUT:
AsyncStorage.removeItem('dark'); AsyncStorage.removeItem('dark');
return { return {
@ -51,14 +46,22 @@ const userReducer = (state = initialState, action: any) => {
...state, ...state,
userSpotifyToken: action.payload userSpotifyToken: action.payload
}; };
case userTypes.ERROR_LOGIN: case userTypes.SET_ERROR_LOGIN:
return { ...state, failedLogin: action.payload } return { ...state, failedLogin: action.payload }
case userTypes.ERROR_SIGNUP: case userTypes.SET_ERROR_SIGNUP:
return { ...state, failedSignup: true, errorMessage: action.payload } return { ...state, failedSignup: true, errorMessage: action.payload }
case userTypes.DARK_MODE: case userTypes.SET_DARK_MODE:
return { ...state, dark: action.payload } return { ...state, dark: action.payload }
case userTypes.ERROR_NETWORK: case userTypes.SET_ERROR_NETWORK:
return { ...state, errorNetwork: action.payload } return { ...state, errorNetwork: action.payload }
case userTypes.SET_ERROR_EMPTY_MUSIC:
return { ...state, errorEmptyMusic: action.payload }
case userTypes.SET_ERROR_ACCESS:
return { ...state, accessError: action.payload }
case userTypes.SET_ERROR_UPDATE:
return { ...state, errorUpdate: action.payload }
case userTypes.SET_ERROR_UPDATE_MESSAGE:
return { ...state, errorUpdateMessage: action.payload, errorUpdate: true }
default: default:
return state; return state;
} }

@ -17,7 +17,7 @@ const store = configureStore({
reducer: reducer, reducer: reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: { serializableCheck: {
ignoredActions: [spotTypes.FETCH_SPOT, spotifyTypes.GET_USER_CURRENT_MUSIC, favoritesTypes.ADD_FAVORITE_MUSICS, favoritesTypes.REMOVE_FAVORITE_MUSICS, spotTypes.REMOVE_SPOT, userTypes.LOGIN, userTypes.SIGNUP ], ignoredActions: [spotTypes.FETCH_SPOT, spotifyTypes.GET_USER_CURRENT_MUSIC, favoritesTypes.ADD_FAVORITE_MUSICS, favoritesTypes.REMOVE_FAVORITE_MUSICS, spotTypes.REMOVE_SPOT, userTypes.LOGIN],
ignoredActionPaths: ['appReducer'], ignoredActionPaths: ['appReducer'],
ignoredPaths: ['appReducer', 'userReducer'] ignoredPaths: ['appReducer', 'userReducer']
} }

@ -0,0 +1,81 @@
import axios from "axios";
import * as SecureStore from 'expo-secure-store';
import { Spot } from "../../models/Spot";
import configs from "../../constants/config";
import { MusicServiceProvider } from "../../models/MusicServiceProvider";
import { setFavoriteMusic, setUserCurrentMusic } from "../actions/appActions";
import { setAccessError, setErrorEmptyMusic } from "../actions/userActions";
import { SpotMapper } from "../../models/mapper/SpotMapper";
export const getUserCurrentMusic = () => {
//@ts-ignore
return async dispatch => {
try {
let idTrack;
const resp = await MusicServiceProvider.musicService.getCurrentlyPlayingMusic();
if (resp === null) {
idTrack = await MusicServiceProvider.musicService.getRecentlyPlayedMusic();
if (idTrack === null) {
dispatch(setErrorEmptyMusic(true));
dispatch(setUserCurrentMusic(null));
return;
}
} else {
idTrack = resp;
}
const music = await MusicServiceProvider.musicService.getMusicById(idTrack);
dispatch(setUserCurrentMusic(music))
} catch (error: any) {
console.error("Error retrieving music currently listened : " + error);
switch (error.response.status) {
case 403:
dispatch(setAccessError(true));
break;
default:
dispatch(setAccessError(true));
break;
}
}
}
}
export const addFavoriteMusic = (spot: Spot) => {
//@ts-ignore
return async dispatch => {
};
}
export const getFavoriteMusic = () => {
//@ts-ignore
return async dispatch => {
try {
let token: string | null = await SecureStore.getItemAsync(configs.key);
const headers = {
'Authorization': 'Bearer ' + token
};
const resp = await axios.get(
configs.API_URL + '/user/musics',
{ headers }
)
const musicIds = resp.data.musics.map((music: any) => music.idMusic);
const musics = await MusicServiceProvider.musicService.getMusicsWithIds(musicIds);
const result = resp.data.musics
.filter((music: any) => musics.some((m: any) => m.id === music.idMusic))
.map((music: any) => {
const matchingMusic = musics.find((m: any) => m.id === music.idMusic);
return {
...music,
music: matchingMusic,
};
});
dispatch(setFavoriteMusic(result.map((item: any) => SpotMapper.toModel(item))));
} catch (error: any) {
console.error(error);
dispatch(setAccessError(true));
}
};
}

@ -1,10 +1,10 @@
import axios from "axios"; import axios from "axios";
import configs from "../../constants/config"; import configs from "../../constants/config";
import { LoginCredentials, RegisterCredentials, restoreToken, userLogin, userLogout, userSignUp, setErrorLogin, setErrorSignup, setErrorNetwork } from "../actions/userActions"; import { LoginCredentials, RegisterCredentials, restoreToken, userLogin, userLogout, setErrorLogin, setErrorSignup, setErrorNetwork } from "../actions/userActions";
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
import { UserMapper } from "../../models/mapper/UserMapper"; import { UserMapper } from "../../models/mapper/UserMapper";
import { MusicServiceProvider } from "../../models/MusicServiceProvider";
const key = 'userToken';
const keyRemember = 'rememberUser'; const keyRemember = 'rememberUser';
export const register = (resgisterCredential: RegisterCredentials) => { export const register = (resgisterCredential: RegisterCredentials) => {
@ -22,7 +22,7 @@ export const register = (resgisterCredential: RegisterCredentials) => {
config config
) )
const token = resp.data.token; const token = resp.data.token;
await SecureStore.setItemAsync(key, token); await SecureStore.setItemAsync(configs.key, token);
await SecureStore.setItemAsync(keyRemember, 'true'); await SecureStore.setItemAsync(keyRemember, 'true');
const headers = { const headers = {
'Authorization': 'Bearer ' + token 'Authorization': 'Bearer ' + token
@ -31,8 +31,8 @@ export const register = (resgisterCredential: RegisterCredentials) => {
configs.API_URL + '/user', configs.API_URL + '/user',
{ headers } { headers }
) )
dispatch(userSignUp(UserMapper.toModel(user.data.data))); MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify);
dispatch(userLogin(UserMapper.toModel(user.data.data)));
} catch (error: any) { } catch (error: any) {
console.error("Error : " + error.message); console.error("Error : " + error.message);
switch (error.response.status) { switch (error.response.status) {
@ -70,7 +70,7 @@ export const login = (loginCredential: LoginCredentials, remember: boolean) => {
) )
const token = resp.data.token; const token = resp.data.token;
await SecureStore.setItemAsync(key, token); await SecureStore.setItemAsync(configs.key, token);
if (remember) { if (remember) {
await SecureStore.setItemAsync(keyRemember, remember.toString()); await SecureStore.setItemAsync(keyRemember, remember.toString());
} }
@ -83,8 +83,8 @@ export const login = (loginCredential: LoginCredentials, remember: boolean) => {
configs.API_URL + '/user', configs.API_URL + '/user',
{ headers } { headers }
) )
MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify);
dispatch(userLogin(UserMapper.toModel(user.data.data))); dispatch(userLogin(UserMapper.toModel(user.data.data)));
} catch (error: any) { } catch (error: any) {
console.error("Error : " + error.message); console.error("Error : " + error.message);
switch (error.response.status) { switch (error.response.status) {
@ -103,7 +103,7 @@ export const getRefreshToken = () => {
//@ts-ignore //@ts-ignore
return async dispatch => { return async dispatch => {
let remember: string | null = await SecureStore.getItemAsync(keyRemember); let remember: string | null = await SecureStore.getItemAsync(keyRemember);
let token: string | null = await SecureStore.getItemAsync(key); let token: string | null = await SecureStore.getItemAsync(configs.key);
if (token) { if (token) {
if (remember) { if (remember) {
const headers = { const headers = {
@ -114,14 +114,13 @@ export const getRefreshToken = () => {
configs.API_URL + '/user', configs.API_URL + '/user',
{ headers } { headers }
) )
MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify);
await dispatch(userLogin(UserMapper.toModel(user.data.data))); await dispatch(userLogin(UserMapper.toModel(user.data.data)));
} catch (error: any) { } catch (error: any) {
await SecureStore.deleteItemAsync(key); dispatch(logout());
dispatch(userLogout());
} }
} else { } else {
await SecureStore.deleteItemAsync(key); dispatch(logout());
dispatch(userLogout());
} }
} }
dispatch(restoreToken()); dispatch(restoreToken());
@ -132,7 +131,7 @@ export const getRefreshToken = () => {
export const deleteUser = () => { export const deleteUser = () => {
//@ts-ignore //@ts-ignore
return async dispatch => { return async dispatch => {
let token: string | null = await SecureStore.getItemAsync(key); let token: string | null = await SecureStore.getItemAsync(configs.key);
if (token) { if (token) {
const headers = { const headers = {
'Authorization': 'Bearer ' + token 'Authorization': 'Bearer ' + token
@ -142,8 +141,7 @@ export const deleteUser = () => {
configs.API_URL + '/user', configs.API_URL + '/user',
{ headers } { headers }
) )
await SecureStore.deleteItemAsync(key); dispatch(logout());
dispatch(userLogout());
} catch (error: any) { } catch (error: any) {
console.error("Error deleting account : " + error.message); console.error("Error deleting account : " + error.message);
} }
@ -154,8 +152,9 @@ export const deleteUser = () => {
export const logout = () => { export const logout = () => {
//@ts-ignore //@ts-ignore
return async dispatch => { return async dispatch => {
await SecureStore.deleteItemAsync(key); await SecureStore.deleteItemAsync(configs.key);
await SecureStore.deleteItemAsync(keyRemember); await SecureStore.deleteItemAsync(keyRemember);
MusicServiceProvider.resetService();
dispatch(userLogout()); dispatch(userLogout());
} }
} }

@ -1,17 +0,0 @@
import axios from "axios";
import { Spot } from "../../models/Spot";
export const likeSpot = async (spot: Spot) => {
return async (dispatch) => {
axios.post("osdj").then(responce => {
if (responce.status == 200) {
dispatch(true);
}
}).catch(error => {
console.log("something goes wrong while searching : " + error);
;
})
};
}

@ -1,69 +1,9 @@
import axios from "axios"; import axios from "axios";
import * as SecureStore from 'expo-secure-store';
import { Spot } from "../../models/Spot";
import SpotifyService from "../../services/spotify/spotify.service";
import { setSpotList, setUserCurrentMusic } from "../actions/spotActions";
const key = 'userToken'; const key = 'userToken';
export type CreateSpotReqBody = { export const getSpotList = () => {
id: string;
name: string;
artist: string;
linkCover: string;
user: string;
}
export const getSpotList = (spotsData : Record<string, string> , resuestHandler: SpotifyService) => {
//@ts-ignore //@ts-ignore
return async dispatch => { return async dispatch => {
try {
//@ts-ignore
if (spotsData) {
const spots = await Promise.all(
Object.entries(spotsData).map(async ([userId, value]) => {
const completeMusic = await resuestHandler.getMusicById(value);
return new Spot(userId, completeMusic);
})
);
dispatch(setSpotList(spots)); // our action is called here
} else {
console.log('Login Failed', 'Username or Password is incorrect');
}
} catch (error) {
console.log('Error---------', error);
}
} }
} }
export const getCurrentUserMusic = (resuestHandler: SpotifyService) => {
//@ts-ignore
return async dispatch => {
try {
//@ts-ignore
var currentTrackResponse = await resuestHandler.getUserCurrentMusic();
if (!currentTrackResponse) {
const recentlyTrackResponse = await resuestHandler.getUserRecentlyPlayedMusic();
if (!recentlyTrackResponse) {
throw new Error;
} else {
currentTrackResponse = recentlyTrackResponse;
}
}
const completeMusic = await resuestHandler.getMusicById(currentTrackResponse);
if(!completeMusic){
return;
}
dispatch(setUserCurrentMusic(completeMusic));
}
catch (error) {
console.log('Error---------', error);
}
}
}
export const searchMusic = async (resuestHandler: SpotifyService, search: string) => {
return async (dispatch) => {
return resuestHandler.searchMusic(search).then(musics => dispatch((musics))).catch(err => console.log("something goes wrong while searching : " + err));
};
}

@ -1,8 +1,72 @@
import { setDarkMode } from "../actions/userActions"; import axios from "axios";
import configs from "../../constants/config";
import { setDarkMode, setErrorNetwork, setErrorUpdateMessage, userLogin } from "../actions/userActions";
import * as SecureStore from 'expo-secure-store';
import { UserMapper } from "../../models/mapper/UserMapper";
export const darkMode = (value: boolean) => { export const darkMode = (value: boolean) => {
//@ts-ignore //@ts-ignore
return async dispatch => { return async dispatch => {
dispatch(setDarkMode(value)); dispatch(setDarkMode(value));
} }
}
export const setName = (name: string) => {
//@ts-ignore
return async dispatch => {
try {
let token: string | null = await SecureStore.getItemAsync(configs.key);
const headers = {
'Authorization': 'Bearer ' + token
};
await axios.put(configs.API_URL + '/user/name', { name }, { headers });
const user = await axios.get(
configs.API_URL + '/user',
{ headers }
)
dispatch(userLogin(UserMapper.toModel(user.data.data)));
} catch (error: any) {
console.error("Error : " + error.message);
switch (error.response.status) {
case 409:
dispatch(setErrorUpdateMessage("Nom déjà utilisé."))
break;
default:
dispatch(setErrorNetwork(true));
break;
}
}
}
}
export const setMail = (email: string) => {
//@ts-ignore
return async dispatch => {
try {
let token: string | null = await SecureStore.getItemAsync(configs.key);
const headers = {
'Authorization': 'Bearer ' + token
};
await axios.put(configs.API_URL + '/user/email', { email }, { headers });
const user = await axios.get(
configs.API_URL + '/user',
{ headers }
)
dispatch(userLogin(UserMapper.toModel(user.data.data)));
} catch (error: any) {
console.error("Error : " + error.message);
switch (error.response.status) {
case 409:
dispatch(setErrorUpdateMessage("Email déjà utilisé."))
break;
default:
dispatch(setErrorNetwork(true));
break;
}
}
}
} }

@ -1,5 +1,3 @@
export const playlistTypes = { export const playlistTypes = {
FETCH_USER_PLAYLISTS: 'FETCH_SPOT',
SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST', SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST',
FETCH_FLAD_PLAYLIST: 'FETCH_SPOT',
} }

@ -1,5 +1,5 @@
export const spotTypes = { export const spotTypes = {
FETCH_SPOT: 'FETCH_SPOT', FETCH_SPOT: 'FETCH_SPOT',
ADD_SPOT_MOCK: 'ADD_SPOT_MOCK', ADD_SPOT_MOCK: 'ADD_SPOT_MOCK',
REMOVE_SPOT: 'REMOVE_SPOT', REMOVE_SPOT: 'REMOVE_SPOT'
} }

@ -1,3 +1,3 @@
export const spotifyTypes = { export const spotifyTypes = {
GET_USER_CURRENT_MUSIC: 'GET_USER_CURRENT_MUSIC', GET_USER_CURRENT_MUSIC: 'GET_USER_CURRENT_MUSIC'
} }

@ -1,12 +1,14 @@
export const userTypes = { export const userTypes = {
RESTORE_TOKEN: "RESTORE_TOKEN", RESTORE_TOKEN: "RESTORE_TOKEN",
LOGIN: 'LOGIN', LOGIN: 'LOGIN',
SIGNUP: 'SIGNUP',
USER_LOGOUT: 'USER_LOGOUT', USER_LOGOUT: 'USER_LOGOUT',
SAVE_SPOTIFY: 'SAVE_SPOTIFY', SAVE_SPOTIFY: 'SAVE_SPOTIFY',
UPDATE_USER: 'UPDATE_USER', SET_ERROR_LOGIN: "SET_ERROR_LOGIN",
ERROR_LOGIN: "ERROR_LOGIN", SET_ERROR_SIGNUP: "SET_ERROR_SIGNUP",
ERROR_SIGNUP: "ERROR_SIGNUP", SET_DARK_MODE: "SET_DARK_MODE",
DARK_MODE: "DARK_MODE", SET_ERROR_NETWORK: "SET_ERROR_NETWORK",
ERROR_NETWORK: "ERROR_NETWORK" SET_ERROR_ACCESS: "SET_ERROR_ACCESS",
SET_ERROR_EMPTY_MUSIC: "SET_ERROR_EMPTY_MUSIC",
SET_ERROR_UPDATE: "SET_ERROR_UPDATE",
SET_ERROR_UPDATE_MESSAGE: "SET_ERROR_UPDATE_MESSAGE"
} }

@ -2,21 +2,21 @@ import { useNavigation } from "@react-navigation/native";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { GiftedChat } from "react-native-gifted-chat"; import { GiftedChat } from "react-native-gifted-chat";
export default function ChatScreen() { export default function Chat() {
const navigation = useNavigation(); const navigation = useNavigation();
useEffect(() => { useEffect(() => {
navigation.getParent()?.setOptions({ navigation.getParent()?.setOptions({
tabBarStyle: { tabBarStyle: {
display: "none" display: "none"
} }
}); });
return () => navigation.getParent()?.setOptions({ return () => navigation.getParent()?.setOptions({
tabBarStyle: undefined tabBarStyle: undefined
}); });
}, [navigation]); }, [navigation]);
return ( return (
<GiftedChat /> <GiftedChat />
) )
} }

@ -14,10 +14,10 @@ export default function ConversationScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const friends = [ const friends = [
{ id: 1, name: "Lucas", lastMessage: "J'en ai marre de provot", source: "https://yt3.googleusercontent.com/CgPFZUSWbFj9txLG_8l48YRCwnrlfQya8sw_UCB-s3NGkQEnLj--KZI0CqSCyP2XqPfOB-j9yQ=s900-c-k-c0x00ffffff-no-rj" }, { id: 1, name: "Lucas", lastMessage: "J'en ai marre de provot", source: "https://i1.sndcdn.com/artworks-ncJnbnDbNOFd-0-t500x500.jpg" },
{ id: 2, name: "Louison", lastMessage: "Tu vien piscine ?", source: "https://yt3.googleusercontent.com/CgPFZUSWbFj9txLG_8l48YRCwnrlfQya8sw_UCB-s3NGkQEnLj--KZI0CqSCyP2XqPfOB-j9yQ=s900-c-k-c0x00ffffff-no-rj" }, { id: 2, name: "Louison", lastMessage: "Tu vien piscine ?", source: "https://i1.sndcdn.com/artworks-ncJnbnDbNOFd-0-t500x500.jpg" },
{ id: 3, name: "Dave", lastMessage: "Ok c noté !", source: "https://img.lemde.fr/2019/04/05/0/0/960/960/664/0/75/0/18299d3_tUvp2AZPH_jnsIL2ypVFGUro.jpg" }, { id: 3, name: "Dave", lastMessage: "Ok c noté !", source: "https://img.lemde.fr/2019/04/05/0/0/960/960/664/0/75/0/18299d3_tUvp2AZPH_jnsIL2ypVFGUro.jpg" },
{ id: 4, name: "Valentin", lastMessage: "Haha react native c incroyable !!!", source: "https://yt3.googleusercontent.com/CgPFZUSWbFj9txLG_8l48YRCwnrlfQya8sw_UCB-s3NGkQEnLj--KZI0CqSCyP2XqPfOB-j9yQ=s900-c-k-c0x00ffffff-no-rj" }, { id: 4, name: "Valentin", lastMessage: "Haha react native c incroyable !!!", source: "https://i1.sndcdn.com/artworks-ncJnbnDbNOFd-0-t500x500.jpg" },
]; ];
const style = isDark ? colorsDark : colorsLight; const style = isDark ? colorsDark : colorsLight;

@ -1,24 +1,20 @@
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Pressable } from "react-native"; import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Pressable, Share, Alert } from "react-native";
import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle, withSpring } from "react-native-reanimated"; import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { Audio } from 'expo-av'; import { Audio } from 'expo-av';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import normalize from '../components/Normalize'; import normalize from '../components/Normalize';
import Music from "../models/Music"; import Music from "../models/Music";
import SpotifyService from "../services/spotify/spotify.service";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import FontAwesome from 'react-native-vector-icons/FontAwesome';
import { Feather as Icon } from "@expo/vector-icons"; import { Feather as Icon } from "@expo/vector-icons";
import HorizontalFlatList from "../components/HorizontalFlatListComponent"; import { MusicServiceProvider } from "../models/MusicServiceProvider";
import * as SecureStore from 'expo-secure-store'; import { HorizontalFlatList } from "../components/HorizontalFlatList";
import { MY_SECURE_AUTH_STATE_KEY } from "./RegisterScreen"; import { LittleCard } from "../components/littleCard";
const halfPi = Math.PI / 2; const halfPi = Math.PI / 2;
//@ts-ignore //@ts-ignore
const DetailScreen = ({ route }) => { export default function DetailScreen({ route }) {
const music: Music = route.params.music; const music: Music = route.params.music;
const [currentspot] = useState(music); const [currentspot] = useState(music);
const [simularMusic, setSimularMusic] = useState<Music[]>([]); const [simularMusic, setSimularMusic] = useState<Music[]>([]);
@ -27,19 +23,13 @@ const DetailScreen = ({ route }) => {
const navigator = useNavigation(); const navigator = useNavigation();
useEffect(() => { useEffect(() => {
getSimilarTrack(); getSimilarTrack();
}, []); }, []);
const getSimilarTrack = async () => { const getSimilarTrack = async () => {
try { const simularMusic = await MusicServiceProvider.musicService.getSimilarTracks(currentspot.id);
let token = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY); setSimularMusic(simularMusic);
const service = new SpotifyService(token);
const simularMusic = await service.getSimilarTrack(currentspot.id, 5, 'FR');
setSimularMusic(simularMusic);
} catch (error) {
console.error('Error ================ in getSimilarTrack', error);
}
} }
const handlePlaySound = async () => { const handlePlaySound = async () => {
@ -48,7 +38,7 @@ const DetailScreen = ({ route }) => {
{ uri: music.trackPreviewUrl }, { uri: music.trackPreviewUrl },
{ shouldPlay: true } { shouldPlay: true }
); );
setSound(newSound); //setSound(newSound);
setIsPlaying(true); setIsPlaying(true);
} else { } else {
@ -75,6 +65,21 @@ const DetailScreen = ({ route }) => {
: undefined; : undefined;
}, [sound]); }, [sound]);
const onShare = async () => {
try {
const result = await Share.share({
message:
music.url,
});
} catch (error: any) {
Alert.alert(error.message);
}
};
const addToPlaylist = async () => {
MusicServiceProvider.musicService.addToPlaylist(music.id);
};
const sensor = useAnimatedSensor(SensorType.ROTATION); const sensor = useAnimatedSensor(SensorType.ROTATION);
const styleAniamatedImage = useAnimatedStyle(() => { const styleAniamatedImage = useAnimatedStyle(() => {
const { pitch, roll } = sensor.sensor.value; const { pitch, roll } = sensor.sensor.value;
@ -101,7 +106,7 @@ const DetailScreen = ({ route }) => {
blurRadius={133} blurRadius={133}
style={styles.back_drop} style={styles.back_drop}
source={{ source={{
uri: currentspot.image, uri: currentspot.cover,
}} }}
></Image> ></Image>
<LinearGradient style={styles.gradientFade} <LinearGradient style={styles.gradientFade}
@ -116,7 +121,7 @@ const DetailScreen = ({ route }) => {
<Animated.Image <Animated.Image
source={{ source={{
uri: currentspot.image, uri: currentspot.cover,
}} }}
style={[ style={[
{ {
@ -133,15 +138,14 @@ const DetailScreen = ({ route }) => {
<View> <View>
</View> </View>
<TouchableOpacity activeOpacity={0.5}onPressIn={handlePlaySound} <TouchableOpacity activeOpacity={0.5} onPressIn={handlePlaySound}
onPressOut={handleStopSound} style={{ onPressOut={handleStopSound} style={{
backgroundColor: '#F80404', backgroundColor: '#F80404',
borderRadius: 100, borderRadius: 100,
padding: normalize(23) padding: normalize(23)
}}> }}>
<View style={{ flex: 1, justifyContent: 'center', alignContent: 'center' }}> <View style={{ flex: 1, justifyContent: 'center', alignContent: 'center' }}>
<FontAwesome name="play" size={32} color="#FFFF" ></FontAwesome>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -151,14 +155,13 @@ const DetailScreen = ({ route }) => {
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-evenly', width: '100%' }}> <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-evenly', width: '100%' }}>
<TouchableOpacity activeOpacity={0.6} style={{ <TouchableOpacity onPress={addToPlaylist} activeOpacity={0.6} style={{
flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center', width: 180, flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center', width: 180,
height: 64, borderRadius: 8, opacity: 0.86, backgroundColor: '#0B0606', height: 64, borderRadius: 8, opacity: 0.86, backgroundColor: '#0B0606',
}}> }}>
<FontAwesome name="bookmark" size={24} color="#FFFF" ></FontAwesome>
<Text style={{ fontSize: normalize(16), fontWeight: "700", color: '#FFFFFF' }}>Dans ma collection</Text> <Text style={{ fontSize: normalize(16), fontWeight: "700", color: '#FFFFFF' }}>Dans ma collection</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity activeOpacity={0.6} style={{ <TouchableOpacity onPress={onShare} activeOpacity={0.6} style={{
flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center', width: 180, flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center', width: 180,
height: 64, borderRadius: 8, opacity: 0.86, backgroundColor: '#0B0606', height: 64, borderRadius: 8, opacity: 0.86, backgroundColor: '#0B0606',
}}> }}>
@ -166,7 +169,7 @@ const DetailScreen = ({ route }) => {
{/* <FontAwesome name="bookmark" size={24} color="#FF0000" ></FontAwesome> */} {/* <FontAwesome name="bookmark" size={24} color="#FF0000" ></FontAwesome> */}
<Text style={{ fontSize: normalize(16), fontWeight: "700", color: '#FFFFFF' }}>Partager cette music</Text> <Text style={{ fontSize: normalize(16), fontWeight: "700", color: '#FFFFFF' }}>Partager cette music</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{simularMusic.length !== 0 && ( {simularMusic.length !== 0 && (
<HorizontalFlatList title={'Similar'} data={simularMusic}> <HorizontalFlatList title={'Similar'} data={simularMusic}>
@ -174,12 +177,13 @@ const DetailScreen = ({ route }) => {
<Pressable <Pressable
onPress={() => { onPress={() => {
// @ts-ignore // @ts-ignore
navigator.replace("DetailsSpot", { "music": props }) }} > navigator.replace("Detail", { "music": props }) }} >
<LittleCard data={props} /> <LittleCard data={props} />
</Pressable> </Pressable>
)} )}
</HorizontalFlatList> </HorizontalFlatList>
)} )}
</ScrollView> </ScrollView>
</View> </View>
</View> </View>
@ -187,8 +191,6 @@ const DetailScreen = ({ route }) => {
); );
}; };
export default DetailScreen;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mainSafeArea: { mainSafeArea: {
flex: 1, flex: 1,

@ -1,15 +1,18 @@
import React from 'react'; import React, { useEffect } from 'react';
import { StyleSheet, Text, View, FlatList, TouchableHighlight, SafeAreaView } from 'react-native'; import { StyleSheet, Text, View, FlatList, SafeAreaView } from 'react-native';
import CardMusic from '../components/CardMusicComponent'; import CardMusic from '../components/CardMusicComponent';
import normalize from '../components/Normalize'; import normalize from '../components/Normalize';
import Music from '../models/Music';
import { Svg, Path } from 'react-native-svg'; import { Svg, Path } from 'react-native-svg';
import FladyComponent from '../components/FladyComponent'; import FladyComponent from '../components/FladyComponent';
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { SharedElement } from 'react-navigation-shared-element';
import { colorsDark } from '../constants/colorsDark'; import { colorsDark } from '../constants/colorsDark';
import { colorsLight } from '../constants/colorsLight'; import { colorsLight } from '../constants/colorsLight';
import { useDispatch } from 'react-redux';
import { getFavoriteMusic } from '../redux/thunk/appThunk';
import { Spot } from '../models/Spot';
import { TouchableOpacity } from 'react-native-gesture-handler';
import Artist from '../models/Artist';
export default function FavoriteScreen() { export default function FavoriteScreen() {
@ -17,26 +20,31 @@ export default function FavoriteScreen() {
const isDark = useSelector(state => state.userReducer.dark); const isDark = useSelector(state => state.userReducer.dark);
const style = isDark ? colorsDark : colorsLight; const style = isDark ? colorsDark : colorsLight;
const navigation = useNavigation();
//@ts-ignore
const favoritesMusic = useSelector(state => state.appReducer.favoriteMusic);
const images = [ const images = [
{ id: 1, source: require('../assets/images/flady_love.png') }, { id: 1, source: require('../assets/images/flady_love.png') },
{ id: 2, source: require('../assets/images/flady_star.png') }, { id: 2, source: require('../assets/images/flady_star.png') },
{ id: 3, source: require('../assets/images/flady_angry.png') }, { id: 3, source: require('../assets/images/flady_angry.png') },
{ id: 4, source: require('../assets/images/flady_cry.png') }, { id: 4, source: require('../assets/images/flady_cry.png') },
]; ];
const navigueToDetail = (music: any) => { const navigation = useNavigation();
// @ts-ignore //@ts-ignore
navigation.navigate("Detail", { "music": music }) const favoriteMusic = useSelector(state => state.appReducer.favoriteMusic);
};
const dispatch = useDispatch();
useEffect(() => {
//@ts-ignore
dispatch(getFavoriteMusic())
}, []);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mainSafeArea: { mainSafeArea: {
flex: 1, flex: 1,
backgroundColor: style.body, backgroundColor: style.body,
}, },
titleContainer: { titleContainer: {
marginTop: 10, marginVertical: 10,
marginLeft: 20, marginLeft: 20,
}, },
header: { header: {
@ -55,30 +63,6 @@ export default function FavoriteScreen() {
fontSize: normalize(20), fontSize: normalize(20),
color: '#787878', color: '#787878',
marginBottom: 5 marginBottom: 5
},
button: {
marginTop: '10%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
backgroundColor: 'white',
width: normalize(100),
height: normalize(100),
borderRadius: 21
},
buttonImage: {
width: normalize(46),
height: normalize(46),
},
shadow: {
shadowColor: '#000',
shadowOffset: {
width: 2,
height: 3,
},
shadowOpacity: 0.50,
shadowRadius: 3.84,
} }
}); });
@ -95,15 +79,14 @@ export default function FavoriteScreen() {
<Text style={styles.description}>Retrouvez ici vos musiques favorites</Text> <Text style={styles.description}>Retrouvez ici vos musiques favorites</Text>
</View> </View>
<FlatList <FlatList
data={favoritesMusic} data={favoriteMusic}
keyExtractor={(item: Spot) => item.music.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<TouchableHighlight onPress={() => { navigueToDetail(item) }}> //@ts-ignore
<SharedElement id={item.id}> <TouchableOpacity onPress={() => { navigation.navigate("Detail", { "music": item.music }) }}>
<CardMusic image={item.image} title={item.title} description={item.bio} id={item.id} /> <CardMusic image={item.music.cover} title={item.music.name} description={item.music.artists.map((artist: Artist) => artist.name).join(', ')} id={item.music.id} />
</SharedElement> </TouchableOpacity>
</TouchableHighlight>
)} )}
keyExtractor={(item: Music) => item.title}
ListFooterComponent={ ListFooterComponent={
<> <>
<Text style={[styles.title, { marginLeft: 20 }]}>What's your mood?</Text> <Text style={[styles.title, { marginLeft: 20 }]}>What's your mood?</Text>

@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity } from 'react-native'; import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity } from 'react-native';
import { setErrorNetwork } from "../redux/actions/userActions"; import { setErrorNetwork, LoginCredentials } from "../redux/actions/userActions";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import normalize from '../components/Normalize'; import normalize from '../components/Normalize';
import { login } from '../redux/thunk/authThunk'; import { login } from '../redux/thunk/authThunk';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Audio } from 'expo-av'; import { Audio } from 'expo-av';
import { LoginCredentials } from '../redux/actions/userActions';
// @ts-ignore // @ts-ignore
const DismissKeyboard = ({ children }) => ( const DismissKeyboard = ({ children }) => (

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { Alert, View, Text, StyleSheet, TouchableWithoutFeedback, Keyboard, ScrollView, Image } from 'react-native'; import { Alert, View, Text, StyleSheet, TouchableWithoutFeedback, Keyboard, ScrollView, Image } from 'react-native';
import { TextInput, TouchableOpacity } from 'react-native-gesture-handler'; import { TextInput, TouchableOpacity } from 'react-native-gesture-handler';
import { Svg, Path } from 'react-native-svg'; import { Svg, Path } from 'react-native-svg';
@ -11,6 +11,8 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { colorsDark } from '../constants/colorsDark'; import { colorsDark } from '../constants/colorsDark';
import { colorsLight } from '../constants/colorsLight'; import { colorsLight } from '../constants/colorsLight';
import { deleteUser } from '../redux/thunk/authThunk'; import { deleteUser } from '../redux/thunk/authThunk';
import { setMail, setName } from '../redux/thunk/userThunk';
import { setErrorUpdate } from '../redux/actions/userActions';
// @ts-ignore // @ts-ignore
const DismissKeyboard = ({ children }) => ( const DismissKeyboard = ({ children }) => (
@ -23,7 +25,13 @@ export default function ProfilScreen() {
// @ts-ignore // @ts-ignore
const isDark = useSelector(state => state.userReducer.dark); const isDark = useSelector(state => state.userReducer.dark);
// @ts-ignore // @ts-ignore
const errorUpdateMessage = useSelector(state => state.userReducer.errorUpdateMessage);
// @ts-ignore
const errorUpdate = useSelector(state => state.userReducer.errorUpdate);
// @ts-ignore
const userCurrent = useSelector(state => state.userReducer.user); const userCurrent = useSelector(state => state.userReducer.user);
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const style = isDark ? colorsDark : colorsLight; const style = isDark ? colorsDark : colorsLight;
const navigation = useNavigation(); const navigation = useNavigation();
const [isModalVisible, setIsModalVisible] = React.useState(false); const [isModalVisible, setIsModalVisible] = React.useState(false);
@ -60,6 +68,90 @@ export default function ProfilScreen() {
}); });
}; };
const submitUsername = () => {
const isUsernameValid = /^\w+$/.test(username);
if (username.length > 30) {
Alert.alert("Erreur modification", "Le nom d'utilisateur ne peut pas être plus grand que 30 caractères.");
return;
}
if (!isUsernameValid) {
Alert.alert("Erreur modification", "Le nom d'utilisateur ne peut pas posséder de caractères spéciaux.");
return;
}
Alert.alert(
'Confirmation',
'Êtes-vous sûr de vouloir changer de nom d\'utilisateur ?',
[
{
text: 'Annuler',
style: 'cancel'
},
{
text: 'Oui',
onPress: () => {
//@ts-ignore
dispatch(setName(username));
setUsername("");
},
style: 'destructive'
},
],
{ cancelable: false }
);
}
const submitEmail = () => {
const isEmailValid = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
if (email.length > 100) {
Alert.alert("Erreur modification", "L'adresse e-mail ne peut pas être plus grand que 100 caractères.");
return;
}
if (!isEmailValid) {
Alert.alert("Erreur modification", "L'adresse e-mail n\'est pas valide.");
return;
}
Alert.alert(
'Confirmation',
'Êtes-vous sûr de vouloir changer l\'adresse e-mail ?',
[
{
text: 'Annuler',
style: 'cancel'
},
{
text: 'Oui',
onPress: () => {
//@ts-ignore
dispatch(setMail(email));
setEmail("");
},
style: 'destructive'
},
],
{ cancelable: false }
);
}
useEffect(() => {
if (errorUpdate) {
Alert.alert(
"Erreur modification",
errorUpdateMessage,
[
{
text: 'Ok',
onPress: () => {
dispatch(setErrorUpdate(false))
},
},
],
{ cancelable: false }
);
}
}, [errorUpdate]);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mainSafeArea: { mainSafeArea: {
flex: 1, flex: 1,
@ -170,15 +262,22 @@ export default function ProfilScreen() {
flexDirection: 'row', flexDirection: 'row',
}, },
textInputId: { textInputId: {
marginLeft: 50,
width: '50%',
color: style.Text,
fontSize: normalize(18),
},
textIdSpotify: {
marginLeft: 50, marginLeft: 50,
width: '57%', width: '57%',
color: style.Text, color: style.Text,
fontWeight: 'bold',
fontSize: normalize(18), fontSize: normalize(18),
}, },
textInputMail: { textInputMail: {
marginLeft: 100, marginLeft: 100,
color: style.Text, color: style.Text,
width: '57%', width: '50%',
fontSize: normalize(18) fontSize: normalize(18)
}, },
passwordOption: { passwordOption: {
@ -201,18 +300,20 @@ export default function ProfilScreen() {
marginTop: 5 marginTop: 5
}, },
cancelText: { cancelText: {
fontSize: normalize(20), fontSize: normalize(18),
color: '#1c77fb' color: '#1c77fb'
}, },
updateText: { updateText: {
marginLeft: 60, marginLeft: 60,
fontSize: normalize(20), fontWeight: 'bold',
fontSize: normalize(18),
color: '#404040' color: '#404040'
}, },
titlePassword: { titlePassword: {
fontSize: normalize(22), fontWeight: 'bold',
fontSize: normalize(20),
color: style.Text, color: style.Text,
marginLeft: 50 marginLeft: 65
}, },
warning: { warning: {
color: '#98989f', color: '#98989f',
@ -286,13 +387,29 @@ export default function ProfilScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.body}> <View style={styles.body}>
<View style={styles.optionId}>
<Text style={styles.textOption}>Id. Spotify</Text>
<Text style={styles.textIdSpotify} numberOfLines={1}>{userCurrent.idSpotify}</Text>
</View>
<View style={styles.optionId}> <View style={styles.optionId}>
<Text style={styles.textOption}>Identifiant</Text> <Text style={styles.textOption}>Identifiant</Text>
<TextInput placeholderTextColor='#828288' placeholder={userCurrent.name} style={styles.textInputId} /> <TextInput placeholderTextColor='#828288' value={username}
onChangeText={setUsername} placeholder={userCurrent.name} style={styles.textInputId} />
{username.length >= 5 && (
<TouchableOpacity onPress={() => submitUsername()}>
<Image source={require("../assets/images/confirm_icon.png")} style={{ width: normalize(25), height: normalize(25) }} />
</TouchableOpacity>
)}
</View> </View>
<View style={styles.optionMail}> <View style={styles.optionMail}>
<Text style={styles.textOption}>Mail</Text> <Text style={styles.textOption}>Mail</Text>
<TextInput placeholderTextColor='#828288' placeholder={userCurrent.email} style={styles.textInputMail} /> <TextInput placeholderTextColor='#828288' value={email}
onChangeText={setEmail} placeholder={userCurrent.email} style={styles.textInputMail} />
{email.length >= 7 && (
<TouchableOpacity onPress={() => submitEmail()}>
<Image source={require("../assets/images/confirm_icon.png")} style={{ width: normalize(25), height: normalize(25) }} />
</TouchableOpacity>
)}
</View> </View>
</View> </View>

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity, Platform } from 'react-native'; import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity } from 'react-native';
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import normalize from '../components/Normalize'; import normalize from '../components/Normalize';
import * as AuthSession from 'expo-auth-session'; import * as AuthSession from 'expo-auth-session';
@ -7,8 +7,6 @@ import { register } from '../redux/thunk/authThunk';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Audio } from 'expo-av'; import { Audio } from 'expo-av';
import { RegisterCredentials } from '../redux/actions/userActions'; import { RegisterCredentials } from '../redux/actions/userActions';
import { setSpotList } from '../redux/actions/spotActions';
import { spotsData } from '../data/data';
import configs from '../constants/config'; import configs from '../constants/config';
// @ts-ignore // @ts-ignore
@ -39,14 +37,14 @@ export default function RegisterScreen() {
await sound.playAsync(); await sound.playAsync();
} }
function addMockSpots() {
dispatch(setSpotList(spotsData))
}
const submitForm = () => { const submitForm = () => {
const isUsernameValid = /^[a-zA-Z0-9_]+$/.test(username); const isUsernameValid = /^\w+$/.test(username);
const isEmailValid = /^[a-zA-Z0-9_]+@[a-zA-Z0-9_]+\.[^\s@]+$/.test(email); const isEmailValid = /^\w+@\w+\.[^\s@]+$/.test(email);
if (username.length > 30) {
Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas être plus grand que 30 caractères.");
return;
}
if (username == "" || username == null) { if (username == "" || username == null) {
Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas être vide."); Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas être vide.");
return; return;
@ -55,6 +53,10 @@ export default function RegisterScreen() {
Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas posséder de caractères spéciaux."); Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas posséder de caractères spéciaux.");
return; return;
} }
if (email.length > 100) {
Alert.alert("Erreur inscription", "L'adresse e-mail ne peut pas être plus grand que 100 caractères.");
return;
}
if (!isEmailValid) { if (!isEmailValid) {
Alert.alert("Erreur inscription", "L'adresse e-mail n\'est pas valide."); Alert.alert("Erreur inscription", "L'adresse e-mail n\'est pas valide.");
return; return;
@ -78,7 +80,6 @@ export default function RegisterScreen() {
//@ts-ignore //@ts-ignore
dispatch(register(credentials)) dispatch(register(credentials))
addMockSpots()
playSound() playSound()
} }

@ -12,6 +12,7 @@ import { darkMode } from '../redux/thunk/userThunk';
import { colorsDark } from '../constants/colorsDark'; import { colorsDark } from '../constants/colorsDark';
import { colorsLight } from '../constants/colorsLight'; import { colorsLight } from '../constants/colorsLight';
import { User } from '../models/User'; import { User } from '../models/User';
import Artist from '../models/Artist';
// @ts-ignore // @ts-ignore
const DismissKeyboard = ({ children }) => ( const DismissKeyboard = ({ children }) => (
@ -54,7 +55,7 @@ export default function SettingScreen() {
const Deconnection = () => { const Deconnection = () => {
//@ts-ignore //@ts-ignore
dispatch(logout()) dispatch(logout());
} }
const [isCheckedLocalisation, setIsCheckedLocalisation] = useState(false); const [isCheckedLocalisation, setIsCheckedLocalisation] = useState(false);
@ -234,11 +235,18 @@ export default function SettingScreen() {
marginBottom: 5 marginBottom: 5
}, },
mascot: { mascot: {
width: normalize(90), width: normalize(70),
height: normalize(90), height: normalize(70),
position: 'absolute', position: 'absolute',
right: normalize(0), right: normalize(0),
top: normalize(10) top: normalize(20)
},
creationDateText: {
marginTop: 10,
fontSize: normalize(13),
fontWeight: '700',
color: style.Text,
opacity: 0.4
} }
}) })
@ -268,7 +276,7 @@ export default function SettingScreen() {
<View style={styles.profil}> <View style={styles.profil}>
<Image source={{ uri: currentUser.image }} style={styles.imageProfil} /> <Image source={{ uri: currentUser.image }} style={styles.imageProfil} />
<View style={styles.profilContainer}> <View style={styles.profilContainer}>
<Text style={styles.NameProfil}>{currentUser.name}</Text> <Text style={styles.NameProfil}>{currentUser.name.charAt(0).toUpperCase() + currentUser.name.slice(1)}</Text>
<Text style={styles.description}>id. Spotify, mail et mot de passe</Text> <Text style={styles.description}>id. Spotify, mail et mot de passe</Text>
</View> </View>
<Image style={styles.buttonSetting} source={require('../assets/images/chevron_right_icon.png')} /> <Image style={styles.buttonSetting} source={require('../assets/images/chevron_right_icon.png')} />
@ -328,7 +336,7 @@ export default function SettingScreen() {
</View> </View>
<View style={styles.musicActually}> <View style={styles.musicActually}>
<CardMusic image={currentMusic.image} title={currentMusic.title} description={currentMusic.bio} id='1' /> <CardMusic image={currentMusic.cover} title={currentMusic.name} description={currentMusic.artists.map((artist: Artist) => artist.name).join(', ')} id='1' />
<Image source={require("../assets/images/flady_icon.png")} style={styles.mascot} /> <Image source={require("../assets/images/flady_icon.png")} style={styles.mascot} />
</View> </View>
</> </>
@ -345,6 +353,15 @@ export default function SettingScreen() {
<Text style={styles.textDeconnectionOption}>Se deconnecter</Text> <Text style={styles.textDeconnectionOption}>Se deconnecter</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={{alignItems: 'center'}}>
<Text style={styles.creationDateText}>Compte créer le {currentUser.creationDate.toLocaleString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
})}</Text>
</View>
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>

@ -4,18 +4,15 @@ import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import Animated from 'react-native-reanimated'; import Animated from 'react-native-reanimated';
import Card from '../components/Card'; import Card from '../components/Card';
import AdjustSize from '../components/AdjustSize';
import normalize from '../components/Normalize'; import normalize from '../components/Normalize';
import LottieView from 'lottie-react-native' import LottieView from 'lottie-react-native'
import Lotties from '../assets/lottie/Lottie'; import Lotties from '../assets/lottie/Lottie';
import Loading from '../components/LoadingComponent'; import Loading from '../components/LoadingComponent';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import Music from '../model/Music';
import { addFavoritesMusic } from '../redux/actions/appActions'; import { addFavoritesMusic } from '../redux/actions/appActions';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Spot } from '../model/Spot'; import { Spot } from '../models/Spot';
import { removeFromSpotList, setSpotList } from '../redux/actions/spotActions'; import { removeFromSpotList, setSpotList } from '../redux/actions/spotActions';
import { spotsData } from '../data/data';
export default function SpotScreen() { export default function SpotScreen() {
//@ts-ignore //@ts-ignore
@ -89,7 +86,7 @@ export default function SpotScreen() {
alignItems: "center", alignItems: "center",
}} }}
source={{ source={{
uri: currentCard.music.image, uri: currentCard.music.cover,
}} }}
></ImageBackground> ></ImageBackground>
<SafeAreaView style={styles.mainSafeArea}> <SafeAreaView style={styles.mainSafeArea}>
@ -100,9 +97,9 @@ export default function SpotScreen() {
left: wWidht / 9, left: wWidht / 9,
top: normalize(87), top: normalize(87),
color: "#FFFFFF", color: "#FFFFFF",
fontSize: normalize(AdjustSize(currentCard.music.title)), fontSize: normalize(currentCard.music.name),
fontWeight: "800", fontWeight: "800",
}}>{currentCard.music.title}</Text> }}>{currentCard.music.name}</Text>
<Text <Text
style={{ style={{
fontStyle: 'normal', fontStyle: 'normal',
@ -110,7 +107,7 @@ export default function SpotScreen() {
top: normalize(87), top: normalize(87),
color: "#FFFFFF", color: "#FFFFFF",
fontSize: normalize(20), fontSize: normalize(20),
}}>{currentCard.music.bio}</Text> }}>{currentCard.music.artists[0].name}</Text>
</LinearGradient> </LinearGradient>
</SafeAreaView> </SafeAreaView>
<View style={{ flex: 8.35 }}> <View style={{ flex: 8.35 }}>
@ -118,11 +115,11 @@ export default function SpotScreen() {
<View style={{ flex: 1.83, justifyContent: 'center', alignItems: 'center' }}> <View style={{ flex: 1.83, justifyContent: 'center', alignItems: 'center' }}>
{cards.map((card) => ( {cards.map((card) => (
<View key={card.userSpotifyId} style={{ position: 'absolute' }} > <View style={{ position: 'absolute' }} >
<Pressable onLongPress={() => { hapti(card) }} > <Pressable onLongPress={() => { hapti(card) }} >
<Card <Card
title={card.music.title} title={card.music.name}
image={card.music.image} image={card.music.cover}
onSwipe={(direction) => { onSwipe(direction) }} onSwipe={(direction) => { onSwipe(direction) }}
/> />
</Pressable> </Pressable>

@ -0,0 +1,26 @@
import Music from "../models/Music";
import IMusicService from "./musics/interfaces/IMusicService";
export default class EmptyMusicService implements IMusicService {
getMusicById(id: string): Promise<Music> {
throw new Error("Method not implemented.");
}
getRecentlyPlayedMusic(): Promise<string | null> {
throw new Error("Method not implemented.");
}
getCurrentlyPlayingMusic(): Promise<string | null> {
throw new Error("Method not implemented.");
}
getMusicsWithIds(ids: string[]): Promise<Music[]> {
throw new Error("Method not implemented.");
}
getMusicsWithName(name: string): Promise<Music[]> {
throw new Error("Method not implemented.");
}
addToPlaylist(idTrack: string): void {
throw new Error("Method not implemented.");
}
getSimilarTracks(idTrack: string): Promise<Music[]> {
throw new Error("Method not implemented.");
}
}

@ -0,0 +1,11 @@
import Music from "../../../models/Music";
export default interface IMusicService {
getMusicById(id: string): Promise<Music>;
getRecentlyPlayedMusic(): Promise<string | null>;
getCurrentlyPlayingMusic(): Promise<string | null>;
getMusicsWithIds(ids: string[]): Promise<Music[]>;
getMusicsWithName(name: string): Promise<Music[]>;
addToPlaylist(idTrack: string): void;
getSimilarTracks(idTrack: string): Promise<Music[]>;
}

@ -0,0 +1,189 @@
import axios from "axios";
import Music from "../../../models/Music";
import IMusicService from "../interfaces/IMusicService";
import TokenSpotify from "./TokenSpotify";
import MusicMapper from "../../../models/mapper/MusicMapper";
export default class SpotifyService implements IMusicService {
private readonly API_URL = "https://api.spotify.com/v1";
private readonly PLAYLIST_NAME = "Flad's discovery";
private _token: TokenSpotify;
private _idSpotify: string;
constructor(refreshToken: string, idSpotify: string) {
this._token = new TokenSpotify(refreshToken);
this._idSpotify = idSpotify;
}
async getMusicById(id: string): Promise<Music> {
const access_token = await this._token.getAccessToken();
try {
const response = await axios.get(`${this.API_URL}/tracks/${id}`, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
return MusicMapper.toModel(response.data)
} catch (error: any) {
console.log("Error retrieving music information : " + error)
throw new Error("Error retrieving music information : " + error)
}
}
async getRecentlyPlayedMusic(limit: number = 1): Promise<string | null> {
const access_token = await this._token.getAccessToken();
const response = await axios.get(`${this.API_URL}/me/player/recently-played?limit=${limit}`, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
if (response.data.items.length === 0) {
return null;
}
return response.data.items[0].track.id
}
async getCurrentlyPlayingMusic(): Promise<string | null> {
const access_token = await this._token.getAccessToken();
const response = await axios.get(`${this.API_URL}/me/player/currently-playing`, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
if (response.data.item === undefined) {
return null;
}
return response.data.item.id
}
async getMusicsWithIds(ids: string[]): Promise<Music[]> {
const access_token = await this._token.getAccessToken();
var url = `${this.API_URL}/tracks?market=FR&ids=`;
if (ids.length == 0) {
return [];
}
url += ids.join('%2C');
try {
const response = await axios.get(url, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
const tracksData = response.data.tracks.filter((musicData: any) => musicData !== null);
return tracksData.map((musicData: any) => MusicMapper.toModel(musicData));
} catch (error: any) {
console.log(error)
return [];
}
}
async getMusicsWithName(name: string, limit: number = 20, offset: number = 0): Promise<Music[]> {
const access_token = await this._token.getAccessToken();
try {
const response = await axios.get(`${this.API_URL}/search?q=track%3A${name}&type=track&market=fr&limit=${limit}&offset=${offset}`, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
return response.data.tracks.items.map((musicData: any) => MusicMapper.toModel(musicData));
} catch (error: any) {
console.log(error)
return [];
}
}
async _getPlaylistId(): Promise<string> {
const access_token = await this._token.getAccessToken();
const headers = {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json'
};
const response = await axios.get(`${this.API_URL}/me/playlists?limit=50`, { headers });
const fladPlaylist = response.data.items.filter((playlist: any) => playlist.name === this.PLAYLIST_NAME);
if (fladPlaylist.length >= 1) {
return fladPlaylist[0].id;
}
return await this._createPlaylist();
}
async _createPlaylist(): Promise<string> {
const access_token = await this._token.getAccessToken();
const data = {
name: this.PLAYLIST_NAME,
description: 'Retrouvez toutes vos découvertes faites sur FladMusic 🎵',
public: true
};
var headers = {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const response = await axios.post(`${this.API_URL}/users/${this._idSpotify}/playlists`, data, { headers });
return response.data.id;
}
async _isInPlaylist(idTrack: string, idPlaylist: string): Promise<Boolean> {
const access_token = await this._token.getAccessToken();
const response = await axios.get(`${this.API_URL}/playlists/${idPlaylist}/tracks?limit=100`, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
var nbTracks = response.data.items.filter((item: any) => item.track.id === idTrack).length;
return (nbTracks >= 1) ? true : false;
}
async addToPlaylist(idTrack: string): Promise<void> {
var idPlaylist = await this._getPlaylistId();
if (await this._isInPlaylist(idTrack, idPlaylist)) {
return;
}
const access_token = await this._token.getAccessToken();
const data = {
uris: [`spotify:track:${idTrack}`],
position: 0
};
const headers = {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
};
await axios.post(`${this.API_URL}/playlists/${idPlaylist}/tracks`, data, { headers })
.then(response => {
console.log('Song successfully added to playlist.');
})
.catch(error => {
console.error('Error adding song to playlist: ', error);
});
}
async getSimilarTracks(idTrack: string, limit: number = 20, offset: number = 0): Promise<Music[]> {
const access_token = await this._token.getAccessToken();
try {
const response = await axios.get(`${this.API_URL}/recommendations?limit=${limit}&offset=${offset}&seed_tracks=${idTrack}`, {
headers: {
'Authorization': `Bearer ${access_token}`
},
});
return response.data.tracks.map((musicData: any) => MusicMapper.toModel(musicData));
} catch (error: any) {
console.log(error)
return [];
}
}
}

@ -0,0 +1,41 @@
import axios from "axios";
import configs from "../../../constants/config";
export default class TokenSpotify {
private _accessToken: string = '';
private _refreshToken: string;
private _tokenEnd: Date;
constructor(refreshToken: string) {
this._refreshToken = refreshToken;
this._tokenEnd = new Date();
this._tokenEnd.setSeconds(this._tokenEnd.getSeconds() - 10);
}
async getAccessToken(): Promise<string> {
if (this._isTokenExpired()) {
await this._getRefreshToken();
}
return this._accessToken;
}
private async _getRefreshToken(): Promise<void> {
const response = await axios.get(
configs.API_URL + '/spotify/refresh?refresh_token=' + this._refreshToken
)
if (response.status === 200) {
const responseData = await response.data;
this._accessToken = responseData.access_token;
this._tokenEnd = new Date();
this._tokenEnd.setSeconds(this._tokenEnd.getSeconds() + responseData.expires_in)
} else {
console.log(`Error refreshing token: ${response.status}`)
}
}
private _isTokenExpired(): boolean {
return new Date() > this._tokenEnd;
}
}

@ -1,4 +0,0 @@
interface IspotifyAuthHandler {
abstract async getUserToken(): Promise<string>;
abstract async getUserTokenFromRefreshToken(): Promise<string>;
}

@ -1,156 +0,0 @@
import axios from "axios";
import MusicMapper from "../../model/mapper/MusicMapper";
import Music from "../../model/Music";
import { FetchOptions, RequestHandler } from "./spotifyRequestHandler/utils";
export class MusicMinimal {
public id: string;
public title: string;
public image: string;
constructor(id: string, title: string, bio: string, image: string, trackPreviewUrl: string) {
this.title = title;
this.image = image;
this.id = id;
}
}
export default class SpotifyService implements IspotifyService {
private readonly API_URL = API_URL;
private spotifyRequestHandler = new RequestHandler();
public token: string;
constructor(token: string) {
this.token = token;
}
public async getMusicById(idMusic: string): Promise<Music| null> {
let requestData: string = '/tracks/' + idMusic;
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
if (respMusic.status != 200) {
return null;
}
console.log(respMusic.data.artists[0].id);
return MusicFactory.mapFromSpotifyTrack(respMusic.data);
}
public async getUserCurrentMusic(): Promise<string | null> {
let requestData: string = '/me/player/currently-playing';
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
if (respMusic.status != 200) {
return null;
}
return respMusic.data.item.id;
}
public async getUserRecentlyPlayedMusic(): Promise<string | null> {
let requestData: string = '/me/player/recently-played';
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
if (respMusic.status != 200) {
return null;
}
if (respMusic.data.items.length <= 0) {
return null;
}
return respMusic.data.items[0].track.id;
}
public async playMusic(idMusic: string): Promise<void> {
const fetchOptions: FetchOptions = {
method: 'PUT',
body: {
uris: [`spotify:track:${idMusic}`],
position_ms: 0
}
};
throw new Error("not Implemented")
}
public async searchMusic(text: string): Promise<Music[]> {
const requestData: string = '/search';
const fetchOptions: FetchOptions = {
params: {
q: text,
type: 'track'
}
};
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, fetchOptions, this.token);
const tracksData = respMusic?.data?.tracks?.items;
if (!tracksData || !Array.isArray(tracksData)) {
return [];
}
const tracks = tracksData.map((trackData: any) => {
return MusicFactory.mapFromSpotifyTrack(trackData)
});
return tracks;
}
// tempo version
public async getMusicMoreDetails(idMusic: string): Promise<string> {
let requestData: string = '/audio-features/' + idMusic;
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
return respMusic.data.audio_features.tempo;
}
public async getRelatedArtist(idArtist: string): Promise<string> {
let requestData: string = '/artists/' + idArtist + '/related-artists';
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
return respMusic.data.audio_features.tempo;
}
public async getArtistTopTracks(idArtist: string): Promise<string> {
let requestData: string = '/artists/' + idArtist + '/top-tracks';
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token);
return respMusic.data.audio_features.tempo;
}
public async addItemToPlayList(playlistId: string, idMusic: string): Promise<void> {
let requestData: string = '/playlists/' + playlistId + '/tracks';
const fetchOptions: FetchOptions = {
method: 'POST',
body: {
uris: [`spotify:track:${idMusic}`]
}
};
const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, fetchOptions, this.token);
console.log(respMusic.data);
}
public async getSimilarTrack(musicId: string, limit: number = 1, market?: string): Promise<Music[]> {
const requestData: string = '/recommendations/' +
'?limit=' + limit +
'&market=FR' +
'&seed_tracks=' + musicId;
let respSimilarMusic;
try {
respSimilarMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, {}, this.token);
} catch (error) {
console.log(error);
}
if (!respSimilarMusic || !respSimilarMusic.data.tracks) {
return [];
}
const similars: Music[] = await Promise.all(
respSimilarMusic.data.tracks.map(async (trackData: any) => {
if (trackData.id != undefined) {
const data = await this.getMusicById(trackData.id);
return data;
}
})
)
return similars.filter((music: Music | undefined) => !!music) as Music[];
}
async getSpotifyCredentials() {
const res = await axios.get(this.API_URL)
const spotifyCredentials = res.data;
return spotifyCredentials
}
}

@ -1,3 +0,0 @@
interface IspotifyService {
getMusicById(idMusic: string): Promise<any>;
}

@ -1,78 +0,0 @@
import axios, { AxiosResponse } from "axios";
import { MY_SECURE_AUTH_STATE_KEY, MY_SECURE_AUTH_STATE_KEY_REFRESH } from "../../../screens/RegisterScreen";
import * as SecureStore from 'expo-secure-store';
export type Methods = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
export interface FetchOptions {
/** The headers that will be applied to the request when sent. */
headers?: Record<string, string>;
/** The type of HTTP method being used. */
method?: Methods;
/** Parameters used for search queries.*/
params?: Record<string, any>;
/**If present, this refers to the JSON data that will be included in the request body. */
body?: Record<string, string | boolean | number | (string | boolean | number)[]>;
}
export class RequestHandler {
private _version: `v${number}` = 'v1';
get version(): string {
return this._version;
}
public async spotifyFetch(url: string, options: FetchOptions = {}, token: string): Promise<AxiosResponse<any, any>> {
try {
const resp = await axios({
url: `https://api.spotify.com/${this.version}${url}`,
method: options.method || 'GET',
params: options.params,
headers: {
Authorization: "Bearer " + token,
Accept: 'application/json',
...options.headers
},
data: options.body
});
return resp;
}
catch(error : any){
const errorMessage = error.response.data?.error?.message;
if (errorMessage === "Invalid access token" || errorMessage === "The access token expired" ) {
console.log('### Warning ! ### try refresh token Request Handler ' +error);
const newToken = await this.refreshToken();
console.log('### GOOD Warning ! ### new token Request Handler ' +newToken);
// Mettez à jour le token dans le store ou le reducer ici
return this.spotifyFetch(url, options, newToken);
}
else {
console.log('### Error ! ### while fetching Data in the SPotify Request Handler ' +error);
throw error;
}
}
}
private async refreshToken(): Promise<string> {
// Faites une demande à votre API pour obtenir un nouveau token Spotify
let refreshToken = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY_REFRESH);
console.log('refresh token : ' + refreshToken);
const response = await axios.get(`https://flad-api-production.up.railway.app/api/spotify/refresh?refresh_token=${refreshToken}`);
// Renvoie le nouveau token
const {
access_token : access_token,
refresh_token: refresh_token,
} = response.data as SpotifyAuthResponse
console.log('new access token : ' + access_token);
console.log('new refresh token : ' + refresh_token);
await SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, access_token);
return access_token;
}
}
interface SpotifyAuthResponse {
access_token: string;
refresh_token: string;
}

@ -1,80 +0,0 @@
import initialize from '../lib';
initialize();
class MqttClient {
constructor() {
const clientId = 'ReactNativeMqtt';
this.client = new Paho.MQTT.Client('127.0.0.1', 9001, clientId);
this.client.onMessageArrived = this.onMessageArrived;
this.callbacks = {};
this.onSuccessHandler = undefined;
this.onConnectionLostHandler = undefined;
this.isConnected = false;
}
onConnect = (onSuccessHandler, onConnectionLostHandler) => {
this.onSuccessHandler = onSuccessHandler;
this.onConnectionLostHandler = onConnectionLostHandler;
this.client.onConnectionLost = () => {
this.isConnected = false;
onConnectionLostHandler();
};
this.client.connect({
timeout: 10,
onSuccess: () => {
this.isConnected = true;
onSuccessHandler();
},
useSSL: false,
onFailure: this.onError,
reconnect: true,
keepAliveInterval: 20,
cleanSession: true,
});
};
onError = ({errorMessage}) => {
console.log(errorMessage);
this.isConnected = false;
Alert.alert('Failed', 'Failed to connect to MQTT', [
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{
text: 'Try Again',
onPress: () =>
this.onConnect(
this.onSuccessHandler,
this.onConnectionLostHandler,
),
},
]);
};
onMessageArrived = message => {
const {payloadString, topic} = message;
console.log('onMessageArrived:', payloadString);
this.callbacks[topic](payloadString);
};
onPublish = (topic, message) => {
this.client.publish(topic, message);
};
onSubscribe = (topic, callback) => {
this.callbacks[topic] = callback;
this.client.subscribe(topic);
};
unsubscribe = topic => {
delete this.callbacks[topic];
this.client.unsubscribe(topic);
};
}
let client = new MqttClient();
export {client as MqttClient};
Loading…
Cancel
Save