Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions example/DownloadExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import React, { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
Alert,
} from 'react-native';
import { TPStreamsDownload } from 'react-native-tpstreams';
import type { DownloadItem } from 'react-native-tpstreams';

interface DownloadExampleProps {
onBack?: () => void;
}

export default function DownloadExample({ onBack }: DownloadExampleProps) {
const [downloads, setDownloads] = useState<DownloadItem[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadDownloads();
}, []);

const loadDownloads = async () => {
try {
setLoading(true);
const downloadItems = await TPStreamsDownload.getDownloads();
setDownloads(downloadItems);
} catch (error) {
Alert.alert('Error', 'Failed to load downloads');
} finally {
setLoading(false);
}
};
Comment on lines +25 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against state updates after unmount

loadDownloads is an async function that sets state after awaiting a promise.
If the component unmounts before the promise resolves (e.g., user quickly navigates away), the setDownloads/setLoading calls will try to update an unmounted component, triggering a memory-leak warning.

Add a simple isMounted ref or an AbortController to ignore the result once unmounted.

+  const isMounted = useRef(true);
+  useEffect(() => {
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);

   const loadDownloads = async () => {
     try {
       setLoading(true);
       const downloadItems = await TPStreamsDownload.getDownloads();
-      setDownloads(downloadItems);
+      if (isMounted.current) setDownloads(downloadItems);
     } catch (error) {
       Alert.alert('Error', 'Failed to load downloads');
     } finally {
-      setLoading(false);
+      if (isMounted.current) setLoading(false);
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const loadDownloads = async () => {
try {
setLoading(true);
const downloadItems = await TPStreamsDownload.getDownloads();
setDownloads(downloadItems);
} catch (error) {
Alert.alert('Error', 'Failed to load downloads');
} finally {
setLoading(false);
}
};
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const loadDownloads = async () => {
try {
setLoading(true);
const downloadItems = await TPStreamsDownload.getDownloads();
if (isMounted.current) setDownloads(downloadItems);
} catch (error) {
Alert.alert('Error', 'Failed to load downloads');
} finally {
if (isMounted.current) setLoading(false);
}
};
🤖 Prompt for AI Agents
In example/DownloadExample.tsx around lines 25 to 35, the async function
loadDownloads updates state after awaiting a promise, which can cause
memory-leak warnings if the component unmounts before the promise resolves. To
fix this, add an isMounted ref or use an AbortController to track the
component's mounted status, and check this status before calling setDownloads
and setLoading to prevent state updates on an unmounted component.


const handlePauseResume = async (item: DownloadItem) => {
try {
if (item.state === 1) {
// DOWNLOADING
await TPStreamsDownload.pauseDownload(item.videoId);
} else if (item.state === 2) {
// PAUSED
await TPStreamsDownload.resumeDownload(item.videoId);
}
loadDownloads();
Comment on lines +37 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace magic numbers with typed enum / constants

Using raw state codes (1, 2, etc.) lowers readability and is brittle if the SDK ever changes.
Expose descriptive constants once and reuse them:

// ideally exported by SDK, otherwise create locally
enum DownloadState {
  Idle = 0,
  Downloading = 1,
  Paused = 2,
  Completed = 3,
  Failed = 4,
}

if (item.state === DownloadState.Downloading) {
  await TPStreamsDownload.pauseDownload(item.videoId);
} else if (item.state === DownloadState.Paused) {
  await TPStreamsDownload.resumeDownload(item.videoId);
}

This also simplifies getStateText.

🤖 Prompt for AI Agents
In example/DownloadExample.tsx around lines 37 to 46, replace the raw numeric
state codes (like 1 and 2) with a typed enum or constants to improve readability
and maintainability. Define a DownloadState enum with descriptive names for each
state (Idle, Downloading, Paused, Completed, Failed) either by importing from
the SDK if available or creating it locally. Then update the conditional checks
to use DownloadState.Downloading and DownloadState.Paused instead of the magic
numbers. Also update any related code like getStateText to use the enum values.

} catch (error) {
Alert.alert('Error', `Failed to pause/resume download`);
}
};

const handleCancel = async (videoId: string) => {
try {
await TPStreamsDownload.cancelDownload(videoId);
loadDownloads();
} catch (error) {
Alert.alert('Error', `Failed to cancel download`);
}
};

const getStateText = (state: number): string => {
switch (state) {
case 0:
return 'Idle';
case 1:
return 'Downloading';
case 2:
return 'Paused';
case 3:
return 'Completed';
case 4:
return 'Failed';
default:
return 'Unknown';
}
};

const renderDownloadItem = ({ item }: { item: DownloadItem }) => {
const isPaused = item.state === 2;
const isDownloading = item.state === 1;

return (
<View style={styles.downloadItem}>
<View style={styles.downloadInfo}>
<Text style={styles.title}>{item.title || 'Untitled'}</Text>
<Text style={styles.status}>Status: {getStateText(item.state)}</Text>
<Text style={styles.progress}>
Progress: {Math.round(item.progressPercentage)}%
</Text>
</View>

<View style={styles.actions}>
{(isDownloading || isPaused) && (
<TouchableOpacity
style={styles.button}
onPress={() => handlePauseResume(item)}
>
<Text style={styles.buttonText}>
{isPaused ? 'Resume' : 'Pause'}
</Text>
</TouchableOpacity>
)}

<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={() => handleCancel(item.videoId)}
>
<Text style={styles.buttonText}>Delete</Text>
</TouchableOpacity>
</View>
</View>
);
};

return (
<View style={styles.container}>
<View style={styles.header}>
{onBack && (
<TouchableOpacity style={styles.backButton} onPress={onBack}>
<Text style={styles.backButtonText}>← Back</Text>
</TouchableOpacity>
)}
<Text style={styles.headerTitle}>Downloads</Text>
</View>

{loading ? (
<Text style={styles.loading}>Loading...</Text>
) : downloads.length === 0 ? (
<Text style={styles.noDownloads}>No downloads found</Text>
) : (
<FlatList
data={downloads}
renderItem={renderDownloadItem}
keyExtractor={(item) => item.videoId}
style={styles.list}
/>
)}

<TouchableOpacity style={styles.refreshButton} onPress={loadDownloads}>
<Text style={styles.buttonText}>Refresh</Text>
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
backButton: {
padding: 8,
marginRight: 8,
},
backButtonText: {
fontSize: 16,
color: '#2196F3',
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
},
loading: {
textAlign: 'center',
marginTop: 20,
},
noDownloads: {
textAlign: 'center',
marginTop: 20,
color: '#666',
},
list: {
flex: 1,
},
downloadItem: {
backgroundColor: 'white',
borderRadius: 8,
padding: 16,
marginBottom: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 1,
elevation: 2,
},
downloadInfo: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
status: {
fontSize: 14,
color: '#666',
marginBottom: 2,
},
progress: {
fontSize: 14,
color: '#666',
},
actions: {
flexDirection: 'row',
},
button: {
backgroundColor: '#2196F3',
borderRadius: 4,
paddingVertical: 6,
paddingHorizontal: 12,
marginLeft: 8,
},
cancelButton: {
backgroundColor: '#F44336',
},
buttonText: {
color: 'white',
fontSize: 14,
fontWeight: '500',
},
refreshButton: {
backgroundColor: '#4CAF50',
borderRadius: 4,
paddingVertical: 12,
alignItems: 'center',
marginTop: 16,
},
});
18 changes: 18 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { useRef, useState } from 'react';
import { View, StyleSheet, Button, Text, ScrollView } from 'react-native';
import { TPStreamsPlayerView } from 'react-native-tpstreams';
import type { TPStreamsPlayerRef } from 'react-native-tpstreams';
import DownloadExample from '../DownloadExample';

export default function App() {
const playerRef = useRef<TPStreamsPlayerRef>(null);
const [lastError, setLastError] = useState<string | null>(null);
const [showDownloads, setShowDownloads] = useState(false);

const handlePlay = () => {
playerRef.current?.play();
Expand Down Expand Up @@ -99,6 +101,10 @@ export default function App() {
setLastError(errorMessage);
};

if (showDownloads) {
return <DownloadExample onBack={() => setShowDownloads(false)} />;
}

return (
<ScrollView style={styles.scrollView}>
<View style={styles.container}>
Expand Down Expand Up @@ -175,6 +181,18 @@ export default function App() {
</View>
</View>
</View>

<View style={styles.buttonSection}>
<Text style={styles.sectionTitle}>Downloads</Text>
<View style={styles.buttonRow}>
<View style={styles.button}>
<Button
title="Show Downloads"
onPress={() => setShowDownloads(true)}
/>
</View>
</View>
</View>
</View>
</ScrollView>
);
Expand Down