mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-13 22:41:06 +08:00
fix(media): restore recognition scrolling and add bulk selection
This commit is contained in:
1 parent
385e68de78
commit
0f06d6e9f3
5 files changed
+31
-4
No files matched your search
@@ -14,7 +14,7 @@ function number(text:string):number|undefined{
|
||||
return text?result+current:undefined;
|
||||
}
|
||||
export function seasonDirectory(name:string):number|undefined{
|
||||
const match=/^(?:season[ ._-]*|s)(\d{1,3})$/i.exec(name)||/^第([零〇一二两三四五六七八九十百\d]+)季$/.exec(name);
|
||||
const match=/^(?:season[ ._-]*|s)(\d{1,3})$/i.exec(name)||/第([零〇一二两三四五六七八九十百\d]+)季$/.exec(name);
|
||||
if(match)return number(match[1]!);
|
||||
if(/^(specials?|特别篇|特別篇)$/i.test(name))return 0;
|
||||
return undefined;
|
||||
|
||||
@@ -19,6 +19,8 @@ test('video parsing separates release tokens from meaningful numerals and resolv
|
||||
assert.equal(cleanVideoTitle('001. Film').title,'001 Film');
|
||||
assert.equal(cleanVideoTitle('001. Film',true).title,'Film');
|
||||
assert.equal(seasonDirectory('第二十一季'),21);assert.equal(seasonDirectory('Specials'),0);
|
||||
const peppa=recognizeVideo('动画/小猪佩奇/小猪佩奇第1季/小猪佩奇第一季.Peppa.Pig.Season.1.E01.4K.WEB-DL.H265.AAC-OurTV.mp4',raw(),[{path:'动画/小猪佩奇',mode:'series',title:'小猪佩奇'}]);
|
||||
assert.equal(peppa.kind,'episode');assert.equal(peppa.metadata.season,1);assert.equal(peppa.metadata.episode,1);
|
||||
for(const [ref,season,episode] of [
|
||||
['Drama/Drama_S01E01_1080p.mp4',1,1],['Drama/Drama.1x02.mkv',1,2],
|
||||
['Drama/Season 02/EP03.mp4',2,3],['Drama/第三季/004.mp4',3,4],
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
.media-recognition-form{max-width:100%;padding:16px;box-sizing:border-box}
|
||||
.media-recognition-form{max-width:100%;padding:16px;box-sizing:border-box;min-height:0;overflow-y:auto;overscroll-behavior:contain;margin:0;display:block}
|
||||
.media-recognition-form>label,.media-recognition-form>.media-toolbar{margin:14px 0}
|
||||
.media-recognition-form .media-toolbar{flex-wrap:wrap;gap:8px}
|
||||
.media-recognition-form .media-recognition-check{display:flex;align-items:flex-start;gap:8px}
|
||||
.media-recognition-form input[type=checkbox]{width:18px;min-width:18px;height:18px;accent-color:var(--media-accent)}
|
||||
.media-recognition-form input[type=checkbox]{width:18px;min-width:18px;height:18px;min-height:18px;max-height:18px;padding:0;margin:3px 0 0;flex:0 0 18px;box-sizing:border-box;accent-color:var(--media-accent)}
|
||||
.media-recognition-form .media-recognition-check{line-height:24px}
|
||||
.media-recognition-bulk{position:sticky;top:-16px;z-index:2;background:var(--reader-paper);padding:10px 0;border-bottom:1px solid var(--border)}
|
||||
.media-recognition-bulk p{margin:0;font-size:13px}
|
||||
.media-recognition-row strong{line-height:24px}
|
||||
.media-recognition-preview{border-top:1px solid var(--border);padding-top:16px}
|
||||
.media-recognition-preview>p{font-size:13px;line-height:1.6;opacity:.8}
|
||||
.media-recognition-form .media-recognition-row{display:flex;flex-direction:row;align-items:flex-start;gap:10px;padding:12px 0;border-bottom:1px solid var(--border)}
|
||||
|
||||
@@ -36,6 +36,9 @@ export function FolderRecognition({api,libraryId,path,disabled,onBusy,onApplied}
|
||||
if(!signal.aborted){setRevision(result.revision);setNotice('目录规则已保存。已有作品尚未更改。');}
|
||||
}
|
||||
const description=(row:Proposal)=>row.after.kind==='ignore'?'跳过扫描':row.after.kind==='episode'?`${row.after.metadata.show} · 第 ${row.after.metadata.season} 季第 ${row.after.metadata.episode} 集`:`电影 · ${row.after.metadata.title}`;
|
||||
const selectable=(row:Proposal)=>['ready','review'].includes(row.status);
|
||||
const visible=preview?.items.slice(page*30,page*30+30)??[];
|
||||
const selectRows=(rows:Proposal[])=>setSelected(current=>[...new Set([...current,...rows.filter(selectable).map(row=>row.assetId)])]);
|
||||
return <>
|
||||
<div className="media-toolbar"><span>影视识别</span><button disabled={disabled} onClick={show}><Settings2 size={16} aria-hidden="true"/>识别规则与预览</button></div>
|
||||
{open&&<Modal title="目录识别规则" busy={busy} onClose={()=>setOpen(false)}><div className="media-form media-recognition-form">
|
||||
@@ -54,10 +57,15 @@ export function FolderRecognition({api,libraryId,path,disabled,onBusy,onApplied}
|
||||
{mode==='ignore'&&<p>后续扫描跳过该目录的新文件。已有作品和文件保留,不会因此被标记缺失。</p>}
|
||||
<div className="media-toolbar"><button disabled={busy||!revision} onClick={()=>void work(async signal=>{await save(signal);if(!signal.aborted){invalidate();setNotice('目录规则已保存。已有作品尚未更改。');}})}>保存规则</button><button className="media-primary" disabled={busy||!revision} onClick={()=>void work(async signal=>{await save(signal);if(signal.aborted)return;const result=await api.request<Preview>(base+'/recognition-preview','POST',{path},signal);if(!signal.aborted){setPreview(result);setSelected(result.items.filter(row=>row.status==='ready').map(row=>row.assetId));setPage(0);}})}>{busy?'正在处理…':'保存并预览重新识别'}</button></div>
|
||||
{preview&&<section className="media-recognition-preview" aria-label="重新识别预览"><h3>识别结果 · {preview.items.length} 个文件</h3><p>待确认结果默认不选;已人工整理、在线确认或共享多版本的作品保留。确认后保留资源、版本和播放片段 ID,已有播放进度与队列继续有效。</p>
|
||||
<div className="media-recognition-bulk"><p role="status">已选 {selected.length} / {preview.items.filter(selectable).length} 项(跨页保留)</p><div className="media-toolbar">
|
||||
<button disabled={busy||!visible.some(selectable)} onClick={()=>selectRows(visible)}>全选本页</button>
|
||||
<button disabled={busy||!preview.items.some(selectable)} onClick={()=>selectRows(preview.items)}>全选可应用项</button>
|
||||
<button disabled={busy||!selected.length} onClick={()=>setSelected([])}>清空选择</button>
|
||||
<button className="media-primary" disabled={busy||!selected.length} onClick={()=>setConfirm(true)}>应用所选 {selected.length} 项</button>
|
||||
</div><p>全选包含待确认结果,请先核对;受保护与忽略项不会选中。</p></div>
|
||||
{!preview.items.length&&<p>没有已扫描且可用的资源,请先扫描媒体库。</p>}
|
||||
{preview.items.slice(page*30,page*30+30).map(row=><label key={row.assetId} className="media-recognition-row"><input type="checkbox" aria-label={'应用 '+row.ref} disabled={busy||!['ready','review'].includes(row.status)} checked={selected.includes(row.assetId)} onChange={e=>setSelected(e.currentTarget.checked?[...selected,row.assetId]:selected.filter(id=>id!==row.assetId))}/><span><strong>{row.ref}</strong><small>当前:{row.before?.title??'未关联'}</small><b>{description(row)}</b><small>{({ready:'可应用',review:'待确认',protected:'保留现有',ignored:'已忽略'} as const)[row.status]} · {row.reason}</small></span></label>)}
|
||||
{preview.items.length>30&&<nav className="media-toolbar" aria-label="识别预览分页"><button disabled={busy||!page} onClick={()=>setPage(page-1)}>上一页</button><span>{page+1} / {Math.ceil(preview.items.length/30)}</span><button disabled={busy||(page+1)*30>=preview.items.length} onClick={()=>setPage(page+1)}>下一页</button></nav>}
|
||||
<button className="media-primary" disabled={busy||!selected.length} onClick={()=>setConfirm(true)}>应用所选 {selected.length} 项</button>
|
||||
</section>}
|
||||
</div></Modal>}
|
||||
{confirm&&<FloatingConfirm title="确认重新识别" text={`将按预览调整 ${selected.length} 个文件的标题和作品归属。同一剧、季、集的版本可能合并到一个作品,收藏随之保留。不修改原文件,保留播放进度与队列。`} confirmText="应用识别结果" onCancel={()=>setConfirm(false)} onConfirm={()=>{setConfirm(false);void work(async signal=>{await api.request(base+'/recognition-apply','POST',{path,previewId:preview!.id,assetIds:selected},signal);if(!signal.aborted){setOpen(false);onApplied();}});}}/>}
|
||||
|
||||
@@ -28,3 +28,15 @@ it('retains saved-rule notice if preview fails and reports no successful applica
|
||||
act(()=>render(<FolderRecognition api={{request} as unknown as MediaApi} libraryId="lib" path="Show" disabled={false} onBusy={vi.fn()} onApplied={onApplied}/>,root));
|
||||
await click('识别规则与预览');await vi.waitFor(()=>expect(root.querySelector<HTMLButtonElement>('.media-primary')?.disabled).toBe(false));await click('保存并预览');await vi.waitFor(()=>expect(root.textContent).toContain('500'));expect(root.textContent).toContain('目录规则已保存');expect(onApplied).not.toHaveBeenCalled();
|
||||
});
|
||||
it('batch selection spans pages, excludes protected resources, and can be cleared',async()=>{
|
||||
const items=Array.from({length:65},(_,i)=>({...preview.items[i%4]!,assetId:String(i),ref:`Show/${i}.mp4`}));
|
||||
const request=vi.fn().mockResolvedValueOnce(rule).mockResolvedValueOnce({revision:'next'}).mockResolvedValueOnce({id:'many',items});
|
||||
act(()=>render(<FolderRecognition api={{request} as unknown as MediaApi} libraryId="lib" path="Show" disabled={false} onBusy={vi.fn()} onApplied={vi.fn()}/>,root));
|
||||
await click('识别规则与预览');await vi.waitFor(()=>expect(root.querySelector<HTMLButtonElement>('.media-primary')?.disabled).toBe(false));await click('保存并预览');
|
||||
await vi.waitFor(()=>expect(root.querySelectorAll('.media-recognition-row')).toHaveLength(30));
|
||||
await click('清空选择');await click('全选本页');expect(root.textContent).toContain('已选 16 / 33 项');
|
||||
await click('下一页');expect(root.textContent).toContain('已选 16 / 33 项');
|
||||
await click('全选可应用项');expect(root.textContent).toContain('已选 33 / 33 项');
|
||||
expect(root.querySelectorAll('.media-recognition-row input:disabled:checked')).toHaveLength(0);
|
||||
await click('清空选择');expect(root.textContent).toContain('已选 0 / 33 项');
|
||||
});
|
||||
Reference in new issue
Block a user