diff --git a/.env.example b/.env.example
index 53bed3e..a124a92 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,4 @@
EXPO_PUBLIC_ADMIN_ID=
-EXPO_PUBLIC_ADMIN_PASSWORD=
\ No newline at end of file
+EXPO_PUBLIC_ADMIN_PASSWORD=
+EXPO_PUBLIC_SUPABASE_URL=
+EXPO_PUBLIC_SUPABASE_ANON_KEY=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fd7248f..19a13cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
# dependencies
node_modules/
-
+.env
# Expo
.expo/
dist/
diff --git a/app.json b/app.json
index 14e3368..f884f87 100644
--- a/app.json
+++ b/app.json
@@ -29,7 +29,8 @@
"plugins": [
"expo-router",
[
- "expo-splash-screen",{
+ "expo-splash-screen",
+ {
"backgroundColor": "#FFFFFF",
"resizeMode": "contain",
"image": "./assets/images/bird-logo.png",
@@ -64,7 +65,7 @@
"origin": false
},
"eas": {
- "projectId": "035efa62-7a9a-42a2-bf65-aee321fba071"
+ "projectId": "0a89ff9f-141b-4259-a757-6b6c1e8d398c"
}
}
}
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 4ef0edd..b20cb47 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -222,6 +222,10 @@ const VideoList = () => {
textStyle={{ fontSize: 11 }}
arrowIconStyle={{ marginHorizontal: -5 }}
/>
+
+ {/* REVERTED THIS CHANGE AS PER FEEDBACK
+ Changed 'onPress' back to 'onLongPress' and added delay
+ */}
router.push(`/login`)} delayLongPress={5000}>
{
{
- loading ? (
- item.toString()}
- renderItem={() => (
-
-
-
-
-
-
-
- )}
- />
- ) : (
- level === "all" || item.level === level)}
- keyExtractor={(item) => item.id}
- renderItem={({ item }) => (
-
- handleVideoPress(item)}
- >
-
-
-
- {/* Video Details along with pdf and translation option */}
-
-
- {videoLanguages[item.id] === "en" ? item.english_title : item.punjabi_title}
-
-
- toggleVideoLanguage(item.id)}
- className="bg-white p-2.5 rounded-full">
-
-
- handlePdfPress(item)}
- className="bg-white p-2.5 rounded-full">
-
-
+ loading ? (
+ item.toString()}
+ renderItem={() => (
+
+
+
+
+
-
- )}
- />
-
+ )}
+ />
+ ) : (
+ level === "all" || item.level === level)}
+ keyExtractor={(item) => item.id}
+ renderItem={({ item }) => (
+
+ handleVideoPress(item)}
+ >
+
+
+
+ {/* Video Details along with pdf and translation option */}
+
+
+ {videoLanguages[item.id] === "en" ? item.english_title : item.punjabi_title}
+
+
+ toggleVideoLanguage(item.id)}
+ className="bg-white p-2.5 rounded-full">
+
+
+ handlePdfPress(item)}
+ className="bg-white p-2.5 rounded-full">
+
+
+
+
+
+ )}
+ />
+
)}
);
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 822f672..ef9bcc3 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,170 +1,199 @@
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, Suspense } from 'react'; // Import Suspense
import 'react-native-reanimated';
-import '../global.css';
+import '../global.css'; // Make sure this path is correct relative to app/_layout.tsx
import { View, Modal, Text } from 'react-native';
import { Asset } from 'expo-asset';
import Animated, { Easing, useSharedValue, useAnimatedStyle, withTiming, withRepeat } from 'react-native-reanimated';
import * as SplashScreen from 'expo-splash-screen';
+// Import from 'expo-sqlite' (not /next)
import { SQLiteProvider } from 'expo-sqlite';
-import { initializeDatabase } from './database/database';
-import { UserProvider } from './userContext';
-import { downloadVideo,clearDownloadedVideos } from "./video/videoDownlaoder";
+import { initializeDatabase } from './database/database'; // Make sure this path is correct
+import { UserProvider } from './userContext'; // Make sure this path is correct
+import { downloadVideo, clearDownloadedVideos } from "./video/videoDownlaoder"; // Make sure this path is correct
import { ProgressBar } from 'react-native-paper';
-
+import SyncToCloud from '@/components/SyncToCloud'; // Make sure this path is correct
// Prevent auto-hide at the start
-
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const VIDEO_LIST = [
- { id: '1_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/A_Cloud_of_Trash_English.mp4' },
- { id: '1_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/A_Cloud_of_Trash_Punjabi.mp4' },
- { id: '2_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/A_Street,_or_a_Zoo_English.mp4' },
- { id: '2_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/A_Street,_or_a_Zoo_Punjabi.mp4' },
- { id: '3_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Aaloo_Maaloo_Kaaloo_English.mp4' },
- { id: '3_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Aaloo_Maaloo_Kaaloo_Punjabi.mp4' },
- { id: '4_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Abdul_Kalam,_A_Lesson_for_my_Teacher_English.mp4' },
- { id: '4_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Abdul_Kalam,_A_Lesson_for_my_Teacher_Punjabi.mp4' },
- { id: '5_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Abdul_Kalam,_Designing_a_Fighter_Jet_English.mp4' },
- { id: '5_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Abdul_Kalam,_Designing_a_Fighter_Jet_Punjabi.mp4' },
- { id: '6_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Abdul_Kalam,_Failure_to_Success_English.mp4' },
- { id: '6_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Abdul_Kalam,_Failure_to_Success_Punjabi.mp4' },
- { id: '7_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Ammus_Puppy_English.mp4' },
- { id: '7_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Ammus_Puppy_Punjabi.mp4' },
- { id: '8_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Bheema,_the_Sleepyhead_English.mp4' },
- { id: '8_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Bheema,_the_Sleepyhead_Punjabi.mp4' },
- { id: '9_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Bunty_and_Bubbly_English.mp4' },
- { id: '9_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Bunty_and_Bubbly_Punjabi.mp4' },
- { id: '10_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/The_Moon_and_the_Cap_English.mp4' },
- { id: '10_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/The_Moon_and_the_Cap_Punjabi.mp4' },
- { id: '11_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/The_Princess_Farmer_English.mp4' },
- { id: '11_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/The_Princess_Farmer_Punjabi.mp4' },
- { id: '12_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Too_Big_Too_Small_English.mp4' },
- { id: '12_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Too_Big!_Too_Small!_Punjabi.mp4' }
+ // This list determines which videos are downloaded on startup
+ { id: '1_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/A_Cloud_of_Trash_English.mp4' },
+ { id: '1_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/A_Cloud_of_Trash_Punjabi.mp4' },
+ { id: '2_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/A_Street,_or_a_Zoo_English.mp4' },
+ { id: '2_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/A_Street,_or_a_Zoo_Punjabi.mp4' },
+ { id: '3_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Aaloo_Maaloo_Kaaloo_English.mp4' },
+ { id: '3_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Aaloo_Maaloo_Kaaloo_Punjabi.mp4' },
+ { id: '4_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Abdul_Kalam,_A_Lesson_for_my_Teacher_English.mp4' },
+ { id: '4_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Abdul_Kalam,_A_Lesson_for_my_Teacher_Punjabi.mp4' },
+ { id: '5_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Abdul_Kalam,_Designing_a_Fighter_Jet_English.mp4' },
+ { id: '5_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Abdul_Kalam,_Designing_a_Fighter_Jet_Punjabi.mp4' },
+ { id: '6_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Abdul_Kalam,_Failure_to_Success_English.mp4' },
+ { id: '6_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Abdul_Kalam,_Failure_to_Success_Punjabi.mp4' },
+ { id: '7_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Ammus_Puppy_English.mp4' },
+ { id: '7_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Ammus_Puppy_Punjabi.mp4' },
+ { id: '8_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Bheema,_the_Sleepyhead_English.mp4' },
+ { id: '8_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Bheema,_the_Sleepyhead_Punjabi.mp4' },
+ { id: '9_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Bunty_and_Bubbly_English.mp4' },
+ { id: '9_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Bunty_and_Bubbly_Punjabi.mp4' },
+ { id: '10_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/The_Moon_and_the_Cap_English.mp4' },
+ { id: '10_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/The_Moon_and_the_Cap_Punjabi.mp4' },
+ { id: '11_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/The_Princess_Farmer_English.mp4' },
+ { id: '11_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/The_Princess_Farmer_Punjabi.mp4' },
+ { id: '12_en', url: 'https://storage.googleapis.com/bird-planet-read/Videos/English/Too_Big_Too_Small_English.mp4' },
+ { id: '12_pa', url: 'https://storage.googleapis.com/bird-planet-read/Videos/punjabi/Too_Big!_Too_Small!_Punjabi.mp4' }
];
-
- const [isLoading, setIsLoading] = useState(true);
- const scale = useSharedValue(0.5);
- const opacity = useSharedValue(0);
- const splash_img = require("@/assets/images/splash_img.png");
- const [downloadProgress, setDownloadProgress] = useState(0);
- const [videoAssetsLoaded, setVideoAssetsLoaded] = useState(false);
- const glowOpacity = useSharedValue(0.3);
+ const [isLoading, setIsLoading] = useState(true); // Tracks if initial assets (like splash image) are loaded
+ const scale = useSharedValue(0.5); // For splash animation
+ const opacity = useSharedValue(0); // For splash animation
+ const splash_img = require("@/assets/images/splash_img.png"); // Make sure path is correct
+ const [downloadProgress, setDownloadProgress] = useState(0); // Tracks video download progress
+ const [videoAssetsLoaded, setVideoAssetsLoaded] = useState(false); // Tracks if all videos are downloaded
+ const glowOpacity = useSharedValue(0.3); // For splash animation glow
+
+ // --- Animation and Asset Preloading Effects ---
useEffect(() => {
- glowOpacity.value = withRepeat(withTiming(1, { duration: 1000 }), -1, true); // Repeats the glow effect
+ // Animate glow effect for splash screen
+ glowOpacity.value = withRepeat(withTiming(1, { duration: 1000 }), -1, true);
}, []);
-
+
const animatedGlow = useAnimatedStyle(() => ({
+ // Style for the splash screen glow (applied below if needed)
shadowOpacity: glowOpacity.value,
shadowRadius: 10,
- shadowColor: "#6B21A8",
+ shadowColor: "#6B21A8", // Purple glow color
}));
useEffect(() => {
+ // Preload splash image and manage splash screen visibility duration
async function preloadAssets() {
try {
- await Asset.loadAsync([splash_img]); // Preload the image
+ await Asset.loadAsync([splash_img]);
+ // Animate splash image appearing
scale.value = withTiming(1, { duration: 1200, easing: Easing.out(Easing.exp) });
opacity.value = withTiming(1, { duration: 1200 });
-
- // Set a minimum display time for the splash screen (e.g., 3000ms = 3 seconds)
- const minimumDisplayTime = 4000;
+
+ const minimumDisplayTime = 4000; // Keep splash visible for at least 4 seconds
const startTime = Date.now();
-
- await SplashScreen.hideAsync(); // Hide the system splash screen
-
- // Ensure our custom splash screen stays visible for the minimum time
+
+ await SplashScreen.hideAsync(); // Hide the native OS splash screen
+
+ // Calculate remaining time needed for our custom splash
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, minimumDisplayTime - elapsedTime);
-
+
+ // Set isLoading to false after the minimum display time
setTimeout(() => {
setIsLoading(false);
}, remainingTime);
} catch (error) {
- console.warn("Error loading assets:", error);
- setIsLoading(false); // Ensure we exit loading state even on error
+ console.warn("Error loading splash assets:", error);
+ setIsLoading(false); // Ensure loading stops even if assets fail
}
}
-
preloadAssets();
- }, []);
+ }, []); // Run only once on mount
- //download the videos on the first installation in the next installation check if they exists or not
- useEffect(() => {
- (async () => {
- // Download all videos
- // await clearDownloadedVideos(); // only for testing purposes don't use it in production
- let completed = 0;
- for (const video of VIDEO_LIST) {
+ // --- Video Download Effect ---
+ useEffect(() => {
+ // Download videos listed in VIDEO_LIST
+ (async () => {
+ // await clearDownloadedVideos(); // Uncomment only if you need to force re-download for testing
+ console.log("Starting video downloads...");
+ let completed = 0;
+ for (const video of VIDEO_LIST) {
+ try {
await downloadVideo(video.id, video.url);
- completed++;
- setDownloadProgress(completed);
+ console.log(`Successfully downloaded video ${video.id}`);
+ } catch (downloadError) {
+ console.error(`Failed to download video ${video.id} from ${video.url}:`, downloadError);
+ // Optional: Add logic here to retry or notify the user
}
- setVideoAssetsLoaded(true);
- })();
- }, []);
+ completed++;
+ setDownloadProgress(completed); // Update progress state
+ }
+ console.log("All video downloads attempted.");
+ setVideoAssetsLoaded(true); // Mark video loading as complete
+ })();
+ }, []); // Run only once on mount
const animatedStyle = useAnimatedStyle(() => ({
+ // Style for splash image animation
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
+ // --- Render Logic ---
- return (
- <>
- { isLoading && (
+ // 1. Show Custom Splash Screen while initial assets load
+ if (isLoading) {
+ return (
+ {/* You could apply animatedGlow here if you want the image itself to glow */}
- )}
-
- { !isLoading && !videoAssetsLoaded && (
- //show a popup of number of videos downloading and stuff.... a progress bar modal
-
-
-
-
- Downloading Videos...
-
-
- {/* Show number of videos downloaded out of total */}
-
- {downloadProgress} videos downloaded out of {VIDEO_LIST.length}
-
-
- {/* Progress Bar */}
-
-
- {/* Optional: Estimated time or animated loading */}
-
- Please wait...
-
-
-
-
- )}
-
- { !isLoading && videoAssetsLoaded && (
-
+ );
+ }
+
+ // 2. Show Video Download Modal while videos are downloading
+ if (!videoAssetsLoaded) {
+ return (
+
+
+
+
+ Downloading Initial Videos...
+
+
+ {downloadProgress} / {VIDEO_LIST.length} completed
+
+ 0 ? downloadProgress / VIDEO_LIST.length : 0}
+ color="#6B21A8" // Purple progress bar
+ style={{ height: 10, width: 200, borderRadius: 5, marginBottom: 5 }}
+ />
+
+ Please wait, this may take a moment...
+
+
+
+
+ );
+ }
+
+ // 3. Render the main app content once everything is ready
+ return (
+ // Wrap the entire app structure in Suspense for SQLite loading
+ Loading Database...}>
+ {/*
+ Provide the SQLite database context to the whole app.
+ IMPORTANT: Change "test.db" to the actual database filename
+ used by your `initializeDatabase` function (e.g., "videos.db").
+ */}
+
+ {/* Provides user context (like login status) to the app */}
+ {/* Defines the navigation structure using Expo Router */}
+ {/* These screens are defined by folders/files in your 'app' directory */}
+ {/* Component for syncing data to the cloud (Supabase) */}
+
+ {/* Controls the appearance of the device's status bar (time, battery, etc.) */}
- )}
- >
- )
-
+
+ );
}
\ No newline at end of file
diff --git a/app/dashboard/index.tsx b/app/dashboard/index.tsx
index 6dee31e..cc6545a 100644
--- a/app/dashboard/index.tsx
+++ b/app/dashboard/index.tsx
@@ -6,7 +6,7 @@ import {
TouchableOpacity,
TextInput,
} from "react-native";
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useRef } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { getVideoAnalyticsByUser, getUsers, deleteAllUserData, createUser, editUserName } from "../database/database";
import { useSQLiteContext } from "expo-sqlite";
@@ -14,9 +14,9 @@ import { Dimensions } from "react-native";
import { videoDetails } from "../../assets/details";
import DropDownPicker from "react-native-dropdown-picker";
import { Ionicons } from "@expo/vector-icons";
-import PieChart from "react-native-pie-chart";
+import PieChart from "react-native-pie-chart";
import { StyleSheet } from "react-native";
-import SyncToCloud from "@/components/SyncToCloud";
+// SyncToCloud removed - automatic sync handled in background
import * as FileSystem from 'expo-file-system';
import Papa from 'papaparse';
import * as Sharing from 'expo-sharing';
@@ -415,22 +415,16 @@ const AnalyticsDashboard = () => {
EXPORT
-
- {/* SyncToCloud component taking half width */}
-
-
-
-
- {/* Delete button taking half width */}
-
- setDeleteModalVisible(true)}
- >
- DELETE USER DATA
-
+ {/* Sync button completely removed - automatic sync only */}
+
+ {/* Delete button only */}
+ setDeleteModalVisible(true)}
+ >
+ DELETE USER DATA
+
-
{/* Level, Language and Date Dropdowns */}
diff --git a/app/pdf/[id].tsx b/app/pdf/[id].tsx
index 59de0b7..5dee214 100644
--- a/app/pdf/[id].tsx
+++ b/app/pdf/[id].tsx
@@ -1,45 +1,49 @@
import React, { useEffect, useState } from 'react';
-import { View, ActivityIndicator, Alert, Text, TouchableOpacity, Image } from 'react-native';
+import { View, ActivityIndicator, Alert, Text } from 'react-native';
import Pdf from 'react-native-pdf';
import * as FileSystem from 'expo-file-system';
-import { Asset } from 'expo-asset';
-import { useLocalSearchParams, useRouter } from 'expo-router';
+import { Asset } from 'expo-asset'; // This is the key
+import { useLocalSearchParams } from 'expo-router';
import { videoDetails } from '../../assets/details';
const PdfViewer = () => {
const [pdfUri, setPdfUri] = useState(null);
const [loading, setLoading] = useState(true);
- const router = useRouter();
const { id, language } = useLocalSearchParams<{ id?: string; language?: string }>();
const video = videoDetails.find((v) => v.id === id);
- const pdf = video ? (language === "pa" ? video.pdf_punjabi : video.pdf_en) : null;
+ const pdfAsset = video ? (language === "pa" ? video.pdf_punjabi : video.pdf_en) : null;
const title = video ? (language === "pa" ? video.punjabi_title : video.english_title) : 'PDF Viewer';
- const back = require('@/assets/images/back.png');
useEffect(() => {
const loadPdf = async () => {
try {
console.log("🔄 Starting to load PDF...");
-
- // Load the asset
- const asset = pdf;
- if (!asset) {
- Alert.alert('Error', 'PDF not found');
- console.error('❌ PDF not found');
+
+ // Check if pdfAsset is valid
+ if (!pdfAsset) {
+ Alert.alert('Error', 'PDF asset definition not found');
+ console.error('❌ PDF asset definition not found in videoDetails');
+ setLoading(false);
return;
}
+
+ // THIS IS THE FIX: pdfAsset is ALREADY an Asset object.
+ // We do NOT need Asset.fromModule().
+ const asset = pdfAsset;
+
+ console.log("🔽 Downloading asset...");
await asset.downloadAsync();
+ console.log("📂 Asset local URI:", asset.localUri || asset.uri);
- console.log("📂 Asset path:", asset.uri);
const fileUri = `${FileSystem.cacheDirectory}${video?.id}_${language}.pdf`;
- // Check if file exists, else copy it
const fileExists = await FileSystem.getInfoAsync(fileUri);
+
if (!fileExists.exists) {
console.log("🚀 Copying file to cache...");
- await FileSystem.copyAsync({ from: asset.uri, to: fileUri });
+ await FileSystem.copyAsync({ from: asset.localUri || asset.uri, to: fileUri });
} else {
- console.log("✅ File already exists in cache");
+ console.log("✅ File already in cache");
}
console.log("✅ PDF successfully loaded:", fileUri);
@@ -52,25 +56,18 @@ const PdfViewer = () => {
};
loadPdf();
- }, []);
+ }, [pdfAsset]);
return (
- {/* Header with Back Button and Title */}
+ {/* Header */}
- {/* router.push("/")} className="p-2">
-
- */}
-
- {title || 'PDF Viewer'}
-
-
-
+
+ {title || 'PDF Viewer'}
+
+
+ {/* Content */}
{loading ? (
) : pdfUri ? (
@@ -84,10 +81,12 @@ const PdfViewer = () => {
onError={(error) => console.log("❌ Error loading PDF:", error)}
/>
) : (
-
+
+ Could not load PDF.
+
)}
);
};
-export default PdfViewer;
+export default PdfViewer;
\ No newline at end of file
diff --git a/app/test.cpp b/app/test.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/app/video/[id].tsx b/app/video/[id].tsx
index caf6384..27074f2 100644
--- a/app/video/[id].tsx
+++ b/app/video/[id].tsx
@@ -2,7 +2,7 @@ import { useLocalSearchParams } from "expo-router";
import { useVideoPlayer, VideoView } from "expo-video";
import { StyleSheet, View, Text, TouchableOpacity, Image, TouchableWithoutFeedback } from "react-native";
import { videoDetails } from "@/assets/details";
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState, useRef } from "react"; // --- FIX: Import 'useRef' ---
import * as ScreenOrientation from "expo-screen-orientation";
import { useRouter } from "expo-router";
import { useKeepAwake } from 'expo-keep-awake';
@@ -11,6 +11,7 @@ import { getVideoAnalyticsByUser, getUsers } from "../database/database";
import { getVideoUri } from "./videoDownlaoder";
import { BackHandler } from "react-native"; // for handling back button press on android
import { useFocusEffect } from "@react-navigation/native";
+import AsyncStorage from '@react-native-async-storage/async-storage';
export default function VideoScreen() {
useKeepAwake();
@@ -21,18 +22,35 @@ export default function VideoScreen() {
const video = videoDetails.find((v) => v.id === id);
const db = useSQLiteContext();
const [fileUri, setFileUri] = useState(null);
+
+ // --- FIX: Create a ref to track if the component is mounted ---
+ const isMountedRef = useRef(true);
// References to track watch time that won't be affected by React's asynchronous updates
const watchStartTimeRef = useRef(null);
const totalWatchTimeRef = useRef(0);
+ const videoCompletedRef = useRef(false); // Track if video has finished playing
const [videoSource, setVideoSource] = useState(null);
// video_id_language -> video_3_en
const videoUri = `${video?.id}_${language == "pa" ? "pa" : "en"}`;
+ useEffect(() => {
+ // --- FIX: Set the ref to false when the component unmounts ---
+ return () => {
+ isMountedRef.current = false;
+ };
+ }, []);
+
useEffect(() => {
const fetchVideoUri = async () => {
const uri = await getVideoUri(videoUri);
+
+ // --- FIX: Add a "guard" to check if component is still mounted ---
+ if (!isMountedRef.current) {
+ return; // Stop execution if component unmounted
+ }
+
setFileUri(uri);
if (uri && video) {
@@ -50,8 +68,31 @@ export default function VideoScreen() {
player.loop = false;
const currentOrientation = await ScreenOrientation.getOrientationAsync();
+
+ // --- FIX: Add a "guard" to check if component is still mounted ---
+ if (!isMountedRef.current) {
+ return; // Stop execution if component unmounted
+ }
+
setOriginalOrientation(currentOrientation);
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
+
+ // Add listener to detect when video finishes
+ player.addListener('statusChange', (status) => {
+ // Check if video has reached the end
+ if (status.status === 'idle' || (status as any).didJustFinish) {
+ console.log('Video finished playing - stopping time tracking');
+ videoCompletedRef.current = true;
+ // Stop current tracking if video is finished
+ if (watchStartTimeRef.current !== null) {
+ const elapsedTime = Math.ceil((Date.now() - watchStartTimeRef.current) / 1000);
+ totalWatchTimeRef.current += elapsedTime;
+ watchStartTimeRef.current = null;
+ console.log(`Video completed - final watch time: ${totalWatchTimeRef.current}s`);
+ }
+ }
+ });
+
await player.play();
}
);
@@ -59,8 +100,30 @@ export default function VideoScreen() {
// More reliable way to track watch time using refs
useEffect(() => {
const interval = setInterval(() => {
+ // Don't track time if video has completed
+ if (videoCompletedRef.current) {
+ return;
+ }
+
+ // Additional check: if video is near the end, consider it completed
+ if (player?.status && typeof player.status === 'object' && 'currentTime' in player.status && 'duration' in player.status) {
+ const currentTime = (player.status as any).currentTime;
+ const duration = (player.status as any).duration;
+ if (currentTime >= duration - 1) { // Within 1 second of the end
+ console.log('Video reached end - stopping time tracking');
+ videoCompletedRef.current = true;
+ if (watchStartTimeRef.current !== null) {
+ const elapsedTime = Math.ceil((Date.now() - watchStartTimeRef.current) / 1000);
+ totalWatchTimeRef.current += elapsedTime;
+ watchStartTimeRef.current = null;
+ }
+ return;
+ }
+ }
+
if (player?.playing && watchStartTimeRef.current === null) {
watchStartTimeRef.current = Date.now(); // Start tracking when video plays
+ console.log('Started tracking video watch time');
}
if (!player?.playing && watchStartTimeRef.current !== null) {
@@ -69,6 +132,7 @@ export default function VideoScreen() {
// Add to total watch time using the ref (not state)
totalWatchTimeRef.current += elapsedTime;
watchStartTimeRef.current = null; // Reset for next play segment
+ console.log(`Paused tracking - added ${elapsedTime}s, total: ${totalWatchTimeRef.current}s`);
}
}, 1000); // Check every second
@@ -132,21 +196,28 @@ export default function VideoScreen() {
);
console.log(`Inserted new analytics for Video ${videoId}, Language: ${videoLang} with ${watchedTime}ms`);
}
+
+ // Trigger sync after analytics update
+ await AsyncStorage.setItem('triggerSync', Date.now().toString());
} catch (error) {
console.error("Error updating video analytics:", error);
}
};
const returnBackToHome = async () => {
+ // --- FIX: We must set the isMountedRef to false *before* navigating away ---
+ isMountedRef.current = false;
+
// Calculate final watch time including current playing segment if video is still playing
let finalWatchTime = totalWatchTimeRef.current;
- if (player?.playing && watchStartTimeRef.current !== null) {
+ // Only add current segment time if video hasn't completed and is still playing
+ if (player?.playing && watchStartTimeRef.current !== null && !videoCompletedRef.current) {
// Add the current play segment if video is still playing
finalWatchTime += (Math.ceil((Date.now() - watchStartTimeRef.current)/1000));
}
- console.log(`Total Watch Time: ${finalWatchTime} seconds`);
+ console.log(`Total Watch Time: ${finalWatchTime} seconds (Video completed: ${videoCompletedRef.current})`);
// Only update analytics if there's actual watch time
if (finalWatchTime > 0) {
diff --git a/components/SyncToCloud.tsx b/components/SyncToCloud.tsx
index 8c803a8..59f7c88 100644
--- a/components/SyncToCloud.tsx
+++ b/components/SyncToCloud.tsx
@@ -1,16 +1,12 @@
-import {
- View,
- Text,
- TouchableOpacity,
- Modal,
- ActivityIndicator,
-} from "react-native";
-import React, { useEffect } from "react";
+// No UI imports needed - completely invisible component
+import React, { useEffect, useRef } from "react";
import { supabase } from "@/utils/SupabaseConfig";
import { getUsers, getVideoAnalyticsByUser } from "@/app/database/database";
import { useSQLiteContext } from "expo-sqlite";
import { videoDetails } from "@/assets/details";
-import { useState } from "react";
+import { useNetInfo } from "@react-native-community/netinfo";
+// No useState needed - completely invisible component
+import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
@@ -20,7 +16,6 @@ interface User {
}
interface VideoAnalytics {
- id: number;
name:string;
video_id: number;
english_title?: string;
@@ -35,33 +30,101 @@ interface VideoAnalytics {
const SyncToCloud = () => {
const db = useSQLiteContext();
- const [syncState, setSyncState] = useState<
- "idle" | "inProgress" | "success" | "failure"
- >("idle");
- const [showModal, setShowModal] = useState(false);
- const [errorMessage, setErrorMessage] = useState("");
+ const netInfo = useNetInfo();
+ const wasConnected = useRef(null);
+ // No UI state needed - completely invisible component
+ const syncIntervalRef = useRef(null);
+ const lastSyncTime = useRef(0);
+ const SYNC_DEBOUNCE_TIME = 30000; // 30 seconds minimum between syncs
+
+ // Periodic sync when connected to internet (every 5 minutes)
+ useEffect(() => {
+ if (netInfo.isConnected) {
+ // Set up periodic sync every 5 minutes
+ syncIntervalRef.current = setInterval(() => {
+ console.log("Periodic sync triggered.");
+ fetchUserDetails("periodic");
+ }, 5 * 60 * 1000); // 5 minutes
+ } else {
+ // Clear interval when disconnected
+ if (syncIntervalRef.current) {
+ clearInterval(syncIntervalRef.current);
+ syncIntervalRef.current = null;
+ }
+ }
- // Function to clean error messages
- const cleanErrorMessage = (error: string): string => {
- return error.replace(/(TypeError|Error|SyntaxError|ReferenceError):\s*/gi, '');
- };
+ // Cleanup on unmount
+ return () => {
+ if (syncIntervalRef.current) {
+ clearInterval(syncIntervalRef.current);
+ }
+ };
+ }, [netInfo.isConnected]);
+
+ // Listen for sync triggers from other parts of the app
+ useEffect(() => {
+ const checkSyncTrigger = async () => {
+ try {
+ const triggerValue = await AsyncStorage.getItem('triggerSync');
+ if (triggerValue && netInfo.isConnected) {
+ console.log("Sync triggered by analytics update.");
+ await AsyncStorage.removeItem('triggerSync'); // Clear the trigger
+ fetchUserDetails("analytics_update");
+ }
+ } catch (error) {
+ console.error("Error checking sync trigger:", error);
+ }
+ };
+
+ // Check for sync triggers every 5 seconds (optimized frequency)
+ const interval = setInterval(checkSyncTrigger, 5000);
+
+ return () => clearInterval(interval);
+ }, [netInfo.isConnected]);
+
+ useEffect(() => {
+ // Check if the device just came online
+ if (netInfo.isConnected && wasConnected.current === false) {
+ console.log("Internet connection restored. Starting automatic sync.");
+ fetchUserDetails("connection_restored");
+ }
+ // Also sync when app starts and internet is already available
+ else if (netInfo.isConnected && wasConnected.current === null) {
+ console.log("App started with internet connection. Starting automatic sync.");
+ // Add a small delay to ensure the app is fully initialized
+ setTimeout(() => {
+ fetchUserDetails("app_start");
+ }, 2000);
+ }
+ // Update the previous connection state
+ wasConnected.current = netInfo.isConnected;
+ }, [netInfo.isConnected]);
+
+ // No error message cleaning needed - completely invisible component
+
+ const fetchUserDetails = async (triggeredBy: string = "unknown") => {
+ // Debounce sync to prevent too frequent syncs
+ const now = Date.now();
+ if (now - lastSyncTime.current < SYNC_DEBOUNCE_TIME) {
+ console.log(`Sync debounced. Last sync was ${Math.round((now - lastSyncTime.current) / 1000)}s ago.`);
+ return;
+ }
+
+ console.log(`Sync triggered by: ${triggeredBy}`);
+ lastSyncTime.current = now;
- const fetchUserDetails = async () => {
console.log("Syncing to cloud...");
- setSyncState("inProgress");
- setShowModal(true);
- setErrorMessage("");
try {
const users: User[] = await getUsers(db); // Fetch all users
// Fetch video analytics for each user
- for (const user of users) {
+ await Promise.all(users.map(async (user) => {
const videoAnalytics: VideoAnalytics[] = await getVideoAnalyticsByUser(
db,
user.id
);
- const mergedAnalytics = videoAnalytics.map((item) => {
+ user.video_analytics = videoAnalytics.map((item) => {
const videoDetail =
videoDetails.find((video) => {
return video.id === item.video_id.toString();
@@ -69,87 +132,69 @@ const SyncToCloud = () => {
return { ...item, ...videoDetail };
});
- user.video_analytics = mergedAnalytics;
- }
+ }));
console.log(users);
const syncResult = await syncUsers(users); // Sync users to the cloud
if (syncResult.success) {
console.log("Synced to cloud successfully");
- setSyncState("success");
} else {
console.error("Error syncing to cloud:", syncResult.error);
- setErrorMessage(
- syncResult.error
- ? cleanErrorMessage(syncResult.error)
- : "Unknown error occurred"
- );
- setSyncState("failure");
}
} catch (error) {
console.error("Error fetching user details:", error);
- const errorMsg =
- error instanceof Error ? error.
- message : "Unknown error occurred";
- setErrorMessage(cleanErrorMessage(errorMsg));
- setSyncState("failure");
}
};
async function syncUsers(users: User[]) {
try {
- for (const user of users) {
- // Upsert user
- const { error: userError } = await supabase.from("user").upsert({
- id: user.id,
- user_name: user.user_name,
- pin: user.pin,
- });
+ // 1. Upsert all users in a single batch
+ const userUpsertData = users.map(user => ({
+ id: user.id,
+ user_name: user.user_name,
+ pin: user.pin,
+ }));
+
+ // Fixed table name
+ const { error: usersError } = await supabase.from("users").upsert(userUpsertData);
+
+ if (usersError) {
+ return { success: false, error: `Error syncing users: ${usersError.message}` };
+ }
+
+ // 2. Collect all video analytics from all users
+ const allAnalytics = users.flatMap(user =>
+ user.video_analytics?.map(analytics => {
+ const lastTimestamp = analytics.last_time_stamp
+ ? new Date(analytics.last_time_stamp).toISOString() // Use ISO string for consistency
+ : null;
- if (userError) {
return {
- success: false,
- error: `Error syncing data: ${userError.message}`,
+
+ user_id: user.id,
+ name: user.user_name,
+ video_id: analytics.video_id,
+ english_title: analytics.english_title,
+ punjabi_title: analytics.punjabi_title,
+ level: analytics.level,
+ date: analytics.date,
+ total_views_day: analytics.total_views_day,
+ total_time_day: analytics.total_time_day,
+ last_time_stamp: lastTimestamp,
+ language: analytics.language,
};
- }
-
- // Upsert video analytics
- if (user.video_analytics?.length) {
- for (const analytics of user.video_analytics) {
- const lastTimestamp = analytics.last_time_stamp
- ? new Date(analytics.last_time_stamp).getTime()
- : null;
+ }) ?? []
+ );
- const { error: analyticsError } = await supabase
- .from("video_analytics")
- .upsert(
- [
- {
- user_id: user.id,
- name:user.user_name,
- video_id: analytics.video_id,
- english_title: analytics.english_title,
- punjabi_title: analytics.punjabi_title,
- level: analytics.level,
- date: analytics.date,
- total_views_day: analytics.total_views_day,
- total_time_day: analytics.total_time_day,
- last_time_stamp: lastTimestamp,
- language: analytics.language,
- }
- ],
- {
- onConflict: 'user_id,video_id,date,language',
- });
+ // 3. Upsert all analytics in a single batch
+ if (allAnalytics.length > 0) {
+ const { error: analyticsError } = await supabase
+ .from("video_analytics")
+ .upsert(allAnalytics, { onConflict: 'user_id,video_id,date,language' });
- if (analyticsError) {
- return {
- success: false,
- error: `Error syncing analytics for video ${analytics.video_id}: ${analyticsError.message}`,
- };
- }
- }
+ if (analyticsError) {
+ return { success: false, error: `Error syncing analytics: ${analyticsError.message}` };
}
}
@@ -165,99 +210,10 @@ const SyncToCloud = () => {
}
}
- const closeModal = () => {
- setShowModal(false);
- setSyncState("idle");
- };
-
- return (
-
-
-
- SYNC TO CLOUD
-
-
-
- {/* Modal for Sync Progress */}
-
-
-
- {syncState === "inProgress" && (
- <>
-
-
- Syncing data to cloud...
-
- >
- )}
-
- {syncState === "success" && (
- <>
-
- Success!
-
-
- All data has been successfully synced to the cloud.
-
-
- Close
-
- >
- )}
+ // No modal functions needed - completely invisible component
- {syncState === "failure" && (
- <>
-
- Error
-
-
- {errorMessage ||
- "Failed to sync data. Please check your connection and try again."}
-
-
- Close
-
- >
- )}
-
-
-
-
- );
+ // Completely invisible component - no UI, just background sync
+ return null;
};
-export default SyncToCloud;
+export default SyncToCloud;
\ No newline at end of file
diff --git a/eas.json b/eas.json
index cf587d3..cdb3b30 100644
--- a/eas.json
+++ b/eas.json
@@ -12,10 +12,13 @@
"distribution": "internal"
},
"production": {
- "autoIncrement": true
+ "autoIncrement": true,
+ "android": {
+ "buildType": "apk"
+ }
}
},
"submit": {
"production": {}
}
-}
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7c99c9a..751d032 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/datetimepicker": "8.2.0",
+ "@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@supabase/auth-js": "^2.68.0",
@@ -4392,6 +4393,15 @@
}
}
},
+ "node_modules/@react-native-community/netinfo": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
+ "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-native": ">=0.59"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
diff --git a/package.json b/package.json
index 54bc822..eb382c8 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/datetimepicker": "8.2.0",
+ "@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@supabase/auth-js": "^2.68.0",
@@ -28,6 +29,7 @@
"expo": "~52.0.46",
"expo-application": "~6.0.2",
"expo-asset": "^11.0.4",
+
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.8",
"expo-dev-client": "~5.0.20",
@@ -70,8 +72,12 @@
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"tailwindcss": "^3.4.17",
- "typescript": "^5.3.3",
+
+
+
+ "typescript": "^5.3.3" ,
"expo-av": "~15.0.2"
+
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -89,4 +95,4 @@
"typescript": "^5.3.3"
},
"private": true
-}
+}
\ No newline at end of file
diff --git a/utils/SupabaseConfig.tsx b/utils/SupabaseConfig.tsx
index 32ecae5..737d911 100644
--- a/utils/SupabaseConfig.tsx
+++ b/utils/SupabaseConfig.tsx
@@ -1,4 +1,29 @@
-import { createClient } from '@supabase/supabase-js'
+import { createClient } from '@supabase/supabase-js';
+import AsyncStorage from '@react-native-async-storage/async-storage'; // Recommended for React Native
+
+// Read values from process.env using the names from your .env file
+const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
+const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY; // Corrected variable name
+
+// Add checks to see if variables loaded
+if (!supabaseUrl) {
+ console.error("❌ Supabase URL is missing! Check .env and restart Metro.");
+}
+if (!supabaseAnonKey) {
+ console.error("❌ Supabase Anon Key is missing! Check .env (using EXPO_PUBLIC_SUPABASE_ANON_KEY) and restart Metro.");
+}
+
+// Log the loaded values (the key will just show if it exists)
+console.log('🔧 Supabase URL Loaded:', supabaseUrl);
+console.log('🔧 Supabase Key Loaded:', supabaseAnonKey ? 'Yes' : 'No');
// Create a single supabase client for interacting with your database
-export const supabase = createClient(process.env.EXPO_PUBLIC_SUPABASE_URL as string, process.env.EXPO_PUBLIC_SUPABASE_API_KEY as string)
\ No newline at end of file
+export const supabase = createClient(supabaseUrl!, supabaseAnonKey!, {
+ // Add AsyncStorage for React Native session persistence
+ auth: {
+ storage: AsyncStorage,
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: false,
+ },
+});
\ No newline at end of file