Add files via upload

This commit is contained in:
Savsis 2026-05-08 23:53:23 +05:00 • committed by GitHub
parent 184247a272
commit a41f9387dd
2 changed files with 192 additions and 4 deletions

View file

@ -3,7 +3,7 @@ import {
View, Text, FlatList, StyleSheet, Pressable, Alert, TextInput, Modal,
ActionSheetIOS, Platform,
} 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 { Image } from 'expo-image';
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 { LinearGradient } from 'expo-linear-gradient';
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
type Tab = 'tracks' | 'playlists' | 'wave';
export default function LibraryScreen() {
@ -208,7 +206,7 @@ export default function LibraryScreen() {
showsVerticalScrollIndicator={false}
ListHeaderComponent={
<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']}
style={StyleSheet.absoluteFill}
/>

190
app/auth/index.tsx Normal file
View 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',
},
});