feat: enhance LivePlayer messages with localized text and improve M3U parsing logic

This commit is contained in:
zimplexing
2025-07-21 14:06:44 +08:00
parent 64cdcb78b6
commit e4e4417ef6
6 changed files with 42 additions and 22 deletions

View File

@@ -38,6 +38,7 @@
"android": {
"package": "com.oriontv",
"usesCleartextTraffic": true,
"hardwareAcceleration": true,
"networkSecurityConfig": "@xml/network_security_config",
"icon": "./assets/images/icon.png",
"permissions": [

View File

@@ -106,7 +106,7 @@ export default function LiveScreen() {
<View style={styles.groupColumn}>
<FlatList
data={channelGroups}
keyExtractor={(item) => item}
keyExtractor={(item, index) => `group-${item}-${index}`}
renderItem={({ item }) => (
<StyledButton
text={item}
@@ -124,7 +124,7 @@ export default function LiveScreen() {
) : (
<FlatList
data={groupedChannels[selectedGroup] || []}
keyExtractor={(item) => item.id}
keyExtractor={(item, index) => `${item.id}-${item.group}-${index}`}
renderItem={({ item }) => (
<StyledButton
text={item.name || "Unknown Channel"}
@@ -190,6 +190,8 @@ const styles = StyleSheet.create({
paddingVertical: 8,
paddingHorizontal: 4,
marginVertical: 4,
paddingLeft: 10,
paddingRight: 10,
},
groupButtonText: {
fontSize: 13,
@@ -198,6 +200,8 @@ const styles = StyleSheet.create({
paddingVertical: 6,
paddingHorizontal: 4,
marginVertical: 3,
paddingLeft: 16,
paddingRight: 16,
},
channelItemText: {
fontSize: 12,

View File

@@ -42,13 +42,13 @@ export default function PlayScreen() {
const {
isLoading,
showControls,
showNextEpisodeOverlay,
// showNextEpisodeOverlay,
initialPosition,
introEndTime,
setVideoRef,
handlePlaybackStatusUpdate,
setShowControls,
setShowNextEpisodeOverlay,
// setShowNextEpisodeOverlay,
reset,
loadVideo,
} = usePlayerStore();
@@ -151,7 +151,7 @@ export default function PlayScreen() {
</View>
)}
<NextEpisodeOverlay visible={showNextEpisodeOverlay} onCancel={() => setShowNextEpisodeOverlay(false)} />
{/* <NextEpisodeOverlay visible={showNextEpisodeOverlay} onCancel={() => setShowNextEpisodeOverlay(false)} /> */}
</TouchableOpacity>
<EpisodeSelectionModal />

View File

@@ -66,7 +66,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
if (!streamUrl) {
return (
<View style={styles.container}>
<Text style={styles.messageText}>Select a channel to play.</Text>
<Text style={styles.messageText}></Text>
</View>
);
}
@@ -74,7 +74,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
if (isTimeout) {
return (
<View style={styles.container}>
<Text style={styles.messageText}>Failed to load stream. It might be offline or unavailable.</Text>
<Text style={styles.messageText}></Text>
</View>
);
}
@@ -98,7 +98,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#fff" />
<Text style={styles.messageText}>Loading...</Text>
<Text style={styles.messageText}>...</Text>
</View>
)}
{channelTitle && !isLoading && !isTimeout && (

View File

@@ -2,7 +2,7 @@
"name": "OrionTV",
"private": true,
"main": "expo-router/entry",
"version": "1.2.5",
"version": "1.2.6",
"scripts": {
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",

View File

@@ -15,23 +15,38 @@ export const parseM3U = (m3uText: string): Channel[] => {
for (const line of lines) {
const trimmedLine = line.trim();
currentChannelInfo = { id: '', name: '', url: '', logo: '', group: '' };
if (trimmedLine.startsWith('#EXTINF:')) {
const commaIndex = trimmedLine.indexOf(',');
currentChannelInfo = {}; // Start a new channel
const commaIndex = trimmedLine.lastIndexOf(',');
if (commaIndex !== -1) {
currentChannelInfo.name = trimmedLine.substring(commaIndex + 1).trim();
const attributesPart = trimmedLine.substring(8, commaIndex);
const logoMatch = attributesPart.match(/tvg-logo="([^"]*)"/i);
if (logoMatch && logoMatch[1]) currentChannelInfo.logo = logoMatch[1];
if (logoMatch && logoMatch[1]) {
currentChannelInfo.logo = logoMatch[1];
}
const groupMatch = attributesPart.match(/group-title="([^"]*)"/i);
if (groupMatch && groupMatch[1]) currentChannelInfo.group = groupMatch[1];
if (groupMatch && groupMatch[1]) {
currentChannelInfo.group = groupMatch[1];
}
} else {
currentChannelInfo.name = trimmedLine.substring(8).trim();
}
} else if (currentChannelInfo && trimmedLine && !trimmedLine.startsWith('#') && trimmedLine.includes('://')) {
currentChannelInfo.url = trimmedLine;
currentChannelInfo.id = currentChannelInfo.url; // Use URL as ID
parsedChannels.push(currentChannelInfo as Channel);
// Ensure all required fields are present, providing defaults if necessary
const finalChannel: Channel = {
id: currentChannelInfo.id,
url: currentChannelInfo.url,
name: currentChannelInfo.name || 'Unknown',
logo: currentChannelInfo.logo || '',
group: currentChannelInfo.group || 'Default',
};
parsedChannels.push(finalChannel);
currentChannelInfo = null; // Reset for the next channel
}
}
return parsedChannels;
@@ -56,14 +71,14 @@ export const getPlayableUrl = (originalUrl: string | null): string | null => {
return null;
}
// In React Native, we use the proxy for all http streams to avoid potential issues.
if (originalUrl.toLowerCase().startsWith('http://')) {
// Use the baseURL from the existing api instance.
if (!api.baseURL) {
console.warn("API base URL is not set. Cannot create proxy URL.")
return originalUrl; // Fallback to original URL
}
return `${api.baseURL}/proxy?url=${encodeURIComponent(originalUrl)}`;
}
// if (originalUrl.toLowerCase().startsWith('http://')) {
// // Use the baseURL from the existing api instance.
// if (!api.baseURL) {
// console.warn("API base URL is not set. Cannot create proxy URL.")
// return originalUrl; // Fallback to original URL
// }
// return `${api.baseURL}/proxy?url=${encodeURIComponent(originalUrl)}`;
// }
// HTTPS streams can be played directly.
return originalUrl;
};