Files
Stardream e3bf77296c
Build and Push Docker Image / build (push) Successful in 17s
Initial release
2026-06-16 20:27:36 +10:00

126 lines
11 KiB
JavaScript

'use strict';
function openModal(id){ const m=$(id); if(!m) return; m.classList.remove('closing'); m.classList.add('show'); }
function closeModal(id){ const m=$(id); if(!m || !m.classList.contains('show')) return; if(id==='settingsModal' && authReady && typeof restoreAppearance==='function') restoreAppearance(); m.classList.add('closing'); const ms=(state.settings.motion==='off')?0:200; setTimeout(()=>m.classList.remove('show','closing'), ms); }
function closeAllModals(){ closeDrawer(); document.querySelectorAll('.modal-mask.show').forEach(m=>closeModal(m.id)); }
function setupDropdown(rootId, hiddenId, labelId, onChange){
const root=$(rootId), hidden=$(hiddenId), label=$(labelId); if(!root || !hidden || !label) return;
const trigger=root.querySelector('.select-trigger'); const menu=root.querySelector('.select-menu');
function close(){ root.classList.remove('open'); }
function setValue(value, silent=false){
hidden.value = String(value); const opt=[...menu.querySelectorAll('button')].find(b=>String(b.dataset.value)===String(value)) || menu.querySelector('button');
if(opt) label.textContent = opt.textContent;
menu.querySelectorAll('button').forEach(b=>b.classList.toggle('active', String(b.dataset.value)===String(value)));
if(!silent) onChange?.(String(value));
}
trigger.onclick=(e)=>{ e.stopPropagation(); document.querySelectorAll('.selectish.open').forEach(x=>{ if(x!==root) x.classList.remove('open'); }); root.classList.toggle('open'); };
menu.querySelectorAll('button').forEach(btn=>btn.onclick=(e)=>{ e.stopPropagation(); setValue(btn.dataset.value); close(); });
document.addEventListener('click', close);
setValue(hidden.value || menu.querySelector('button')?.dataset.value || '', true);
return {setValue};
}
let typeDropdownCtl = null;
function syncCustomControls(){
typeDropdownCtl = setupDropdown('typeDropdown','typeFilter','typeDropdownLabel',(value)=>{ resetRenderLimit(); filters.type=Number(value); filters.tag='全部'; render(); });
setupDropdown('sortDropdown','sortSelect','sortDropdownLabel',(value)=>{ resetRenderLimit(); filters.sort=value; renderCards(); });
}
// Reset every filter (sort is ordering, not a filter, so it's kept). Also resets the related UI controls.
function clearFilters(){
filters.status=0; filters.type=0; filters.tag='全部'; filters.q=''; filters.nsfw='all'; filters.hasComment=false;
if($('searchInput')) $('searchInput').value='';
typeDropdownCtl?.setValue('0', true);
refreshNsfwBtn();
resetRenderLimit(); render();
}
function bindEvents(){
syncCustomControls();
bindSegmentedControls();
$('settingsBtn').onclick=()=>{
fillSettingsUI();
if(typeof snapshotAppearance==='function') snapshotAppearance();
if(!authReady && isPublicMode){
document.querySelectorAll('.settings-tab').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.settings-pane').forEach(p=>p.classList.remove('active'));
const at=document.querySelector('[data-tab="stab-appearance"]'), ap=$('stab-appearance');
if(at) at.classList.add('active');
if(ap) ap.classList.add('active');
}
openModal('settingsModal');
};
document.querySelectorAll('.settings-tab').forEach(tab=>{
tab.onclick=()=>{
document.querySelectorAll('.settings-tab').forEach(t=>t.classList.remove('active'));
document.querySelectorAll('.settings-pane').forEach(p=>p.classList.remove('active'));
tab.classList.add('active');
const pane=$( tab.dataset.tab); if(pane) pane.classList.add('active');
};
});
$('exportBtn').onclick=()=> openModal('exportModal');
document.querySelectorAll('[data-close]').forEach(b=>b.onclick=()=>closeModal(b.dataset.close));
document.querySelectorAll('.modal-mask').forEach(m=>{ let downOnMask=false; m.addEventListener('pointerdown', e=>{ downOnMask=(e.target===m); }); m.addEventListener('click', e=>{ if(e.target===m && downOnMask){ closeModal(m.id); if(m.id==='syncModal') showSyncMini(); } }); });
$('syncMiniBar').onclick=()=>{ hideSyncMini(); openModal('syncModal'); };
$('openTokenBtn').onclick=()=> window.open(TOKEN_URL,'_blank','noopener'); $('testLoginBtn').onclick=testLogin;
if($('tokenInput')){ const emptyClip=e=>{ e.preventDefault(); try{ e.clipboardData&&e.clipboardData.setData('text/plain',''); }catch{} }; $('tokenInput').addEventListener('copy',emptyClip); $('tokenInput').addEventListener('cut',emptyClip); } $('saveSettingsBtn').onclick=saveSettingsFromUI; $('changePasswordBtn').onclick=changeAdminPassword; $('logoutBtn').onclick=logout;
$('syncBtn').onclick=()=>openSyncConfirm('recent'); $('fullSyncBtn').onclick=()=>openSyncConfirm('full'); $('syncConfirmStartBtn').onclick=confirmStartSync; $('cancelSyncBtn').onclick=()=>{ closeModal('syncModal'); showSyncMini(); };
// Appearance number inputs apply live (preview). Admin: preview only, persisted via 保存设置, reverted on close-without-save
// (these keys are in APPEARANCE_KEYS). Guest: no save button, so persist to guestSettings now.
if($('commentLinesInput')) $('commentLinesInput').addEventListener('input',()=>{ state.settings.commentLines=clampNum($('commentLinesInput').value,0,6,0); applyAppearance(false); if(!authReady) saveSettings(); });
if($('borderWidthInput')) $('borderWidthInput').addEventListener('input',()=>{ state.settings.borderWidth=clampNum($('borderWidthInput').value,0,4,1); applyAppearance(false); if(!authReady) saveSettings(); });
if($('pageSizeInput')) $('pageSizeInput').addEventListener('change',()=>{ state.settings.pageSize=clampNum($('pageSizeInput').value,40,360,120); resetRenderLimit(); renderCards(); if(!authReady) saveSettings(); });
$('searchInput').oninput=e=>{ resetRenderLimit(); filters.q=e.target.value; renderCards(); };
if($('tagFilterInput')) $('tagFilterInput').oninput=()=>renderTags();
// Mobile: off-canvas sidebar drawer
const openSide=(v)=>{ document.querySelector('.side')?.classList.toggle('open',v); $('sideBackdrop')?.classList.toggle('open',v); $('menuBtn')?.classList.toggle('open',v); };
if($('menuBtn')) $('menuBtn').onclick=()=>openSide(true);
if($('sideBackdrop')) $('sideBackdrop').onclick=()=>openSide(false);
window.addEventListener('keydown',e=>{ if(e.key==='Escape') openSide(false); });
// Filter-summary pill doubles as a one-click "clear all filters" button (shows ✕ when active — see CSS)
if($('filterSummary')) $('filterSummary').onclick=clearFilters;
// "有吐槽" stat: toggle a filter that shows only items with a comment
const toggleHasComment=()=>{ filters.hasComment=!filters.hasComment; resetRenderLimit(); render(); openSide(false); };
if($('statCommentsCard')){ $('statCommentsCard').onclick=toggleHasComment; $('statCommentsCard').onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggleHasComment(); } }; }
$('statusNav')?.addEventListener('click',e=>{ if(e.target.closest('button')) openSide(false); });
$('tagList')?.addEventListener('click',e=>{ if(e.target.closest('button')) openSide(false); });
// Drawer "操作" buttons delegate to the real (hidden-on-mobile) toolbar buttons
[['mSettings','settingsBtn'],['mSyncRecent','syncBtn'],['mFullSync','fullSyncBtn'],['mExport','exportBtn'],['mImport','importBtn'],['mCacheCovers','cacheCoversBtn'],['mEnrichTags','enrichTagsBtn']].forEach(([m,real])=>{ const b=$(m); if(b) b.onclick=()=>{ $(real)?.click(); openSide(false); }; });
if($('tagListMore')) $('tagListMore').onclick=()=>{ const tl=$('tagList'); const exp=tl.classList.toggle('tags-expanded'); $('tagListMore').textContent=exp?'收起':'展开更多'; };
if($('tagSearchToggle')) $('tagSearchToggle').onclick=()=>{ const f=$('tagFilter'); const inp=$('tagFilterInput'); const willOpen=!f.classList.contains('open'); $('tagSearchToggle').classList.toggle('active',willOpen); if(willOpen){ f.classList.add('open'); const ms=document.documentElement.dataset.motion==='off'?0:300; setTimeout(()=>{ if(f.classList.contains('open')){ f.style.overflow='visible'; inp?.focus(); } }, ms); } else { f.style.overflow=''; f.classList.remove('open'); if(inp){ inp.value=''; renderTags(); } } };
$('typeFilter').onchange=e=>{ resetRenderLimit(); filters.type=Number(e.target.value); filters.tag='全部'; render(); };
$('sortSelect').onchange=e=>{ resetRenderLimit(); filters.sort=e.target.value; renderCards(); };
document.querySelectorAll('.theme-toggle').forEach(b=>b.onclick=(e)=>toggleTheme(e));
$('cacheCoversBtn').onclick=cacheCoversCurrent; if($('enrichTagsBtn')) $('enrichTagsBtn').onclick=enrichPublicTagsCurrent;
$('nsfwBtn').onclick=()=>{ const next={all:'hide',hide:'only',only:'all'}; filters.nsfw=next[filters.nsfw]||'all'; resetRenderLimit(); refreshNsfwBtn(); render(); };
bindCronEditor();
const plsb=$('publicLoginSubmitBtn');
if(plsb) plsb.onclick=async()=>{ const pw=$('publicLoginPasswordInput')?.value||''; const st=$('publicLoginStatus'); if(st) st.textContent='验证中…'; try{ await apiJSON('/api/login',{method:'POST',body:JSON.stringify({password:pw})}); location.reload(); }catch(err){ if(st) st.textContent='密码不正确,请重试'; } };
$('exportJsonBtn').onclick=exportJson; $('exportCsvBtn').onclick=exportCsv; $('exportHtmlBtn').onclick=exportHtml; $('exportDocxBtn').onclick=exportDocx; $('exportXlsxBtn').onclick=exportXlsx; $('exportZipBtn').onclick=exportZip;
$('importBtn').onclick=()=> $('importFile').click(); $('importFile').onchange=e=>{ const f=e.target.files[0]; if(f) importBackup(f).catch(err=>toast('导入失败:'+err.message)); e.target.value=''; };
window.addEventListener('keydown',e=>{ if(e.key==='Escape') closeAllModals(); });
}
function hideLoadingScreen(){ const el=document.getElementById('loadingScreen'); if(el) el.classList.add('done'); }
async function init(){
try{
// Offline single-file export: no server — render the embedded state directly.
if(window.BANGUMI_VAULT_OFFLINE_STATE){
await loadState();
bindEvents();
refreshSegmentedControls();
render();
applyAuthVisibility();
return;
}
if(!(await ensureSession())){ hideLoadingScreen(); return; }
await loadState();
bindEvents();
refreshSegmentedControls();
window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change',()=>{ if((state.settings.themeMode||'dark')==='system') applyAppearance(true); });
render();
applyAuthVisibility();
// Scheduled sync now runs in the container (server cron). If a sync is already
// running server-side when the page opens, attach the progress UI to it.
resumeSyncIfRunning();
} finally { hideLoadingScreen(); }
}
init();