small UI changes

This commit is contained in:
tapframe 2025-09-29 14:25:51 +05:30
parent a877f5ac13
commit f86e6256a7
8 changed files with 312 additions and 18 deletions

13
App.tsx
View file

@ -29,6 +29,8 @@ import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import { TrailerProvider } from './src/contexts/TrailerContext';
import SplashScreen from './src/components/SplashScreen';
import UpdatePopup from './src/components/UpdatePopup';
import MajorUpdateOverlay from './src/components/MajorUpdateOverlay';
import { useGithubMajorUpdate } from './src/hooks/useGithubMajorUpdate';
import { useUpdatePopup } from './src/hooks/useUpdatePopup';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Sentry from '@sentry/react-native';
@ -84,6 +86,9 @@ const ThemedApp = () => {
handleUpdateLater,
handleDismiss,
} = useUpdatePopup();
// GitHub major/minor release overlay
const githubUpdate = useGithubMajorUpdate();
// Check onboarding status and initialize services
useEffect(() => {
@ -165,6 +170,14 @@ const ThemedApp = () => {
isInstalling={isInstalling}
/>
)}
<MajorUpdateOverlay
visible={githubUpdate.visible}
latestTag={githubUpdate.latestTag}
releaseNotes={githubUpdate.releaseNotes}
releaseUrl={githubUpdate.releaseUrl}
onDismiss={githubUpdate.onDismiss}
onLater={githubUpdate.onLater}
/>
</View>
</NavigationContainer>
</PaperProvider>

View file

@ -0,0 +1,84 @@
import React from 'react';
import { Modal, View, Text, StyleSheet, TouchableOpacity, Linking } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
interface Props {
visible: boolean;
latestTag?: string;
releaseNotes?: string;
releaseUrl?: string;
onDismiss: () => void;
onLater: () => void;
}
const MajorUpdateOverlay: React.FC<Props> = ({ visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater }) => {
const { currentTheme } = useTheme();
if (!visible) return null;
return (
<Modal visible={visible} transparent animationType="fade" statusBarTranslucent presentationStyle="overFullScreen">
<View style={styles.backdrop}>
<View style={[styles.card, { backgroundColor: currentTheme.colors.darkBackground, borderColor: currentTheme.colors.elevation3 }]}>
<View style={styles.header}>
<View style={[styles.iconCircle, { backgroundColor: `${currentTheme.colors.primary}22` }]}>
<MaterialIcons name="new-releases" size={28} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>Major update available</Text>
{!!latestTag && (
<Text style={[styles.version, { color: currentTheme.colors.mediumEmphasis }]}>Latest: {latestTag}</Text>
)}
</View>
{!!releaseNotes && (
<View style={styles.notesBox}>
<Text style={[styles.notes, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={10}>
{releaseNotes}
</Text>
</View>
)}
<View style={styles.actions}>
{releaseUrl ? (
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: currentTheme.colors.primary }]} onPress={() => Linking.openURL(releaseUrl)}>
<MaterialIcons name="open-in-new" size={18} color="#fff" />
<Text style={styles.primaryText}>View release</Text>
</TouchableOpacity>
) : null}
<View style={styles.secondaryRow}>
<TouchableOpacity style={[styles.secondaryBtn, { borderColor: currentTheme.colors.elevation3 }]} onPress={onLater}>
<Text style={[styles.secondaryText, { color: currentTheme.colors.mediumEmphasis }]}>Later</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.secondaryBtn, { borderColor: currentTheme.colors.elevation3 }]} onPress={onDismiss}>
<Text style={[styles.secondaryText, { color: currentTheme.colors.mediumEmphasis }]}>Dismiss</Text>
</TouchableOpacity>
</View>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', alignItems: 'center', justifyContent: 'center', padding: 20 },
card: { width: 380, maxWidth: '100%', borderRadius: 20, borderWidth: 1, overflow: 'hidden' },
header: { alignItems: 'center', paddingTop: 28, paddingBottom: 16, paddingHorizontal: 20 },
iconCircle: { width: 56, height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center', marginBottom: 12 },
title: { fontSize: 20, fontWeight: '700', marginBottom: 6 },
version: { fontSize: 14 },
notesBox: { marginHorizontal: 20, marginBottom: 16, padding: 12, borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.08)' },
notes: { fontSize: 14, lineHeight: 20 },
actions: { paddingHorizontal: 20, paddingBottom: 20 },
primaryBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, justifyContent: 'center', paddingVertical: 12, borderRadius: 12, marginBottom: 12 },
primaryText: { color: '#fff', fontSize: 16, fontWeight: '600' },
secondaryRow: { flexDirection: 'row', gap: 10 },
secondaryBtn: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 12, borderRadius: 12, borderWidth: 1 },
secondaryText: { fontSize: 15, fontWeight: '500' },
});
export default MajorUpdateOverlay;

