Spaces:
Running
Running
Fix VTV channel loading spinner hanging
Browse filesFix spinner 'Đang tải danh sách kênh...' that never disappears when API is slow. Add 5s max timeout, always populate static fallback, hide loader reliably.
- static/app_v2.js +22 -17
static/app_v2.js
CHANGED
|
@@ -80,9 +80,17 @@ function buildVTVBlockHTML(){
|
|
| 80 |
}
|
| 81 |
|
| 82 |
async function loadVTVStreams(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
let apiOk = false;
|
| 84 |
try {
|
| 85 |
-
const
|
|
|
|
|
|
|
|
|
|
| 86 |
if(r.ok){
|
| 87 |
const data = await r.json();
|
| 88 |
VTV_CHANNELS.forEach(ch => {
|
|
@@ -95,28 +103,25 @@ async function loadVTVStreams(){
|
|
| 95 |
});
|
| 96 |
}
|
| 97 |
} catch(e) {
|
| 98 |
-
console.warn('VTV API error:', e);
|
| 99 |
-
}
|
| 100 |
-
if(!apiOk){
|
| 101 |
-
VTV_CHANNELS.forEach(ch => {
|
| 102 |
-
if(!_vtvStreams[ch.id] || _vtvStreams[ch.id].length === 0){
|
| 103 |
-
const fallback = VTV_STATIC_SOURCES[ch.id] || [];
|
| 104 |
-
_vtvStreams[ch.id] = fallback.map(u => '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(u));
|
| 105 |
-
}
|
| 106 |
-
});
|
| 107 |
}
|
|
|
|
|
|
|
| 108 |
VTV_CHANNELS.forEach(ch => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
const tab = document.getElementById('vtvt-'+ch.id);
|
| 110 |
if(tab){
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
} else {
|
| 115 |
-
tab.style.opacity = '0.35';
|
| 116 |
-
tab.textContent = ch.name + ' ✕';
|
| 117 |
-
}
|
| 118 |
}
|
| 119 |
});
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
|
| 122 |
function _vtvRetry(){
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
async function loadVTVStreams(){
|
| 83 |
+
// Safety: always hide loader after 5s max
|
| 84 |
+
const loadEl = document.getElementById('vtv-load');
|
| 85 |
+
const hideLoader = () => { const el = document.getElementById('vtv-load'); if(el) el.style.display = 'none'; };
|
| 86 |
+
setTimeout(hideLoader, 5000);
|
| 87 |
+
|
| 88 |
let apiOk = false;
|
| 89 |
try {
|
| 90 |
+
const ctrl = new AbortController();
|
| 91 |
+
const tid = setTimeout(() => ctrl.abort(), 4000);
|
| 92 |
+
const r = await fetch('/api/vtv/streams', {signal: ctrl.signal});
|
| 93 |
+
clearTimeout(tid);
|
| 94 |
if(r.ok){
|
| 95 |
const data = await r.json();
|
| 96 |
VTV_CHANNELS.forEach(ch => {
|
|
|
|
| 103 |
});
|
| 104 |
}
|
| 105 |
} catch(e) {
|
| 106 |
+
console.warn('VTV API timeout/error:', e);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
+
|
| 109 |
+
// Always populate from static sources (fallback ensures tabs are always clickable)
|
| 110 |
VTV_CHANNELS.forEach(ch => {
|
| 111 |
+
if(!_vtvStreams[ch.id] || _vtvStreams[ch.id].length === 0){
|
| 112 |
+
const fallback = VTV_STATIC_SOURCES[ch.id] || [];
|
| 113 |
+
_vtvStreams[ch.id] = fallback.map(u => '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(u));
|
| 114 |
+
}
|
| 115 |
+
// Update tab: always show as available (opacity 1) since we have static fallback
|
| 116 |
const tab = document.getElementById('vtvt-'+ch.id);
|
| 117 |
if(tab){
|
| 118 |
+
tab.classList.remove('off');
|
| 119 |
+
tab.textContent = ch.name;
|
| 120 |
+
tab.style.opacity = '1';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
});
|
| 123 |
+
|
| 124 |
+
hideLoader();
|
| 125 |
}
|
| 126 |
|
| 127 |
function _vtvRetry(){
|