parallel streams loading fixed
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 1 MiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 1 MiB |
BIN
assets/images/app-icon.png
Normal file
|
After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 1 MiB |
122
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "nuvio",
|
"name": "nuvio",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/slider": "^4.5.6",
|
"@react-native-community/slider": "^4.5.6",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
"expo": "~52.0.43",
|
"expo": "~52.0.43",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
|
"expo-intent-launcher": "~12.0.2",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
|
|
@ -38,7 +40,8 @@
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "^15.8.0",
|
"react-native-svg": "^15.8.0",
|
||||||
"react-native-video": "^6.12.0"
|
"react-native-video": "^6.12.0",
|
||||||
|
"react-native-web": "~0.19.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|
@ -2650,6 +2653,15 @@
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@expo/metro-runtime": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@expo/osascript": {
|
"node_modules/@expo/osascript": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.1.6.tgz",
|
||||||
|
|
@ -5790,6 +5802,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-in-js-utils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hyphenate-style-name": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||||
|
|
@ -6617,6 +6638,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-intent-launcher": {
|
||||||
|
"version": "12.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-intent-launcher/-/expo-intent-launcher-12.0.2.tgz",
|
||||||
|
"integrity": "sha512-9JYyuuONE9AxoZgRyztcgfAaIIGvrUQAbFnYOPZcrHSBWhbF1203S3ho1Hk67jWhC72Um8+pJ+rXBxAp3ZZlxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-keep-awake": {
|
"node_modules/expo-keep-awake": {
|
||||||
"version": "14.0.3",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
|
||||||
|
|
@ -6802,6 +6832,12 @@
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-loops": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
|
|
@ -7430,6 +7466,12 @@
|
||||||
"node": ">=10.17.0"
|
"node": ">=10.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hyphenate-style-name": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -7537,6 +7579,16 @@
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/inline-style-prefixer": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-in-js-utils": "^3.1.0",
|
||||||
|
"fast-loops": "^1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/internal-ip": {
|
"node_modules/internal-ip": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz",
|
||||||
|
|
@ -10119,6 +10171,12 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||||
|
|
@ -10361,6 +10419,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom/node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-freeze": {
|
"node_modules/react-freeze": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
|
||||||
|
|
@ -10699,6 +10781,38 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-web": {
|
||||||
|
"version": "0.19.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
||||||
|
"integrity": "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.6",
|
||||||
|
"@react-native/normalize-colors": "^0.74.1",
|
||||||
|
"fbjs": "^3.0.4",
|
||||||
|
"inline-style-prefixer": "^6.0.1",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"nullthrows": "^1.1.1",
|
||||||
|
"postcss-value-parser": "^4.2.0",
|
||||||
|
"styleq": "^0.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
|
||||||
|
"version": "0.74.89",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
|
||||||
|
"integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-native-web/node_modules/memoize-one": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
|
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
|
||||||
"version": "0.23.1",
|
"version": "0.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz",
|
||||||
|
|
@ -11765,6 +11879,12 @@
|
||||||
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
|
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/styleq": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,10 @@
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "^15.8.0",
|
"react-native-svg": "^15.8.0",
|
||||||
"react-native-video": "^6.12.0"
|
"react-native-video": "^6.12.0",
|
||||||
|
"expo-intent-launcher": "~12.0.2",
|
||||||
|
"react-native-web": "~0.19.13",
|
||||||
|
"@expo/metro-runtime": "~4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,68 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
const [recommendations, setRecommendations] = useState<StreamingContent[]>([]);
|
const [recommendations, setRecommendations] = useState<StreamingContent[]>([]);
|
||||||
const [loadingRecommendations, setLoadingRecommendations] = useState(false);
|
const [loadingRecommendations, setLoadingRecommendations] = useState(false);
|
||||||
|
|
||||||
const processStreamSource = async (sourceType: string, promise: Promise<any>, isEpisode = false) => {
|
const processStremioSource = async (type: string, id: string, isEpisode = false) => {
|
||||||
|
const sourceStartTime = Date.now();
|
||||||
|
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
||||||
|
const sourceName = 'stremio';
|
||||||
|
|
||||||
|
logger.log(`🔍 [${logPrefix}:${sourceName}] Starting fetch`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stremioService.getStreams(type, id,
|
||||||
|
(streams, addonId, addonName, error) => {
|
||||||
|
const processTime = Date.now() - sourceStartTime;
|
||||||
|
if (error) {
|
||||||
|
logger.error(`❌ [${logPrefix}:${sourceName}] Error for addon ${addonName} (${addonId}):`, error);
|
||||||
|
// Optionally update state to show error for this specific addon?
|
||||||
|
// For now, just log the error.
|
||||||
|
} else if (streams && addonId && addonName) {
|
||||||
|
logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`);
|
||||||
|
if (streams.length > 0) {
|
||||||
|
const streamsWithAddon = streams.map(stream => ({
|
||||||
|
...stream,
|
||||||
|
name: stream.name || stream.title || 'Unnamed Stream',
|
||||||
|
addonId: addonId,
|
||||||
|
addonName: addonName
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateState = (prevState: GroupedStreams): GroupedStreams => {
|
||||||
|
logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`);
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
[addonId]: {
|
||||||
|
addonName: addonName,
|
||||||
|
streams: streamsWithAddon
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEpisode) {
|
||||||
|
setEpisodeStreams(updateState);
|
||||||
|
} else {
|
||||||
|
setGroupedStreams(updateState);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log(`🤷 [${logPrefix}:${sourceName}] No streams found for addon ${addonName} (${addonId})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle case where callback provides null streams without error (e.g., empty results)
|
||||||
|
logger.log(`🏁 [${logPrefix}:${sourceName}] Finished fetching for addon ${addonName} (${addonId}) with no streams after ${processTime}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// The function now returns void, just await to let callbacks fire
|
||||||
|
logger.log(`🏁 [${logPrefix}:${sourceName}] Stremio fetching process initiated`);
|
||||||
|
} catch (error) {
|
||||||
|
// Catch errors from the initial call to getStreams (e.g., initialization errors)
|
||||||
|
logger.error(`❌ [${logPrefix}:${sourceName}] Initial call failed:`, error);
|
||||||
|
// Maybe update state to show a general Stremio error?
|
||||||
|
}
|
||||||
|
// Note: This function completes when getStreams returns, not when all callbacks have fired.
|
||||||
|
// Loading indicators should probably be managed based on callbacks completing.
|
||||||
|
};
|
||||||
|
|
||||||
|
const processExternalSource = async (sourceType: string, promise: Promise<any>, isEpisode = false) => {
|
||||||
const sourceStartTime = Date.now();
|
const sourceStartTime = Date.now();
|
||||||
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
||||||
|
|
||||||
|
|
@ -120,35 +181,26 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
logger.log(`✅ [${logPrefix}:${sourceType}] Completed in ${Date.now() - sourceStartTime}ms`);
|
logger.log(`✅ [${logPrefix}:${sourceType}] Completed in ${Date.now() - sourceStartTime}ms`);
|
||||||
|
|
||||||
// If we have results, update immediately
|
|
||||||
if (Object.keys(result).length > 0) {
|
if (Object.keys(result).length > 0) {
|
||||||
// Calculate total streams for logging
|
const totalStreams = Object.values(result).reduce((acc, group: any) => acc + (group.streams?.length || 0), 0);
|
||||||
const totalStreams = Object.values(result).reduce((acc, group: any) => {
|
|
||||||
return acc + (group.streams?.length || 0);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
logger.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
|
logger.log(`📦 [${logPrefix}:${sourceType}] Found ${totalStreams} streams`);
|
||||||
|
|
||||||
// Update state for this source
|
const updateState = (prevState: GroupedStreams) => {
|
||||||
|
logger.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`);
|
||||||
|
return { ...prevState, ...result };
|
||||||
|
};
|
||||||
|
|
||||||
if (isEpisode) {
|
if (isEpisode) {
|
||||||
setEpisodeStreams(prev => {
|
setEpisodeStreams(updateState);
|
||||||
const newState = {...prev, ...result};
|
|
||||||
console.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`);
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setGroupedStreams(prev => {
|
setGroupedStreams(updateState);
|
||||||
const newState = {...prev, ...result};
|
|
||||||
console.log(`🔄 [${logPrefix}:${sourceType}] Updating state with ${Object.keys(result).length} providers`);
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`⚠️ [${logPrefix}:${sourceType}] No streams found`);
|
logger.log(`⚠️ [${logPrefix}:${sourceType}] No streams found`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ [${logPrefix}:${sourceType}] Error:`, error);
|
logger.error(`❌ [${logPrefix}:${sourceType}] Error:`, error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -346,43 +398,17 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
console.log('ℹ️ [loadStreams] Using ID as TMDB ID:', tmdbId);
|
console.log('ℹ️ [loadStreams] Using ID as TMDB ID:', tmdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔄 [loadStreams] Starting parallel stream requests');
|
console.log('🔄 [loadStreams] Starting stream requests');
|
||||||
|
|
||||||
// Create an array to store all fetching promises
|
|
||||||
const fetchPromises = [];
|
const fetchPromises = [];
|
||||||
|
|
||||||
// Start Stremio request
|
// Start Stremio request using the new callback method
|
||||||
const stremioPromise = processStreamSource('stremio', (async () => {
|
// We don't push this promise anymore, as results are handled by callback
|
||||||
const newGroupedStreams: GroupedStreams = {};
|
processStremioSource(type, id, false);
|
||||||
try {
|
|
||||||
const responses = await stremioService.getStreams(type, id);
|
|
||||||
responses.forEach(response => {
|
|
||||||
const addonId = response.addon;
|
|
||||||
if (addonId && response.streams.length > 0) {
|
|
||||||
const streamsWithAddon = response.streams.map(stream => ({
|
|
||||||
...stream,
|
|
||||||
name: stream.name || stream.title || 'Unnamed Stream',
|
|
||||||
addonId: response.addon,
|
|
||||||
addonName: response.addonName
|
|
||||||
}));
|
|
||||||
|
|
||||||
newGroupedStreams[addonId] = {
|
|
||||||
addonName: response.addonName,
|
|
||||||
streams: streamsWithAddon
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newGroupedStreams;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ [loadStreams:stremio] Error fetching Stremio streams:', error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
})(), false);
|
|
||||||
fetchPromises.push(stremioPromise);
|
|
||||||
|
|
||||||
// Start Source 1 request if we have a TMDB ID
|
// Start Source 1 request if we have a TMDB ID
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
const source1Promise = processStreamSource('source1', (async () => {
|
const source1Promise = processExternalSource('source1', (async () => {
|
||||||
try {
|
try {
|
||||||
const streams = await fetchExternalStreams(
|
const streams = await fetchExternalStreams(
|
||||||
`https://nice-month-production.up.railway.app/embedsu/${tmdbId}`,
|
`https://nice-month-production.up.railway.app/embedsu/${tmdbId}`,
|
||||||
|
|
@ -408,7 +434,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
|
|
||||||
// Start Source 2 request if we have a TMDB ID
|
// Start Source 2 request if we have a TMDB ID
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
const source2Promise = processStreamSource('source2', (async () => {
|
const source2Promise = processExternalSource('source2', (async () => {
|
||||||
try {
|
try {
|
||||||
const streams = await fetchExternalStreams(
|
const streams = await fetchExternalStreams(
|
||||||
`https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}`,
|
`https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}`,
|
||||||
|
|
@ -432,12 +458,12 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
fetchPromises.push(source2Promise);
|
fetchPromises.push(source2Promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all promises to complete - but we already showed results as they arrived
|
// Wait only for external promises now
|
||||||
const results = await Promise.allSettled(fetchPromises);
|
const results = await Promise.allSettled(fetchPromises);
|
||||||
const totalTime = Date.now() - startTime;
|
const totalTime = Date.now() - startTime;
|
||||||
console.log(`✅ [loadStreams] All requests completed in ${totalTime}ms`);
|
console.log(`✅ [loadStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`);
|
||||||
|
|
||||||
const sourceTypes = ['stremio', 'source1', 'source2'];
|
const sourceTypes = ['source1', 'source2']; // Removed 'stremio'
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
|
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
|
||||||
console.log(`📊 [loadStreams:${source}] Status: ${result.status}`);
|
console.log(`📊 [loadStreams:${source}] Status: ${result.status}`);
|
||||||
|
|
@ -447,18 +473,19 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🧮 [loadStreams] Summary:');
|
console.log('🧮 [loadStreams] Summary:');
|
||||||
console.log(' Total time:', totalTime + 'ms');
|
console.log(' Total time for external sources:', totalTime + 'ms');
|
||||||
|
|
||||||
// Log the final states
|
// Log the final states - this might not include all Stremio addons yet
|
||||||
console.log('📦 [loadStreams] Final streams count:',
|
console.log('📦 [loadStreams] Current combined streams count:',
|
||||||
Object.keys(groupedStreams).length > 0 ?
|
Object.keys(groupedStreams).length > 0 ?
|
||||||
Object.values(groupedStreams).reduce((acc, group: any) => acc + group.streams.length, 0) :
|
Object.values(groupedStreams).reduce((acc, group: any) => acc + group.streams.length, 0) :
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cache the final streams state
|
// Cache the final streams state - Note: This might be incomplete if Stremio addons are slow
|
||||||
setGroupedStreams(prev => {
|
setGroupedStreams(prev => {
|
||||||
cacheService.setStreams(id, type, prev);
|
// We might want to reconsider when exactly to cache or mark loading as fully complete
|
||||||
|
// cacheService.setStreams(id, type, prev); // Maybe cache incrementally in callback?
|
||||||
setPreloadedStreams(prev);
|
setPreloadedStreams(prev);
|
||||||
return prev;
|
return prev;
|
||||||
});
|
});
|
||||||
|
|
@ -467,9 +494,11 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
console.error('❌ [loadStreams] Failed to load streams:', error);
|
console.error('❌ [loadStreams] Failed to load streams:', error);
|
||||||
setError('Failed to load streams');
|
setError('Failed to load streams');
|
||||||
} finally {
|
} finally {
|
||||||
|
// Loading is now complete when external sources finish, Stremio updates happen independently.
|
||||||
|
// We need a better way to track overall completion if we want a final 'FINISHED' log.
|
||||||
const endTime = Date.now() - startTime;
|
const endTime = Date.now() - startTime;
|
||||||
console.log(`🏁 [loadStreams] FINISHED in ${endTime}ms`);
|
console.log(`🏁 [loadStreams] External sources FINISHED in ${endTime}ms`);
|
||||||
setLoadingStreams(false);
|
setLoadingStreams(false); // Mark loading=false, but Stremio might still be working
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -500,43 +529,17 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
const episodeQuery = `?s=${season}&e=${episode}`;
|
const episodeQuery = `?s=${season}&e=${episode}`;
|
||||||
console.log(`ℹ️ [loadEpisodeStreams] Episode query: ${episodeQuery}`);
|
console.log(`ℹ️ [loadEpisodeStreams] Episode query: ${episodeQuery}`);
|
||||||
|
|
||||||
console.log('🔄 [loadEpisodeStreams] Starting parallel stream requests');
|
console.log('🔄 [loadEpisodeStreams] Starting stream requests');
|
||||||
|
|
||||||
// Create an array to store all fetching promises
|
|
||||||
const fetchPromises = [];
|
const fetchPromises = [];
|
||||||
|
|
||||||
// Start Stremio request
|
// Start Stremio request using the new callback method
|
||||||
const stremioPromise = processStreamSource('stremio', (async () => {
|
// We don't push this promise anymore
|
||||||
const newGroupedStreams: GroupedStreams = {};
|
processStremioSource('series', episodeId, true);
|
||||||
try {
|
|
||||||
const responses = await stremioService.getStreams('series', episodeId);
|
|
||||||
responses.forEach(response => {
|
|
||||||
const addonId = response.addon;
|
|
||||||
if (addonId && response.streams.length > 0) {
|
|
||||||
const streamsWithAddon = response.streams.map(stream => ({
|
|
||||||
...stream,
|
|
||||||
name: stream.name || stream.title || 'Unnamed Stream',
|
|
||||||
addonId: response.addon,
|
|
||||||
addonName: response.addonName
|
|
||||||
}));
|
|
||||||
|
|
||||||
newGroupedStreams[addonId] = {
|
|
||||||
addonName: response.addonName,
|
|
||||||
streams: streamsWithAddon
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newGroupedStreams;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ [loadEpisodeStreams:stremio] Error fetching Stremio streams:', error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
})(), true);
|
|
||||||
fetchPromises.push(stremioPromise);
|
|
||||||
|
|
||||||
// Start Source 1 request if we have a TMDB ID
|
// Start Source 1 request if we have a TMDB ID
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
const source1Promise = processStreamSource('source1', (async () => {
|
const source1Promise = processExternalSource('source1', (async () => {
|
||||||
try {
|
try {
|
||||||
const streams = await fetchExternalStreams(
|
const streams = await fetchExternalStreams(
|
||||||
`https://nice-month-production.up.railway.app/embedsu/${tmdbId}${episodeQuery}`,
|
`https://nice-month-production.up.railway.app/embedsu/${tmdbId}${episodeQuery}`,
|
||||||
|
|
@ -563,7 +566,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
|
|
||||||
// Start Source 2 request if we have a TMDB ID
|
// Start Source 2 request if we have a TMDB ID
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
const source2Promise = processStreamSource('source2', (async () => {
|
const source2Promise = processExternalSource('source2', (async () => {
|
||||||
try {
|
try {
|
||||||
const streams = await fetchExternalStreams(
|
const streams = await fetchExternalStreams(
|
||||||
`https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}${episodeQuery}`,
|
`https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}${episodeQuery}`,
|
||||||
|
|
@ -588,12 +591,12 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
fetchPromises.push(source2Promise);
|
fetchPromises.push(source2Promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all promises to complete - but we already showed results as they arrived
|
// Wait only for external promises now
|
||||||
const results = await Promise.allSettled(fetchPromises);
|
const results = await Promise.allSettled(fetchPromises);
|
||||||
const totalTime = Date.now() - startTime;
|
const totalTime = Date.now() - startTime;
|
||||||
console.log(`✅ [loadEpisodeStreams] All requests completed in ${totalTime}ms`);
|
console.log(`✅ [loadEpisodeStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`);
|
||||||
|
|
||||||
const sourceTypes = ['stremio', 'source1', 'source2'];
|
const sourceTypes = ['source1', 'source2']; // Removed 'stremio'
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
|
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
|
||||||
console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`);
|
console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`);
|
||||||
|
|
@ -603,18 +606,18 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🧮 [loadEpisodeStreams] Summary:');
|
console.log('🧮 [loadEpisodeStreams] Summary:');
|
||||||
console.log(' Total time:', totalTime + 'ms');
|
console.log(' Total time for external sources:', totalTime + 'ms');
|
||||||
|
|
||||||
// Log the final states
|
// Log the final states - might not include all Stremio addons yet
|
||||||
console.log('📦 [loadEpisodeStreams] Final streams count:',
|
console.log('📦 [loadEpisodeStreams] Current combined streams count:',
|
||||||
Object.keys(episodeStreams).length > 0 ?
|
Object.keys(episodeStreams).length > 0 ?
|
||||||
Object.values(episodeStreams).reduce((acc, group: any) => acc + group.streams.length, 0) :
|
Object.values(episodeStreams).reduce((acc, group: any) => acc + group.streams.length, 0) :
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cache the final streams state
|
// Cache the final streams state - Might be incomplete
|
||||||
setEpisodeStreams(prev => {
|
setEpisodeStreams(prev => {
|
||||||
// Cache episode streams
|
// Cache episode streams - maybe incrementally?
|
||||||
setPreloadedEpisodeStreams(currentPreloaded => ({
|
setPreloadedEpisodeStreams(currentPreloaded => ({
|
||||||
...currentPreloaded,
|
...currentPreloaded,
|
||||||
[episodeId]: prev
|
[episodeId]: prev
|
||||||
|
|
@ -626,9 +629,10 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
||||||
console.error('❌ [loadEpisodeStreams] Failed to load episode streams:', error);
|
console.error('❌ [loadEpisodeStreams] Failed to load episode streams:', error);
|
||||||
setError('Failed to load episode streams');
|
setError('Failed to load episode streams');
|
||||||
} finally {
|
} finally {
|
||||||
const totalTime = Date.now() - startTime;
|
// Loading is now complete when external sources finish
|
||||||
console.log(`🏁 [loadEpisodeStreams] FINISHED in ${totalTime}ms`);
|
const endTime = Date.now() - startTime;
|
||||||
setLoadingEpisodeStreams(false);
|
console.log(`🏁 [loadEpisodeStreams] External sources FINISHED in ${endTime}ms`);
|
||||||
|
setLoadingEpisodeStreams(false); // Mark loading=false, but Stremio might still be working
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ import {
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
import { RouteProp } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { Meta, stremioService } from '../services/stremioService';
|
import { Meta, stremioService } from '../services/stremioService';
|
||||||
|
|
@ -25,9 +24,20 @@ type CatalogScreenProps = {
|
||||||
navigation: StackNavigationProp<RootStackParamList, 'Catalog'>;
|
navigation: StackNavigationProp<RootStackParamList, 'Catalog'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Consistent spacing variables
|
||||||
|
const SPACING = {
|
||||||
|
xs: 4,
|
||||||
|
sm: 8,
|
||||||
|
md: 12,
|
||||||
|
lg: 16,
|
||||||
|
xl: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Screen dimensions and grid layout
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const NUM_COLUMNS = 3;
|
const NUM_COLUMNS = 3;
|
||||||
const ITEM_WIDTH = width / NUM_COLUMNS - 20;
|
const ITEM_MARGIN = SPACING.sm;
|
||||||
|
const ITEM_WIDTH = (width - (SPACING.md * 2) - (ITEM_MARGIN * 2 * NUM_COLUMNS)) / NUM_COLUMNS;
|
||||||
|
|
||||||
const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
const { addonId, type, id, name, genreFilter } = route.params;
|
const { addonId, type, id, name, genreFilter } = route.params;
|
||||||
|
|
@ -172,58 +182,85 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.item}
|
style={styles.item}
|
||||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||||
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image' }}
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image' }}
|
||||||
style={styles.poster}
|
style={styles.poster}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
|
transition={200}
|
||||||
/>
|
/>
|
||||||
<Text
|
<View style={styles.itemContent}>
|
||||||
style={styles.title}
|
|
||||||
numberOfLines={2}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
{item.releaseInfo && (
|
|
||||||
<Text
|
<Text
|
||||||
style={styles.releaseInfo}
|
style={styles.title}
|
||||||
|
numberOfLines={2}
|
||||||
>
|
>
|
||||||
{item.releaseInfo}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
{item.releaseInfo && (
|
||||||
|
<Text style={styles.releaseInfo}>
|
||||||
|
{item.releaseInfo}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
const renderEmptyState = () => (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
No content found for the selected genre
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleRefresh}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Try Again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderErrorState = () => (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<Text style={styles.errorText}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={() => loadItems(1)}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Retry</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderLoadingState = () => (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
if (loading && items.length === 0) {
|
if (loading && items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, styles.centered]}>
|
<SafeAreaView style={styles.container}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
||||||
</View>
|
{renderLoadingState()}
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && items.length === 0) {
|
if (error && items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, styles.centered]}>
|
<SafeAreaView style={styles.container}>
|
||||||
<Text style={{ color: colors.white }}>
|
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
||||||
{error}
|
{renderErrorState()}
|
||||||
</Text>
|
</SafeAreaView>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.retryButton}
|
|
||||||
onPress={() => loadItems(1)}
|
|
||||||
>
|
|
||||||
<Text style={styles.retryText}>Retry</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[
|
<SafeAreaView style={styles.container}>
|
||||||
styles.container,
|
|
||||||
{ backgroundColor: colors.darkBackground }
|
|
||||||
]}>
|
|
||||||
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
@ -249,20 +286,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
contentContainerStyle={styles.list}
|
contentContainerStyle={styles.list}
|
||||||
|
columnWrapperStyle={styles.columnWrapper}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : renderEmptyState()}
|
||||||
<View style={styles.centered}>
|
|
||||||
<Text style={{ color: colors.white, fontSize: 16, marginBottom: 10 }}>
|
|
||||||
No content found for the selected genre
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.retryButton}
|
|
||||||
onPress={handleRefresh}
|
|
||||||
>
|
|
||||||
<Text style={styles.retryText}>Try Again</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -272,59 +298,73 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: colors.darkBackground,
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
padding: 16,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: colors.white,
|
|
||||||
},
|
|
||||||
list: {
|
list: {
|
||||||
padding: 10,
|
padding: SPACING.md,
|
||||||
|
},
|
||||||
|
columnWrapper: {
|
||||||
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
width: ITEM_WIDTH,
|
width: ITEM_WIDTH,
|
||||||
margin: 5,
|
marginBottom: SPACING.md,
|
||||||
|
borderRadius: 8,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
poster: {
|
poster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
aspectRatio: 2/3,
|
aspectRatio: 2/3,
|
||||||
borderRadius: 4,
|
borderRadius: 8,
|
||||||
backgroundColor: colors.transparentLight,
|
backgroundColor: colors.transparentLight,
|
||||||
},
|
},
|
||||||
|
itemContent: {
|
||||||
|
padding: SPACING.xs,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
marginTop: 5,
|
marginTop: SPACING.xs,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
releaseInfo: {
|
releaseInfo: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginTop: 2,
|
marginTop: SPACING.xs,
|
||||||
color: colors.lightGray,
|
color: colors.lightGray,
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
padding: 20,
|
padding: SPACING.lg,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
retryButton: {
|
button: {
|
||||||
marginTop: 15,
|
marginTop: SPACING.md,
|
||||||
padding: 10,
|
paddingVertical: SPACING.md,
|
||||||
|
paddingHorizontal: SPACING.xl,
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 5,
|
borderRadius: 8,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
retryText: {
|
buttonText: {
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
centered: {
|
centered: {
|
||||||
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.darkBackground,
|
padding: SPACING.xl,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: SPACING.md,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: SPACING.md,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -770,7 +770,12 @@ const HomeScreen = () => {
|
||||||
<View style={styles.catalogHeader}>
|
<View style={styles.catalogHeader}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.catalogTitle}>{item.name}</Text>
|
<Text style={styles.catalogTitle}>{item.name}</Text>
|
||||||
<View style={styles.titleUnderline} />
|
<LinearGradient
|
||||||
|
colors={[colors.primary, colors.secondary]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={styles.titleUnderline}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
|
|
@ -1041,9 +1046,8 @@ const styles = StyleSheet.create<any>({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: -4,
|
bottom: -4,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: 40,
|
width: 60,
|
||||||
height: 3,
|
height: 3,
|
||||||
backgroundColor: colors.primary,
|
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
},
|
},
|
||||||
seeAllButton: {
|
seeAllButton: {
|
||||||
|
|
|
||||||
|
|
@ -1123,22 +1123,17 @@ const styles = StyleSheet.create({
|
||||||
ratingContainer: {
|
ratingContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.elevation3,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.1)',
|
|
||||||
},
|
},
|
||||||
imdbLogo: {
|
imdbLogo: {
|
||||||
width: 40,
|
width: 35,
|
||||||
height: 20,
|
height: 18,
|
||||||
marginRight: 6,
|
marginRight: 4,
|
||||||
},
|
},
|
||||||
ratingText: {
|
ratingText: {
|
||||||
color: '#fff',
|
color: colors.text,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
fontSize: 13,
|
fontSize: 15,
|
||||||
|
letterSpacing: 0.3,
|
||||||
},
|
},
|
||||||
descriptionContainer: {
|
descriptionContainer: {
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
|
|
|
||||||
|
|
@ -1,357 +0,0 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
View,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
Text,
|
|
||||||
ActivityIndicator,
|
|
||||||
StatusBar,
|
|
||||||
Dimensions,
|
|
||||||
Platform,
|
|
||||||
SafeAreaView
|
|
||||||
} from 'react-native';
|
|
||||||
import Video from 'react-native-video';
|
|
||||||
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
|
|
||||||
interface PlayerParams {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
title?: string;
|
|
||||||
poster?: string;
|
|
||||||
stream?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PlayerScreen = () => {
|
|
||||||
const route = useRoute<RouteProp<Record<string, PlayerParams>, string>>();
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const { id, type, title, poster, stream } = route.params;
|
|
||||||
|
|
||||||
const [isPlaying, setIsPlaying] = useState(true);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
|
||||||
const [duration, setDuration] = useState(0);
|
|
||||||
const [controlsVisible, setControlsVisible] = useState(true);
|
|
||||||
|
|
||||||
// Use any for now to fix the type error
|
|
||||||
const videoRef = useRef<any>(null);
|
|
||||||
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
// Auto-hide controls after a delay
|
|
||||||
useEffect(() => {
|
|
||||||
if (controlsVisible) {
|
|
||||||
if (controlsTimeoutRef.current) {
|
|
||||||
clearTimeout(controlsTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
controlsTimeoutRef.current = setTimeout(() => {
|
|
||||||
setControlsVisible(false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (controlsTimeoutRef.current) {
|
|
||||||
clearTimeout(controlsTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [controlsVisible]);
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => setIsPlaying(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleControls = () => {
|
|
||||||
setControlsVisible(!controlsVisible);
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePlayPause = () => {
|
|
||||||
setIsPlaying(!isPlaying);
|
|
||||||
};
|
|
||||||
|
|
||||||
const seekTo = (time: number) => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.seek(time);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (seconds: number): string => {
|
|
||||||
const h = Math.floor(seconds / 3600);
|
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
|
||||||
const s = Math.floor(seconds % 60);
|
|
||||||
|
|
||||||
let result = '';
|
|
||||||
if (h > 0) {
|
|
||||||
result += `${h}:${m < 10 ? '0' : ''}`;
|
|
||||||
}
|
|
||||||
result += `${m}:${s < 10 ? '0' : ''}${s}`;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoad = (data: any) => {
|
|
||||||
setDuration(data.duration);
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProgress = (data: any) => {
|
|
||||||
setCurrentTime(data.currentTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnd = () => {
|
|
||||||
setIsPlaying(false);
|
|
||||||
setCurrentTime(duration);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (err: any) => {
|
|
||||||
setError(err.error?.errorString || 'Failed to load video');
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GoBackButton = () => (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.backButton}
|
|
||||||
onPress={() => navigation.goBack()}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="arrow-back" size={24} color="#FFFFFF" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Controls = () => (
|
|
||||||
<View style={styles.controlsContainer}>
|
|
||||||
<View style={styles.controlsHeader}>
|
|
||||||
<GoBackButton />
|
|
||||||
<Text style={styles.videoTitle}>{title || 'Video Player'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.controlsCenter}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.playPauseButton}
|
|
||||||
onPress={togglePlayPause}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name={isPlaying ? 'pause' : 'play-arrow'}
|
|
||||||
size={48}
|
|
||||||
color="#FFFFFF"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.controlsBottom}>
|
|
||||||
<Text style={styles.timeText}>{formatTime(currentTime)}</Text>
|
|
||||||
|
|
||||||
<View style={styles.progressBarContainer}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.progressBar,
|
|
||||||
{ width: `${(currentTime / duration) * 100}%` }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.progressKnob,
|
|
||||||
{ left: `${(currentTime / duration) * 100}%` }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.timeText}>{formatTime(duration)}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!stream) {
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container}>
|
|
||||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
|
||||||
<GoBackButton />
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<MaterialIcons name="error-outline" size={64} color="#E50914" />
|
|
||||||
<Text style={styles.errorText}>No stream URL provided</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.errorButton}
|
|
||||||
onPress={() => navigation.goBack()}
|
|
||||||
>
|
|
||||||
<Text style={styles.errorButtonText}>Go Back</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.videoContainer}
|
|
||||||
activeOpacity={1}
|
|
||||||
onPress={toggleControls}
|
|
||||||
>
|
|
||||||
<Video
|
|
||||||
ref={videoRef}
|
|
||||||
source={{ uri: stream }}
|
|
||||||
style={styles.video}
|
|
||||||
resizeMode="contain"
|
|
||||||
poster={poster}
|
|
||||||
paused={!isPlaying}
|
|
||||||
onLoad={handleLoad}
|
|
||||||
onProgress={handleProgress}
|
|
||||||
onEnd={handleEnd}
|
|
||||||
onError={handleError}
|
|
||||||
repeat={false}
|
|
||||||
playInBackground={false}
|
|
||||||
playWhenInactive={false}
|
|
||||||
ignoreSilentSwitch="ignore"
|
|
||||||
fullscreen={false}
|
|
||||||
progressUpdateInterval={500}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<View style={styles.loaderContainer}>
|
|
||||||
<ActivityIndicator size="large" color="#E50914" />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<MaterialIcons name="error-outline" size={64} color="#E50914" />
|
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.errorButton}
|
|
||||||
onPress={() => navigation.goBack()}
|
|
||||||
>
|
|
||||||
<Text style={styles.errorButtonText}>Go Back</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{controlsVisible && !error && <Controls />}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#000000',
|
|
||||||
},
|
|
||||||
videoContainer: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
video: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
},
|
|
||||||
controlsContainer: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
controlsHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 16,
|
|
||||||
paddingTop: Platform.OS === 'ios' ? 50 : 40,
|
|
||||||
},
|
|
||||||
backButton: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 16,
|
|
||||||
},
|
|
||||||
videoTitle: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
controlsCenter: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
playPauseButton: {
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 40,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
controlsBottom: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 16,
|
|
||||||
paddingBottom: Platform.OS === 'ios' ? 40 : 16,
|
|
||||||
},
|
|
||||||
timeText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 14,
|
|
||||||
width: 50,
|
|
||||||
},
|
|
||||||
progressBarContainer: {
|
|
||||||
flex: 1,
|
|
||||||
height: 4,
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.3)',
|
|
||||||
marginHorizontal: 8,
|
|
||||||
borderRadius: 2,
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
progressBar: {
|
|
||||||
height: '100%',
|
|
||||||
backgroundColor: '#E50914',
|
|
||||||
borderRadius: 2,
|
|
||||||
},
|
|
||||||
progressKnob: {
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
borderRadius: 6,
|
|
||||||
backgroundColor: '#E50914',
|
|
||||||
position: 'absolute',
|
|
||||||
top: -4,
|
|
||||||
marginLeft: -6,
|
|
||||||
},
|
|
||||||
loaderContainer: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
},
|
|
||||||
errorContainer: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 16,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
errorButton: {
|
|
||||||
backgroundColor: '#E50914',
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
errorButtonText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default PlayerScreen;
|
|
||||||
|
|
@ -20,6 +20,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { colors } from '../styles/colors';
|
import { colors } from '../styles/colors';
|
||||||
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { stremioService } from '../services/stremioService';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -223,6 +224,66 @@ const SettingsScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
onPress={() => navigation.navigate('Addons')}
|
onPress={() => navigation.navigate('Addons')}
|
||||||
/>
|
/>
|
||||||
|
<SettingItem
|
||||||
|
title="Check TMDB Addon"
|
||||||
|
description="Verify TMDB Embed Streams addon installation"
|
||||||
|
icon="bug-report"
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
renderControl={() => (
|
||||||
|
<View style={[styles.actionButton, { backgroundColor: colors.primary }]}>
|
||||||
|
<Text style={styles.actionButtonText}>Check</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
onPress={() => {
|
||||||
|
// Check if the addon is installed
|
||||||
|
const installedAddons = stremioService.getInstalledAddons();
|
||||||
|
const tmdbAddon = installedAddons.find(addon => addon.id === 'org.tmdbembedapi');
|
||||||
|
|
||||||
|
if (tmdbAddon) {
|
||||||
|
// Addon is installed, check its configuration
|
||||||
|
Alert.alert(
|
||||||
|
'TMDB Embed Streams Addon',
|
||||||
|
`Addon is installed:\n\nName: ${tmdbAddon.name}\nID: ${tmdbAddon.id}\nURL: ${tmdbAddon.url}\n\nResources: ${JSON.stringify(tmdbAddon.resources)}\n\nTypes: ${JSON.stringify(tmdbAddon.types)}`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Reinstall',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
// Remove and reinstall the addon
|
||||||
|
stremioService.removeAddon('org.tmdbembedapi');
|
||||||
|
await stremioService.installAddon('https://http-addon-production.up.railway.app/manifest.json');
|
||||||
|
Alert.alert('Success', 'Addon was reinstalled successfully');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', `Failed to reinstall addon: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ text: 'Close', style: 'cancel' }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Addon is not installed, offer to install it
|
||||||
|
Alert.alert(
|
||||||
|
'TMDB Embed Streams Addon',
|
||||||
|
'Addon is not installed. Would you like to install it now?',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Install',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
await stremioService.installAddon('https://http-addon-production.up.railway.app/manifest.json');
|
||||||
|
Alert.alert('Success', 'Addon was installed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', `Failed to install addon: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ text: 'Cancel', style: 'cancel' }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Reset All Settings"
|
title="Reset All Settings"
|
||||||
description="Restore default settings"
|
description="Restore default settings"
|
||||||
|
|
|
||||||
|
|
@ -69,25 +69,6 @@ const StreamCard = memo(({ stream, onPress, index, torrentProgress, isLoading, s
|
||||||
const displayTitle = stream.name || stream.title || 'Unnamed Stream';
|
const displayTitle = stream.name || stream.title || 'Unnamed Stream';
|
||||||
const displayAddonName = stream.title || '';
|
const displayAddonName = stream.title || '';
|
||||||
|
|
||||||
const entering = useMemo(() =>
|
|
||||||
FadeInDown
|
|
||||||
.delay(50 + Math.min(index, 10) * 30)
|
|
||||||
.springify()
|
|
||||||
.damping(15)
|
|
||||||
.mass(0.9)
|
|
||||||
, [index]);
|
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
|
||||||
logger.log('StreamCard pressed:', {
|
|
||||||
isTorrent,
|
|
||||||
isDebrid,
|
|
||||||
hasProgress: !!torrentProgress,
|
|
||||||
url: stream.url,
|
|
||||||
behaviorHints: stream.behaviorHints
|
|
||||||
});
|
|
||||||
onPress();
|
|
||||||
}, [isTorrent, isDebrid, torrentProgress, stream.url, stream.behaviorHints, onPress]);
|
|
||||||
|
|
||||||
// Only disable if it's a torrent that's not debrid and not currently downloading
|
// Only disable if it's a torrent that's not debrid and not currently downloading
|
||||||
const isDisabled = isTorrent && !isDebrid && !torrentProgress && !stream.behaviorHints?.notWebReady;
|
const isDisabled = isTorrent && !isDebrid && !torrentProgress && !stream.behaviorHints?.notWebReady;
|
||||||
|
|
||||||
|
|
@ -95,107 +76,109 @@ const StreamCard = memo(({ stream, onPress, index, torrentProgress, isLoading, s
|
||||||
const isDownloading = !!torrentProgress && isTorrent;
|
const isDownloading = !!torrentProgress && isTorrent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View entering={entering}>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
style={[
|
||||||
style={[
|
styles.streamCard,
|
||||||
styles.streamCard,
|
isDisabled && styles.streamCardDisabled,
|
||||||
isDisabled && styles.streamCardDisabled,
|
isLoading && styles.streamCardLoading
|
||||||
isLoading && styles.streamCardLoading
|
]}
|
||||||
]}
|
onPress={onPress}
|
||||||
onPress={handlePress}
|
disabled={isDisabled || isLoading}
|
||||||
disabled={isDisabled || isLoading}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.streamDetails}>
|
<View style={styles.streamDetails}>
|
||||||
<View style={styles.streamNameRow}>
|
<View style={styles.streamNameRow}>
|
||||||
<View style={styles.streamTitleContainer}>
|
<View style={styles.streamTitleContainer}>
|
||||||
<Text style={styles.streamName}>
|
<Text style={styles.streamName}>
|
||||||
{displayTitle}
|
{displayTitle}
|
||||||
|
</Text>
|
||||||
|
{displayAddonName && displayAddonName !== displayTitle && (
|
||||||
|
<Text style={styles.streamAddonName}>
|
||||||
|
{displayAddonName}
|
||||||
</Text>
|
</Text>
|
||||||
{displayAddonName && displayAddonName !== displayTitle && (
|
|
||||||
<Text style={styles.streamAddonName}>
|
|
||||||
{displayAddonName}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Show loading indicator if stream is loading */}
|
|
||||||
{isLoading && (
|
|
||||||
<View style={styles.loadingIndicator}>
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
<Text style={styles.loadingText}>
|
|
||||||
{statusMessage || "Loading..."}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show download indicator for active downloads */}
|
|
||||||
{isDownloading && (
|
|
||||||
<View style={styles.downloadingIndicator}>
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
<Text style={styles.downloadingText}>Downloading...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.streamMetaRow}>
|
{/* Show loading indicator if stream is loading */}
|
||||||
{quality && quality >= "720" && (
|
{isLoading && (
|
||||||
<QualityBadge type="HD" />
|
<View style={styles.loadingIndicator}>
|
||||||
)}
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>
|
||||||
{isDolby && (
|
{statusMessage || "Loading..."}
|
||||||
<QualityBadge type="VISION" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{size && (
|
|
||||||
<View style={[styles.chip, { backgroundColor: colors.darkGray }]}>
|
|
||||||
<Text style={styles.chipText}>{size}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isTorrent && !isDebrid && (
|
|
||||||
<View style={[styles.chip, { backgroundColor: colors.error }]}>
|
|
||||||
<Text style={styles.chipText}>TORRENT</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isDebrid && (
|
|
||||||
<View style={[styles.chip, { backgroundColor: colors.success }]}>
|
|
||||||
<Text style={styles.chipText}>DEBRID</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Render progress bar if there's progress */}
|
|
||||||
{torrentProgress && (
|
|
||||||
<View style={styles.progressContainer}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.progressBar,
|
|
||||||
{ width: `${torrentProgress.bufferProgress}%` }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Text style={styles.progressText}>
|
|
||||||
{`${Math.round(torrentProgress.bufferProgress)}% • ${Math.round(torrentProgress.downloadSpeed / 1024)} KB/s • ${torrentProgress.seeds} seeds`}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show download indicator for active downloads */}
|
||||||
|
{isDownloading && (
|
||||||
|
<View style={styles.downloadingIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.downloadingText}>Downloading...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.streamAction}>
|
<View style={styles.streamMetaRow}>
|
||||||
<MaterialIcons
|
{quality && quality >= "720" && (
|
||||||
name="play-arrow"
|
<QualityBadge type="HD" />
|
||||||
size={24}
|
)}
|
||||||
color={isDisabled ? colors.textMuted : colors.primary}
|
|
||||||
/>
|
{isDolby && (
|
||||||
|
<QualityBadge type="VISION" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{size && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: colors.darkGray }]}>
|
||||||
|
<Text style={styles.chipText}>{size}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isTorrent && !isDebrid && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: colors.error }]}>
|
||||||
|
<Text style={styles.chipText}>TORRENT</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDebrid && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: colors.success }]}>
|
||||||
|
<Text style={styles.chipText}>DEBRID</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
{/* Render progress bar if there's progress */}
|
||||||
|
{torrentProgress && (
|
||||||
|
<View style={styles.progressContainer}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.progressBar,
|
||||||
|
{ width: `${torrentProgress.bufferProgress}%` }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Text style={styles.progressText}>
|
||||||
|
{`${Math.round(torrentProgress.bufferProgress)}% • ${Math.round(torrentProgress.downloadSpeed / 1024)} KB/s • ${torrentProgress.seeds} seeds`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.streamAction}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="play-arrow"
|
||||||
|
size={24}
|
||||||
|
color={isDisabled ? colors.textMuted : colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
// Custom comparison to prevent unnecessary re-renders
|
// Simplified memo comparison that won't interfere with onPress
|
||||||
return prevProps.stream.url === nextProps.stream.url &&
|
return (
|
||||||
prevProps.index === nextProps.index &&
|
prevProps.stream.url === nextProps.stream.url &&
|
||||||
prevProps.torrentProgress?.bufferProgress === nextProps.torrentProgress?.bufferProgress;
|
prevProps.isLoading === nextProps.isLoading &&
|
||||||
|
prevProps.torrentProgress?.bufferProgress === nextProps.torrentProgress?.bufferProgress &&
|
||||||
|
prevProps.statusMessage === nextProps.statusMessage
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const QualityTag = React.memo(({ text, color }: { text: string; color: string }) => (
|
const QualityTag = React.memo(({ text, color }: { text: string; color: string }) => (
|
||||||
|
|
@ -260,6 +243,26 @@ export const StreamsScreen = () => {
|
||||||
const { id, type, episodeId } = route.params;
|
const { id, type, episodeId } = route.params;
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
|
||||||
|
// Log the stream details and installed addons for debugging
|
||||||
|
useEffect(() => {
|
||||||
|
// Log installed addons
|
||||||
|
const installedAddons = stremioService.getInstalledAddons();
|
||||||
|
console.log('📦 [StreamsScreen] INSTALLED ADDONS:', installedAddons.map(addon => ({
|
||||||
|
id: addon.id,
|
||||||
|
name: addon.name,
|
||||||
|
version: addon.version,
|
||||||
|
resources: addon.resources,
|
||||||
|
types: addon.types
|
||||||
|
})));
|
||||||
|
|
||||||
|
// Log request details
|
||||||
|
console.log('🎬 [StreamsScreen] REQUEST DETAILS:', {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
episodeId: episodeId || 'none'
|
||||||
|
});
|
||||||
|
}, [id, type, episodeId]);
|
||||||
|
|
||||||
// Add timing logs
|
// Add timing logs
|
||||||
const [loadStartTime, setLoadStartTime] = useState(0);
|
const [loadStartTime, setLoadStartTime] = useState(0);
|
||||||
const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({});
|
const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({});
|
||||||
|
|
@ -278,6 +281,20 @@ export const StreamsScreen = () => {
|
||||||
groupedEpisodes,
|
groupedEpisodes,
|
||||||
} = useMetadata({ id, type });
|
} = useMetadata({ id, type });
|
||||||
|
|
||||||
|
// Log stream results when they arrive
|
||||||
|
useEffect(() => {
|
||||||
|
const streams = type === 'series' ? episodeStreams : groupedStreams;
|
||||||
|
console.log('🔍 [StreamsScreen] STREAM RESULTS:', {
|
||||||
|
totalProviders: Object.keys(streams).length,
|
||||||
|
providers: Object.keys(streams),
|
||||||
|
streamCounts: Object.entries(streams).map(([provider, data]) => ({
|
||||||
|
provider,
|
||||||
|
addonName: data.addonName,
|
||||||
|
streams: data.streams.length
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}, [episodeStreams, groupedStreams, type]);
|
||||||
|
|
||||||
const [selectedProvider, setSelectedProvider] = React.useState('all');
|
const [selectedProvider, setSelectedProvider] = React.useState('all');
|
||||||
const [availableProviders, setAvailableProviders] = React.useState<Set<string>>(new Set());
|
const [availableProviders, setAvailableProviders] = React.useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
|
@ -555,18 +572,17 @@ export const StreamsScreen = () => {
|
||||||
if (settings.useExternalPlayer) {
|
if (settings.useExternalPlayer) {
|
||||||
logger.log('Using external player for URL:', stream.url);
|
logger.log('Using external player for URL:', stream.url);
|
||||||
// Use VideoPlayerService to launch external player
|
// Use VideoPlayerService to launch external player
|
||||||
try {
|
const videoPlayerService = VideoPlayerService;
|
||||||
const videoPlayerService = VideoPlayerService;
|
const launched = await videoPlayerService.playVideo(stream.url, {
|
||||||
await videoPlayerService.playVideo(stream.url, {
|
useExternalPlayer: true,
|
||||||
useExternalPlayer: true,
|
title: metadata?.name || '',
|
||||||
title: metadata?.name || '',
|
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined,
|
||||||
episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined,
|
releaseDate: metadata?.year?.toString(),
|
||||||
releaseDate: metadata?.year?.toString(),
|
});
|
||||||
});
|
|
||||||
} catch (externalPlayerError) {
|
if (!launched) {
|
||||||
logger.error('External player error:', externalPlayerError);
|
logger.log('External player launch failed, falling back to built-in player');
|
||||||
// Fallback to built-in player if external player fails
|
|
||||||
navigation.navigate('Player', {
|
navigation.navigate('Player', {
|
||||||
uri: stream.url,
|
uri: stream.url,
|
||||||
title: metadata?.name || '',
|
title: metadata?.name || '',
|
||||||
|
|
@ -695,18 +711,17 @@ export const StreamsScreen = () => {
|
||||||
if (settings.useExternalPlayer) {
|
if (settings.useExternalPlayer) {
|
||||||
logger.log('[StreamsScreen] Using external player for torrent video path:', videoPath);
|
logger.log('[StreamsScreen] Using external player for torrent video path:', videoPath);
|
||||||
// Use VideoPlayerService to launch external player
|
// Use VideoPlayerService to launch external player
|
||||||
try {
|
const videoPlayerService = VideoPlayerService;
|
||||||
const videoPlayerService = VideoPlayerService;
|
const launched = await videoPlayerService.playVideo(`file://${videoPath}`, {
|
||||||
await videoPlayerService.playVideo(`file://${videoPath}`, {
|
useExternalPlayer: true,
|
||||||
useExternalPlayer: true,
|
title: metadata?.name || '',
|
||||||
title: metadata?.name || '',
|
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined,
|
||||||
episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined,
|
releaseDate: metadata?.year?.toString(),
|
||||||
releaseDate: metadata?.year?.toString(),
|
});
|
||||||
});
|
|
||||||
} catch (externalPlayerError) {
|
if (!launched) {
|
||||||
logger.error('[StreamsScreen] External player error:', externalPlayerError);
|
logger.log('[StreamsScreen] External player launch failed, falling back to built-in player');
|
||||||
// Fallback to built-in player if external player fails
|
|
||||||
navigation.navigate('Player', {
|
navigation.navigate('Player', {
|
||||||
uri: `file://${videoPath}`,
|
uri: `file://${videoPath}`,
|
||||||
title: metadata?.name || '',
|
title: metadata?.name || '',
|
||||||
|
|
@ -869,6 +884,7 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StreamCard
|
<StreamCard
|
||||||
|
key={`${stream.url}-${index}`}
|
||||||
stream={stream}
|
stream={stream}
|
||||||
onPress={() => handleStreamPress(stream)}
|
onPress={() => handleStreamPress(stream)}
|
||||||
index={index}
|
index={index}
|
||||||
|
|
@ -881,7 +897,7 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
const renderSectionHeader = useCallback(({ section }: { section: { title: string } }) => (
|
const renderSectionHeader = useCallback(({ section }: { section: { title: string } }) => (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.delay(150).springify()}
|
entering={FadeIn.duration(300)}
|
||||||
>
|
>
|
||||||
<Text style={styles.streamGroupTitle}>{section.title}</Text>
|
<Text style={styles.streamGroupTitle}>{section.title}</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -1060,7 +1076,7 @@ const styles = StyleSheet.create({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
zIndex: 1,
|
zIndex: 2,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -1078,6 +1094,7 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: colors.darkBackground,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
|
zIndex: 0,
|
||||||
},
|
},
|
||||||
streamsMainContentMovie: {
|
streamsMainContentMovie: {
|
||||||
paddingTop: Platform.OS === 'android' ? 90 : 100,
|
paddingTop: Platform.OS === 'android' ? 90 : 100,
|
||||||
|
|
@ -1113,6 +1130,7 @@ const styles = StyleSheet.create({
|
||||||
streamsContent: {
|
streamsContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
streamsContainer: {
|
streamsContainer: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
|
|
@ -1142,6 +1160,7 @@ const styles = StyleSheet.create({
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(255,255,255,0.05)',
|
borderColor: 'rgba(255,255,255,0.05)',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
streamCardDisabled: {
|
streamCardDisabled: {
|
||||||
backgroundColor: colors.elevation2,
|
backgroundColor: colors.elevation2,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@ export interface StreamResponse {
|
||||||
addonName: string;
|
addonName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modify the callback signature to include addon ID
|
||||||
|
interface StreamCallback {
|
||||||
|
(streams: Stream[] | null, addonId: string | null, addonName: string | null, error: Error | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface CatalogFilter {
|
interface CatalogFilter {
|
||||||
title: string;
|
title: string;
|
||||||
value: any;
|
value: any;
|
||||||
|
|
@ -446,95 +451,112 @@ class StremioService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStreams(type: string, id: string, callback?: (streams: Stream[] | null, addonName: string | null, error: Error | null) => void): Promise<StreamResponse[]> {
|
// Modify getStreams to use the new callback signature and rely on callbacks for results
|
||||||
|
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
const addons = this.getInstalledAddons();
|
const addons = this.getInstalledAddons();
|
||||||
logger.log('Installed addons:', addons.map(a => ({ id: a.id, url: a.url })));
|
logger.log('📌 [getStreams] Installed addons:', addons.map(a => ({ id: a.id, name: a.name, url: a.url })));
|
||||||
|
|
||||||
const streamResponses: StreamResponse[] = [];
|
// Check specifically for TMDB Embed addon
|
||||||
|
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
||||||
|
if (tmdbEmbed) {
|
||||||
|
logger.log('🔍 [getStreams] Found TMDB Embed Streams addon:', {
|
||||||
|
id: tmdbEmbed.id,
|
||||||
|
name: tmdbEmbed.name,
|
||||||
|
url: tmdbEmbed.url,
|
||||||
|
resources: tmdbEmbed.resources,
|
||||||
|
types: tmdbEmbed.types
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log('⚠️ [getStreams] TMDB Embed Streams addon not found among installed addons');
|
||||||
|
}
|
||||||
|
|
||||||
// Find addons that provide streams and sort them by installation order
|
// Find addons that provide streams and sort them by installation order
|
||||||
const streamAddons = addons
|
const streamAddons = addons
|
||||||
.filter(addon => {
|
.filter(addon => {
|
||||||
if (!addon.resources) {
|
if (!addon.resources) {
|
||||||
logger.log(`Addon ${addon.id} has no resources`);
|
logger.log(`⚠️ [getStreams] Addon ${addon.id} has no resources`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the detailed resources structure for debugging
|
||||||
|
// logger.log(`📋 [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources)); // Verbose, uncomment if needed
|
||||||
|
|
||||||
|
// Check if the addon has a stream resource for this type
|
||||||
const hasStreamResource = addon.resources.some(
|
const hasStreamResource = addon.resources.some(
|
||||||
resource => resource.name === 'stream' && resource.types.includes(type)
|
resource => {
|
||||||
|
const result = resource.name === 'stream' && resource.types && resource.types.includes(type);
|
||||||
|
// logger.log(`🔎 [getStreams] Addon ${addon.id} resource ${resource.name}: supports ${type}? ${result}`); // Verbose
|
||||||
|
return result;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasStreamResource) {
|
if (!hasStreamResource) {
|
||||||
logger.log(`Addon ${addon.id} does not support streaming ${type}`);
|
// logger.log(`❌ [getStreams] Addon ${addon.id} does not support streaming ${type}`); // Verbose
|
||||||
|
} else {
|
||||||
|
// logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type}`); // Verbose
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasStreamResource;
|
return hasStreamResource;
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log('Stream capable addons:', streamAddons.map(a => a.id));
|
logger.log('📊 [getStreams] Stream capable addons:', streamAddons.map(a => a.id));
|
||||||
|
|
||||||
if (streamAddons.length === 0) {
|
if (streamAddons.length === 0) {
|
||||||
logger.warn('No addons found that can provide streams');
|
logger.warn('⚠️ [getStreams] No addons found that can provide streams');
|
||||||
return [];
|
// Optionally call callback with an empty result or specific status?
|
||||||
|
// For now, just return if no addons.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map to store promises for each addon
|
// Process each addon and call the callback individually
|
||||||
const addonPromises = new Map<string, Promise<void>>();
|
streamAddons.forEach(addon => {
|
||||||
|
// Use an IIFE to create scope for async operation inside forEach
|
||||||
// Process each addon
|
(async () => {
|
||||||
for (const addon of streamAddons) {
|
|
||||||
const promise = (async () => {
|
|
||||||
try {
|
try {
|
||||||
if (!addon.url) {
|
if (!addon.url) {
|
||||||
logger.warn(`Addon ${addon.id} has no URL`);
|
logger.warn(`⚠️ [getStreams] Addon ${addon.id} has no URL`);
|
||||||
|
if (callback) callback(null, addon.id, addon.name, new Error('Addon has no URL'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = this.getAddonBaseURL(addon.url);
|
const baseUrl = this.getAddonBaseURL(addon.url);
|
||||||
const url = `${baseUrl}/stream/${type}/${id}.json`;
|
const url = `${baseUrl}/stream/${type}/${id}.json`;
|
||||||
|
|
||||||
|
logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}): ${url}`);
|
||||||
|
|
||||||
const response = await this.retryRequest(async () => {
|
const response = await this.retryRequest(async () => {
|
||||||
return await axios.get(url);
|
return await axios.get(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let processedStreams: Stream[] = [];
|
||||||
if (response.data && response.data.streams) {
|
if (response.data && response.data.streams) {
|
||||||
const processedStreams = this.processStreams(response.data.streams, addon);
|
logger.log(`✅ [getStreams] Got ${response.data.streams.length} streams from ${addon.name} (${addon.id})`);
|
||||||
if (processedStreams.length > 0) {
|
processedStreams = this.processStreams(response.data.streams, addon);
|
||||||
streamResponses.push({
|
logger.log(`✅ [getStreams] Processed ${processedStreams.length} valid streams from ${addon.name} (${addon.id})`);
|
||||||
addon: addon.id,
|
} else {
|
||||||
addonName: addon.name,
|
logger.log(`⚠️ [getStreams] No streams found in response from ${addon.name} (${addon.id})`);
|
||||||
streams: processedStreams
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(response.data?.streams || null, addon.name, null);
|
// Call callback with processed streams (can be empty array)
|
||||||
|
callback(processedStreams, addon.id, addon.name, null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to get streams from ${addon.name}:`, error);
|
logger.error(`❌ [getStreams] Failed to get streams from ${addon.name} (${addon.id}):`, error);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(null, addon.name, error as Error);
|
// Call callback with error
|
||||||
|
callback(null, addon.id, addon.name, error as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})(); // Immediately invoke the async function
|
||||||
|
|
||||||
addonPromises.set(addon.id, promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all promises to complete
|
|
||||||
await Promise.all(addonPromises.values());
|
|
||||||
|
|
||||||
// Sort stream responses to maintain installed addon order
|
|
||||||
streamResponses.sort((a, b) => {
|
|
||||||
const indexA = streamAddons.findIndex(addon => addon.id === a.addon);
|
|
||||||
const indexB = streamAddons.findIndex(addon => addon.id === b.addon);
|
|
||||||
return indexA - indexB;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return streamResponses;
|
// No longer waiting here, callbacks handle results asynchronously
|
||||||
|
// Removed: await Promise.all(addonPromises.values());
|
||||||
|
// No longer returning aggregated results
|
||||||
|
// Removed: return streamResponses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchStreamsFromAddon(addon: Manifest, type: string, id: string): Promise<StreamResponse | null> {
|
private async fetchStreamsFromAddon(addon: Manifest, type: string, id: string): Promise<StreamResponse | null> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { NativeModules } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import * as IntentLauncher from 'expo-intent-launcher';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
const { VideoPlayerModule } = NativeModules;
|
|
||||||
|
|
||||||
interface VideoPlayerOptions {
|
interface VideoPlayerOptions {
|
||||||
useExternalPlayer: boolean;
|
useExternalPlayer: boolean;
|
||||||
|
|
@ -16,11 +15,35 @@ interface VideoPlayerOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoPlayerService = {
|
export const VideoPlayerService = {
|
||||||
playVideo: (url: string, options?: Partial<VideoPlayerOptions>): Promise<boolean> => {
|
playVideo: async (url: string, options?: Partial<VideoPlayerOptions>): Promise<boolean> => {
|
||||||
if (options) {
|
if (!options?.useExternalPlayer || Platform.OS !== 'android') {
|
||||||
return VideoPlayerModule.playVideo(url, options);
|
return false;
|
||||||
} else {
|
}
|
||||||
return VideoPlayerModule.playVideo(url);
|
|
||||||
|
try {
|
||||||
|
// Create a title that includes all relevant metadata
|
||||||
|
const fullTitle = [
|
||||||
|
options.title,
|
||||||
|
options.episodeNumber,
|
||||||
|
options.episodeTitle,
|
||||||
|
options.releaseDate
|
||||||
|
].filter(Boolean).join(' - ');
|
||||||
|
|
||||||
|
// Launch the intent to play the video
|
||||||
|
await IntentLauncher.startActivityAsync('android.intent.action.VIEW', {
|
||||||
|
data: url,
|
||||||
|
flags: 1, // FLAG_ACTIVITY_NEW_TASK
|
||||||
|
type: 'video/*',
|
||||||
|
extra: {
|
||||||
|
'android.intent.extra.TITLE': fullTitle,
|
||||||
|
'position': 0, // Start from beginning
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to launch external player:', error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Color palette for the app following Material Design 3
|
// Color palette for the app following Material Design 3
|
||||||
export const colors = {
|
export const colors = {
|
||||||
// Primary colors
|
// Primary colors
|
||||||
primary: '#00A4A4', // Brighter, more vibrant teal
|
primary: '#2d9cdb', // Brighter, more vibrant teal
|
||||||
secondary: '#FF6B6B', // Coral color that complements teal
|
secondary: '#FF6B6B', // Coral color that complements teal
|
||||||
|
|
||||||
// Background colors - Deep dark theme
|
// Background colors - Deep dark theme
|
||||||
|
|
|
||||||