28 Commits

Author SHA1 Message Date
Clément Le Bihan
0db8d49618 nothing important 2024-01-04 15:30:05 +01:00
Clément Le Bihan
4923fc72b2 reactènative-sounds 2023-12-31 17:59:56 +01:00
Arthur Jamet
60a73781bd Front: Lint + format 2023-12-29 18:13:40 +01:00
Arthur Jamet
4e3b378d6a Front: Lint + format 2023-12-29 18:13:40 +01:00
Clément Le Bihan
2bf1e783a9 removed unused var 2023-12-29 18:13:40 +01:00
Clément Le Bihan
375d36f6c5 Fixed google logo for mobile 2023-12-29 18:13:40 +01:00
Clément Le Bihan
495380ec43 Fix CI 2023-12-29 18:13:40 +01:00
Clément Le Bihan
af0531bb0c Fixed the like button and now desactivated the click on card to go to song and changed default display for score from '?' to '-' 2023-12-29 18:13:40 +01:00
Arthur Jamet
c5124fa6ad Front: MusicView: Fix Wrong Mutation 2023-12-29 18:13:40 +01:00
Arthur Jamet
962cf58e77 Front: DiscoveryView: USe Like status 2023-12-29 18:13:40 +01:00
Arthur Jamet
60988dd599 Front: Use Mutations to update 'liked' state 2023-12-29 18:13:40 +01:00
Arthur Jamet
004a541302 Front: Lint + format 2023-12-28 12:07:35 +01:00
Arthur Jamet
f4cd9e18ea Front: Explain how to DL the APK 2023-12-28 12:07:35 +01:00
Arthur Jamet
2dc301addf Front: add Button to Download APK From Web 2023-12-28 12:07:35 +01:00
Arthur Jamet
e85a959c26 Front: remove Visible IDs 2023-12-22 17:37:21 +01:00
Arthur Jamet
339e808d27 Front: SettingsView: Fox ordering of tabs 2023-12-21 17:17:47 +01:00
Arthur Jamet
22d1a97abd Front: SettingsView: Fox ordering of tabs 2023-12-21 17:17:47 +01:00
Arthur Jamet
ce4baa61dc Front: serve Google logo ourselves 2023-12-21 17:17:47 +01:00
Arthur Jamet
e90c7f05a8 Front: Remove use of external images for placeholders 2023-12-21 17:17:47 +01:00
Arthur Jamet
fb0e43af88 Front: Prettier 2023-12-21 17:17:47 +01:00
Arthur Jamet
4577997b1c Front :add spanish translations 2023-12-21 17:17:47 +01:00
Arthur Jamet
9bb256f2ee front: add missing translation components 2023-12-21 17:17:47 +01:00
Arthur Jamet
d3994ff26e Front: First Pass on translations + remove unused setting tabs 2023-12-21 17:17:47 +01:00
Clément Le Bihan
00d097f643 Fixes prettier 2023-12-20 12:01:55 +01:00
Arthur Jamet
99da77f23e Front: Fix cirular dependecy between validators 2023-12-20 12:01:55 +01:00
Arthur Jamet
7a6dc8b0c9 Front: Use history include to get best/last score for a song 2023-12-20 12:01:55 +01:00
Clément Le Bihan
b4f04f9b71 Fixed number of lignes on DiscoveryCard 2023-12-19 17:06:30 +01:00
Arthur Jamet
9df0c98100 Front: DiscoveryView: Remove Dummy Data 2023-12-19 15:03:18 +01:00
131 changed files with 835 additions and 759 deletions

BIN
front/assets/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,40 @@
import { ArrowCircleDown2 } from 'iconsax-react-native';
import ButtonBase from './UI/ButtonBase';
import { translate } from '../i18n/i18n';
import { Linking } from 'react-native';
import { useState } from 'react';
import PopupCC from './UI/PopupCC';
const APKDownloadButton = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<ButtonBase
style={{}}
icon={ArrowCircleDown2}
type={'filled'}
title={translate('downloadAPK')}
onPress={() => setIsOpen(true)}
/>
<PopupCC
title={translate('downloadAPK')}
description={translate('downloadAPKInstructions')}
isVisible={isOpen}
setIsVisible={setIsOpen}
>
<ButtonBase
style={{}}
icon={ArrowCircleDown2}
type={'filled'}
title={translate('downloadAPK')}
onPress={() =>
Linking.openURL('https://github.com/Chroma-Case/Chromacase/releases')
}
/>
</PopupCC>
</>
);
};
export default APKDownloadButton;

