|
|
|
|
|
let selectedAPIs = JSON.parse(localStorage.getItem('selectedAPIs') || '["tyyszy","dyttzy", "bfzy", "ruyi"]'); |
|
|
let customAPIs = JSON.parse(localStorage.getItem('customAPIs') || '[]'); |
|
|
|
|
|
|
|
|
let currentEpisodeIndex = 0; |
|
|
|
|
|
let currentEpisodes = []; |
|
|
|
|
|
let currentVideoTitle = ''; |
|
|
|
|
|
let episodesReversed = false; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
|
|
|
initAPICheckboxes(); |
|
|
|
|
|
|
|
|
renderCustomAPIsList(); |
|
|
|
|
|
|
|
|
updateSelectedApiCount(); |
|
|
|
|
|
|
|
|
renderSearchHistory(); |
|
|
|
|
|
|
|
|
if (!localStorage.getItem('hasInitializedDefaults')) { |
|
|
|
|
|
selectedAPIs = ["tyyszy", "bfzy", "dyttzy", "ruyi"]; |
|
|
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); |
|
|
|
|
|
|
|
|
localStorage.setItem('yellowFilterEnabled', 'true'); |
|
|
localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, 'true'); |
|
|
|
|
|
|
|
|
localStorage.setItem('doubanEnabled', 'true'); |
|
|
|
|
|
|
|
|
localStorage.setItem('hasInitializedDefaults', 'true'); |
|
|
} |
|
|
|
|
|
|
|
|
const yellowFilterToggle = document.getElementById('yellowFilterToggle'); |
|
|
if (yellowFilterToggle) { |
|
|
yellowFilterToggle.checked = localStorage.getItem('yellowFilterEnabled') === 'true'; |
|
|
} |
|
|
|
|
|
|
|
|
const adFilterToggle = document.getElementById('adFilterToggle'); |
|
|
if (adFilterToggle) { |
|
|
adFilterToggle.checked = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; |
|
|
} |
|
|
|
|
|
|
|
|
setupEventListeners(); |
|
|
|
|
|
|
|
|
setTimeout(checkAdultAPIsSelected, 100); |
|
|
}); |
|
|
|
|
|
|
|
|
function initAPICheckboxes() { |
|
|
const container = document.getElementById('apiCheckboxes'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
|
|
|
const normaldiv = document.createElement('div'); |
|
|
normaldiv.id = 'normaldiv'; |
|
|
normaldiv.className = 'grid grid-cols-2 gap-2'; |
|
|
const normalTitle = document.createElement('div'); |
|
|
normalTitle.className = 'api-group-title'; |
|
|
normalTitle.textContent = '普通资源'; |
|
|
normaldiv.appendChild(normalTitle); |
|
|
|
|
|
|
|
|
Object.keys(API_SITES).forEach(apiKey => { |
|
|
const api = API_SITES[apiKey]; |
|
|
if (api.adult) return; |
|
|
|
|
|
const checked = selectedAPIs.includes(apiKey); |
|
|
|
|
|
const checkbox = document.createElement('div'); |
|
|
checkbox.className = 'flex items-center'; |
|
|
checkbox.innerHTML = ` |
|
|
<input type="checkbox" id="api_${apiKey}" |
|
|
class="form-checkbox h-3 w-3 text-blue-600 bg-[#222] border border-[#333]" |
|
|
${checked ? 'checked' : ''} |
|
|
data-api="${apiKey}"> |
|
|
<label for="api_${apiKey}" class="ml-1 text-xs text-gray-400 truncate">${api.name}</label> |
|
|
`; |
|
|
normaldiv.appendChild(checkbox); |
|
|
|
|
|
|
|
|
checkbox.querySelector('input').addEventListener('change', function () { |
|
|
updateSelectedAPIs(); |
|
|
checkAdultAPIsSelected(); |
|
|
}); |
|
|
}); |
|
|
container.appendChild(normaldiv); |
|
|
|
|
|
|
|
|
addAdultAPI(); |
|
|
|
|
|
|
|
|
checkAdultAPIsSelected(); |
|
|
} |
|
|
|
|
|
|
|
|
function addAdultAPI() { |
|
|
|
|
|
if (!HIDE_BUILTIN_ADULT_APIS && (localStorage.getItem('yellowFilterEnabled') === 'false')) { |
|
|
const container = document.getElementById('apiCheckboxes'); |
|
|
|
|
|
|
|
|
const adultdiv = document.createElement('div'); |
|
|
adultdiv.id = 'adultdiv'; |
|
|
adultdiv.className = 'grid grid-cols-2 gap-2'; |
|
|
const adultTitle = document.createElement('div'); |
|
|
adultTitle.className = 'api-group-title adult'; |
|
|
adultTitle.innerHTML = `黄色资源采集站 <span class="adult-warning"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> |
|
|
</svg> |
|
|
</span>`; |
|
|
adultdiv.appendChild(adultTitle); |
|
|
|
|
|
|
|
|
Object.keys(API_SITES).forEach(apiKey => { |
|
|
const api = API_SITES[apiKey]; |
|
|
if (!api.adult) return; |
|
|
|
|
|
const checked = selectedAPIs.includes(apiKey); |
|
|
|
|
|
const checkbox = document.createElement('div'); |
|
|
checkbox.className = 'flex items-center'; |
|
|
checkbox.innerHTML = ` |
|
|
<input type="checkbox" id="api_${apiKey}" |
|
|
class="form-checkbox h-3 w-3 text-blue-600 bg-[#222] border border-[#333] api-adult" |
|
|
${checked ? 'checked' : ''} |
|
|
data-api="${apiKey}"> |
|
|
<label for="api_${apiKey}" class="ml-1 text-xs text-pink-400 truncate">${api.name}</label> |
|
|
`; |
|
|
adultdiv.appendChild(checkbox); |
|
|
|
|
|
|
|
|
checkbox.querySelector('input').addEventListener('change', function () { |
|
|
updateSelectedAPIs(); |
|
|
checkAdultAPIsSelected(); |
|
|
}); |
|
|
}); |
|
|
container.appendChild(adultdiv); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function checkAdultAPIsSelected() { |
|
|
|
|
|
const adultBuiltinCheckboxes = document.querySelectorAll('#apiCheckboxes .api-adult:checked'); |
|
|
|
|
|
|
|
|
const customApiCheckboxes = document.querySelectorAll('#customApisList .api-adult:checked'); |
|
|
|
|
|
const hasAdultSelected = adultBuiltinCheckboxes.length > 0 || customApiCheckboxes.length > 0; |
|
|
|
|
|
const yellowFilterToggle = document.getElementById('yellowFilterToggle'); |
|
|
const yellowFilterContainer = yellowFilterToggle.closest('div').parentNode; |
|
|
const filterDescription = yellowFilterContainer.querySelector('p.filter-description'); |
|
|
|
|
|
|
|
|
if (hasAdultSelected) { |
|
|
yellowFilterToggle.checked = false; |
|
|
yellowFilterToggle.disabled = true; |
|
|
localStorage.setItem('yellowFilterEnabled', 'false'); |
|
|
|
|
|
|
|
|
yellowFilterContainer.classList.add('filter-disabled'); |
|
|
|
|
|
|
|
|
if (filterDescription) { |
|
|
filterDescription.innerHTML = '<strong class="text-pink-300">选中黄色资源站时无法启用此过滤</strong>'; |
|
|
} |
|
|
|
|
|
|
|
|
const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip'); |
|
|
if (existingTooltip) { |
|
|
existingTooltip.remove(); |
|
|
} |
|
|
} else { |
|
|
|
|
|
yellowFilterToggle.disabled = false; |
|
|
yellowFilterContainer.classList.remove('filter-disabled'); |
|
|
|
|
|
|
|
|
if (filterDescription) { |
|
|
filterDescription.innerHTML = '过滤"伦理片"等黄色内容'; |
|
|
} |
|
|
|
|
|
|
|
|
const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip'); |
|
|
if (existingTooltip) { |
|
|
existingTooltip.remove(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderCustomAPIsList() { |
|
|
const container = document.getElementById('customApisList'); |
|
|
if (!container) return; |
|
|
|
|
|
if (customAPIs.length === 0) { |
|
|
container.innerHTML = '<p class="text-xs text-gray-500 text-center my-2">未添加自定义API</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
container.innerHTML = ''; |
|
|
customAPIs.forEach((api, index) => { |
|
|
const apiItem = document.createElement('div'); |
|
|
apiItem.className = 'flex items-center justify-between p-1 mb-1 bg-[#222] rounded'; |
|
|
const textColorClass = api.isAdult ? 'text-pink-400' : 'text-white'; |
|
|
const adultTag = api.isAdult ? '<span class="text-xs text-pink-400 mr-1">(18+)</span>' : ''; |
|
|
|
|
|
const detailLine = api.detail ? `<div class="text-xs text-gray-400 truncate">detail: ${api.detail}</div>` : ''; |
|
|
apiItem.innerHTML = ` |
|
|
<div class="flex items-center flex-1 min-w-0"> |
|
|
<input type="checkbox" id="custom_api_${index}" |
|
|
class="form-checkbox h-3 w-3 text-blue-600 mr-1 ${api.isAdult ? 'api-adult' : ''}" |
|
|
${selectedAPIs.includes('custom_' + index) ? 'checked' : ''} |
|
|
data-custom-index="${index}"> |
|
|
<div class="flex-1 min-w-0"> |
|
|
<div class="text-xs font-medium ${textColorClass} truncate"> |
|
|
${adultTag}${api.name} |
|
|
</div> |
|
|
<div class="text-xs text-gray-500 truncate">${api.url}</div> |
|
|
${detailLine} |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex items-center"> |
|
|
<button class="text-blue-500 hover:text-blue-700 text-xs px-1" onclick="editCustomApi(${index})">✎</button> |
|
|
<button class="text-red-500 hover:text-red-700 text-xs px-1" onclick="removeCustomApi(${index})">✕</button> |
|
|
</div> |
|
|
`; |
|
|
container.appendChild(apiItem); |
|
|
apiItem.querySelector('input').addEventListener('change', function () { |
|
|
updateSelectedAPIs(); |
|
|
checkAdultAPIsSelected(); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function editCustomApi(index) { |
|
|
if (index < 0 || index >= customAPIs.length) return; |
|
|
const api = customAPIs[index]; |
|
|
document.getElementById('customApiName').value = api.name; |
|
|
document.getElementById('customApiUrl').value = api.url; |
|
|
document.getElementById('customApiDetail').value = api.detail || ''; |
|
|
const isAdultInput = document.getElementById('customApiIsAdult'); |
|
|
if (isAdultInput) isAdultInput.checked = api.isAdult || false; |
|
|
const form = document.getElementById('addCustomApiForm'); |
|
|
if (form) { |
|
|
form.classList.remove('hidden'); |
|
|
const buttonContainer = form.querySelector('div:last-child'); |
|
|
buttonContainer.innerHTML = ` |
|
|
<button onclick="updateCustomApi(${index})" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">更新</button> |
|
|
<button onclick="cancelEditCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateCustomApi(index) { |
|
|
if (index < 0 || index >= customAPIs.length) return; |
|
|
const nameInput = document.getElementById('customApiName'); |
|
|
const urlInput = document.getElementById('customApiUrl'); |
|
|
const detailInput = document.getElementById('customApiDetail'); |
|
|
const isAdultInput = document.getElementById('customApiIsAdult'); |
|
|
const name = nameInput.value.trim(); |
|
|
let url = urlInput.value.trim(); |
|
|
const detail = detailInput ? detailInput.value.trim() : ''; |
|
|
const isAdult = isAdultInput ? isAdultInput.checked : false; |
|
|
if (!name || !url) { |
|
|
showToast('请输入API名称和链接', 'warning'); |
|
|
return; |
|
|
} |
|
|
if (!/^https?:\/\/.+/.test(url)) { |
|
|
showToast('API链接格式不正确,需以http://或https://开头', 'warning'); |
|
|
return; |
|
|
} |
|
|
if (url.endsWith('/')) url = url.slice(0, -1); |
|
|
|
|
|
customAPIs[index] = { name, url, detail, isAdult }; |
|
|
localStorage.setItem('customAPIs', JSON.stringify(customAPIs)); |
|
|
renderCustomAPIsList(); |
|
|
checkAdultAPIsSelected(); |
|
|
restoreAddCustomApiButtons(); |
|
|
nameInput.value = ''; |
|
|
urlInput.value = ''; |
|
|
if (detailInput) detailInput.value = ''; |
|
|
if (isAdultInput) isAdultInput.checked = false; |
|
|
document.getElementById('addCustomApiForm').classList.add('hidden'); |
|
|
showToast('已更新自定义API: ' + name, 'success'); |
|
|
} |
|
|
|
|
|
|
|
|
function cancelEditCustomApi() { |
|
|
|
|
|
document.getElementById('customApiName').value = ''; |
|
|
document.getElementById('customApiUrl').value = ''; |
|
|
document.getElementById('customApiDetail').value = ''; |
|
|
const isAdultInput = document.getElementById('customApiIsAdult'); |
|
|
if (isAdultInput) isAdultInput.checked = false; |
|
|
|
|
|
|
|
|
document.getElementById('addCustomApiForm').classList.add('hidden'); |
|
|
|
|
|
|
|
|
restoreAddCustomApiButtons(); |
|
|
} |
|
|
|
|
|
|
|
|
function restoreAddCustomApiButtons() { |
|
|
const form = document.getElementById('addCustomApiForm'); |
|
|
const buttonContainer = form.querySelector('div:last-child'); |
|
|
buttonContainer.innerHTML = ` |
|
|
<button onclick="addCustomApi()" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">添加</button> |
|
|
<button onclick="cancelAddCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
function updateSelectedAPIs() { |
|
|
|
|
|
const builtInApiCheckboxes = document.querySelectorAll('#apiCheckboxes input:checked'); |
|
|
|
|
|
|
|
|
const builtInApis = Array.from(builtInApiCheckboxes).map(input => input.dataset.api); |
|
|
|
|
|
|
|
|
const customApiCheckboxes = document.querySelectorAll('#customApisList input:checked'); |
|
|
const customApiIndices = Array.from(customApiCheckboxes).map(input => 'custom_' + input.dataset.customIndex); |
|
|
|
|
|
|
|
|
selectedAPIs = [...builtInApis, ...customApiIndices]; |
|
|
|
|
|
|
|
|
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); |
|
|
|
|
|
|
|
|
updateSelectedApiCount(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateSelectedApiCount() { |
|
|
const countEl = document.getElementById('selectedApiCount'); |
|
|
if (countEl) { |
|
|
countEl.textContent = selectedAPIs.length; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function selectAllAPIs(selectAll = true, excludeAdult = false) { |
|
|
const checkboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]'); |
|
|
|
|
|
checkboxes.forEach(checkbox => { |
|
|
if (excludeAdult && checkbox.classList.contains('api-adult')) { |
|
|
checkbox.checked = false; |
|
|
} else { |
|
|
checkbox.checked = selectAll; |
|
|
} |
|
|
}); |
|
|
|
|
|
updateSelectedAPIs(); |
|
|
checkAdultAPIsSelected(); |
|
|
} |
|
|
|
|
|
|
|
|
function showAddCustomApiForm() { |
|
|
const form = document.getElementById('addCustomApiForm'); |
|
|
if (form) { |
|
|
form.classList.remove('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function cancelAddCustomApi() { |
|
|
const form = document.getElementById('addCustomApiForm'); |
|
|
if (form) { |
|
|
form.classList.add('hidden'); |
|
|
document.getElementById('customApiName').value = ''; |
|
|
document.getElementById('customApiUrl').value = ''; |
|
|
document.getElementById('customApiDetail').value = ''; |
|
|
const isAdultInput = document.getElementById('customApiIsAdult'); |
|
|
if (isAdultInput) isAdultInput.checked = false; |
|
|
|
|
|
|
|
|
restoreAddCustomApiButtons(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function addCustomApi() { |
|
|
const nameInput = document.getElementById('customApiName'); |
|
|
const urlInput = document.getElementById('customApiUrl'); |
|
|
const detailInput = document.getElementById('customApiDetail'); |
|
|
const isAdultInput = document.getElementById('customApiIsAdult'); |
|
|
const name = nameInput.value.trim(); |
|
|
let url = urlInput.value.trim(); |
|
|
const detail = detailInput ? detailInput.value.trim() : ''; |
|
|
const isAdult = isAdultInput ? isAdultInput.checked : false; |
|
|
if (!name || !url) { |
|
|
showToast('请输入API名称和链接', 'warning'); |
|
|
return; |
|
|
} |
|
|
if (!/^https?:\/\/.+/.test(url)) { |
|
|
showToast('API链接格式不正确,需以http://或https://开头', 'warning'); |
|
|
return; |
|
|
} |
|
|
if (url.endsWith('/')) { |
|
|
url = url.slice(0, -1); |
|
|
} |
|
|
|
|
|
customAPIs.push({ name, url, detail, isAdult }); |
|
|
localStorage.setItem('customAPIs', JSON.stringify(customAPIs)); |
|
|
const newApiIndex = customAPIs.length - 1; |
|
|
selectedAPIs.push('custom_' + newApiIndex); |
|
|
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); |
|
|
|
|
|
|
|
|
renderCustomAPIsList(); |
|
|
updateSelectedApiCount(); |
|
|
checkAdultAPIsSelected(); |
|
|
nameInput.value = ''; |
|
|
urlInput.value = ''; |
|
|
if (detailInput) detailInput.value = ''; |
|
|
if (isAdultInput) isAdultInput.checked = false; |
|
|
document.getElementById('addCustomApiForm').classList.add('hidden'); |
|
|
showToast('已添加自定义API: ' + name, 'success'); |
|
|
} |
|
|
|
|
|
|
|
|
function removeCustomApi(index) { |
|
|
if (index < 0 || index >= customAPIs.length) return; |
|
|
|
|
|
const apiName = customAPIs[index].name; |
|
|
|
|
|
|
|
|
customAPIs.splice(index, 1); |
|
|
localStorage.setItem('customAPIs', JSON.stringify(customAPIs)); |
|
|
|
|
|
|
|
|
const customApiId = 'custom_' + index; |
|
|
selectedAPIs = selectedAPIs.filter(id => id !== customApiId); |
|
|
|
|
|
|
|
|
selectedAPIs = selectedAPIs.map(id => { |
|
|
if (id.startsWith('custom_')) { |
|
|
const currentIndex = parseInt(id.replace('custom_', '')); |
|
|
if (currentIndex > index) { |
|
|
return 'custom_' + (currentIndex - 1); |
|
|
} |
|
|
} |
|
|
return id; |
|
|
}); |
|
|
|
|
|
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); |
|
|
|
|
|
|
|
|
renderCustomAPIsList(); |
|
|
|
|
|
|
|
|
updateSelectedApiCount(); |
|
|
|
|
|
|
|
|
checkAdultAPIsSelected(); |
|
|
|
|
|
showToast('已移除自定义API: ' + apiName, 'info'); |
|
|
} |
|
|
|
|
|
function toggleSettings(e) { |
|
|
const settingsPanel = document.getElementById('settingsPanel'); |
|
|
if (!settingsPanel) return; |
|
|
|
|
|
if (settingsPanel.classList.contains('show')) { |
|
|
settingsPanel.classList.remove('show'); |
|
|
} else { |
|
|
settingsPanel.classList.add('show'); |
|
|
} |
|
|
|
|
|
if (e) { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function setupEventListeners() { |
|
|
|
|
|
document.getElementById('searchInput').addEventListener('keypress', function (e) { |
|
|
if (e.key === 'Enter') { |
|
|
search(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('click', function (e) { |
|
|
|
|
|
const settingsPanel = document.querySelector('#settingsPanel.show'); |
|
|
const settingsButton = document.querySelector('#settingsPanel .close-btn'); |
|
|
|
|
|
if (settingsPanel && settingsButton && |
|
|
!settingsPanel.contains(e.target) && |
|
|
!settingsButton.contains(e.target)) { |
|
|
settingsPanel.classList.remove('show'); |
|
|
} |
|
|
|
|
|
|
|
|
const historyPanel = document.querySelector('#historyPanel.show'); |
|
|
const historyButton = document.querySelector('#historyPanel .close-btn'); |
|
|
|
|
|
if (historyPanel && historyButton && |
|
|
!historyPanel.contains(e.target) && |
|
|
!historyButton.contains(e.target)) { |
|
|
historyPanel.classList.remove('show'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const yellowFilterToggle = document.getElementById('yellowFilterToggle'); |
|
|
if (yellowFilterToggle) { |
|
|
yellowFilterToggle.addEventListener('change', function (e) { |
|
|
localStorage.setItem('yellowFilterEnabled', e.target.checked); |
|
|
|
|
|
|
|
|
const adultdiv = document.getElementById('adultdiv'); |
|
|
if (adultdiv) { |
|
|
if (e.target.checked === true) { |
|
|
adultdiv.style.display = 'none'; |
|
|
} else if (e.target.checked === false) { |
|
|
adultdiv.style.display = '' |
|
|
} |
|
|
} else { |
|
|
|
|
|
addAdultAPI(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const adFilterToggle = document.getElementById('adFilterToggle'); |
|
|
if (adFilterToggle) { |
|
|
adFilterToggle.addEventListener('change', function (e) { |
|
|
localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, e.target.checked); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function resetSearchArea() { |
|
|
|
|
|
document.getElementById('results').innerHTML = ''; |
|
|
document.getElementById('searchInput').value = ''; |
|
|
|
|
|
|
|
|
document.getElementById('searchArea').classList.add('flex-1'); |
|
|
document.getElementById('searchArea').classList.remove('mb-8'); |
|
|
document.getElementById('resultsArea').classList.add('hidden'); |
|
|
|
|
|
|
|
|
const footer = document.querySelector('.footer'); |
|
|
if (footer) { |
|
|
footer.style.position = ''; |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof updateDoubanVisibility === 'function') { |
|
|
updateDoubanVisibility(); |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
window.history.pushState( |
|
|
{}, |
|
|
`LibreTV - 免费在线视频搜索与观看平台`, |
|
|
`/` |
|
|
); |
|
|
|
|
|
document.title = `LibreTV - 免费在线视频搜索与观看平台`; |
|
|
} catch (e) { |
|
|
console.error('更新浏览器历史失败:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function getCustomApiInfo(customApiIndex) { |
|
|
const index = parseInt(customApiIndex); |
|
|
if (isNaN(index) || index < 0 || index >= customAPIs.length) { |
|
|
return null; |
|
|
} |
|
|
return customAPIs[index]; |
|
|
} |
|
|
|
|
|
|
|
|
async function search() { |
|
|
|
|
|
try { |
|
|
if (window.ensurePasswordProtection) { |
|
|
window.ensurePasswordProtection(); |
|
|
} else { |
|
|
|
|
|
if (window.isPasswordProtected && window.isPasswordVerified) { |
|
|
if (window.isPasswordProtected() && !window.isPasswordVerified()) { |
|
|
showPasswordModal && showPasswordModal(); |
|
|
return; |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn('Password protection check failed:', error.message); |
|
|
return; |
|
|
} |
|
|
const query = document.getElementById('searchInput').value.trim(); |
|
|
|
|
|
if (!query) { |
|
|
showToast('请输入搜索内容', 'info'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (selectedAPIs.length === 0) { |
|
|
showToast('请至少选择一个API源', 'warning'); |
|
|
return; |
|
|
} |
|
|
|
|
|
showLoading(); |
|
|
|
|
|
try { |
|
|
|
|
|
saveSearchHistory(query); |
|
|
|
|
|
|
|
|
let allResults = []; |
|
|
const searchPromises = selectedAPIs.map(apiId => |
|
|
searchByAPIAndKeyWord(apiId, query) |
|
|
); |
|
|
|
|
|
|
|
|
const resultsArray = await Promise.all(searchPromises); |
|
|
|
|
|
|
|
|
resultsArray.forEach(results => { |
|
|
if (Array.isArray(results) && results.length > 0) { |
|
|
allResults = allResults.concat(results); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
allResults.sort((a, b) => { |
|
|
|
|
|
const nameCompare = (a.vod_name || '').localeCompare(b.vod_name || ''); |
|
|
if (nameCompare !== 0) return nameCompare; |
|
|
|
|
|
|
|
|
return (a.source_name || '').localeCompare(b.source_name || ''); |
|
|
}); |
|
|
|
|
|
|
|
|
const searchResultsCount = document.getElementById('searchResultsCount'); |
|
|
if (searchResultsCount) { |
|
|
searchResultsCount.textContent = allResults.length; |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('searchArea').classList.remove('flex-1'); |
|
|
document.getElementById('searchArea').classList.add('mb-8'); |
|
|
document.getElementById('resultsArea').classList.remove('hidden'); |
|
|
|
|
|
|
|
|
const doubanArea = document.getElementById('doubanArea'); |
|
|
if (doubanArea) { |
|
|
doubanArea.classList.add('hidden'); |
|
|
} |
|
|
|
|
|
const resultsDiv = document.getElementById('results'); |
|
|
|
|
|
|
|
|
if (!allResults || allResults.length === 0) { |
|
|
resultsDiv.innerHTML = ` |
|
|
<div class="col-span-full text-center py-16"> |
|
|
<svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
|
|
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|
|
</svg> |
|
|
<h3 class="mt-2 text-lg font-medium text-gray-400">没有找到匹配的结果</h3> |
|
|
<p class="mt-1 text-sm text-gray-500">请尝试其他关键词或更换数据源</p> |
|
|
</div> |
|
|
`; |
|
|
hideLoading(); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const encodedQuery = encodeURIComponent(query); |
|
|
|
|
|
window.history.pushState( |
|
|
{ search: query }, |
|
|
`搜索: ${query} - LibreTV`, |
|
|
`/s=${encodedQuery}` |
|
|
); |
|
|
|
|
|
document.title = `搜索: ${query} - LibreTV`; |
|
|
} catch (e) { |
|
|
console.error('更新浏览器历史失败:', e); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const yellowFilterEnabled = localStorage.getItem('yellowFilterEnabled') === 'true'; |
|
|
if (yellowFilterEnabled) { |
|
|
const banned = ['伦理片', '福利', '里番动漫', '门事件', '萝莉少女', '制服诱惑', '国产传媒', 'cosplay', '黑丝诱惑', '无码', '日本无码', '有码', '日本有码', 'SWAG', '网红主播', '色情片', '同性片', '福利视频', '福利片']; |
|
|
allResults = allResults.filter(item => { |
|
|
const typeName = item.type_name || ''; |
|
|
return !banned.some(keyword => typeName.includes(keyword)); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const safeResults = allResults.map(item => { |
|
|
const safeId = item.vod_id ? item.vod_id.toString().replace(/[^\w-]/g, '') : ''; |
|
|
const safeName = (item.vod_name || '').toString() |
|
|
.replace(/</g, '<') |
|
|
.replace(/>/g, '>') |
|
|
.replace(/"/g, '"'); |
|
|
const sourceInfo = item.source_name ? |
|
|
`<span class="bg-[#222] text-xs px-1.5 py-0.5 rounded-full">${item.source_name}</span>` : ''; |
|
|
const sourceCode = item.source_code || ''; |
|
|
|
|
|
|
|
|
const apiUrlAttr = item.api_url ? |
|
|
`data-api-url="${item.api_url.replace(/"/g, '"')}"` : ''; |
|
|
|
|
|
// 修改为水平卡片布局,图片在左侧,文本在右侧,并优化样式 |
|
|
const hasCover = item.vod_pic && item.vod_pic.startsWith('http'); |
|
|
|
|
|
return ` |
|
|
<div class="card-hover bg-[#111] rounded-lg overflow-hidden cursor-pointer transition-all hover:scale-[1.02] h-full shadow-sm hover:shadow-md" |
|
|
onclick="showDetails('${safeId}','${safeName}','${sourceCode}')" ${apiUrlAttr}> |
|
|
<div class="flex h-full"> |
|
|
${hasCover ? ` |
|
|
<div class="relative flex-shrink-0 search-card-img-container"> |
|
|
<img src="${item.vod_pic}" alt="${safeName}" |
|
|
class="h-full w-full object-cover transition-transform hover:scale-110" |
|
|
onerror="this.onerror=null; this.src='https://via.placeholder.com/300x450?text=无封面'; this.classList.add('object-contain');" |
|
|
loading="lazy"> |
|
|
<div class="absolute inset-0 bg-gradient-to-r from-black/30 to-transparent"></div> |
|
|
</div>` : ''} |
|
|
|
|
|
<div class="p-2 flex flex-col flex-grow"> |
|
|
<div class="flex-grow"> |
|
|
<h3 class="font-semibold mb-2 break-words line-clamp-2 ${hasCover ? '' : 'text-center'}" title="${safeName}">${safeName}</h3> |
|
|
|
|
|
<div class="flex flex-wrap ${hasCover ? '' : 'justify-center'} gap-1 mb-2"> |
|
|
${(item.type_name || '').toString().replace(/</g, '<') ? |
|
|
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-blue-500 text-blue-300"> |
|
|
${(item.type_name || '').toString().replace(/</g, '<')} |
|
|
</span>` : ''} |
|
|
${(item.vod_year || '') ? |
|
|
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-purple-500 text-purple-300"> |
|
|
${item.vod_year} |
|
|
</span>` : ''} |
|
|
</div> |
|
|
<p class="text-gray-400 line-clamp-2 overflow-hidden ${hasCover ? '' : 'text-center'} mb-2"> |
|
|
${(item.vod_remarks || '暂无介绍').toString().replace(/</g, '<')} |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-between items-center mt-1 pt-1 border-t border-gray-800"> |
|
|
${sourceInfo ? `<div>${sourceInfo}</div>` : '<div></div>'} |
|
|
<!-- 接口名称过长会被挤变形 |
|
|
<div> |
|
|
<span class="text-gray-500 flex items-center hover:text-blue-400 transition-colors"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /> |
|
|
</svg> |
|
|
播放 |
|
|
</span> |
|
|
</div> |
|
|
--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
resultsDiv.innerHTML = safeResults; |
|
|
} catch (error) { |
|
|
console.error('搜索错误:', error); |
|
|
if (error.name === 'AbortError') { |
|
|
showToast('搜索请求超时,请检查网络连接', 'error'); |
|
|
} else { |
|
|
showToast('搜索请求失败,请稍后重试', 'error'); |
|
|
} |
|
|
} finally { |
|
|
hideLoading(); |
|
|
} |
|
|
} |
|
|
|
|
|
// 切换清空按钮的显示状态 |
|
|
function toggleClearButton() { |
|
|
const searchInput = document.getElementById('searchInput'); |
|
|
const clearButton = document.getElementById('clearSearchInput'); |
|
|
if (searchInput.value !== '') { |
|
|
clearButton.classList.remove('hidden'); |
|
|
} else { |
|
|
clearButton.classList.add('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
// 清空搜索框内容 |
|
|
function clearSearchInput() { |
|
|
const searchInput = document.getElementById('searchInput'); |
|
|
searchInput.value = ''; |
|
|
const clearButton = document.getElementById('clearSearchInput'); |
|
|
clearButton.classList.add('hidden'); |
|
|
} |
|
|
|
|
|
// 劫持搜索框的value属性以检测外部修改 |
|
|
function hookInput() { |
|
|
const input = document.getElementById('searchInput'); |
|
|
const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); |
|
|
|
|
|
// 重写 value 属性的 getter 和 setter |
|
|
Object.defineProperty(input, 'value', { |
|
|
get: function () { |
|
|
// 确保读取时返回字符串(即使原始值为 undefined/null) |
|
|
const originalValue = descriptor.get.call(this); |
|
|
return originalValue != null ? String(originalValue) : ''; |
|
|
}, |
|
|
set: function (value) { |
|
|
// 显式将值转换为字符串后写入 |
|
|
const strValue = String(value); |
|
|
descriptor.set.call(this, strValue); |
|
|
this.dispatchEvent(new Event('input', { bubbles: true })); |
|
|
} |
|
|
}); |
|
|
|
|
|
// 初始化输入框值为空字符串(避免初始值为 undefined) |
|
|
input.value = ''; |
|
|
} |
|
|
document.addEventListener('DOMContentLoaded', hookInput); |
|
|
|
|
|
// 显示详情 - 修改为支持自定义API |
|
|
async function showDetails(id, vod_name, sourceCode) { |
|
|
// 密码保护校验 |
|
|
if (window.isPasswordProtected && window.isPasswordVerified) { |
|
|
if (window.isPasswordProtected() && !window.isPasswordVerified()) { |
|
|
showPasswordModal && showPasswordModal(); |
|
|
return; |
|
|
} |
|
|
} |
|
|
if (!id) { |
|
|
showToast('视频ID无效', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
showLoading(); |
|
|
try { |
|
|
// 构建API参数 |
|
|
let apiParams = ''; |
|
|
|
|
|
// 处理自定义API源 |
|
|
if (sourceCode.startsWith('custom_')) { |
|
|
const customIndex = sourceCode.replace('custom_', ''); |
|
|
const customApi = getCustomApiInfo(customIndex); |
|
|
if (!customApi) { |
|
|
showToast('自定义API配置无效', 'error'); |
|
|
hideLoading(); |
|
|
return; |
|
|
} |
|
|
// 传递 detail 字段 |
|
|
if (customApi.detail) { |
|
|
apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&customDetail=' + encodeURIComponent(customApi.detail) + '&source=custom'; |
|
|
} else { |
|
|
apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&source=custom'; |
|
|
} |
|
|
} else { |
|
|
// 内置API |
|
|
apiParams = '&source=' + sourceCode; |
|
|
} |
|
|
|
|
|
// Add a timestamp to prevent caching |
|
|
const timestamp = new Date().getTime(); |
|
|
const cacheBuster = `&_t=${timestamp}`; |
|
|
const response = await fetch(`/api/detail?id=${encodeURIComponent(id)}${apiParams}${cacheBuster}`); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
const modal = document.getElementById('modal'); |
|
|
const modalTitle = document.getElementById('modalTitle'); |
|
|
const modalContent = document.getElementById('modalContent'); |
|
|
|
|
|
// 显示来源信息 |
|
|
const sourceName = data.videoInfo && data.videoInfo.source_name ? |
|
|
` <span class="text-sm font-normal text-gray-400">(${data.videoInfo.source_name})</span>` : ''; |
|
|
|
|
|
// 不对标题进行截断处理,允许完整显示 |
|
|
modalTitle.innerHTML = `<span class="break-words">${vod_name || '未知视频'}</span>${sourceName}`; |
|
|
currentVideoTitle = vod_name || '未知视频'; |
|
|
|
|
|
if (data.episodes && data.episodes.length > 0) { |
|
|
// 构建详情信息HTML |
|
|
let detailInfoHtml = ''; |
|
|
if (data.videoInfo) { |
|
|
// Prepare description text, strip HTML and trim whitespace |
|
|
const descriptionText = data.videoInfo.desc ? data.videoInfo.desc.replace(/<[^>]+>/g, '').trim() : ''; |
|
|
|
|
|
// Check if there's any actual grid content |
|
|
const hasGridContent = data.videoInfo.type || data.videoInfo.year || data.videoInfo.area || data.videoInfo.director || data.videoInfo.actor || data.videoInfo.remarks; |
|
|
|
|
|
if (hasGridContent || descriptionText) { // Only build if there's something to show |
|
|
detailInfoHtml = ` |
|
|
<div class="modal-detail-info"> |
|
|
${hasGridContent ? ` |
|
|
<div class="detail-grid"> |
|
|
${data.videoInfo.type ? `<div class="detail-item"><span class="detail-label">类型:</span> <span class="detail-value">${data.videoInfo.type}</span></div>` : ''} |
|
|
${data.videoInfo.year ? `<div class="detail-item"><span class="detail-label">年份:</span> <span class="detail-value">${data.videoInfo.year}</span></div>` : ''} |
|
|
${data.videoInfo.area ? `<div class="detail-item"><span class="detail-label">地区:</span> <span class="detail-value">${data.videoInfo.area}</span></div>` : ''} |
|
|
${data.videoInfo.director ? `<div class="detail-item"><span class="detail-label">导演:</span> <span class="detail-value">${data.videoInfo.director}</span></div>` : ''} |
|
|
${data.videoInfo.actor ? `<div class="detail-item"><span class="detail-label">主演:</span> <span class="detail-value">${data.videoInfo.actor}</span></div>` : ''} |
|
|
${data.videoInfo.remarks ? `<div class="detail-item"><span class="detail-label">备注:</span> <span class="detail-value">${data.videoInfo.remarks}</span></div>` : ''} |
|
|
</div>` : ''} |
|
|
${descriptionText ? ` |
|
|
<div class="detail-desc"> |
|
|
<p class="detail-label">简介:</p> |
|
|
<p class="detail-desc-content">${descriptionText}</p> |
|
|
</div>` : ''} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
currentEpisodes = data.episodes; |
|
|
currentEpisodeIndex = 0; |
|
|
|
|
|
modalContent.innerHTML = ` |
|
|
${detailInfoHtml} |
|
|
<div class="flex flex-wrap items-center justify-between mb-4 gap-2"> |
|
|
<div class="flex items-center gap-2"> |
|
|
<button onclick="toggleEpisodeOrder('${sourceCode}', '${id}')" |
|
|
class="px-3 py-1.5 bg-[#333] hover:bg-[#444] border border-[#444] rounded text-sm transition-colors flex items-center gap-1"> |
|
|
<svg class="w-4 h-4 transform ${episodesReversed ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path> |
|
|
</svg> |
|
|
<span>${episodesReversed ? '正序排列' : '倒序排列'}</span> |
|
|
</button> |
|
|
<span class="text-gray-400 text-sm">共 ${data.episodes.length} 集</span> |
|
|
</div> |
|
|
<button onclick="copyLinks()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors"> |
|
|
复制链接 |
|
|
</button> |
|
|
</div> |
|
|
<div id="episodesGrid" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2"> |
|
|
${renderEpisodes(vod_name, sourceCode, id)} |
|
|
</div> |
|
|
`; |
|
|
} else { |
|
|
modalContent.innerHTML = ` |
|
|
<div class="text-center py-8"> |
|
|
<div class="text-red-400 mb-2">❌ 未找到播放资源</div> |
|
|
<div class="text-gray-500 text-sm">该视频可能暂时无法播放,请尝试其他视频</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
modal.classList.remove('hidden'); |
|
|
} catch (error) { |
|
|
console.error('获取详情错误:', error); |
|
|
showToast('获取详情失败,请稍后重试', 'error'); |
|
|
} finally { |
|
|
hideLoading(); |
|
|
} |
|
|
} |
|
|
|
|
|
// 更新播放视频函数,修改为使用/watch路径而不是直接打开player.html |
|
|
function playVideo(url, vod_name, sourceCode, episodeIndex = 0, vodId = '') { |
|
|
// 密码保护校验 |
|
|
if (window.isPasswordProtected && window.isPasswordVerified) { |
|
|
if (window.isPasswordProtected() && !window.isPasswordVerified()) { |
|
|
showPasswordModal && showPasswordModal(); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 获取当前路径作为返回页面 |
|
|
let currentPath = window.location.href; |
|
|
|
|
|
// 构建播放页面URL,使用watch.html作为中间跳转页 |
|
|
let watchUrl = `watch.html?id=${vodId || ''}&source=${sourceCode || ''}&url=${encodeURIComponent(url)}&index=${episodeIndex}&title=${encodeURIComponent(vod_name || '')}`; |
|
|
|
|
|
// 添加返回URL参数 |
|
|
if (currentPath.includes('index.html') || currentPath.endsWith('/')) { |
|
|
watchUrl += `&back=${encodeURIComponent(currentPath)}`; |
|
|
} |
|
|
|
|
|
// 保存当前状态到localStorage |
|
|
try { |
|
|
localStorage.setItem('currentVideoTitle', vod_name || '未知视频'); |
|
|
localStorage.setItem('currentEpisodes', JSON.stringify(currentEpisodes)); |
|
|
localStorage.setItem('currentEpisodeIndex', episodeIndex); |
|
|
localStorage.setItem('currentSourceCode', sourceCode || ''); |
|
|
localStorage.setItem('lastPlayTime', Date.now()); |
|
|
localStorage.setItem('lastSearchPage', currentPath); |
|
|
localStorage.setItem('lastPageUrl', currentPath); // 确保保存返回页面URL |
|
|
} catch (e) { |
|
|
console.error('保存播放状态失败:', e); |
|
|
} |
|
|
|
|
|
// 在当前标签页中打开播放页面 |
|
|
window.location.href = watchUrl; |
|
|
} |
|
|
|
|
|
// 弹出播放器页面 |
|
|
function showVideoPlayer(url) { |
|
|
// 在打开播放器前,隐藏详情弹窗 |
|
|
const detailModal = document.getElementById('modal'); |
|
|
if (detailModal) { |
|
|
detailModal.classList.add('hidden'); |
|
|
} |
|
|
// 临时隐藏搜索结果和豆瓣区域,防止高度超出播放器而出现滚动条 |
|
|
document.getElementById('resultsArea').classList.add('hidden'); |
|
|
document.getElementById('doubanArea').classList.add('hidden'); |
|
|
// 在框架中打开播放页面 |
|
|
videoPlayerFrame = document.createElement('iframe'); |
|
|
videoPlayerFrame.id = 'VideoPlayerFrame'; |
|
|
videoPlayerFrame.className = 'fixed w-full h-screen z-40'; |
|
|
videoPlayerFrame.src = url; |
|
|
document.body.appendChild(videoPlayerFrame); |
|
|
// 将焦点移入iframe |
|
|
videoPlayerFrame.focus(); |
|
|
} |
|
|
|
|
|
// 关闭播放器页面 |
|
|
function closeVideoPlayer(home = false) { |
|
|
videoPlayerFrame = document.getElementById('VideoPlayerFrame'); |
|
|
if (videoPlayerFrame) { |
|
|
videoPlayerFrame.remove(); |
|
|
// 恢复搜索结果显示 |
|
|
document.getElementById('resultsArea').classList.remove('hidden'); |
|
|
// 关闭播放器时也隐藏详情弹窗 |
|
|
const detailModal = document.getElementById('modal'); |
|
|
if (detailModal) { |
|
|
detailModal.classList.add('hidden'); |
|
|
} |
|
|
// 如果启用豆瓣区域则显示豆瓣区域 |
|
|
if (localStorage.getItem('doubanEnabled') === 'true') { |
|
|
document.getElementById('doubanArea').classList.remove('hidden'); |
|
|
} |
|
|
} |
|
|
if (home) { |
|
|
// 刷新主页 |
|
|
window.location.href = '/' |
|
|
} |
|
|
} |
|
|
|
|
|
// 播放上一集 |
|
|
function playPreviousEpisode(sourceCode) { |
|
|
if (currentEpisodeIndex > 0) { |
|
|
const prevIndex = currentEpisodeIndex - 1; |
|
|
const prevUrl = currentEpisodes[prevIndex]; |
|
|
playVideo(prevUrl, currentVideoTitle, sourceCode, prevIndex); |
|
|
} |
|
|
} |
|
|
|
|
|
// 播放下一集 |
|
|
function playNextEpisode(sourceCode) { |
|
|
if (currentEpisodeIndex < currentEpisodes.length - 1) { |
|
|
const nextIndex = currentEpisodeIndex + 1; |
|
|
const nextUrl = currentEpisodes[nextIndex]; |
|
|
playVideo(nextUrl, currentVideoTitle, sourceCode, nextIndex); |
|
|
} |
|
|
} |
|
|
|
|
|
// 处理播放器加载错误 |
|
|
function handlePlayerError() { |
|
|
hideLoading(); |
|
|
showToast('视频播放加载失败,请尝试其他视频源', 'error'); |
|
|
} |
|
|
|
|
|
// 辅助函数用于渲染剧集按钮(使用当前的排序状态) |
|
|
function renderEpisodes(vodName, sourceCode, vodId) { |
|
|
const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes; |
|
|
return episodes.map((episode, index) => { |
|
|
// 根据倒序状态计算真实的剧集索引 |
|
|
const realIndex = episodesReversed ? currentEpisodes.length - 1 - index : index; |
|
|
return ` |
|
|
<button id="episode-${realIndex}" onclick="playVideo('${episode}','${vodName.replace(/"/g, '"')}', '${sourceCode}', ${realIndex}, '${vodId}')" |
|
|
class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors text-center episode-btn"> |
|
|
${realIndex + 1} |
|
|
</button> |
|
|
`; |
|
|
}).join(''); |
|
|
} |
|
|
|
|
|
|
|
|
function copyLinks() { |
|
|
const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes; |
|
|
const linkList = episodes.join('\r\n'); |
|
|
navigator.clipboard.writeText(linkList).then(() => { |
|
|
showToast('播放链接已复制', 'success'); |
|
|
}).catch(err => { |
|
|
showToast('复制失败,请检查浏览器权限', 'error'); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function toggleEpisodeOrder(sourceCode, vodId) { |
|
|
episodesReversed = !episodesReversed; |
|
|
|
|
|
const episodesGrid = document.getElementById('episodesGrid'); |
|
|
if (episodesGrid) { |
|
|
episodesGrid.innerHTML = renderEpisodes(currentVideoTitle, sourceCode, vodId); |
|
|
} |
|
|
|
|
|
|
|
|
const toggleBtn = document.querySelector(`button[onclick="toggleEpisodeOrder('${sourceCode}', '${vodId}')"]`); |
|
|
if (toggleBtn) { |
|
|
toggleBtn.querySelector('span').textContent = episodesReversed ? '正序排列' : '倒序排列'; |
|
|
const arrowIcon = toggleBtn.querySelector('svg'); |
|
|
if (arrowIcon) { |
|
|
arrowIcon.style.transform = episodesReversed ? 'rotate(180deg)' : 'rotate(0deg)'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function importConfigFromUrl() { |
|
|
|
|
|
let modal = document.getElementById('importUrlModal'); |
|
|
if (modal) { |
|
|
document.body.removeChild(modal); |
|
|
} |
|
|
|
|
|
modal = document.createElement('div'); |
|
|
modal.id = 'importUrlModal'; |
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40'; |
|
|
|
|
|
modal.innerHTML = ` |
|
|
<div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative"> |
|
|
<button id="closeUrlModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">×</button> |
|
|
|
|
|
<h3 class="text-xl font-bold mb-4">从URL导入配置</h3> |
|
|
|
|
|
<div class="mb-4"> |
|
|
<input type="text" id="configUrl" placeholder="输入配置文件URL" |
|
|
class="w-full px-3 py-2 bg-[#222] border border-[#333] rounded-lg text-white focus:outline-none focus:ring-1 focus:ring-blue-500"> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-end space-x-2"> |
|
|
<button id="confirmUrlImport" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">导入</button> |
|
|
<button id="cancelUrlImport" class="bg-[#444] hover:bg-[#555] text-white px-4 py-2 rounded">取消</button> |
|
|
</div> |
|
|
</div>`; |
|
|
|
|
|
document.body.appendChild(modal); |
|
|
|
|
|
|
|
|
document.getElementById('closeUrlModal').addEventListener('click', () => { |
|
|
document.body.removeChild(modal); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('cancelUrlImport').addEventListener('click', () => { |
|
|
document.body.removeChild(modal); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('confirmUrlImport').addEventListener('click', async () => { |
|
|
const url = document.getElementById('configUrl').value.trim(); |
|
|
if (!url) { |
|
|
showToast('请输入配置文件URL', 'warning'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const urlObj = new URL(url); |
|
|
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { |
|
|
showToast('URL必须以http://或https://开头', 'warning'); |
|
|
return; |
|
|
} |
|
|
} catch (e) { |
|
|
showToast('URL格式不正确', 'warning'); |
|
|
return; |
|
|
} |
|
|
|
|
|
showLoading('正在从URL导入配置...'); |
|
|
|
|
|
try { |
|
|
|
|
|
const response = await fetch(url, { |
|
|
mode: 'cors', |
|
|
headers: { |
|
|
'Accept': 'application/json' |
|
|
} |
|
|
}); |
|
|
if (!response.ok) throw '获取配置文件失败'; |
|
|
|
|
|
|
|
|
const contentType = response.headers.get('content-type'); |
|
|
if (!contentType || !contentType.includes('application/json')) { |
|
|
throw '响应不是有效的JSON格式'; |
|
|
} |
|
|
|
|
|
const config = await response.json(); |
|
|
if (config.name !== 'LibreTV-Settings') throw '配置文件格式不正确'; |
|
|
|
|
|
|
|
|
const dataHash = await sha256(JSON.stringify(config.data)); |
|
|
if (dataHash !== config.hash) throw '配置文件哈希值不匹配'; |
|
|
|
|
|
|
|
|
for (let item in config.data) { |
|
|
localStorage.setItem(item, config.data[item]); |
|
|
} |
|
|
|
|
|
showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success'); |
|
|
setTimeout(() => { |
|
|
window.location.reload(); |
|
|
}, 3000); |
|
|
} catch (error) { |
|
|
const message = typeof error === 'string' ? error : '导入配置失败'; |
|
|
showToast(`从URL导入配置出错 (${message})`, 'error'); |
|
|
} finally { |
|
|
hideLoading(); |
|
|
document.body.removeChild(modal); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
modal.addEventListener('click', (e) => { |
|
|
if (e.target === modal) { |
|
|
document.body.removeChild(modal); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async function importConfig() { |
|
|
showImportBox(async (file) => { |
|
|
try { |
|
|
|
|
|
if (!(file.type === 'application/json' || file.name.endsWith('.json'))) throw '文件类型不正确'; |
|
|
|
|
|
|
|
|
if (file.size > 1024 * 1024 * 10) throw new Error('文件大小超过 10MB'); |
|
|
|
|
|
|
|
|
const content = await new Promise((resolve, reject) => { |
|
|
const reader = new FileReader(); |
|
|
reader.onload = () => resolve(reader.result); |
|
|
reader.onerror = () => reject('文件读取失败'); |
|
|
reader.readAsText(file); |
|
|
}); |
|
|
|
|
|
|
|
|
const config = JSON.parse(content); |
|
|
if (config.name !== 'LibreTV-Settings') throw '配置文件格式不正确'; |
|
|
|
|
|
|
|
|
const dataHash = await sha256(JSON.stringify(config.data)); |
|
|
if (dataHash !== config.hash) throw '配置文件哈希值不匹配'; |
|
|
|
|
|
|
|
|
for (let item in config.data) { |
|
|
localStorage.setItem(item, config.data[item]); |
|
|
} |
|
|
|
|
|
showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success'); |
|
|
setTimeout(() => { |
|
|
window.location.reload(); |
|
|
}, 3000); |
|
|
} catch (error) { |
|
|
const message = typeof error === 'string' ? error : '配置文件格式错误'; |
|
|
showToast(`配置文件读取出错 (${message})`, 'error'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async function exportConfig() { |
|
|
|
|
|
const config = {}; |
|
|
const items = {}; |
|
|
|
|
|
const settingsToExport = [ |
|
|
'selectedAPIs', |
|
|
'customAPIs', |
|
|
'yellowFilterEnabled', |
|
|
'adFilteringEnabled', |
|
|
'doubanEnabled', |
|
|
'hasInitializedDefaults' |
|
|
]; |
|
|
|
|
|
|
|
|
settingsToExport.forEach(key => { |
|
|
const value = localStorage.getItem(key); |
|
|
if (value !== null) { |
|
|
items[key] = value; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const viewingHistory = localStorage.getItem('viewingHistory'); |
|
|
if (viewingHistory) { |
|
|
items['viewingHistory'] = viewingHistory; |
|
|
} |
|
|
|
|
|
const searchHistory = localStorage.getItem(SEARCH_HISTORY_KEY); |
|
|
if (searchHistory) { |
|
|
items[SEARCH_HISTORY_KEY] = searchHistory; |
|
|
} |
|
|
|
|
|
const times = Date.now().toString(); |
|
|
config['name'] = 'LibreTV-Settings'; |
|
|
config['time'] = times; |
|
|
config['cfgVer'] = '1.0.0'; |
|
|
config['data'] = items; |
|
|
config['hash'] = await sha256(JSON.stringify(config['data'])); |
|
|
|
|
|
|
|
|
saveStringAsFile(JSON.stringify(config), 'LibreTV-Settings_' + times + '.json'); |
|
|
} |
|
|
|
|
|
|
|
|
function saveStringAsFile(content, fileName) { |
|
|
|
|
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); |
|
|
|
|
|
const url = window.URL.createObjectURL(blob); |
|
|
|
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = fileName; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
|
|
|
document.body.removeChild(a); |
|
|
window.URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
|
|
|
|