mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-05-13 02:57:30 +08:00
feat: enhance LivePlayer messages with localized text and improve M3U parsing logic
This commit is contained in:
1
app.json
1
app.json
@@ -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": [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user