diff --git a/App.tsx b/App.tsx
index da0ca8d2..e59370ab 100644
--- a/App.tsx
+++ b/App.tsx
@@ -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}
/>
)}
+
diff --git a/src/components/MajorUpdateOverlay.tsx b/src/components/MajorUpdateOverlay.tsx
new file mode 100644
index 00000000..c595e24c
--- /dev/null
+++ b/src/components/MajorUpdateOverlay.tsx
@@ -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 = ({ visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater }) => {
+ const { currentTheme } = useTheme();
+
+ if (!visible) return null;
+
+ return (
+
+
+
+
+
+
+
+ Major update available
+ {!!latestTag && (
+ Latest: {latestTag}
+ )}
+
+
+ {!!releaseNotes && (
+
+
+ {releaseNotes}
+
+
+ )}
+
+
+ {releaseUrl ? (
+ Linking.openURL(releaseUrl)}>
+
+ View release
+
+ ) : null}
+
+
+
+ Later
+
+
+ Dismiss
+
+
+
+
+
+
+ );
+};
+
+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;
+
+
diff --git a/src/hooks/useGithubMajorUpdate.ts b/src/hooks/useGithubMajorUpdate.ts
new file mode 100644
index 00000000..1e311c6b
--- /dev/null
+++ b/src/hooks/useGithubMajorUpdate.ts
@@ -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();
+ const [releaseNotes, setReleaseNotes] = useState();
+ const [releaseUrl, setReleaseUrl] = useState();
+
+ 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 };
+}
+
+
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index a5455bbc..2089337f 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -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 = () => {
/>
= 768;
@@ -65,6 +69,7 @@ const UpdateScreen: React.FC = () => {
const navigation = useNavigation>();
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 */}
+ {/* GitHub Release (compact) – only show when update is available */}
+ {github.latestTag && isAnyUpgrade(getDisplayedAppVersion(), github.latestTag) ? (
+
+
+
+
+
+
+ Current:
+
+ {getDisplayedAppVersion()}
+
+
+
+
+
+
+
+ Latest:
+
+ {github.latestTag}
+
+
+
+ {github.releaseNotes ? (
+
+ Notes:
+
+ {github.releaseNotes}
+
+
+ ) : null}
+
+
+
+ github.releaseUrl ? Linking.openURL(github.releaseUrl as string) : null}
+ activeOpacity={0.8}
+ >
+
+ View Release
+
+
+
+
+
+ ) : null}
+
{false && (
diff --git a/src/services/githubReleaseService.ts b/src/services/githubReleaseService.ts
new file mode 100644
index 00000000..48471898
--- /dev/null
+++ b/src/services/githubReleaseService.ts
@@ -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 {
+ 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];
+}
+
+
diff --git a/src/utils/version.ts b/src/utils/version.ts
new file mode 100644
index 00000000..4e8c6df6
--- /dev/null
+++ b/src/utils/version.ts
@@ -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;
+}
+
+
diff --git a/update-version.sh b/update-version.sh
index 8d278716..899335c4 100644
--- a/update-version.sh
+++ b/update-version.sh
@@ -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"