From a41f9387ddf1d19c95ad73b708d3b43749f5a178 Mon Sep 17 00:00:00 2001 From: Savsis Date: Fri, 8 May 2026 23:53:23 +0500 Subject: [PATCH] Add files via upload --- app/(tabs)/library.tsx | 6 +- app/auth/index.tsx | 190 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 app/auth/index.tsx diff --git a/app/(tabs)/library.tsx b/app/(tabs)/library.tsx index 1cc5b99..7129795 100644 --- a/app/(tabs)/library.tsx +++ b/app/(tabs)/library.tsx @@ -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={ - diff --git a/app/auth/index.tsx b/app/auth/index.tsx new file mode 100644 index 0000000..1e93587 --- /dev/null +++ b/app/auth/index.tsx @@ -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 ( + + {/* Sleek dark background */} + + + {/* Modern gradient accent for nodeshiftmusic style */} + + + + + + + + + + + NodeShift + + + + Музыкальная экосистема + + + + [ + s.btn, + pressed && s.btnPressed + ]} + onPress={handleLogin} + disabled={loading} + > + + {loading ? ( + + ) : ( + Войти + )} + + + + + {!!error && ( + {error} + )} + + + Продолжая, вы соглашаетесь с условиями NodeShift + + + + ); +} + +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', + }, +});