View File

@@ -3,7 +3,7 @@ import Card, { CardBorderRadius } from './Card';
import { VStack, Text, Image } from 'native-base';
type ArtistCardProps = {
image: string;
image?: string;
name: string;
id: number;
onPress: () => void;
@@ -18,6 +18,7 @@ const ArtistCard = (props: ArtistCardProps) => {
<Image
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }}
source={{ uri: image }}
fallbackSource={{ uri: require('../assets/icon.jpg') }}
alt={name}
/>
<VStack>
@@ -30,11 +31,4 @@ const ArtistCard = (props: ArtistCardProps) => {
);
};
ArtistCard.defaultProps = {
image: 'https://picsum.photos/200',
name: 'Artist',
id: 0,
onPress: () => {},
};
export default ArtistCard;

View File

@@ -2,9 +2,9 @@ import { HStack, IconButton, Image, Text } from 'native-base';
import RowCustom from './RowCustom';
import TextButton from './TextButton';
import { MaterialIcons } from '@expo/vector-icons';
import API from '../API';
import DurationComponent from './DurationComponent';
import Song from '../models/Song';
import { useLikeSongMutation } from '../utils/likeSongMutation';
type FavSongRowProps = {
song: Song;
@@ -13,6 +13,8 @@ type FavSongRowProps = {
};
const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
const { mutate } = useLikeSongMutation();
return (
<RowCustom width={'100%'}>
<HStack px={2} space={5} justifyContent={'space-between'}>
@@ -63,7 +65,7 @@ const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
variant={'ghost'}
borderRadius={'full'}
onPress={() => {
API.removeLikedSong(song.id);
mutate({ songId: song.id, like: false });
}}
_icon={{
as: MaterialIcons,

View File

@@ -30,6 +30,7 @@ const GenreCard = (props: GenreCardProps) => {
source={{
uri: image,
}}
fallbackSource={{ uri: require('../assets/icon.jpg') }}
size="md"
/>
</Box>
@@ -43,10 +44,4 @@ const GenreCard = (props: GenreCardProps) => {
);
};
GenreCard.defaultProps = {
icon: 'https://picsum.photos/200',
name: 'Genre',
onPress: () => {},
};
export default GenreCard;

View File

@@ -20,6 +20,7 @@ import {
} from './ElementTypes';
import { ArrowDown2 } from 'iconsax-react-native';
import { useWindowDimensions } from 'react-native';
import Translate from '../Translate';
type RawElementProps = {
element: ElementProps;
@@ -149,7 +150,7 @@ export const RawElement = ({ element }: RawElementProps) => {
/>
);
default:
return <Text>Unknown type</Text>;
return <Translate translationKey="unknownError" />;
}
})()}
</Row>

View File

@@ -0,0 +1,135 @@
// credit to https://gist.github.com/ianmartorell/32bb7df95e5eff0a5ee2b2f55095e6a6
// this file was repurosed from there
// via this issue https://gist.github.com/necolas/1c494e44e23eb7f8c5864a2fac66299a
// because RNW's pressable doesn't bubble events to parent pressables: https://github.com/necolas/react-native-web/issues/1875
/* eslint-disable no-inner-declarations */
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
let isEnabled = false;
if (canUseDOM) {
/**
* Web browsers emulate mouse events (and hover states) after touch events.
* This code infers when the currently-in-use modality supports hover
* (including for multi-modality devices) and considers "hover" to be enabled
* if a mouse movement occurs more than 1 second after the last touch event.
* This threshold is long enough to account for longer delays between the
* browser firing touch and mouse events on low-powered devices.
*/
const HOVER_THRESHOLD_MS = 1000;
let lastTouchTimestamp = 0;
function enableHover() {
if (isEnabled || Date.now() - lastTouchTimestamp < HOVER_THRESHOLD_MS) {
return;
}
isEnabled = true;
}
function disableHover() {
lastTouchTimestamp = Date.now();
if (isEnabled) {
isEnabled = false;
}
}
document.addEventListener('touchstart', disableHover, true);
document.addEventListener('touchmove', disableHover, true);
document.addEventListener('mousemove', enableHover, true);
}
function isHoverEnabled(): boolean {
return isEnabled;
}
import React, { useCallback, ReactChild, useRef } from 'react';
import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated';
import { Platform } from 'react-native';
export interface HoverableProps {
onHoverIn?: () => void;
onHoverOut?: () => void;
onPressIn?: () => void;
onPressOut?: () => void;
children: NonNullable<ReactChild>;
}
export default function Hoverable({
onHoverIn,
onHoverOut,
children,
onPressIn,
onPressOut,
}: HoverableProps) {
const showHover = useSharedValue(true);
const isHovered = useSharedValue(false);
const hoverIn = useRef<undefined | (() => void)>(() => onHoverIn?.());
const hoverOut = useRef<undefined | (() => void)>(() => onHoverOut?.());
const pressIn = useRef<undefined | (() => void)>(() => onPressIn?.());
const pressOut = useRef<undefined | (() => void)>(() => onPressOut?.());
hoverIn.current = onHoverIn;
hoverOut.current = onHoverOut;
pressIn.current = onPressIn;
pressOut.current = onPressOut;
useAnimatedReaction(
() => {
return Platform.OS === 'web' && showHover.value && isHovered.value;
},
(hovered, previouslyHovered) => {
if (hovered !== previouslyHovered) {
if (hovered && hoverIn.current) {
// no need for runOnJS, it's always web
hoverIn.current();
} else if (hoverOut.current) {
hoverOut.current();
}
}
},
[]
);
const handleMouseEnter = useCallback(() => {
if (isHoverEnabled() && !isHovered.value) {
isHovered.value = true;
}
}, [isHovered]);
const handleMouseLeave = useCallback(() => {
if (isHovered.value) {
isHovered.value = false;
}
}, [isHovered]);
const handleGrant = useCallback(() => {
showHover.value = false;
pressIn.current?.();
}, [showHover]);
const handleRelease = useCallback(() => {
showHover.value = true;
pressOut.current?.();
}, [showHover]);
let webProps = {};
if (Platform.OS === 'web') {
webProps = {
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
// prevent hover showing while responder
onResponderGrant: handleGrant,
onResponderRelease: handleRelease,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return React.cloneElement(React.Children.only(children) as any, {
...webProps,
// if child is Touchable
onPressIn: handleGrant,
onPressOut: handleRelease,
});
}

View File

@@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from 'react';
import { Slider, Text, View, IconButton, Icon } from 'native-base';
import { Slider, View, IconButton, Icon } from 'native-base';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Audio } from 'expo-av';
// import { Audio } from 'expo-av';
import { VolumeHigh, VolumeSlash } from 'iconsax-react-native';
import { Translate } from '../i18n/i18n';
export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; bpm: number }) => {
const audio = useRef<Audio.Sound | null>(null);
const audio = useRef<null>(null);
const [enabled, setEnabled] = useState<boolean>(false);
const volume = useRef<number>(50);
@@ -14,12 +15,12 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
return;
} else if (!audio.current) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Audio.Sound.createAsync(require('../assets/metronome.mp3')).then((a) => {
audio.current = a.sound;
});
// Audio.Sound.createAsync(require('../assets/metronome.mp3')).then((a) => {
// audio.current = a.sound;
// });
}
return () => {
audio.current?.unloadAsync();
// audio.current?.unloadAsync();
};
}, [enabled]);
useEffect(() => {
@@ -27,12 +28,12 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
const int = setInterval(() => {
if (!enabled) return;
if (!audio.current) return;
audio.current?.playAsync();
// audio.current?.playAsync();
}, 60000 / bpm);
return () => clearInterval(int);
}, [bpm, paused]);
useEffect(() => {
audio.current?.setVolumeAsync(volume.current / 100);
// audio.current?.setVolumeAsync(volume.current / 100);
}, [volume.current]);
return (
<View flex={1}>
@@ -43,7 +44,7 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
justifyContent: 'space-between',
}}
>
<Text>Metronome</Text>
<Translate translationKey="metronome" />
<Icon as={<MaterialCommunityIcons name="metronome" size={24} color="white" />} />
</View>
<View

