Compare commits
28 Commits
v0.8.4
...
sound-expe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db8d49618 | ||
|
|
4923fc72b2 | ||
|
|
60a73781bd | ||
|
|
4e3b378d6a | ||
|
|
2bf1e783a9 | ||
|
|
375d36f6c5 | ||
|
|
495380ec43 | ||
|
|
af0531bb0c | ||
|
|
c5124fa6ad | ||
|
|
962cf58e77 | ||
|
|
60988dd599 | ||
|
|
004a541302 | ||
|
|
f4cd9e18ea | ||
|
|
2dc301addf | ||
|
|
e85a959c26 | ||
|
|
339e808d27 | ||
|
|
22d1a97abd | ||
|
|
ce4baa61dc | ||
|
|
e90c7f05a8 | ||
|
|
fb0e43af88 | ||
|
|
4577997b1c | ||
|
|
9bb256f2ee | ||
|
|
d3994ff26e | ||
|
|
00d097f643 | ||
|
|
99da77f23e | ||
|
|
7a6dc8b0c9 | ||
|
|
b4f04f9b71 | ||
|
|
9df0c98100 |
BIN
front/assets/google.png
Normal file
BIN
front/assets/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
40
front/components/APKDownloadButton.tsx
Normal file
40
front/components/APKDownloadButton.tsx
Normal 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;
|
||||||
@@ -3,7 +3,7 @@ import Card, { CardBorderRadius } from './Card';
|
|||||||
import { VStack, Text, Image } from 'native-base';
|
import { VStack, Text, Image } from 'native-base';
|
||||||
|
|
||||||
type ArtistCardProps = {
|
type ArtistCardProps = {
|
||||||
image: string;
|
image?: string;
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
@@ -18,6 +18,7 @@ const ArtistCard = (props: ArtistCardProps) => {
|
|||||||
<Image
|
<Image
|
||||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }}
|
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }}
|
||||||
source={{ uri: image }}
|
source={{ uri: image }}
|
||||||
|
fallbackSource={{ uri: require('../assets/icon.jpg') }}
|
||||||
alt={name}
|
alt={name}
|
||||||
/>
|
/>
|
||||||
<VStack>
|
<VStack>
|
||||||
@@ -30,11 +31,4 @@ const ArtistCard = (props: ArtistCardProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistCard.defaultProps = {
|
|
||||||
image: 'https://picsum.photos/200',
|
|
||||||
name: 'Artist',
|
|
||||||
id: 0,
|
|
||||||
onPress: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArtistCard;
|
export default ArtistCard;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { HStack, IconButton, Image, Text } from 'native-base';
|
|||||||
import RowCustom from './RowCustom';
|
import RowCustom from './RowCustom';
|
||||||
import TextButton from './TextButton';
|
import TextButton from './TextButton';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import API from '../API';
|
|
||||||
import DurationComponent from './DurationComponent';
|
import DurationComponent from './DurationComponent';
|
||||||
import Song from '../models/Song';
|
import Song from '../models/Song';
|
||||||
|
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||||
|
|
||||||
type FavSongRowProps = {
|
type FavSongRowProps = {
|
||||||
song: Song;
|
song: Song;
|
||||||
@@ -13,6 +13,8 @@ type FavSongRowProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
||||||
|
const { mutate } = useLikeSongMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowCustom width={'100%'}>
|
<RowCustom width={'100%'}>
|
||||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||||
@@ -63,7 +65,7 @@ const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
|||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
borderRadius={'full'}
|
borderRadius={'full'}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
API.removeLikedSong(song.id);
|
mutate({ songId: song.id, like: false });
|
||||||
}}
|
}}
|
||||||
_icon={{
|
_icon={{
|
||||||
as: MaterialIcons,
|
as: MaterialIcons,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const GenreCard = (props: GenreCardProps) => {
|
|||||||
source={{
|
source={{
|
||||||
uri: image,
|
uri: image,
|
||||||
}}
|
}}
|
||||||
|
fallbackSource={{ uri: require('../assets/icon.jpg') }}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -43,10 +44,4 @@ const GenreCard = (props: GenreCardProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GenreCard.defaultProps = {
|
|
||||||
icon: 'https://picsum.photos/200',
|
|
||||||
name: 'Genre',
|
|
||||||
onPress: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GenreCard;
|
export default GenreCard;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from './ElementTypes';
|
} from './ElementTypes';
|
||||||
import { ArrowDown2 } from 'iconsax-react-native';
|
import { ArrowDown2 } from 'iconsax-react-native';
|
||||||
import { useWindowDimensions } from 'react-native';
|
import { useWindowDimensions } from 'react-native';
|
||||||
|
import Translate from '../Translate';
|
||||||
|
|
||||||
type RawElementProps = {
|
type RawElementProps = {
|
||||||
element: ElementProps;
|
element: ElementProps;
|
||||||
@@ -149,7 +150,7 @@ export const RawElement = ({ element }: RawElementProps) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return <Text>Unknown type</Text>;
|
return <Translate translationKey="unknownError" />;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
135
front/components/Hoverable.ts
Normal file
135
front/components/Hoverable.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
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 { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { Audio } from 'expo-av';
|
// import { Audio } from 'expo-av';
|
||||||
import { VolumeHigh, VolumeSlash } from 'iconsax-react-native';
|
import { VolumeHigh, VolumeSlash } from 'iconsax-react-native';
|
||||||
|
import { Translate } from '../i18n/i18n';
|
||||||
|
|
||||||
export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; bpm: number }) => {
|
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 [enabled, setEnabled] = useState<boolean>(false);
|
||||||
const volume = useRef<number>(50);
|
const volume = useRef<number>(50);
|
||||||
|
|
||||||
@@ -14,12 +15,12 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
|
|||||||
return;
|
return;
|
||||||
} else if (!audio.current) {
|
} else if (!audio.current) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
Audio.Sound.createAsync(require('../assets/metronome.mp3')).then((a) => {
|
// Audio.Sound.createAsync(require('../assets/metronome.mp3')).then((a) => {
|
||||||
audio.current = a.sound;
|
// audio.current = a.sound;
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
audio.current?.unloadAsync();
|
// audio.current?.unloadAsync();
|
||||||
};
|
};
|
||||||
}, [enabled]);
|
}, [enabled]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,12 +28,12 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
|
|||||||
const int = setInterval(() => {
|
const int = setInterval(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
if (!audio.current) return;
|
if (!audio.current) return;
|
||||||
audio.current?.playAsync();
|
// audio.current?.playAsync();
|
||||||
}, 60000 / bpm);
|
}, 60000 / bpm);
|
||||||
return () => clearInterval(int);
|
return () => clearInterval(int);
|
||||||
}, [bpm, paused]);
|
}, [bpm, paused]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
audio.current?.setVolumeAsync(volume.current / 100);
|
// audio.current?.setVolumeAsync(volume.current / 100);
|
||||||
}, [volume.current]);
|
}, [volume.current]);
|
||||||
return (
|
return (
|
||||||
<View flex={1}>
|
<View flex={1}>
|
||||||
@@ -43,7 +44,7 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>Metronome</Text>
|
<Translate translationKey="metronome" />
|
||||||
<Icon as={<MaterialCommunityIcons name="metronome" size={24} color="white" />} />
|
<Icon as={<MaterialCommunityIcons name="metronome" size={24} color="white" />} />
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import { useQuery } from '../../Queries';
|
|||||||
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
|
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
|
||||||
import { CursorInfoItem } from '../../models/SongCursorInfos';
|
import { CursorInfoItem } from '../../models/SongCursorInfos';
|
||||||
import { PianoNotes } from '../../state/SoundPlayerSlice';
|
import { PianoNotes } from '../../state/SoundPlayerSlice';
|
||||||
import { Audio } from 'expo-av';
|
// import { Audio } from 'expo-av';
|
||||||
import { SvgContainer } from './SvgContainer';
|
import { SvgContainer } from './SvgContainer';
|
||||||
import LoadingComponent from '../Loading';
|
import LoadingComponent from '../Loading';
|
||||||
|
import Sound from 'react-native-sound';
|
||||||
|
|
||||||
|
Sound.setCategory('Playback');
|
||||||
|
|
||||||
// note we are also using timestamp in a context
|
// note we are also using timestamp in a context
|
||||||
export type ParitionMagicProps = {
|
export type ParitionMagicProps = {
|
||||||
@@ -51,7 +54,7 @@ const PartitionMagic = ({
|
|||||||
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
||||||
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
|
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
|
||||||
const partitionOffset = useSharedValue(0);
|
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 cursorPaddingVertical = 10;
|
||||||
const cursorPaddingHorizontal = 3;
|
const cursorPaddingHorizontal = 3;
|
||||||
|
|
||||||
@@ -72,21 +75,40 @@ const PartitionMagic = ({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!pianoSounds.current) {
|
if (!pianoSounds.current) {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
Object.entries(PianoNotes).map(([midiNumber, noteResource]) =>
|
Object.entries(PianoNotes).map(([midiNumber, noteResource]) => {
|
||||||
Audio.Sound.createAsync(noteResource, {
|
// Audio.Sound.createAsync(noteResource, {
|
||||||
volume: 1,
|
// volume: 1,
|
||||||
progressUpdateIntervalMillis: 100,
|
// progressUpdateIntervalMillis: 100,
|
||||||
}).then((sound) => [midiNumber, sound.sound] as const)
|
// }).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) => {
|
).then((res) => {
|
||||||
pianoSounds.current = res.reduce(
|
// pianoSounds.current = res.reduce(
|
||||||
(prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
|
// (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');
|
console.log('sound loaded');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [
|
||||||
|
() => {
|
||||||
|
pianoSounds?.current?.forEach((sound) => {
|
||||||
|
sound.release();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
]);
|
||||||
const partitionDims = React.useMemo<[number, number]>(() => {
|
const partitionDims = React.useMemo<[number, number]>(() => {
|
||||||
return [data?.pageWidth ?? 0, data?.pageHeight ?? 1];
|
return [data?.pageWidth ?? 0, data?.pageHeight ?? 1];
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -122,12 +144,16 @@ const PartitionMagic = ({
|
|||||||
cursor.notes.forEach(({ note, duration }) => {
|
cursor.notes.forEach(({ note, duration }) => {
|
||||||
try {
|
try {
|
||||||
const sound = pianoSounds.current![note]!;
|
const sound = pianoSounds.current![note]!;
|
||||||
sound.playAsync().catch(console.error);
|
sound.play((success) => {
|
||||||
|
if (!success) {
|
||||||
|
console.log('Sound did not play');
|
||||||
|
}
|
||||||
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sound.stopAsync();
|
sound.stop();
|
||||||
}, duration - 10);
|
}, duration - 10);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log('Error key: ', note, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ const PlayViewControlBar = ({
|
|||||||
<Text color={textColor[800]} fontSize={14} maxW={'100%'} isTruncated>
|
<Text color={textColor[800]} fontSize={14} maxW={'100%'} isTruncated>
|
||||||
{song.name}
|
{song.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={textColor[900]} fontSize={12} maxW={'100%'} isTruncated>
|
{song.artist && (
|
||||||
{song.artistId}
|
<Text color={textColor[900]} fontSize={12} maxW={'100%'} isTruncated>
|
||||||
</Text>
|
{song.artist?.name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { translate } from '../i18n/i18n';
|
import { Translate } from '../i18n/i18n';
|
||||||
import { Box, Text, VStack, Progress, Stack } from 'native-base';
|
import { Box, VStack, Progress, Stack } from 'native-base';
|
||||||
import { useNavigation } from '../Navigation';
|
import { useNavigation } from '../Navigation';
|
||||||
import Card from '../components/Card';
|
import Card from '../components/Card';
|
||||||
import UserAvatar from './UserAvatar';
|
import UserAvatar from './UserAvatar';
|
||||||
@@ -18,13 +18,14 @@ const ProgressBar = ({ xp }: { xp: number }) => {
|
|||||||
<Stack padding={4} space={2} direction="row" alignItems="center">
|
<Stack padding={4} space={2} direction="row" alignItems="center">
|
||||||
<UserAvatar />
|
<UserAvatar />
|
||||||
<VStack alignItems={'center'} flexGrow={1} space={2}>
|
<VStack alignItems={'center'} flexGrow={1} space={2}>
|
||||||
<Text>{`${translate('level')} ${level}`}</Text>
|
<Translate translationKey="level" format={(e) => `${e} ${level}`} />
|
||||||
<Box w="100%">
|
<Box w="100%">
|
||||||
<Progress value={progessValue} mx="4" />
|
<Progress value={progessValue} mx="4" />
|
||||||
</Box>
|
</Box>
|
||||||
<Text>
|
<Translate
|
||||||
{xp} / {nextLevelThreshold} {translate('levelProgress')}
|
translationKey="levelProgress"
|
||||||
</Text>
|
format={(e) => `${xp} / ${nextLevelThreshold} ${e}`}
|
||||||
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import {
|
import {
|
||||||
VStack,
|
VStack,
|
||||||
Heading,
|
Heading,
|
||||||
Text,
|
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native';
|
import { SafeAreaView } from 'react-native';
|
||||||
import { SearchContext } from '../views/SearchView';
|
import { SearchContext } from '../views/SearchView';
|
||||||
import { useQuery } from '../Queries';
|
import { useQuery } from '../Queries';
|
||||||
import { translate } from '../i18n/i18n';
|
import { Translate, translate } from '../i18n/i18n';
|
||||||
import API from '../API';
|
import API from '../API';
|
||||||
import LoadingComponent, { LoadingView } from './Loading';
|
import LoadingComponent, { LoadingView } from './Loading';
|
||||||
import ArtistCard from './ArtistCard';
|
import ArtistCard from './ArtistCard';
|
||||||
@@ -25,12 +24,13 @@ import Song from '../models/Song';
|
|||||||
import { useNavigation } from '../Navigation';
|
import { useNavigation } from '../Navigation';
|
||||||
import SongRow from '../components/SongRow';
|
import SongRow from '../components/SongRow';
|
||||||
import FavSongRow from './FavSongRow';
|
import FavSongRow from './FavSongRow';
|
||||||
|
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||||
|
|
||||||
const swaToSongCardProps = (song: Song) => ({
|
const swaToSongCardProps = (song: Song) => ({
|
||||||
songId: song.id,
|
songId: song.id,
|
||||||
name: song.name,
|
name: song.name,
|
||||||
artistName: song.artist!.name,
|
artistName: song.artist!.name,
|
||||||
cover: song.cover ?? 'https://picsum.photos/200',
|
cover: song.cover,
|
||||||
});
|
});
|
||||||
|
|
||||||
const HomeSearchComponent = () => {
|
const HomeSearchComponent = () => {
|
||||||
@@ -84,18 +84,12 @@ type SongsSearchComponentProps = {
|
|||||||
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { songData } = React.useContext(SearchContext);
|
const { songData } = React.useContext(SearchContext);
|
||||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
const favoritesQuery = useQuery(API.getLikedSongs(['artist']));
|
||||||
|
const { mutate } = useLikeSongMutation();
|
||||||
const handleFavoriteButton = async (state: boolean, songId: number): Promise<void> => {
|
|
||||||
if (state == false) await API.removeLikedSong(songId);
|
|
||||||
else await API.addLikedSong(songId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||||
{translate('songsFilter')}
|
|
||||||
</Text>
|
|
||||||
<Box>
|
<Box>
|
||||||
{songData?.length ? (
|
{songData?.length ? (
|
||||||
songData.slice(0, props.maxRows).map((comp, index) => (
|
songData.slice(0, props.maxRows).map((comp, index) => (
|
||||||
@@ -105,8 +99,8 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
|||||||
isLiked={
|
isLiked={
|
||||||
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
|
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
|
||||||
}
|
}
|
||||||
handleLike={(state: boolean, songId: number) =>
|
handleLike={async (state: boolean, songId: number) =>
|
||||||
handleFavoriteButton(state, songId)
|
mutate({ songId: songId, like: state })
|
||||||
}
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
API.createSearchHistoryEntry(comp.name, 'song');
|
API.createSearchHistoryEntry(comp.name, 'song');
|
||||||
@@ -115,7 +109,7 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text>{translate('errNoResults')}</Text>
|
<Translate translationKey="errNoResults" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -132,9 +126,7 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
<Translate translationKey="artistFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||||
{translate('artistFilter')}
|
|
||||||
</Text>
|
|
||||||
{artistData?.length ? (
|
{artistData?.length ? (
|
||||||
<CardGridCustom
|
<CardGridCustom
|
||||||
content={artistData
|
content={artistData
|
||||||
@@ -151,7 +143,7 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
cardComponent={ArtistCard}
|
cardComponent={ArtistCard}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text>{translate('errNoResults')}</Text>
|
<Translate translationKey="errNoResults" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -163,9 +155,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
<Translate translationKey="genreFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||||
{translate('genreFilter')}
|
|
||||||
</Text>
|
|
||||||
{genreData?.length ? (
|
{genreData?.length ? (
|
||||||
<CardGridCustom
|
<CardGridCustom
|
||||||
content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
|
content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
|
||||||
@@ -180,7 +170,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
cardComponent={GenreCard}
|
cardComponent={GenreCard}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text>{translate('errNoResults')}</Text>
|
<Translate translationKey="errNoResults" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -200,9 +190,7 @@ const FavoritesComponent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||||
{translate('songsFilter')}
|
|
||||||
</Text>
|
|
||||||
<Box>
|
<Box>
|
||||||
{favoritesQuery.data?.map((songData) => (
|
{favoritesQuery.data?.map((songData) => (
|
||||||
<FavSongRow
|
<FavSongRow
|
||||||
@@ -268,7 +256,9 @@ const FilterSwitch = () => {
|
|||||||
case 'favorites':
|
case 'favorites':
|
||||||
return <FavoritesComponent />;
|
return <FavoritesComponent />;
|
||||||
default:
|
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
Reference in New Issue
Block a user