diff --git a/app.json b/app.json
index 82051ea..7d59077 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "NodeShiftMusic",
"slug": "nodeshiftmusic",
- "version": "1.0.0",
+ "version": "1.0.7",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark",
@@ -15,7 +15,7 @@
"ios": {
"supportsTablet": false,
"bundleIdentifier": "com.nodeshiftmusic",
- "buildNumber": "1",
+ "buildNumber": "7",
"infoPlist": {
"UIBackgroundModes": ["audio"],
"NSMicrophoneUsageDescription": "Не используется",
@@ -29,21 +29,10 @@
},
"scheme": "burmalda",
"plugins": [
- "expo-router",
- [
- "expo-updates",
- {
- "username": "nodeshift"
- }
- ]
+ "expo-router"
],
"updates": {
- "enabled": true,
- "checkAutomatically": "ON_LOAD",
- "url": "https://nodeshift.space/api/ios/updates"
- },
- "runtimeVersion": {
- "policy": "appVersion"
+ "enabled": false
},
"extra": {
"apiBase": "https://music.nodeshift.space",
diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx
index 9048446..7b8fc80 100644
--- a/app/(tabs)/profile.tsx
+++ b/app/(tabs)/profile.tsx
@@ -84,7 +84,7 @@ export default function ProfileScreen() {
- Burmalda Music · v1.0.0 · by NodeShift
+ Burmalda Music · v1.0.7 BETA · by NodeShift
);
diff --git a/app/_layout.tsx b/app/_layout.tsx
index c8a2cea..db49d61 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,41 +1,40 @@
-import { useEffect } from 'react';
-import { Stack } from 'expo-router';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
-import { StatusBar } from 'expo-status-bar';
-import * as Updates from 'expo-updates';
-import { useBootAuth } from '../hooks/useAuth';
-import { Colors } from '../constants/theme';
-
-export default function RootLayout() {
- useBootAuth();
- useOTAUpdates();
-
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-function useOTAUpdates() {
- useEffect(() => {
- if (__DEV__) return;
- (async () => {
- try {
- const check = await Updates.checkForUpdateAsync();
- if (check.isAvailable) {
- await Updates.fetchUpdateAsync();
- await Updates.reloadAsync();
- }
- // eslint-disable-next-line no-empty
- } catch {
- // silent fail
- }
- })();
- }, []);
-}
+import { useEffect } from 'react';
+import { Stack } from 'expo-router';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import { StatusBar } from 'expo-status-bar';
+import { useBootAuth } from '../hooks/useAuth';
+import { Colors } from '../constants/theme';
+
+export default function RootLayout() {
+ useBootAuth();
+ useOTAUpdates();
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function useOTAUpdates() {
+ useEffect(() => {
+ if (__DEV__) return;
+ // Lazy import so expo-updates crash doesn't kill the app
+ (async () => {
+ try {
+ const Updates = await import('expo-updates');
+ const check = await Updates.checkForUpdateAsync();
+ if (check.isAvailable) {
+ await Updates.fetchUpdateAsync();
+ await Updates.reloadAsync();
+ }
+ } catch {
+ // Silently ignore — OTA is optional
+ }
+ })();
+ }, []);
+}
diff --git a/app/auth.tsx b/app/auth.tsx
index 62c35c2..9c44a9c 100644
--- a/app/auth.tsx
+++ b/app/auth.tsx
@@ -5,15 +5,15 @@ import {
} from 'react-native';
import Animated, {
useSharedValue, useAnimatedStyle, withSpring, withTiming,
- withRepeat, withSequence, FadeIn, FadeInDown, FadeInUp,
- interpolate, Extrapolate, withDelay, Easing,
+ withRepeat, withSequence, withDelay, Easing,
+ 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 Svg, { Path, Circle, Defs, RadialGradient, Stop } from 'react-native-svg';
+import Svg, { Path, Circle } from 'react-native-svg';
import { Colors, Font } from '../constants/theme';
import { buildOAuthUrl, exchangeCode, saveTokens } from '../services/api';
import { useAuth } from '../store';
@@ -23,8 +23,7 @@ WebBrowser.maybeCompleteAuthSession();
const { width, height } = Dimensions.get('window');
-// Fake album-like gradient cards for the carousel
-const CARDS = [
+const CARD_COLORS: [string, string][] = [
['#1a1a5e', '#3a0ca3'],
['#0d1b2a', '#1b4332'],
['#2d0a3a', '#7b2d8b'],
@@ -34,96 +33,87 @@ const CARDS = [
['#0a1a00', '#1a4a00'],
['#2a0010', '#6a0030'],
['#001a2a', '#004a5a'],
+ ['#1e0030', '#5e0080'],
+ ['#002200', '#004400'],
+ ['#300000', '#700000'],
];
+function CardRow({ reverse, speed }: { reverse: boolean; speed: number }) {
+ const ref = useRef(null);
+ const pos = useRef(0);
+ const doubled = [...CARD_COLORS, ...CARD_COLORS, ...CARD_COLORS];
+ const max = CARD_COLORS.length * 86;
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ if (reverse) {
+ pos.current = pos.current <= 0 ? max : pos.current - 0.7;
+ } else {
+ pos.current = pos.current >= max ? 0 : pos.current + 0.7;
+ }
+ ref.current?.scrollTo({ x: pos.current, animated: false });
+ }, speed);
+ return () => clearInterval(id);
+ }, []);
+
+ return (
+
+ {doubled.map((c, i) => (
+
+ ))}
+
+ );
+}
+
+const card: object = {
+ width: 76, height: 76, borderRadius: 14, marginHorizontal: 5, opacity: 0.65,
+};
+
export default function AuthScreen() {
const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
+ const [error, setError] = useState('');
const { setUser } = useAuth();
- // Button animation
- const btnScale = useSharedValue(1);
- const btnGlow = useSharedValue(0);
-
- // Orb animations
- const orb1X = useSharedValue(0);
- const orb1Y = useSharedValue(0);
- const orb2X = useSharedValue(0);
- const orb2Y = useSharedValue(0);
- const orb3X = useSharedValue(0);
-
- // Title word stagger
- const titleOpacity = useSharedValue(0);
- const titleY = useSharedValue(30);
+ const btnScale = useSharedValue(1);
+ const orb1y = useSharedValue(0);
+ const orb2y = useSharedValue(0);
+ const orb1x = useSharedValue(0);
+ const orb2x = useSharedValue(0);
+ const glowOpacity = useSharedValue(0.3);
useEffect(() => {
- // Orb 1 - slow drift
- orb1X.value = withRepeat(
- withSequence(
- withTiming(40, { duration: 9000, easing: Easing.inOut(Easing.sin) }),
- withTiming(-30, { duration: 9000, easing: Easing.inOut(Easing.sin) }),
- ), -1, true,
- );
- orb1Y.value = withRepeat(
- withSequence(
- withTiming(-50, { duration: 7000, easing: Easing.inOut(Easing.sin) }),
- withTiming(30, { duration: 7000, easing: Easing.inOut(Easing.sin) }),
- ), -1, true,
- );
- // Orb 2
- orb2X.value = withRepeat(
- withSequence(
- withTiming(-60, { duration: 11000, easing: Easing.inOut(Easing.sin) }),
- withTiming(40, { duration: 11000, easing: Easing.inOut(Easing.sin) }),
- ), -1, true,
- );
- orb2Y.value = withRepeat(
- withSequence(
- withTiming(60, { duration: 8000, easing: Easing.inOut(Easing.sin) }),
- withTiming(-40, { duration: 8000, easing: Easing.inOut(Easing.sin) }),
- ), -1, true,
- );
- // Orb 3
- orb3X.value = withRepeat(
- withSequence(
- withTiming(50, { duration: 13000, easing: Easing.inOut(Easing.sin) }),
- withTiming(-35, { duration: 13000, easing: Easing.inOut(Easing.sin) }),
- ), -1, true,
- );
-
- // Title entrance
- titleOpacity.value = withDelay(300, withTiming(1, { duration: 900 }));
- titleY.value = withDelay(300, withSpring(0, { damping: 18, stiffness: 80 }));
-
- // Button glow pulse
- btnGlow.value = withDelay(1200, withRepeat(
- withSequence(
- withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
- withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
- ), -1, false,
- ));
+ orb1y.value = withRepeat(withSequence(
+ withTiming(-50, { duration: 8000, easing: Easing.inOut(Easing.sin) }),
+ withTiming(30, { duration: 8000, easing: Easing.inOut(Easing.sin) }),
+ ), -1, true);
+ orb1x.value = withRepeat(withSequence(
+ withTiming(40, { duration: 9000, easing: Easing.inOut(Easing.sin) }),
+ withTiming(-30, { duration: 9000, easing: Easing.inOut(Easing.sin) }),
+ ), -1, true);
+ orb2y.value = withRepeat(withSequence(
+ withTiming(60, { duration: 10000, easing: Easing.inOut(Easing.sin) }),
+ withTiming(-40, { duration: 10000, easing: Easing.inOut(Easing.sin) }),
+ ), -1, true);
+ orb2x.value = withRepeat(withSequence(
+ withTiming(-50, { duration: 11000, easing: Easing.inOut(Easing.sin) }),
+ withTiming(40, { duration: 11000, easing: Easing.inOut(Easing.sin) }),
+ ), -1, true);
+ glowOpacity.value = withDelay(800, withRepeat(withSequence(
+ withTiming(0.8, { duration: 2200, easing: Easing.inOut(Easing.ease) }),
+ withTiming(0.3, { duration: 2200, easing: Easing.inOut(Easing.ease) }),
+ ), -1, false));
}, []);
- const orb1Style = useAnimatedStyle(() => ({
- transform: [{ translateX: orb1X.value }, { translateY: orb1Y.value }],
- }));
- const orb2Style = useAnimatedStyle(() => ({
- transform: [{ translateX: orb2X.value }, { translateY: orb2Y.value }],
- }));
- const orb3Style = useAnimatedStyle(() => ({
- transform: [{ translateX: orb3X.value }],
- }));
- const titleStyle = useAnimatedStyle(() => ({
- opacity: titleOpacity.value,
- transform: [{ translateY: titleY.value }],
- }));
- const btnStyle = useAnimatedStyle(() => ({
- transform: [{ scale: btnScale.value }],
- }));
- const glowStyle = useAnimatedStyle(() => ({
- opacity: interpolate(btnGlow.value, [0, 1], [0.35, 0.75], Extrapolate.CLAMP),
- transform: [{ scale: interpolate(btnGlow.value, [0, 1], [1, 1.08], Extrapolate.CLAMP) }],
- }));
+ const orb1Style = useAnimatedStyle(() => ({ transform: [{ translateX: orb1x.value }, { translateY: orb1y.value }] }));
+ const orb2Style = useAnimatedStyle(() => ({ transform: [{ translateX: orb2x.value }, { translateY: orb2y.value }] }));
+ const btnStyle = useAnimatedStyle(() => ({ transform: [{ scale: btnScale.value }] }));
+ const glowStyle = useAnimatedStyle(() => ({ opacity: glowOpacity.value }));
const handleLogin = useCallback(async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
@@ -133,17 +123,10 @@ export default function AuthScreen() {
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;
- }
+ 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;
- }
+ 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);
@@ -158,232 +141,159 @@ export default function AuthScreen() {
return (
- {/* Deep dark base */}
+ {/* Dark background */}
+
- {/* Animated color orbs */}
-
-
+ {/* Orbs */}
+
+
-
-
-
-
-
+
+
- {/* Album cards carousel — auto scroll */}
-
-
-
-
+ {/* Scrolling cards — top half */}
+
+
+
+
- {/* Gradient fade over carousel */}
+ {/* Gradient covers carousel bottom half */}
+
- {/* Bottom gradient */}
-
-
- {/* Content */}
+ {/* Main content */}
- {/* Logo */}
-
-
-
- {/* Abstract waveform / sound lines — no music note */}
-
-
-
+ {/* Logo icon */}
+
+
+
+
+
+
+
+
- Burmalda
+ {/* Brand name */}
+
+ Burmalda
+
-
- Твоя музыка. Твой ритм.
+ {/* Tagline */}
+
+ «Если я говорю сукин сын и сын шлюхи — это не одно и то же»
+
+
+ {/* Feature list */}
+
+
+
+
-
-
-
-
-
-
- {/* Login button */}
-
+ {/* Button */}
+
{ btnScale.value = withSpring(0.96, { damping: 12 }); }}
- onPressOut={() => { btnScale.value = withSpring(1, { damping: 12 }); }}
+ onPressOut={() => { btnScale.value = withSpring(1, { damping: 12 }); }}
onPress={handleLogin}
disabled={loading}
- style={s.btnOuter}
>
- {loading ? (
-
- ) : (
-
- )}
+ {loading
+ ?
+ : (
+
+ )
+ }
{loading ? 'Входим…' : 'Войти через NodeShift'}
-
- {!!error && (
- {error}
- )}
-
-
- Продолжая, вы соглашаетесь с условиями NodeShift
-
+
+ {!!error && (
+ {error}
+ )}
+
+
+ Продолжая, вы соглашаетесь с условиями NodeShift
+
);
}
-// ── Auto-scrolling card row ────────────────────────────────────────────────────
-
-function AutoScrollRow({ reverse, speed }: { reverse: boolean; speed: number }) {
- const scrollRef = useRef(null);
- const offset = useRef(0);
- const maxScroll = useRef(CARDS.length * 90);
-
- useEffect(() => {
- const interval = setInterval(() => {
- offset.current = reverse
- ? offset.current <= 0 ? maxScroll.current : offset.current - 0.8
- : offset.current >= maxScroll.current ? 0 : offset.current + 0.8;
- scrollRef.current?.scrollTo({ x: offset.current, animated: false });
- }, speed / 10);
- return () => clearInterval(interval);
- }, []);
-
- const doubled = [...CARDS, ...CARDS, ...CARDS];
-
+function FeatureItem({ color, text }: { color: string; text: string }) {
return (
-
- {doubled.map((colors, i) => (
-
- ))}
-
- );
-}
-
-function GradientCard({ colors }: { colors: [string, string] }) {
- return (
-
- );
-}
-
-// ── Feature row item ──────────────────────────────────────────────────────────
-
-function FeatureRow({ icon, text, delay }: { icon: string; text: string; delay: number }) {
- const icons: Record = {
- wave: (
-
- ),
- cloud: (
-
- ),
- heart: (
-
- ),
- };
-
- return (
-
- {icons[icon]}
+
+
{text}
-
+
);
}
-// ── Styles ────────────────────────────────────────────────────────────────────
-
-const CARD_W = 76;
-const CARD_H = 76;
-
const s = StyleSheet.create({
- root: { flex: 1, backgroundColor: '#070711' },
+ root: { flex: 1 },
- // Orbs
- orb: { position: 'absolute', borderRadius: 999 },
- orbInner: { flex: 1, borderRadius: 999 },
- orb1: { width: 340, height: 340, top: -80, left: -100, opacity: 0.22 },
- orb2: { width: 280, height: 280, top: height * 0.25, right: -80, opacity: 0.18 },
- orb3: { width: 200, height: 200, bottom: height * 0.3, left: -60, opacity: 0.14 },
+ orb1: { position: 'absolute', width: 320, height: 320, top: -60, left: -100, borderRadius: 160, overflow: 'hidden', opacity: 0.2 },
+ orb2: { position: 'absolute', width: 260, height: 260, top: height * 0.2, right: -80, borderRadius: 130, overflow: 'hidden', opacity: 0.15 },
+ orbGrad: { flex: 1 },
- // Carousel
- carouselWrap: { position: 'absolute', top: 0, left: 0, right: 0, height: height * 0.55, gap: 10, paddingTop: 60, overflow: 'hidden' },
- cardRow: { flexGrow: 0, paddingHorizontal: 8 },
- card: { width: CARD_W, height: CARD_H, borderRadius: 14, marginHorizontal: 5, opacity: 0.7 },
+ carousel: { position: 'absolute', top: 0, left: 0, right: 0, height: height * 0.52, gap: 10, paddingTop: 56, overflow: 'hidden' },
+ fadeBottom: { position: 'absolute', top: height * 0.35, left: 0, right: 0, height: height * 0.2 },
+ fadeTop: { position: 'absolute', top: 0, left: 0, right: 0, height: 80 },
- // Content
- content: { flex: 1, justifyContent: 'flex-end', paddingBottom: 48, paddingHorizontal: 28 },
- logoWrap: { alignItems: 'flex-start', marginBottom: 16 },
- logoIconWrap:{ alignSelf: 'flex-start' },
- logoIcon: { width: 64, height: 64, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },
+ content: { flex: 1, justifyContent: 'flex-end', paddingBottom: 50, paddingHorizontal: 28 },
- brand: { fontSize: 48, fontWeight: '800', color: '#fff', letterSpacing: -2, marginBottom: 8, lineHeight: 52 },
- tagline: { fontSize: 18, color: 'rgba(255,255,255,0.55)', fontWeight: '500', marginBottom: 32, letterSpacing: -0.3 },
+ logoRow: { marginBottom: 18 },
+ logoShadow: { shadowColor: '#7b2ff7', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.6, shadowRadius: 20 },
+ logoBox: { width: 62, height: 62, borderRadius: 17, alignItems: 'center', justifyContent: 'center' },
- // Features
- features: { gap: 12, marginBottom: 36 },
- featureRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
- featureIcon: { width: 32, height: 32, borderRadius: 10, backgroundColor: 'rgba(67,97,238,0.15)', alignItems: 'center', justifyContent: 'center' },
- featureText: { color: 'rgba(255,255,255,0.7)', fontSize: 15, fontWeight: '500' },
+ brand: { fontSize: 46, fontWeight: '800', color: '#fff', letterSpacing: -2, marginBottom: 10, lineHeight: 50 },
+ tagline: { fontSize: 13, color: 'rgba(255,255,255,0.38)', fontWeight: '400', fontStyle: 'italic', marginBottom: 30, lineHeight: 19, letterSpacing: 0.1 },
- // Button
- btnArea: { gap: 16 },
- btnOuter: { borderRadius: 18, overflow: 'hidden', shadowColor: '#7b2ff7', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.5, shadowRadius: 24 },
- btn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, paddingVertical: 18, paddingHorizontal: 24 },
- btnText: { color: '#fff', fontSize: 16, fontWeight: '700', letterSpacing: -0.3 },
- error: { color: '#ff6b6b', fontSize: 13, textAlign: 'center' },
- terms: { color: 'rgba(255,255,255,0.28)', fontSize: 12, textAlign: 'center', lineHeight: 18 },
+ features: { gap: 11, marginBottom: 32 },
+ featureRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
+ dot: { width: 8, height: 8, borderRadius: 4 },
+ featureText:{ color: 'rgba(255,255,255,0.65)', fontSize: 14, fontWeight: '500' },
+
+ btn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, paddingVertical: 17, paddingHorizontal: 24, borderRadius: 17 },
+ btnText: { color: '#fff', fontSize: 16, fontWeight: '700', letterSpacing: -0.3 },
+ error: { color: '#ff6b6b', fontSize: 13, textAlign: 'center', marginTop: 12 },
+ terms: { color: 'rgba(255,255,255,0.25)', fontSize: 11, textAlign: 'center', marginTop: 16, lineHeight: 17 },
});
diff --git a/app/auth/callback.tsx b/app/auth/callback.tsx
index 008aa07..e8c32b1 100644
--- a/app/auth/callback.tsx
+++ b/app/auth/callback.tsx
@@ -1,57 +1,24 @@
-/**
- * app/auth/callback.tsx
- * Handles OAuth deep link: burmalda://auth/callback?code=...
- * expo-web-browser closes the session, then this page completes the flow.
- */
-import { useEffect, useState } from 'react';
-import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
-import { useLocalSearchParams, router } from 'expo-router';
+import { useEffect } from 'react';
+import { View, ActivityIndicator } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
-import { Colors, Font } from '../../constants/theme';
-import { exchangeCode, saveTokens } from '../../services/api';
-import { useAuth } from '../../store';
-import { saveUser } from '../../hooks/useAuth';
+import { router } from 'expo-router';
-// Complete the auth session so expo-web-browser can close
+// Complete the browser auth session so WebBrowser.openAuthSessionAsync returns
WebBrowser.maybeCompleteAuthSession();
export default function AuthCallback() {
- const params = useLocalSearchParams<{ code?: string; error?: string }>();
- const { setUser } = useAuth();
- const [status, setStatus] = useState('Завершаем вход…');
-
useEffect(() => {
- async function finish() {
- const code = params.code;
- if (!code) {
- const msg = params.error || 'Код авторизации отсутствует';
- setStatus(`Ошибка: ${msg}`);
- setTimeout(() => router.replace('/auth'), 2000);
- return;
- }
- try {
- 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) {
- setStatus(`Ошибка: ${e.message || 'Не удалось войти'}`);
- setTimeout(() => router.replace('/auth'), 2500);
- }
- }
- finish();
+ // The actual code exchange is handled in auth.tsx via openAuthSessionAsync
+ // This page only shows briefly if opened directly; redirect to auth
+ const t = setTimeout(() => {
+ router.replace('/auth');
+ }, 500);
+ return () => clearTimeout(t);
}, []);
return (
-
-
- {status}
+
+
);
}
-
-const s = StyleSheet.create({
- root: { flex: 1, backgroundColor: Colors.bg, alignItems: 'center', justifyContent: 'center', gap: 20 },
- text: { color: Colors.text, fontSize: Font.md, textAlign: 'center', opacity: 0.7 },
-});
diff --git a/package.json b/package.json
index ed12191..694ec0f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "burmalda-music",
- "version": "1.0.0",
+ "version": "1.0.7-beta",
"main": "./index.js",
"scripts": {
"start": "expo start",