Add files via upload
This commit is contained in:
parent
184247a272
commit
a41f9387dd
2 changed files with 192 additions and 4 deletions
|
|
@ -3,7 +3,7 @@ import {
|
||||||
View, Text, FlatList, StyleSheet, Pressable, Alert, TextInput, Modal,
|
View, Text, FlatList, StyleSheet, Pressable, Alert, TextInput, Modal,
|
||||||
ActionSheetIOS, Platform,
|
ActionSheetIOS, Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Animated, { FadeInDown, useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
import Animated, { FadeInDown, useSharedValue, useAnimatedStyle, withSpring, withRepeat } from 'react-native-reanimated';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import Svg, { Path, Circle, Line, Rect } from 'react-native-svg';
|
import Svg, { Path, Circle, Line, Rect } from 'react-native-svg';
|
||||||
|
|
@ -15,8 +15,6 @@ import { getMyWaveTracks } from '../../services/mywave';
|
||||||
import WaveAnimation from '../../components/WaveAnimation';
|
import WaveAnimation from '../../components/WaveAnimation';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
||||||
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
|
|
||||||
|
|
||||||
type Tab = 'tracks' | 'playlists' | 'wave';
|
type Tab = 'tracks' | 'playlists' | 'wave';
|
||||||
|
|
||||||
export default function LibraryScreen() {
|
export default function LibraryScreen() {
|
||||||
|
|
@ -208,7 +206,7 @@ export default function LibraryScreen() {
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<Animated.View entering={FadeInDown.duration(400)} style={s.waveHeader}>
|
<Animated.View entering={FadeInDown.duration(400)} style={s.waveHeader}>
|
||||||
<AnimatedGradient
|
<LinearGradient
|
||||||
colors={['rgba(67, 97, 238, 0.2)', 'rgba(123, 47, 247, 0.05)', 'transparent']}
|
colors={['rgba(67, 97, 238, 0.2)', 'rgba(123, 47, 247, 0.05)', 'transparent']}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
190
app/auth/index.tsx
Normal file
190
app/auth/index.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View, Text, StyleSheet, Pressable, ActivityIndicator, Image, Dimensions
|
||||||
|
} from 'react-native';
|
||||||
|
import Animated, { FadeIn, FadeInDown, FadeInUp } from 'react-native-reanimated';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import * as Haptics from 'expo-haptics';
|
||||||
|
import { Colors } from '../../constants/theme';
|
||||||
|
import { buildOAuthUrl, exchangeCode, saveTokens } from '../../services/api';
|
||||||
|
import { useAuth } from '../../store';
|
||||||
|
import { saveUser } from '../../hooks/useAuth';
|
||||||
|
|
||||||
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
|
|
||||||
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
export default function AuthScreen() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const { setUser } = useAuth();
|
||||||
|
|
||||||
|
const handleLogin = useCallback(async () => {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const state = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||||
|
const url = buildOAuthUrl(state);
|
||||||
|
const result = await WebBrowser.openAuthSessionAsync(url, 'burmalda://auth/callback');
|
||||||
|
if (result.type !== 'success') { setError('Авторизация отменена'); return; }
|
||||||
|
const parsed = Linking.parse(result.url);
|
||||||
|
const code = parsed.queryParams?.code as string | undefined;
|
||||||
|
if (!code) { setError((parsed.queryParams?.error as string) || 'Ошибка авторизации'); return; }
|
||||||
|
const data = await exchangeCode(code);
|
||||||
|
await saveTokens(data.access_token, data.refresh_token);
|
||||||
|
await saveUser(data.user);
|
||||||
|
setUser(data.user);
|
||||||
|
router.replace('/(tabs)');
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || 'Что-то пошло не так');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [setUser]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.root}>
|
||||||
|
{/* Sleek dark background */}
|
||||||
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#050505' }]} />
|
||||||
|
|
||||||
|
{/* Modern gradient accent for nodeshiftmusic style */}
|
||||||
|
<Animated.View entering={FadeIn.duration(1000)} style={StyleSheet.absoluteFill}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(67, 97, 238, 0.1)', 'rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)']}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<View style={s.content}>
|
||||||
|
<Animated.View entering={FadeInDown.delay(200).springify()} style={s.logoContainer}>
|
||||||
|
<Image
|
||||||
|
source={require('../../assets/icon.png')}
|
||||||
|
style={s.logo}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.Text entering={FadeInDown.delay(300).springify()} style={s.brand}>
|
||||||
|
NodeShift
|
||||||
|
</Animated.Text>
|
||||||
|
|
||||||
|
<Animated.Text entering={FadeInDown.delay(400).springify()} style={s.subtitle}>
|
||||||
|
Музыкальная экосистема
|
||||||
|
</Animated.Text>
|
||||||
|
|
||||||
|
<Animated.View entering={FadeInUp.delay(500).springify()} style={s.btnWrapper}>
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) => [
|
||||||
|
s.btn,
|
||||||
|
pressed && s.btnPressed
|
||||||
|
]}
|
||||||
|
onPress={handleLogin}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#4361ee', '#3a0ca3']}
|
||||||
|
start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }}
|
||||||
|
style={s.btnGradient}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color="#fff" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={s.btnText}>Войти</Text>
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
</Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{!!error && (
|
||||||
|
<Animated.Text entering={FadeIn} style={s.error}>{error}</Animated.Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Animated.Text entering={FadeIn.delay(700)} style={s.terms}>
|
||||||
|
Продолжая, вы соглашаетесь с условиями NodeShift
|
||||||
|
</Animated.Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = StyleSheet.create({
|
||||||
|
root: { flex: 1 },
|
||||||
|
content: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24, paddingBottom: 40 },
|
||||||
|
logoContainer: {
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
marginBottom: 24,
|
||||||
|
shadowColor: '#4361ee',
|
||||||
|
shadowOffset: { width: 0, height: 8 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 24,
|
||||||
|
elevation: 10,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 35,
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: '#ffffff',
|
||||||
|
letterSpacing: -1,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
marginBottom: 56,
|
||||||
|
fontWeight: '500',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
btnWrapper: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 320,
|
||||||
|
shadowColor: '#4361ee',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
btn: {
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
btnGradient: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
btnPressed: {
|
||||||
|
opacity: 0.9,
|
||||||
|
transform: [{ scale: 0.97 }],
|
||||||
|
},
|
||||||
|
btnText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: '#ff4757',
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
terms: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 40,
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue