| |
| 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; |
|
|
| |
| const hasAdminPassword = window.__ENV__?.ADMINPASSWORD && |
| window.__ENV__.ADMINPASSWORD.length === 64 && |
| !/^0+$/.test(window.__ENV__.ADMINPASSWORD); |
|
|
| if (settingsPanel.classList.contains('show')) { |
| settingsPanel.classList.remove('show'); |
| } else { |
| |
| if (hasAdminPassword && !isAdminVerified()) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| showAdminPasswordModal(); |
| return; |
| } |
| 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() { |
| |
| if (window.isPasswordProtected && window.isPasswordVerified) { |
| if (window.isPasswordProtected() && !window.isPasswordVerified()) { |
| showPasswordModal && showPasswordModal(); |
| 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); |
| } |
|
|
| |
|
|