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": { "android": {
"package": "com.oriontv", "package": "com.oriontv",
"usesCleartextTraffic": true, "usesCleartextTraffic": true,
"hardwareAcceleration": true,
"networkSecurityConfig": "@xml/network_security_config", "networkSecurityConfig": "@xml/network_security_config",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"permissions": [ "permissions": [

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
if (!streamUrl) { if (!streamUrl) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.messageText}>Select a channel to play.</Text> <Text style={styles.messageText}></Text>
</View> </View>
); );
} }
@@ -74,7 +74,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
if (isTimeout) { if (isTimeout) {
return ( return (
<View style={styles.container}> <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> </View>
); );
} }
@@ -98,7 +98,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
{isLoading && ( {isLoading && (
<View style={styles.loadingOverlay}> <View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#fff" /> <ActivityIndicator size="large" color="#fff" />
<Text style={styles.messageText}>Loading...</Text> <Text style={styles.messageText}>...</Text>
</View> </View>
)} )}
{channelTitle && !isLoading && !isTimeout && ( {channelTitle && !isLoading && !isTimeout && (

View File

@@ -2,7 +2,7 @@
"name": "OrionTV", "name": "OrionTV",
"private": true, "private": true,
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.2.5", "version": "1.2.6",
"scripts": { "scripts": {
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start", "start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
"start-tv": "EXPO_TV=1 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) { for (const line of lines) {
const trimmedLine = line.trim(); const trimmedLine = line.trim();
currentChannelInfo = { id: '', name: '', url: '', logo: '', group: '' };
if (trimmedLine.startsWith('#EXTINF:')) { if (trimmedLine.startsWith('#EXTINF:')) {
const commaIndex = trimmedLine.indexOf(','); currentChannelInfo = {}; // Start a new channel
const commaIndex = trimmedLine.lastIndexOf(',');
if (commaIndex !== -1) { if (commaIndex !== -1) {
currentChannelInfo.name = trimmedLine.substring(commaIndex + 1).trim(); currentChannelInfo.name = trimmedLine.substring(commaIndex + 1).trim();
const attributesPart = trimmedLine.substring(8, commaIndex); const attributesPart = trimmedLine.substring(8, commaIndex);
const logoMatch = attributesPart.match(/tvg-logo="([^"]*)"/i); 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); const groupMatch = attributesPart.match(/group-title="([^"]*)"/i);
if (groupMatch && groupMatch[1]) currentChannelInfo.group = groupMatch[1]; if (groupMatch && groupMatch[1]) {
currentChannelInfo.group = groupMatch[1];
}
} else { } else {
currentChannelInfo.name = trimmedLine.substring(8).trim(); currentChannelInfo.name = trimmedLine.substring(8).trim();
} }
} else if (currentChannelInfo && trimmedLine && !trimmedLine.startsWith('#') && trimmedLine.includes('://')) { } else if (currentChannelInfo && trimmedLine && !trimmedLine.startsWith('#') && trimmedLine.includes('://')) {
currentChannelInfo.url = trimmedLine; currentChannelInfo.url = trimmedLine;
currentChannelInfo.id = currentChannelInfo.url; // Use URL as ID 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; return parsedChannels;
@@ -56,14 +71,14 @@ export const getPlayableUrl = (originalUrl: string | null): string | null => {
return null; return null;
} }
// In React Native, we use the proxy for all http streams to avoid potential issues. // In React Native, we use the proxy for all http streams to avoid potential issues.
if (originalUrl.toLowerCase().startsWith('http://')) { // if (originalUrl.toLowerCase().startsWith('http://')) {
// Use the baseURL from the existing api instance. // // Use the baseURL from the existing api instance.
if (!api.baseURL) { // if (!api.baseURL) {
console.warn("API base URL is not set. Cannot create proxy URL.") // console.warn("API base URL is not set. Cannot create proxy URL.")
return originalUrl; // Fallback to original URL // return originalUrl; // Fallback to original URL
} // }
return `${api.baseURL}/proxy?url=${encodeURIComponent(originalUrl)}`; // return `${api.baseURL}/proxy?url=${encodeURIComponent(originalUrl)}`;
} // }
// HTTPS streams can be played directly. // HTTPS streams can be played directly.
return originalUrl; return originalUrl;
}; };