View File

@@ -5,9 +5,12 @@ import { useQuery } from '../../Queries';
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
import { CursorInfoItem } from '../../models/SongCursorInfos';
import { PianoNotes } from '../../state/SoundPlayerSlice';
import { Audio } from 'expo-av';
// import { Audio } from 'expo-av';
import { SvgContainer } from './SvgContainer';
import LoadingComponent from '../Loading';
import Sound from 'react-native-sound';
Sound.setCategory('Playback');
// note we are also using timestamp in a context
export type ParitionMagicProps = {
@@ -51,7 +54,7 @@ const PartitionMagic = ({
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
const partitionOffset = useSharedValue(0);
const pianoSounds = React.useRef<Record<string, Audio.Sound> | null>(null);
const pianoSounds = React.useRef<Record<string, Sound> | null>(null);
const cursorPaddingVertical = 10;
const cursorPaddingHorizontal = 3;
@@ -72,21 +75,40 @@ const PartitionMagic = ({
React.useEffect(() => {
if (!pianoSounds.current) {
Promise.all(
Object.entries(PianoNotes).map(([midiNumber, noteResource]) =>
Audio.Sound.createAsync(noteResource, {
volume: 1,
progressUpdateIntervalMillis: 100,
}).then((sound) => [midiNumber, sound.sound] as const)
)
Object.entries(PianoNotes).map(([midiNumber, noteResource]) => {
// Audio.Sound.createAsync(noteResource, {
// volume: 1,
// progressUpdateIntervalMillis: 100,
// }).then((sound) => [midiNumber, sound.sound] as const)
return new Promise((resolve, reject) => {
const sound = new Sound(noteResource, Sound.MAIN_BUNDLE, (error: any) => {
if (error) {
reject(error);
} else {
resolve([midiNumber, sound] as const);
}
});
});
})
).then((res) => {
pianoSounds.current = res.reduce(
(prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
{}
);
// pianoSounds.current = res.reduce(
// (prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
// {}
// );
pianoSounds.current = {};
(res as [string, Sound][]).forEach((curr) => {
pianoSounds.current![curr[0]] = curr[1];
});
console.log('sound loaded');
});
}
}, []);
}, [
() => {
pianoSounds?.current?.forEach((sound) => {
sound.release();
});
},
]);
const partitionDims = React.useMemo<[number, number]>(() => {
return [data?.pageWidth ?? 0, data?.pageHeight ?? 1];
}, [data]);
@@ -122,12 +144,16 @@ const PartitionMagic = ({
cursor.notes.forEach(({ note, duration }) => {
try {
const sound = pianoSounds.current![note]!;
sound.playAsync().catch(console.error);
sound.play((success) => {
if (!success) {
console.log('Sound did not play');
}
});
setTimeout(() => {
sound.stopAsync();
sound.stop();
}, duration - 10);
} catch (e) {
console.log(e);
console.log('Error key: ', note, e);
}
});
}

View File

@@ -89,9 +89,11 @@ const PlayViewControlBar = ({
<Text color={textColor[800]} fontSize={14} maxW={'100%'} isTruncated>
{song.name}
</Text>
<Text color={textColor[900]} fontSize={12} maxW={'100%'} isTruncated>
{song.artistId}
</Text>
{song.artist && (
<Text color={textColor[900]} fontSize={12} maxW={'100%'} isTruncated>
{song.artist?.name}
</Text>
)}
</View>
</View>
</View>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { translate } from '../i18n/i18n';
import { Box, Text, VStack, Progress, Stack } from 'native-base';
import { Translate } from '../i18n/i18n';
import { Box, VStack, Progress, Stack } from 'native-base';
import { useNavigation } from '../Navigation';
import Card from '../components/Card';
import UserAvatar from './UserAvatar';
@@ -18,13 +18,14 @@ const ProgressBar = ({ xp }: { xp: number }) => {
<Stack padding={4} space={2} direction="row" alignItems="center">
<UserAvatar />
<VStack alignItems={'center'} flexGrow={1} space={2}>
<Text>{`${translate('level')} ${level}`}</Text>
<Translate translationKey="level" format={(e) => `${e} ${level}`} />
<Box w="100%">
<Progress value={progessValue} mx="4" />
</Box>
<Text>
{xp} / {nextLevelThreshold} {translate('levelProgress')}
</Text>
<Translate
translationKey="levelProgress"
format={(e) => `${xp} / ${nextLevelThreshold} ${e}`}
/>
</VStack>
</Stack>
</Card>

View File

@@ -2,7 +2,6 @@ import React from 'react';
import {
VStack,
Heading,
Text,
Box,
Card,
Flex,
@@ -13,7 +12,7 @@ import {
import { SafeAreaView } from 'react-native';
import { SearchContext } from '../views/SearchView';
import { useQuery } from '../Queries';
import { translate } from '../i18n/i18n';
import { Translate, translate } from '../i18n/i18n';
import API from '../API';
import LoadingComponent, { LoadingView } from './Loading';
import ArtistCard from './ArtistCard';
@@ -25,12 +24,13 @@ import Song from '../models/Song';
import { useNavigation } from '../Navigation';
import SongRow from '../components/SongRow';
import FavSongRow from './FavSongRow';
import { useLikeSongMutation } from '../utils/likeSongMutation';
const swaToSongCardProps = (song: Song) => ({
songId: song.id,
name: song.name,
artistName: song.artist!.name,
cover: song.cover ?? 'https://picsum.photos/200',
cover: song.cover,
});
const HomeSearchComponent = () => {
@@ -84,18 +84,12 @@ type SongsSearchComponentProps = {
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
const navigation = useNavigation();
const { songData } = React.useContext(SearchContext);
const favoritesQuery = useQuery(API.getLikedSongs());
const handleFavoriteButton = async (state: boolean, songId: number): Promise<void> => {
if (state == false) await API.removeLikedSong(songId);
else await API.addLikedSong(songId);
};
const favoritesQuery = useQuery(API.getLikedSongs(['artist']));
const { mutate } = useLikeSongMutation();
return (
<ScrollView>
<Text fontSize="xl" fontWeight="bold" mt={4}>
{translate('songsFilter')}
</Text>
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
<Box>
{songData?.length ? (
songData.slice(0, props.maxRows).map((comp, index) => (
@@ -105,8 +99,8 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
isLiked={
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
}
handleLike={(state: boolean, songId: number) =>
handleFavoriteButton(state, songId)
handleLike={async (state: boolean, songId: number) =>
mutate({ songId: songId, like: state })
}
onPress={() => {
API.createSearchHistoryEntry(comp.name, 'song');
@@ -115,7 +109,7 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
/>
))
) : (
<Text>{translate('errNoResults')}</Text>
<Translate translationKey="errNoResults" />
)}
</Box>
</ScrollView>
@@ -132,9 +126,7 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
return (
<Box>
<Text fontSize="xl" fontWeight="bold" mt={4}>
{translate('artistFilter')}
</Text>
<Translate translationKey="artistFilter" fontSize="xl" fontWeight="bold" mt={4} />
{artistData?.length ? (
<CardGridCustom
content={artistData
@@ -151,7 +143,7 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
cardComponent={ArtistCard}
/>
) : (
<Text>{translate('errNoResults')}</Text>
<Translate translationKey="errNoResults" />
)}
</Box>
);
@@ -163,9 +155,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
return (
<Box>
<Text fontSize="xl" fontWeight="bold" mt={4}>
{translate('genreFilter')}
</Text>
<Translate translationKey="genreFilter" fontSize="xl" fontWeight="bold" mt={4} />
{genreData?.length ? (
<CardGridCustom
content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
@@ -180,7 +170,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
cardComponent={GenreCard}
/>
) : (
<Text>{translate('errNoResults')}</Text>
<Translate translationKey="errNoResults" />
)}
</Box>
);
@@ -200,9 +190,7 @@ const FavoritesComponent = () => {
return (
<ScrollView>
<Text fontSize="xl" fontWeight="bold" mt={4}>
{translate('songsFilter')}
</Text>
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
<Box>
{favoritesQuery.data?.map((songData) => (
<FavSongRow
@@ -268,7 +256,9 @@ const FilterSwitch = () => {
case 'favorites':
return <FavoritesComponent />;
default:
return <Text>Something very bad happened: {currentFilter}</Text>;
return (
<Translate translationKey="unknownError" format={(e) => `${e}: ${currentFilter}`} />
);
}
};

Some files were not shown because too many files have changed in this diff Show More