View file

@ -0,0 +1,65 @@
import { useCallback, useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Updates from 'expo-updates';
import { getDisplayedAppVersion } from '../utils/version';
import { fetchLatestGithubRelease, isAnyUpgrade } from '../services/githubReleaseService';
const DISMISSED_KEY = '@github_major_update_dismissed_version';
export interface MajorUpdateData {
visible: boolean;
latestTag?: string;
releaseNotes?: string;
releaseUrl?: string;
onDismiss: () => void;
onLater: () => void;
refresh: () => void;
}
export function useGithubMajorUpdate(): MajorUpdateData {
const [visible, setVisible] = useState(false);
const [latestTag, setLatestTag] = useState<string | undefined>();
const [releaseNotes, setReleaseNotes] = useState<string | undefined>();
const [releaseUrl, setReleaseUrl] = useState<string | undefined>();
const check = useCallback(async () => {
try {
// Always compare with Settings screen version
const current = getDisplayedAppVersion() || Updates.runtimeVersion || '0.0.0';
const info = await fetchLatestGithubRelease();
if (!info?.tag_name) return;
const dismissed = await AsyncStorage.getItem(DISMISSED_KEY);
if (dismissed === info.tag_name) return;
// "Later" is session-only now, no persisted snooze
const shouldShow = isAnyUpgrade(current, info.tag_name);
if (shouldShow) {
setLatestTag(info.tag_name);
setReleaseNotes(info.body);
setReleaseUrl(info.html_url);
setVisible(true);
}
} catch {
// ignore
}
}, []);
useEffect(() => {
check();
}, [check]);
const onDismiss = useCallback(async () => {
if (latestTag) await AsyncStorage.setItem(DISMISSED_KEY, latestTag);
setVisible(false);
}, [latestTag]);
const onLater = useCallback(async () => {
setVisible(false);
}, []);
return { visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater, refresh: check };
}

View file

@ -30,6 +30,7 @@ import { useAccount } from '../contexts/AccountContext';
import { catalogService } from '../services/catalogService';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Sentry from '@sentry/react-native';
import { getDisplayedAppVersion } from '../utils/version';
import CustomAlert from '../components/CustomAlert';
import ProfileIcon from '../components/icons/ProfileIcon';
import PluginIcon from '../components/icons/PluginIcon';
@ -643,7 +644,7 @@ const SettingsScreen: React.FC = () => {
/>
<SettingItem
title="Version"
description="1.2.1"
description={getDisplayedAppVersion()}
icon="info-outline"
isLast={true}
isTablet={isTablet}

View file

@ -8,7 +8,8 @@ import {
SafeAreaView,
StatusBar,
Platform,
Dimensions
Dimensions,
Linking
} from 'react-native';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import { useNavigation } from '@react-navigation/native';
@ -20,6 +21,9 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import UpdateService from '../services/updateService';
import CustomAlert from '../components/CustomAlert';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useGithubMajorUpdate } from '../hooks/useGithubMajorUpdate';
import { getDisplayedAppVersion } from '../utils/version';
import { isAnyUpgrade } from '../services/githubReleaseService';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@ -65,6 +69,7 @@ const UpdateScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const github = useGithubMajorUpdate();
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
@ -144,6 +149,8 @@ const UpdateScreen: React.FC = () => {
})();
}
checkForUpdates();
// Also refresh GitHub section on mount (works in dev and prod)
try { github.refresh(); } catch {}
if (Platform.OS === 'android') {
try {
toast('Checking for updates…', { duration: 1200, position: ToastPosition.TOP });
@ -517,6 +524,58 @@ const UpdateScreen: React.FC = () => {
{/* Developer Logs removed */}
</SettingsCard>
{/* GitHub Release (compact) only show when update is available */}
{github.latestTag && isAnyUpgrade(getDisplayedAppVersion(), github.latestTag) ? (
<SettingsCard title="GITHUB RELEASE" isTablet={isTablet}>
<View style={styles.infoSection}>
<View style={styles.infoItem}>
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="new-releases" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Current:</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{getDisplayedAppVersion()}
</Text>
</View>
<View style={styles.infoItem}>
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="tag" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Latest:</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{github.latestTag}
</Text>
</View>
{github.releaseNotes ? (
<View style={{ marginTop: 4 }}>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Notes:</Text>
<Text
numberOfLines={3}
style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}
>
{github.releaseNotes}
</Text>
</View>
) : null}
<View style={[styles.actionSection, { marginTop: 8 }]}>
<View style={{ flexDirection: 'row', gap: 10 }}>
<TouchableOpacity
style={[styles.modernButton, { backgroundColor: currentTheme.colors.primary, flex: 1 }]}
onPress={() => github.releaseUrl ? Linking.openURL(github.releaseUrl as string) : null}
activeOpacity={0.8}
>
<MaterialIcons name="open-in-new" size={18} color="white" />
<Text style={styles.modernButtonText}>View Release</Text>
</TouchableOpacity>
</View>
</View>
</View>
</SettingsCard>
) : null}
{false && (
<SettingsCard title="UPDATE LOGS" isTablet={isTablet}>
<View style={styles.logsContainer}>

View file

@ -0,0 +1,62 @@
import { Platform } from 'react-native';
export interface GithubReleaseInfo {
tag_name: string;
name?: string;
body?: string;
html_url?: string;
published_at?: string;
}
const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/tapframe/NuvioStreaming/releases/latest';
export async function fetchLatestGithubRelease(): Promise<GithubReleaseInfo | null> {
try {
const res = await fetch(GITHUB_LATEST_RELEASE_URL, {
headers: {
'Accept': 'application/vnd.github+json',
// Identify app a bit; avoid user agent blocks
'User-Agent': `Nuvio/${Platform.OS}`,
},
});
if (!res.ok) return null;
const json = await res.json();
return {
tag_name: json.tag_name,
name: json.name,
body: json.body,
html_url: json.html_url,
published_at: json.published_at,
};
} catch {
return null;
}
}
export function parseSemver(version: string): [number, number, number] | null {
const m = version.trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
if (!m) return null;
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
}
export function isMajorOrMinorUpgrade(current: string, latest: string): boolean {
const a = parseSemver(current);
const b = parseSemver(latest);
if (!a || !b) return false;
// Major or minor bump when (b.major > a.major) or (same major and b.minor > a.minor)
if (b[0] > a[0]) return true;
if (b[0] === a[0] && b[1] > a[1]) return true;
return false;
}
// Return true if latest > current for semver including patch
export function isAnyUpgrade(current: string, latest: string): boolean {
const a = parseSemver(current);
const b = parseSemver(latest);
if (!a || !b) return false;
if (b[0] !== a[0]) return b[0] > a[0];
if (b[1] !== a[1]) return b[1] > a[1];
return b[2] > a[2];
}

10
src/utils/version.ts Normal file
View file

@ -0,0 +1,10 @@
// Single source of truth for the app version displayed in Settings
// Update this when bumping app version
export const APP_VERSION = '1.2.1';
export function getDisplayedAppVersion(): string {
return APP_VERSION;
}

View file

@ -1,7 +1,7 @@
#!/bin/bash
# Version Update Script for Nuvio App
# Updates version across app.json, SettingsScreen.tsx, and iOS Info.plist
# Updates version across app.json, src/utils/version.ts, and iOS/Android build files
set -e # Exit on any error
@ -56,12 +56,12 @@ fi
# File paths
APP_JSON="./app.json"
SETTINGS_SCREEN="./src/screens/SettingsScreen.tsx"
VERSION_TS="./src/utils/version.ts"
INFO_PLIST="./ios/Nuvio/Info.plist"
ANDROID_BUILD_GRADLE="./android/app/build.gradle"
# Check if files exist
for file in "$APP_JSON" "$SETTINGS_SCREEN" "$INFO_PLIST" "$ANDROID_BUILD_GRADLE"; do
for file in "$APP_JSON" "$VERSION_TS" "$INFO_PLIST" "$ANDROID_BUILD_GRADLE"; do
if [ ! -f "$file" ]; then
print_error "File not found: $file"
exit 1
@ -88,7 +88,7 @@ print_status "New build number: $NEW_BUILD_NUMBER"
# Backup files
print_status "Creating backups..."
cp "$APP_JSON" "${APP_JSON}.backup"
cp "$SETTINGS_SCREEN" "${SETTINGS_SCREEN}.backup"
cp "$VERSION_TS" "${VERSION_TS}.backup"
cp "$INFO_PLIST" "${INFO_PLIST}.backup"
cp "$ANDROID_BUILD_GRADLE" "${ANDROID_BUILD_GRADLE}.backup"
@ -96,7 +96,7 @@ cp "$ANDROID_BUILD_GRADLE" "${ANDROID_BUILD_GRADLE}.backup"
restore_backups() {
print_warning "Restoring backups due to error..."
mv "${APP_JSON}.backup" "$APP_JSON"
mv "${SETTINGS_SCREEN}.backup" "$SETTINGS_SCREEN"
mv "${VERSION_TS}.backup" "$VERSION_TS"
mv "${INFO_PLIST}.backup" "$INFO_PLIST"
mv "${ANDROID_BUILD_GRADLE}.backup" "$ANDROID_BUILD_GRADLE"
}
@ -116,11 +116,11 @@ sed -i '' "s/\"versionCode\": [0-9]*/\"versionCode\": $NEW_BUILD_NUMBER/g" "$APP
sed -i '' "s/\"buildNumber\": \"[^\"]*\"/\"buildNumber\": \"$NEW_BUILD_NUMBER\"/g" "$APP_JSON"
print_success "Updated app.json"
# Update SettingsScreen.tsx
print_status "Updating SettingsScreen.tsx..."
# Note: Use BSD sed compatible regex on macOS. Enable extended regex with -E.
sed -E -i '' "s/description=\"[0-9]+\.[0-9]+\.[0-9]+(-[^\"]*)?\"/description=\"$NEW_VERSION\"/g" "$SETTINGS_SCREEN"
print_success "Updated SettingsScreen.tsx"
# Update src/utils/version.ts
print_status "Updating src/utils/version.ts..."
# Replace the APP_VERSION constant value
sed -E -i '' "s/export const APP_VERSION = '\\S*';/export const APP_VERSION = '$NEW_VERSION';/g" "$VERSION_TS"
print_success "Updated src/utils/version.ts"
# Update Info.plist
print_status "Updating Info.plist..."
@ -152,11 +152,11 @@ else
exit 1
fi
# Check SettingsScreen.tsx
if grep -q "description=\"$NEW_VERSION\"" "$SETTINGS_SCREEN"; then
print_success "SettingsScreen.tsx updated correctly"
# Check src/utils/version.ts
if grep -q "export const APP_VERSION = '$NEW_VERSION';" "$VERSION_TS"; then
print_success "src/utils/version.ts updated correctly"
else
print_error "SettingsScreen.tsx update verification failed"
print_error "src/utils/version.ts update verification failed"
exit 1
fi
@ -180,14 +180,14 @@ fi
# Clean up backups
print_status "Cleaning up backups..."
rm "${APP_JSON}.backup" "${SETTINGS_SCREEN}.backup" "${INFO_PLIST}.backup" "${ANDROID_BUILD_GRADLE}.backup"
rm "${APP_JSON}.backup" "${VERSION_TS}.backup" "${INFO_PLIST}.backup" "${ANDROID_BUILD_GRADLE}.backup"
print_success "Version update completed successfully!"
print_status "Summary:"
echo " Version: $NEW_VERSION"
echo " Runtime Version: $NEW_VERSION"
echo " Build Number: $NEW_BUILD_NUMBER"
echo " Files updated: app.json, SettingsScreen.tsx, Info.plist, Android build.gradle"
echo " Files updated: app.json, src/utils/version.ts, Info.plist, Android build.gradle"
echo ""
print_status "Next steps:"
echo " 1. Test the app to ensure everything works correctly"