[ADD] LibCC ChromaCase:
- IconButton and MusicItem creation and documentation - Update native base theme
This commit is contained in:
@@ -34,6 +34,7 @@ import SignupView from './views/SignupView';
|
|||||||
import PasswordResetView from './views/PasswordResetView';
|
import PasswordResetView from './views/PasswordResetView';
|
||||||
import ForgotPasswordView from './views/ForgotPasswordView';
|
import ForgotPasswordView from './views/ForgotPasswordView';
|
||||||
import DiscoveryView from './views/V2/DiscoveryView';
|
import DiscoveryView from './views/V2/DiscoveryView';
|
||||||
|
import MusicView from './views/MusicView';
|
||||||
|
|
||||||
// Util function to hide route props in URL
|
// Util function to hide route props in URL
|
||||||
const removeMe = () => '';
|
const removeMe = () => '';
|
||||||
@@ -45,6 +46,11 @@ const protectedRoutes = () =>
|
|||||||
options: { headerShown: false },
|
options: { headerShown: false },
|
||||||
link: '/',
|
link: '/',
|
||||||
},
|
},
|
||||||
|
Music: {
|
||||||
|
component: MusicView,
|
||||||
|
options: { headerShown: false },
|
||||||
|
link: '/music',
|
||||||
|
},
|
||||||
HomeNew: {
|
HomeNew: {
|
||||||
component: DiscoveryView,
|
component: DiscoveryView,
|
||||||
options: { headerShown: false },
|
options: { headerShown: false },
|
||||||
@@ -99,11 +105,6 @@ const protectedRoutes = () =>
|
|||||||
|
|
||||||
const publicRoutes = () =>
|
const publicRoutes = () =>
|
||||||
({
|
({
|
||||||
// Start: {
|
|
||||||
// component: StartPageView,
|
|
||||||
// options: { title: 'Chromacase', headerShown: false },
|
|
||||||
// link: '/',
|
|
||||||
// },
|
|
||||||
Login: {
|
Login: {
|
||||||
component: SigninView,
|
component: SigninView,
|
||||||
options: { title: translate('signInBtn'), headerShown: false },
|
options: { title: translate('signInBtn'), headerShown: false },
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
|||||||
700: 'rgba(255,255,255,0.7)',
|
700: 'rgba(255,255,255,0.7)',
|
||||||
800: 'rgba(255,255,255,0.8)',
|
800: 'rgba(255,255,255,0.8)',
|
||||||
900: 'rgba(255,255,255,0.9)',
|
900: 'rgba(255,255,255,0.9)',
|
||||||
|
1000: 'rgba(255,255,255,1)',
|
||||||
};
|
};
|
||||||
const darkGlassmorphism = {
|
const darkGlassmorphism = {
|
||||||
50: 'rgba(16,16,20,0.05)',
|
50: 'rgba(16,16,20,0.05)',
|
||||||
@@ -27,11 +28,15 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
|||||||
700: 'rgba(16,16,20,0.7)',
|
700: 'rgba(16,16,20,0.7)',
|
||||||
800: 'rgba(16,16,20,0.8)',
|
800: 'rgba(16,16,20,0.8)',
|
||||||
900: 'rgba(16,16,20,0.9)',
|
900: 'rgba(16,16,20,0.9)',
|
||||||
|
1000: 'rgba(16,16,20,1)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const glassmorphism = colorScheme === 'light'
|
const glassmorphism = colorScheme === 'light'
|
||||||
? lightGlassmorphism
|
? lightGlassmorphism
|
||||||
: darkGlassmorphism
|
: darkGlassmorphism
|
||||||
|
const text = colorScheme === 'light'
|
||||||
|
? darkGlassmorphism
|
||||||
|
: lightGlassmorphism
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NativeBaseProvider
|
<NativeBaseProvider
|
||||||
@@ -46,6 +51,7 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
|||||||
mono: 'Lexend',
|
mono: 'Lexend',
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
text: text,
|
||||||
primary: {
|
primary: {
|
||||||
50: '#eff1fe',
|
50: '#eff1fe',
|
||||||
100: '#e7eafe',
|
100: '#e7eafe',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const GenreCard = (props: GenreCardProps) => {
|
|||||||
<Card shadow={3} onPress={props.onPress}>
|
<Card shadow={3} onPress={props.onPress}>
|
||||||
<VStack m={1.5} space={3} alignItems="center">
|
<VStack m={1.5} space={3} alignItems="center">
|
||||||
<Box
|
<Box
|
||||||
bg={theme.colors.primary[400]}
|
bg={theme.colors.primary[300]}
|
||||||
w={20}
|
w={20}
|
||||||
h={20}
|
h={20}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
|
|||||||
@@ -10,6 +10,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 useColorScheme from '../../hooks/colorScheme';
|
||||||
|
|
||||||
type RawElementProps = {
|
type RawElementProps = {
|
||||||
element: ElementProps;
|
element: ElementProps;
|
||||||
@@ -20,6 +21,9 @@ export const RawElement = ({ element }: RawElementProps) => {
|
|||||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
const isSmallScreen = screenSize === 'small';
|
const isSmallScreen = screenSize === 'small';
|
||||||
const { width: screenWidth } = useWindowDimensions();
|
const { width: screenWidth } = useWindowDimensions();
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const color = colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
style={{
|
style={{
|
||||||
@@ -122,7 +126,7 @@ export const RawElement = ({ element }: RawElementProps) => {
|
|||||||
case 'custom':
|
case 'custom':
|
||||||
return data;
|
return data;
|
||||||
case 'sectionDropdown':
|
case 'sectionDropdown':
|
||||||
return <ArrowDown2 size="24" color="#fff" variant="Outline" />;
|
return <ArrowDown2 size="24" color={color} variant="Outline" />;
|
||||||
default:
|
default:
|
||||||
return <Text>Unknown type</Text>;
|
return <Text>Unknown type</Text>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,28 +38,28 @@ const ButtonBase: React.FC<ButtonProps> = ({
|
|||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 4.65,
|
shadowRadius: 4.65,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
backgroundColor: colors.primary[400],
|
backgroundColor: colors.primary[300],
|
||||||
},
|
},
|
||||||
onHover: {
|
onHover: {
|
||||||
scale: 1.02,
|
scale: 1.02,
|
||||||
shadowOpacity: 0.37,
|
shadowOpacity: 0.37,
|
||||||
shadowRadius: 7.49,
|
shadowRadius: 7.49,
|
||||||
elevation: 12,
|
elevation: 12,
|
||||||
backgroundColor: colors.primary[500],
|
backgroundColor: colors.primary[400],
|
||||||
},
|
},
|
||||||
onPressed: {
|
onPressed: {
|
||||||
scale: 0.98,
|
scale: 0.98,
|
||||||
shadowOpacity: 0.23,
|
shadowOpacity: 0.23,
|
||||||
shadowRadius: 2.62,
|
shadowRadius: 2.62,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
backgroundColor: colors.primary[600],
|
backgroundColor: colors.primary[500],
|
||||||
},
|
},
|
||||||
Disabled: {
|
Disabled: {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 4.65,
|
shadowRadius: 4.65,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
backgroundColor: colors.primary[400],
|
backgroundColor: colors.primary[300],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
|
|||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
size="small"
|
size="small"
|
||||||
color={type === 'outlined' ? '#6075F9' : '#FFFFFF'}
|
color={type === 'outlined' ? colors.primary[300] : '#FFFFFF'}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
@@ -127,7 +127,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
|
|||||||
{icon && (
|
{icon && (
|
||||||
<MyIcon
|
<MyIcon
|
||||||
size={'18'}
|
size={'18'}
|
||||||
color={type === 'outlined' ? '#6075F9' : colorScheme === 'dark' || type === 'filled' ? '#FFFFFF' : colors.black[500] }
|
color={type === 'outlined' ? colors.primary[300] : colorScheme === 'dark' || type === 'filled' ? '#FFFFFF' : colors.black[500] }
|
||||||
variant={iconVariant}
|
variant={iconVariant}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ const CheckboxBase: React.FC<CheckboxProps> = ({ title, color, style, check, set
|
|||||||
{check ? (
|
{check ? (
|
||||||
<TickSquare
|
<TickSquare
|
||||||
size="24"
|
size="24"
|
||||||
color={color ?? colors.primary[400]}
|
color={color ?? colors.primary[300]}
|
||||||
variant="Bold"
|
variant="Bold"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AddSquare
|
<AddSquare
|
||||||
size="24"
|
size="24"
|
||||||
color={color ?? colors.primary[400]}
|
color={color ?? colors.primary[300]}
|
||||||
variant="Outline"
|
variant="Outline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
259
front/components/UI/IconButton.tsx
Normal file
259
front/components/UI/IconButton.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// Import required dependencies and components.
|
||||||
|
import { Icon } from "iconsax-react-native";
|
||||||
|
import { useRef, useState, useMemo } from "react";
|
||||||
|
import { Animated, StyleProp, TouchableOpacity, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
// Default values for the component props.
|
||||||
|
const DEFAULT_SCALE_FACTOR: number = 1.25;
|
||||||
|
const DEFAULT_ANIMATION_DURATION: number = 250;
|
||||||
|
const DEFAULT_PADDING: number = 0;
|
||||||
|
|
||||||
|
// Define the type for the IconButton props.
|
||||||
|
type IconButtonProps = {
|
||||||
|
/**
|
||||||
|
* Indicates if the button starts in an active state.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color of the icon.
|
||||||
|
*/
|
||||||
|
color: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional color of the icon when active.
|
||||||
|
*/
|
||||||
|
colorActive?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function triggered when the button is pressed.
|
||||||
|
*/
|
||||||
|
onPress?: () => void | Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the icon.
|
||||||
|
* @default 24
|
||||||
|
*/
|
||||||
|
size?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon to display.
|
||||||
|
*/
|
||||||
|
icon: Icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional icon to display when active.
|
||||||
|
*/
|
||||||
|
iconActive?: Icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant style of the icon.
|
||||||
|
* @default "Outline"
|
||||||
|
*/
|
||||||
|
variant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant style of the icon when active.
|
||||||
|
* @default "Outline"
|
||||||
|
*/
|
||||||
|
activeVariant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom style for the icon.
|
||||||
|
*/
|
||||||
|
style?: ViewStyle | ViewStyle[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom style for the icon's container.
|
||||||
|
*/
|
||||||
|
containerStyle?: ViewStyle | ViewStyle[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale factor when the icon animates on press.
|
||||||
|
* @default 1.25
|
||||||
|
*/
|
||||||
|
scaleFactor?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the icon animation in milliseconds.
|
||||||
|
* @default 250
|
||||||
|
*/
|
||||||
|
animationDuration?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Padding around the icon.
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
padding?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `IconButton` Component
|
||||||
|
*
|
||||||
|
* Render an interactive icon that can toggle between active and inactive states.
|
||||||
|
* Supports customization of colors, icons, animation speed, size, and more.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Can render two different icons for active and inactive states.
|
||||||
|
* - Includes an animation that scales the icon when pressed.
|
||||||
|
* - Supports custom styling for both the icon and its container.
|
||||||
|
* - Accepts various icon variants for flexibility in design.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <IconButton
|
||||||
|
* color="#000"
|
||||||
|
* icon={SomeIcon}
|
||||||
|
* onPress={() => console.log('Icon pressed!')}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* To use with active states:
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <IconButton
|
||||||
|
* isActive={true}
|
||||||
|
* color="#000"
|
||||||
|
* colorActive="#ff0000"
|
||||||
|
* icon={SomeIcon}
|
||||||
|
* iconActive={SomeActiveIcon}
|
||||||
|
* onPress={() => console.log('Icon toggled!')}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Note: If `iconActive` is provided but `colorActive` is not,
|
||||||
|
* the `color` prop will be used for both active and inactive states.
|
||||||
|
*/
|
||||||
|
const IconButton: React.FC<IconButtonProps> = ({
|
||||||
|
isActive = false,
|
||||||
|
color,
|
||||||
|
colorActive,
|
||||||
|
onPress,
|
||||||
|
size = 24,
|
||||||
|
icon: Icon,
|
||||||
|
iconActive: IconActive,
|
||||||
|
variant = "Outline",
|
||||||
|
activeVariant = "Outline",
|
||||||
|
style,
|
||||||
|
containerStyle,
|
||||||
|
scaleFactor = DEFAULT_SCALE_FACTOR,
|
||||||
|
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||||
|
padding = DEFAULT_PADDING,
|
||||||
|
}) => {
|
||||||
|
// State to track active status.
|
||||||
|
const [isActiveState, setIsActiveState] = useState<boolean>(isActive);
|
||||||
|
|
||||||
|
// Animation values.
|
||||||
|
const scaleValue: Animated.Value = useRef(new Animated.Value(1)).current;
|
||||||
|
const animateValue: Animated.Value = useRef(new Animated.Value(isActive ? 0 : 1)).current;
|
||||||
|
|
||||||
|
// Check for active icon.
|
||||||
|
const hasActiveIcon: boolean = !!IconActive;
|
||||||
|
|
||||||
|
// Interpolation for icon colors between active and inactive states.
|
||||||
|
const colorValue: Animated.AnimatedInterpolation<string | number> = animateValue.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [color, colorActive || color],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine styles for the icon container.
|
||||||
|
const combinedContainerStyle: StyleProp<ViewStyle> = useMemo<(ViewStyle | ViewStyle[] | undefined)[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
position: 'relative',
|
||||||
|
// Adjust width and height to account for specified padding. Since the icons
|
||||||
|
// are absolutely positioned inside the container, the container's size needs
|
||||||
|
// to include the padding to ensure the icons are properly centered and spaced.
|
||||||
|
width: size + (padding * 2),
|
||||||
|
height: size + (padding * 2),
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
containerStyle
|
||||||
|
],
|
||||||
|
[padding, containerStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the active state of the icon.
|
||||||
|
* Executes the onPress callback and triggers the animation.
|
||||||
|
*/
|
||||||
|
const toggleState = async () => {
|
||||||
|
// Execute onPress if provided.
|
||||||
|
if (onPress) {
|
||||||
|
await onPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle isActiveState.
|
||||||
|
setIsActiveState(!isActiveState);
|
||||||
|
|
||||||
|
// Animation sequences.
|
||||||
|
const animations: Animated.CompositeAnimation[] = [
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(scaleValue, {
|
||||||
|
toValue: scaleFactor,
|
||||||
|
duration: animationDuration,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(scaleValue, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: animationDuration,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (hasActiveIcon || colorActive) {
|
||||||
|
animations.push(
|
||||||
|
Animated.timing(animateValue, {
|
||||||
|
toValue: isActiveState ? 1 : 0,
|
||||||
|
duration: animationDuration,
|
||||||
|
useNativeDriver: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animations in parallel.
|
||||||
|
Animated.parallel(animations).start();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={toggleState}
|
||||||
|
style={combinedContainerStyle}
|
||||||
|
>
|
||||||
|
{hasActiveIcon && (
|
||||||
|
<Animated.View
|
||||||
|
style={[{
|
||||||
|
padding: padding,
|
||||||
|
color: colorValue,
|
||||||
|
opacity: animateValue.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [1, 0],
|
||||||
|
}),
|
||||||
|
position: 'absolute',
|
||||||
|
transform: [{ scale: scaleValue }],
|
||||||
|
}, style]}
|
||||||
|
>
|
||||||
|
<IconActive size={size} variant={activeVariant}/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Animated.View
|
||||||
|
style={[{
|
||||||
|
padding: padding,
|
||||||
|
color: colorValue,
|
||||||
|
opacity: animateValue,
|
||||||
|
position: 'absolute',
|
||||||
|
transform: [{ scale: scaleValue }],
|
||||||
|
}, style]}
|
||||||
|
>
|
||||||
|
<Icon size={size} variant={variant} />
|
||||||
|
</Animated.View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconButton;
|
||||||
@@ -219,7 +219,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const animatedStyle = {
|
const animatedStyle = {
|
||||||
backgroundColor: isOutlined ? colors.coolGray[200] : backgroundColorValue,
|
backgroundColor: isOutlined ? colors.coolGray[100] : backgroundColorValue,
|
||||||
borderColor: isOutlined ? backgroundColorValue : 'transparent',
|
borderColor: isOutlined ? backgroundColorValue : 'transparent',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
transform: [{ scale: scaleValue }],
|
transform: [{ scale: scaleValue }],
|
||||||
@@ -229,7 +229,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const disableStyle = {
|
const disableStyle = {
|
||||||
backgroundColor: isOutlined ? colors.coolGray[200] : styleAnimate.Disabled.backgroundColor,
|
backgroundColor: isOutlined ? colors.coolGray[100] : styleAnimate.Disabled.backgroundColor,
|
||||||
borderColor: isOutlined ? styleAnimate.Disabled.backgroundColor : 'transparent',
|
borderColor: isOutlined ? styleAnimate.Disabled.backgroundColor : 'transparent',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
scale: styleAnimate.Disabled.scale,
|
scale: styleAnimate.Disabled.scale,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const LinkBase: React.FC<LinkBaseProps> = ({ text, onPress }) => {
|
|||||||
<Animated.View style={[
|
<Animated.View style={[
|
||||||
styles.underline,
|
styles.underline,
|
||||||
{
|
{
|
||||||
backgroundColor: theme.colors.primary[400],
|
backgroundColor: theme.colors.primary[300],
|
||||||
height: underlineHeight,
|
height: underlineHeight,
|
||||||
opacity: opacity
|
opacity: opacity
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const LogoutButtonCC = ({collapse = false, isGuest = false, buttonType = 'menu',
|
|||||||
<ButtonBase
|
<ButtonBase
|
||||||
style={style}
|
style={style}
|
||||||
icon={LogoutCurve}
|
icon={LogoutCurve}
|
||||||
title={collapse ? translate('signOutBtn') : undefined}
|
title={!collapse ? translate('signOutBtn') : undefined}
|
||||||
type={buttonType}
|
type={buttonType}
|
||||||
onPress={async () => {isGuest ? setIsVisible(true) : dispatch(unsetAccessToken());}}
|
onPress={async () => {isGuest ? setIsVisible(true) : dispatch(unsetAccessToken());}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
184
front/components/UI/MusicItem.tsx
Normal file
184
front/components/UI/MusicItem.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React, { useMemo, memo } from 'react';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { Column, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
|
||||||
|
import { HeartAdd, HeartRemove, Play } from 'iconsax-react-native';
|
||||||
|
import { Image } from 'react-native';
|
||||||
|
import IconButton from './IconButton';
|
||||||
|
import Spacer from '../../components/UI/Spacer';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the MusicItem component.
|
||||||
|
*/
|
||||||
|
interface MusicItemProps {
|
||||||
|
/** The artist's name. */
|
||||||
|
artist: string;
|
||||||
|
|
||||||
|
/** The song's title. */
|
||||||
|
song: string;
|
||||||
|
|
||||||
|
/** The URL for the song's cover image. */
|
||||||
|
image: string;
|
||||||
|
|
||||||
|
/** The level of the song difficulty . */
|
||||||
|
level: number;
|
||||||
|
|
||||||
|
/** The last score achieved for this song. */
|
||||||
|
lastScore: number;
|
||||||
|
|
||||||
|
/** The highest score achieved for this song. */
|
||||||
|
bestScore: number;
|
||||||
|
|
||||||
|
/** Indicates whether the song is liked/favorited by the user. */
|
||||||
|
liked: boolean;
|
||||||
|
|
||||||
|
/** Callback function triggered when the like button is pressed. */
|
||||||
|
onLike: () => void;
|
||||||
|
|
||||||
|
/** Callback function triggered when the song is played. */
|
||||||
|
onPlay?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook to handle the number formatting based on the current user's language.
|
||||||
|
function useNumberFormatter() {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
// Memoizing the number formatter to avoid unnecessary recalculations.
|
||||||
|
// It will be recreated only when the language changes.
|
||||||
|
const formatter = useMemo(() => {
|
||||||
|
return new Intl.NumberFormat(i18n.language, {
|
||||||
|
notation: "compact",
|
||||||
|
compactDisplay: "short"
|
||||||
|
});
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
|
return (num: number) => formatter.format(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `MusicItem` Component
|
||||||
|
*
|
||||||
|
* Display individual music tracks with artist information, cover image, song title, and associated stats.
|
||||||
|
* Designed for optimal performance and responsiveness across different screen sizes.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Displays artist name, song title, and track cover image.
|
||||||
|
* - Indicates user interaction with a like/favorite feature.
|
||||||
|
* - Provides a play button for user interaction.
|
||||||
|
* - Adapts its layout and design responsively according to screen size.
|
||||||
|
* - Optimized performance to ensure smooth rendering even in long lists.
|
||||||
|
* - Automatic number formatting based on user's language preference.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* ```jsx
|
||||||
|
* <MusicItem
|
||||||
|
* artist="John Doe"
|
||||||
|
* song="A Beautiful Song"
|
||||||
|
* image="https://example.com/image.jpg"
|
||||||
|
* level={5}
|
||||||
|
* lastScore={3200}
|
||||||
|
* bestScore={5000}
|
||||||
|
* liked={true}
|
||||||
|
* onLike={() => {() => console.log('Music liked!')}}
|
||||||
|
* onPlay={() => {() => console.log('Play music!')}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Note:
|
||||||
|
* - The number formatting for `level`, `lastScore`, and `bestScore` adapts automatically based on the user's language preference using the i18n module.
|
||||||
|
* - Given its optimized performance characteristics, this component is suitable for rendering in lists with potentially hundreds of items.
|
||||||
|
*/
|
||||||
|
const MusicItem: React.FC<MusicItemProps> = memo((props) => {
|
||||||
|
// Accessing theme colors and breakpoint values for responsive design
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const screenSize = useBreakpointValue({ base: 'small', md: 'md', xl: 'xl' });
|
||||||
|
const formatNumber = useNumberFormatter();
|
||||||
|
|
||||||
|
// Styles are memoized to optimize performance.
|
||||||
|
const styles = useMemo(() => StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.coolGray[500],
|
||||||
|
paddingRight: screenSize === 'small' ? 8 : 16,
|
||||||
|
},
|
||||||
|
playButtonContainer: {
|
||||||
|
zIndex: 1,
|
||||||
|
position: 'absolute',
|
||||||
|
right: -8,
|
||||||
|
bottom: -6,
|
||||||
|
},
|
||||||
|
playButton: {
|
||||||
|
backgroundColor: colors.primary[300],
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
position: 'relative',
|
||||||
|
width: screenSize === 'xl' ? 80 : 60,
|
||||||
|
height: screenSize === 'xl' ? 80 : 60,
|
||||||
|
},
|
||||||
|
artistText: {
|
||||||
|
color: colors.text[700],
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
songContainer: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
maxWidth: screenSize === 'xl' ? 150 : 50,
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: screenSize === 'xl' ? 'flex-end' : 'center',
|
||||||
|
}
|
||||||
|
}), [colors, screenSize]);
|
||||||
|
|
||||||
|
// Memoizing formatted numbers to avoid unnecessary computations.
|
||||||
|
const formattedLevel = useMemo(() => formatNumber(props.level), [props.level]);
|
||||||
|
const formattedLastScore = useMemo(() => formatNumber(props.lastScore), [props.lastScore]);
|
||||||
|
const formattedBestScore = useMemo(() => formatNumber(props.bestScore), [props.bestScore]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack space={screenSize === 'xl' ? 2 : 1} style={styles.container}>
|
||||||
|
<Stack style={{ position: 'relative', overflow: 'hidden' }}>
|
||||||
|
<IconButton
|
||||||
|
containerStyle={styles.playButtonContainer}
|
||||||
|
style={styles.playButton}
|
||||||
|
padding={8}
|
||||||
|
onPress={props.onPlay}
|
||||||
|
color='#FFF'
|
||||||
|
icon={Play}
|
||||||
|
variant="Bold"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
<Image source={{ uri: props.image }} style={styles.image} />
|
||||||
|
</Stack>
|
||||||
|
<Column style={{ flex: 4, width: '100%', justifyContent: 'center' }}>
|
||||||
|
<Text numberOfLines={1} style={styles.artistText}>
|
||||||
|
{props.artist}
|
||||||
|
</Text>
|
||||||
|
{screenSize === 'xl' && <Spacer height='xs' />}
|
||||||
|
<Row space={2} style={styles.songContainer}>
|
||||||
|
<Text numberOfLines={1}>{props.song}</Text>
|
||||||
|
<IconButton
|
||||||
|
colorActive={colors.text[700]}
|
||||||
|
color={colors.primary[300]}
|
||||||
|
icon={HeartAdd}
|
||||||
|
iconActive={HeartRemove}
|
||||||
|
activeVariant="Bold"
|
||||||
|
size={screenSize === 'xl' ? 24 : 18}
|
||||||
|
isActive={props.liked}
|
||||||
|
onPress={props.onLike}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
{[formattedLevel, formattedLastScore, formattedBestScore].map((value, index) => (
|
||||||
|
<Text key={index} style={styles.stats}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MusicItem;
|
||||||
93
front/components/UI/README.md
Normal file
93
front/components/UI/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
## IconButton
|
||||||
|
|
||||||
|
The `IconButton` is a responsive component displaying an interactive icon. It can toggle between active and inactive states, with customizable animations and styles for each state.
|
||||||
|
|
||||||
|
### Features:
|
||||||
|
|
||||||
|
- Toggle between active and inactive icons.
|
||||||
|
- Scale animation on press.
|
||||||
|
- Full customization of icon colors, sizes, and variants.
|
||||||
|
- Customizable styles for both the icon and its container.
|
||||||
|
|
||||||
|
### Preview
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<IconButton
|
||||||
|
color="#000"
|
||||||
|
icon={SomeIcon}
|
||||||
|
onPress={() => console.log('Icon pressed!')}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
With active states:
|
||||||
|
```jsx
|
||||||
|
<IconButton
|
||||||
|
isActive={true}
|
||||||
|
color="#000"
|
||||||
|
colorActive="#ff0000"
|
||||||
|
icon={SomeIcon}
|
||||||
|
iconActive={SomeActiveIcon}
|
||||||
|
onPress={() => console.log('Icon toggled!')}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Description | Default Value |
|
||||||
|
|-------------------|--------------|----------------------------------------------------|----------------|
|
||||||
|
| isActive | boolean | Indicates if the button starts in an active state. | false |
|
||||||
|
| color | string | Color of the icon. | - |
|
||||||
|
| colorActive | string | Optional active state color for the icon. | Value of color |
|
||||||
|
| icon | Component | Icon to display. | - |
|
||||||
|
| iconActive | Component | Optional icon to display when active. | - |
|
||||||
|
| variant | string | Icon's variant style. | "Outline" |
|
||||||
|
| activeVariant | string | Icon's variant style when active. | "Outline" |
|
||||||
|
| size | number | Size of the icon. | 24 |
|
||||||
|
| scaleFactor | number | Scale factor for animation. | 1.25 |
|
||||||
|
| animationDuration | number | Animation duration in milliseconds. | 250 |
|
||||||
|
| padding | number | Padding around the icon. | 0 |
|
||||||
|
| onPress | function | Callback triggered on button press. | - |
|
||||||
|
| style | object/style | Custom style for the icon. | - |
|
||||||
|
| containerStyle | object/style | Custom style for the icon's container. | - |
|
||||||
|
|
||||||
|
|
||||||
|
## MusicItem
|
||||||
|
|
||||||
|
The MusicItem is a responsive component designed to display individual music tracks with key details and interactive buttons, adapting its layout and design across various screen sizes.
|
||||||
|
|
||||||
|
### Features:
|
||||||
|
|
||||||
|
- Displays artist name, song title, and track cover image.
|
||||||
|
- Provides interactivity through a play button and a like button.
|
||||||
|
- Indicates song difficulty level, last score, and best score with automatic number formatting based on user's language preference.
|
||||||
|
- Optimized for performance, ensuring smooth rendering even in extensive lists.
|
||||||
|
|
||||||
|
### Preview
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<MusicItem
|
||||||
|
artist="John Doe"
|
||||||
|
song="A Beautiful Song"
|
||||||
|
image="https://example.com/image.jpg"
|
||||||
|
level={5}
|
||||||
|
lastScore={3200}
|
||||||
|
bestScore={5000}
|
||||||
|
liked={true}
|
||||||
|
onLike={() => console.log('Music liked!')}
|
||||||
|
onPlay={() => console.log('Play music!')}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Description | Default Value |
|
||||||
|
|-----------|----------|-------------------------------------------------------|---------------|
|
||||||
|
| artist | string | Artist's name. | - |
|
||||||
|
| song | string | Song's title. | - |
|
||||||
|
| image | string | URL for the song's cover image. | - |
|
||||||
|
| level | number | Level of the song difficulty. | - |
|
||||||
|
| lastScore | number | Last score achieved for this song. | - |
|
||||||
|
| bestScore | number | Highest score achieved for this song. | - |
|
||||||
|
| liked | boolean | Whether the song is liked/favorited by the user. | false |
|
||||||
|
| onLike | function | Callback triggered when the like button is pressed. | - |
|
||||||
|
| onPlay | function | Callback triggered when the song is played. Optional. | - |
|
||||||
@@ -24,7 +24,7 @@ const menu: {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{ type: "main", title: 'menuDiscovery', icon: Discover, link: 'HomeNew' },
|
{ type: "main", title: 'menuDiscovery', icon: Discover, link: 'HomeNew' },
|
||||||
{ type: "main", title: 'menuProfile', icon: User, link: 'User' },
|
{ type: "main", title: 'menuProfile', icon: User, link: 'User' },
|
||||||
{ type: "main", title: 'menuMusic', icon: Music, link: 'Home' },
|
{ type: "main", title: 'menuMusic', icon: Music, link: 'Music' },
|
||||||
{ type: "main", title: 'menuSearch', icon: SearchNormal1, link: 'Search' },
|
{ type: "main", title: 'menuSearch', icon: SearchNormal1, link: 'Search' },
|
||||||
{ type: "main", title: 'menuLeaderBoard', icon: Cup, link: 'Score' },
|
{ type: "main", title: 'menuLeaderBoard', icon: Cup, link: 'Score' },
|
||||||
{ type: "sub", title: 'menuSettings', icon: Setting2, link: 'Settings' },
|
{ type: "sub", title: 'menuSettings', icon: Setting2, link: 'Settings' },
|
||||||
@@ -56,6 +56,7 @@ const ScaffoldCC = ({children, routeName, withPadding = true}: ScaffoldCCProps)
|
|||||||
logo={logo}
|
logo={logo}
|
||||||
routeName={routeName}
|
routeName={routeName}
|
||||||
menu={menu}
|
menu={menu}
|
||||||
|
widthPadding={withPadding}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScaffoldMobileCC>
|
</ScaffoldMobileCC>
|
||||||
|
|||||||
@@ -93,14 +93,13 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={!isSmallScreen ? { width: '100%' } : {}}>
|
<View style={!isSmallScreen ? { width: '100%' } : {}}>
|
||||||
<Row space={2} flex={1} style={{ justifyContent: 'center' }}>
|
<Row space={2} flex={1} style={{ justifyContent: isSmallScreen ? 'center' : 'flex-start' }}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: props.logo }}
|
source={{ uri: props.logo }}
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
alignItems: isSmallScreen ? 'center' : 'flex-start',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isSmallScreen &&
|
{!isSmallScreen &&
|
||||||
@@ -171,7 +170,7 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Spacer height='xs'/>
|
<Spacer height='xs'/>
|
||||||
<LogoutButtonCC collapse={!isSmallScreen} isGuest={props.user.isGuest} style={!isSmallScreen ? { width: '100%' } : {}} buttonType={'menu'}/>
|
<LogoutButtonCC collapse={isSmallScreen} isGuest={props.user.isGuest} style={!isSmallScreen ? { width: '100%' } : {}} buttonType={'menu'}/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type ScaffoldMobileCCProps = {
|
|||||||
user: User;
|
user: User;
|
||||||
logo: string;
|
logo: string;
|
||||||
routeName: string;
|
routeName: string;
|
||||||
|
widthPadding: number;
|
||||||
menu: {
|
menu: {
|
||||||
type: "main" | "sub";
|
type: "main" | "sub";
|
||||||
title: string;
|
title: string;
|
||||||
@@ -32,14 +33,14 @@ const ScaffoldMobileCC = (props: ScaffoldMobileCCProps) => {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
maxHeight: '100vh',
|
maxHeight: '100%',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
padding: 16
|
padding: props.widthPadding ? 8 : 0
|
||||||
}}
|
}}
|
||||||
contentContainerStyle={{ flex: 1 }}
|
contentContainerStyle={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, minHeight: 'fit-content' }}>
|
<View style={{ flex: 1 }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</View>
|
</View>
|
||||||
<Spacer/>
|
<Spacer/>
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ export const en = {
|
|||||||
signinLinkLabel: "You don't have an account? ",
|
signinLinkLabel: "You don't have an account? ",
|
||||||
signinLinkText: 'Sign up for free.',
|
signinLinkText: 'Sign up for free.',
|
||||||
|
|
||||||
|
|
||||||
|
//music
|
||||||
|
musicTabFavorites: 'Favorites',
|
||||||
|
musicTabRecentlyPlayed: 'Recently Played',
|
||||||
|
musicTabStepUp: 'Recommendation',
|
||||||
|
|
||||||
//search
|
//search
|
||||||
allFilter: 'All',
|
allFilter: 'All',
|
||||||
artistFilter: 'Artists',
|
artistFilter: 'Artists',
|
||||||
@@ -337,6 +343,11 @@ export const fr: typeof en = {
|
|||||||
signinLinkLabel: "Vous n'avez pas de compte ? ",
|
signinLinkLabel: "Vous n'avez pas de compte ? ",
|
||||||
signinLinkText: 'Inscrivez-vous gratuitement',
|
signinLinkText: 'Inscrivez-vous gratuitement',
|
||||||
|
|
||||||
|
//music
|
||||||
|
musicTabFavorites : 'Favoris',
|
||||||
|
musicTabRecentlyPlayed : 'Récemment joué',
|
||||||
|
musicTabStepUp : 'Recommandation',
|
||||||
|
|
||||||
//search
|
//search
|
||||||
allFilter: 'Tout',
|
allFilter: 'Tout',
|
||||||
artistFilter: 'Artistes',
|
artistFilter: 'Artistes',
|
||||||
@@ -622,6 +633,11 @@ export const sp: typeof en = {
|
|||||||
signinLinkLabel: "¿No tienes una cuenta? ",
|
signinLinkLabel: "¿No tienes una cuenta? ",
|
||||||
signinLinkText: "Regístrate gratis.",
|
signinLinkText: "Regístrate gratis.",
|
||||||
|
|
||||||
|
//music
|
||||||
|
musicTabFavorites: 'Favoritos',
|
||||||
|
musicTabRecentlyPlayed: 'Jugado recientemente',
|
||||||
|
musicTabStepUp: 'Recomendación',
|
||||||
|
|
||||||
//search
|
//search
|
||||||
allFilter: 'Todos',
|
allFilter: 'Todos',
|
||||||
artistFilter: 'Artistas',
|
artistFilter: 'Artistas',
|
||||||
|
|||||||
189
front/views/MusicView.tsx
Normal file
189
front/views/MusicView.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Center, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
|
||||||
|
import { useWindowDimensions } from 'react-native';
|
||||||
|
import {
|
||||||
|
TabView,
|
||||||
|
SceneMap,
|
||||||
|
TabBar,
|
||||||
|
NavigationState,
|
||||||
|
Route,
|
||||||
|
SceneRendererProps,
|
||||||
|
} from 'react-native-tab-view';
|
||||||
|
import { Heart, Clock, StatusUp, FolderCross, Chart2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
|
||||||
|
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
|
||||||
|
import useColorScheme from '../hooks/colorScheme';
|
||||||
|
import { RouteProps } from '../Navigation';
|
||||||
|
import { translate } from '../i18n/i18n';
|
||||||
|
import ScaffoldCC from '../components/UI/ScaffoldCC';
|
||||||
|
import MusicItem from '../components/UI/MusicItem';
|
||||||
|
|
||||||
|
interface MusicItemTitleProps {
|
||||||
|
text: string;
|
||||||
|
icon: Icon;
|
||||||
|
isBigScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicItemTitle = (props: MusicItemTitleProps) => {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row style={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
maxWidth: props.isBigScreen ? 150 : 50,
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: props.isBigScreen ? 'flex-end' : 'center',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
{props.isBigScreen && <Text fontSize="lg" style={{paddingRight: 8}}>{props.text}</Text>}
|
||||||
|
<props.icon size={18} color={colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}/>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FavoritesMusic = () => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const screenSize = useBreakpointValue({ base: 'small', md: 'md', xl: 'xl' });
|
||||||
|
const isSmallScreen = screenSize === 'small';
|
||||||
|
const isBigScreen = screenSize === 'xl';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack style={{gap: 2, borderRadius: 10, overflow: 'hidden'}}>
|
||||||
|
<HStack space={isSmallScreen ? 1 : 2} style={{backgroundColor: colors.coolGray[500], paddingHorizontal: isSmallScreen ? 8 : 16, paddingVertical: 12}}>
|
||||||
|
<Text fontSize="lg" style={{flex: 4, width: '100%', justifyContent: 'center', paddingRight: 60}}>
|
||||||
|
Song
|
||||||
|
</Text>
|
||||||
|
{
|
||||||
|
[{text: "level", icon: Chart2}, {text: "lastScore", icon: ArrowRotateLeft}, {text: "BastScore", icon: Cup}].map((value, index) => (
|
||||||
|
<MusicItemTitle key={value.text + "key"} text={value.text} icon={value.icon} isBigScreen={isBigScreen}/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</HStack>
|
||||||
|
<MusicItem
|
||||||
|
image={"https://static.vecteezy.com/system/resources/previews/016/552/335/non_2x/luffy-kawai-chibi-cute-onepiece-anime-design-and-doodle-art-for-icon-logo-collection-and-others-free-vector.jpg"}
|
||||||
|
liked={false}
|
||||||
|
onLike={() => { } }
|
||||||
|
level={3}
|
||||||
|
lastScore={25550}
|
||||||
|
bestScore={420}
|
||||||
|
artist={'Ludwig van Beethoven'}
|
||||||
|
song={'Piano Sonata No. 8'}
|
||||||
|
/>
|
||||||
|
<MusicItem
|
||||||
|
image={"https://static.vecteezy.com/system/resources/previews/016/552/335/non_2x/luffy-kawai-chibi-cute-onepiece-anime-design-and-doodle-art-for-icon-logo-collection-and-others-free-vector.jpg"}
|
||||||
|
liked={true}
|
||||||
|
onLike={() => { } }
|
||||||
|
level={3}
|
||||||
|
lastScore={255500000}
|
||||||
|
bestScore={42000} artist={'Ludwig van Beethoven'} song={'Sonata for Piano no. 20 in G major, op. 49 no. 2'} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecentlyPlayedMusic = () => {
|
||||||
|
return (
|
||||||
|
<Center style={{ flex: 1 }}>
|
||||||
|
<Text>RecentlyPlayedMusic</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StepUpMusic = () => {
|
||||||
|
return (
|
||||||
|
<Center style={{ flex: 1 }}>
|
||||||
|
<Text>StepUpMusic</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderScene = SceneMap({
|
||||||
|
favorites: FavoritesMusic,
|
||||||
|
recentlyPlayed: RecentlyPlayedMusic,
|
||||||
|
stepUp: StepUpMusic,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTabData = (key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'favorites':
|
||||||
|
return { index: 0, icon: Heart };
|
||||||
|
case 'recentlyPlayed':
|
||||||
|
return { index: 1, icon: Clock };
|
||||||
|
case 'stepUp':
|
||||||
|
return { index: 2, icon: StatusUp };
|
||||||
|
default:
|
||||||
|
return { index: 3, icon: FolderCross };
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetttingsNavigator = (props: RouteProps<{}>) => {
|
||||||
|
const layout = useWindowDimensions();
|
||||||
|
const [index, setIndex] = React.useState(0);
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
|
const isSmallScreen = screenSize === 'small';
|
||||||
|
const [routes] = React.useState<Route[]>([
|
||||||
|
{ key: 'favorites', title: 'musicTabFavorites' },
|
||||||
|
{ key: 'recentlyPlayed', title: 'musicTabRecentlyPlayed' },
|
||||||
|
{ key: 'stepUp', title: 'musicTabStepUp' },
|
||||||
|
]);
|
||||||
|
const renderTabBar = (
|
||||||
|
props: SceneRendererProps & { navigationState: NavigationState<Route> }
|
||||||
|
) => (
|
||||||
|
<TabBar
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderColor: colors.primary[300],
|
||||||
|
}}
|
||||||
|
activeColor={ colorScheme === 'light' ? '#000' : '#fff'}
|
||||||
|
inactiveColor={ colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}
|
||||||
|
indicatorStyle={{ backgroundColor: colors.primary[300] }}
|
||||||
|
renderIcon={(
|
||||||
|
scene: Scene<Route> & {
|
||||||
|
focused: boolean;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const tabHeader = getTabData(scene.route!.key);
|
||||||
|
return (
|
||||||
|
<tabHeader.icon size="18" color="#6075F9" variant={scene.focused ? "Bold" : "Outline"} />
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderLabel={({ route, color }) =>
|
||||||
|
layout.width > 800 && (
|
||||||
|
<Text style={{ color: color, paddingLeft: 10, overflow: 'hidden' }}>
|
||||||
|
{translate(route.title as 'musicTabFavorites' | 'musicTabRecentlyPlayed' | 'musicTabStepUp')}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tabStyle={{ flexDirection: 'row' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScaffoldCC routeName={props.route.name} withPadding={false}>
|
||||||
|
<TabView
|
||||||
|
sceneContainerStyle={{
|
||||||
|
flex: 1,
|
||||||
|
alignSelf: 'center',
|
||||||
|
padding: isSmallScreen ? 4 : 20,
|
||||||
|
paddingTop: 32,
|
||||||
|
// maxWidth: 850,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
// style={{ height: 'fit-content' }}
|
||||||
|
renderTabBar={renderTabBar}
|
||||||
|
navigationState={{ index, routes }}
|
||||||
|
renderScene={renderScene}
|
||||||
|
onIndexChange={setIndex}
|
||||||
|
initialLayout={{ width: layout.width }}
|
||||||
|
/>
|
||||||
|
</ScaffoldCC>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SetttingsNavigator;
|
||||||
@@ -87,11 +87,11 @@ const SetttingsNavigator = (props: RouteProps<{}>) => {
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderColor: colors.primary[500],
|
borderColor: colors.primary[300],
|
||||||
}}
|
}}
|
||||||
activeColor={ colorScheme === 'light' ? '#000' : '#fff'}
|
activeColor={ colorScheme === 'light' ? '#000' : '#fff'}
|
||||||
inactiveColor={ colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}
|
inactiveColor={ colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}
|
||||||
indicatorStyle={{ backgroundColor: colors.primary[500] }}
|
indicatorStyle={{ backgroundColor: colors.primary[300] }}
|
||||||
renderIcon={(
|
renderIcon={(
|
||||||
scene: Scene<Route> & {
|
scene: Scene<Route> & {
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user