diff --git a/web/src/media/folder-cleanup.tsx b/web/src/media/folder-cleanup.tsx index 5452f4f..09285b5 100644 --- a/web/src/media/folder-cleanup.tsx +++ b/web/src/media/folder-cleanup.tsx @@ -8,8 +8,8 @@ export interface CleanupPreview { } export interface CleanupResult extends CleanupPreview {returnPath:string} -export function FolderCleanup({api,libraryId,path,disabled,onBusy,onCleaned}:{ - api:MediaApi;libraryId:string;path:string;disabled:boolean;onBusy:(busy:boolean)=>void;onCleaned:(result:CleanupResult)=>void; +export function FolderCleanup({api,libraryId,path,disabled,onBusy,onCleaned,menuOnly=false}:{ + api:MediaApi;libraryId:string;path:string;disabled:boolean;onBusy:(busy:boolean)=>void;onCleaned:(result:CleanupResult)=>void;menuOnly?:boolean; }){ const [preview,setPreview]=useState(null),[message,setMessage]=useState(''),[error,setError]=useState(''); const request=useRef(null); @@ -30,7 +30,7 @@ export function FolderCleanup({api,libraryId,path,disabled,onBusy,onCleaned}:{ finally{if(!abort.signal.aborted){request.current=null;onBusy(false);}} } return <> -
失效资源检查当前目录及子目录的缺失记录
+ <>{menuOnly?:
失效资源检查当前目录及子目录的缺失记录
} {message&&

{message}

}{error&&

{error}

} {preview&&setPreview(null)} onConfirm={()=>void run(true)} text={`清理「${path||'库内根目录'}」及其子目录中的 ${preview.assets} 个缺失文件记录。将移除 ${preview.parts} 个播放片段、${preview.editions} 个空版本和 ${preview.items} 个空作品/分类,以及所有用户关联的 ${preview.favorites} 条收藏、${preview.progress} 条播放进度和 ${preview.queue} 条队列记录。不会删除原始文件或仍有资源的作品。此操作不可撤销;如目录暂未挂载,请取消并在恢复后重新扫描。`}/>} ; diff --git a/web/src/media/folder-recognition.tsx b/web/src/media/folder-recognition.tsx index ac21dce..357fecb 100644 --- a/web/src/media/folder-recognition.tsx +++ b/web/src/media/folder-recognition.tsx @@ -11,7 +11,7 @@ interface Proposal {assetId:string;ref:string;before:{id:string;title:string;kin interface Preview {id:string;items:Proposal[]} const labels:Record={auto:'自动识别',movie:'电影目录',series:'剧集目录',season:'指定季目录',ignore:'忽略目录'}; -export function FolderRecognition({api,libraryId,path,disabled,onBusy,onApplied}:{api:MediaApi;libraryId:string;path:string;disabled:boolean;onBusy:(busy:boolean)=>void;onApplied:()=>void}){ +export function FolderRecognition({api,libraryId,path,disabled,onBusy,onApplied,menuOnly=false}:{api:MediaApi;libraryId:string;path:string;disabled:boolean;onBusy:(busy:boolean)=>void;onApplied:()=>void;menuOnly?:boolean}){ const [open,setOpen]=useState(false),[busy,setBusy]=useState(false),[error,setError]=useState(''),[notice,setNotice]=useState(''); const [mode,setMode]=useState('inherit'),[title,setTitle]=useState(''),[season,setSeason]=useState(''),[year,setYear]=useState(''),[strip,setStrip]=useState(false),[filePattern,setFilePattern]=useState(''); const [revision,setRevision]=useState(''),[inherited,setInherited]=useState(null),[preview,setPreview]=useState(null),[selected,setSelected]=useState([]),[confirm,setConfirm]=useState(false),[page,setPage]=useState(0); @@ -41,7 +41,7 @@ export function FolderRecognition({api,libraryId,path,disabled,onBusy,onApplied} 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 <> -
影视识别设置当前目录及子目录的识别规则
+ <>{menuOnly?:
影视识别设置当前目录及子目录的识别规则
} {open&&setOpen(false)}>

{path||'库内根目录'} · 包含子目录,子目录自己的规则优先。目录内文件统一应用下方文件正则。规则保存后用于后续扫描;已有作品通过下方预览确认重新识别。

{inherited&&

继承自「{inherited.path||'库内根目录'}」:{labels[inherited.mode]}{inherited.title?' · '+inherited.title:''}

} diff --git a/web/src/media/folders.tsx b/web/src/media/folders.tsx index 4eb6de4..7047c4a 100644 --- a/web/src/media/folders.tsx +++ b/web/src/media/folders.tsx @@ -41,11 +41,12 @@ export function MediaFolders({api,admin=false,video=false,libraryId,initialLocat const segments=path?path.split('/'):[]; return
+ {listing&&
更多操作
} {(path||assetId)&&} {error&&setRetry(value=>value+1)}/>}{actionError&&
{actionError}
}{notice&&

{notice}

} {loading&&} {cleanupNotice&&

{cleanupNotice}

} - {listing&&<>{admin&&video&&{setCleanupNotice('已应用识别结果,原文件与播放进度保留。');setRetry(value=>value+1);}}/>}{admin&&{setCleanupNotice('已清理 '+result.assets+' 个失效资源记录'+(result.returnPath!==path?',已返回上级有效目录。':'。'));setPath(result.returnPath);setOffset(0);setRetry(value=>value+1);}}/>}{listing.items.map(entry=>
)}{!listing.total&&

暂无已扫描媒体文件,请先扫描媒体库。

}{listing.total>60&&}

按最近扫描结果显示媒体文件;不包含空目录及其他文件。原始目录只读。

} + {listing&&<>{listing.items.map(entry=>
)}{!listing.total&&

暂无已扫描媒体文件,请先扫描媒体库。

}{listing.total>60&&}

按最近扫描结果显示媒体文件;不包含空目录及其他文件。原始目录只读。

} {file&&<>
{file.items.map(item=>setEditions({...editions,[item.id]:id})} positions={chapters} onPosition={(id,position)=>setChapters({...chapters,[id]:position})} onDetail={()=>onDetail(item.id,{path,offset,assetId,editions,chapters})} onRefresh={()=>setRetry(value=>value+1)} onPlay={(parts,index)=>void action(()=>onPlay(parts,index,item.title))} onQueue={ids=>void action(()=>onQueue(ids),true)}/>)}{!file.items.length&&
}
}
; diff --git a/web/src/media/resource-panel.tsx b/web/src/media/resource-panel.tsx index e784a44..6d1c9c3 100644 --- a/web/src/media/resource-panel.tsx +++ b/web/src/media/resource-panel.tsx @@ -5,15 +5,14 @@ import {ResourceInfo} from './resource-info.tsx'; import {EditionTools,type EditionDetailsProps} from './edition-details.tsx'; import type {MediaApi} from './api.ts'; -export function ResourcePanel({api,assets,summary,editionOptions,versionPicker,sourceInfo}:{api:MediaApi;assets:Array<{id:string;title:string}>;summary:string;versionPicker?:ComponentChildren;sourceInfo?:ComponentChildren;editionOptions?:EditionDetailsProps|undefined}){ +export function ResourcePanel({api,assets,summary,editionOptions,versionPicker,sourceInfo,openPanel,onPanelChange}:{api:MediaApi;assets:Array<{id:string;title:string}>;summary:string;versionPicker?:ComponentChildren;sourceInfo?:ComponentChildren;openPanel?:'files'|'source'|null;onPanelChange?:(panel:'files'|'source'|null)=>void;editionOptions?:EditionDetailsProps|undefined}){ const [panel,setPanel]=useState<'files'|'versions'|'source'|null>(null),menu=useRef(null); const files=[...new Map(assets.map(asset=>[asset.id,asset])).values()]; const manage=!!(editionOptions?.onUpdated||editionOptions?.onAssigned||editionOptions?.onRename); + useEffect(()=>{if(openPanel)setPanel(openPanel);},[openPanel]); useEffect(()=>{const close=(event:PointerEvent)=>{if(menu.current&&!menu.current.contains(event.target as Node))menu.current.open=false;};document.addEventListener('pointerdown',close);return()=>document.removeEventListener('pointerdown',close);},[]); function open(value:'files'|'versions'|'source'){if(menu.current){menu.current.open=false;menu.current.querySelector('summary')?.focus();}setPanel(value);} - function close(){setPanel(null);menu.current?.querySelector('summary')?.focus();} - return
{if(event.target instanceof HTMLDialogElement&&!editionOptions?.busy){const rect=event.target.getBoundingClientRect();if(event.clientXrect.right||event.clientYrect.bottom)close();}}}>

资源信息

{(files.length>0||manage||versionPicker||sourceInfo)&&
{if(event.key==='Escape'){event.preventDefault();event.stopPropagation();menu.current!.open=false;menu.current!.querySelector('summary')?.focus();}}}>
}
-

{summary}

- {panel&&
{panel==='files'?files.map(asset=>):panel==='source'?sourceInfo:<>{versionPicker}{manage&&editionOptions&&}}
} -
; + function close(){setPanel(null);onPanelChange?.(null);menu.current?.querySelector('summary')?.focus();} + return <>{panel&&
{panel==='files'?files.map(asset=>):panel==='source'?sourceInfo:<>{versionPicker}{manage&&editionOptions&&}}
}
{if(event.target instanceof HTMLDialogElement&&!editionOptions?.busy){const rect=event.target.getBoundingClientRect();if(event.clientXrect.right||event.clientYrect.bottom)close();}}}>

资源信息

{(files.length>0||manage||versionPicker||sourceInfo)&&
{if(event.key==='Escape'){event.preventDefault();event.stopPropagation();menu.current!.open=false;menu.current!.querySelector('summary')?.focus();}}}>
}

{summary}

; } + diff --git a/web/src/media/screen.tsx b/web/src/media/screen.tsx index 0d9081b..fef0ade 100644 --- a/web/src/media/screen.tsx +++ b/web/src/media/screen.tsx @@ -118,6 +118,7 @@ export class MediaScreen { private historyTotal=0; private activity:Activity[]=[]; private clearQueueIds:string[]|null=null; + private resourcePanelRequest:'files'|'source'|null=null; private favorite=false; private managing=false; private managerTab:'libraries'|'tasks'='libraries'; @@ -460,19 +461,19 @@ export class MediaScreen { } private trackList(items:Item[]){return
{items.map((item,index)=>
)}
;} private grid(items:Item[],compactSeasons=false){return
item.kind==='artist')?"media-grid media-people-grid":"media-grid"}>{items.map(item=>)}
;} - private detailMenu(){const person=this.detail?.kind==='artist';return
;} + private detailMenu(){const person=this.detail?.kind==='artist';return
;} private detailView(detail:Detail){ const edition=detail.editions.find(entry=>entry.id===this.selectedEditionId)||detail.editions[0]; const playable=edition?.parts.filter(part=>part.available)||[]; const plot=String(detail.overrides.plot??detail.metadata.plot??'').trim(); const favorite=; const editionOptions:EditionDetailsProps|undefined=edition?{api:this.api,item:detail,edition,busy:this.busy,...(detail.editions.length>1?{onChooseVersion:()=>{this.element.querySelector('[aria-label="资源信息操作"]')?.querySelector('summary')?.click();}}:{}),onRefresh:()=>void this.run(async()=>{this.detail=await this.api.detail(detail.id,this.abort.signal);}),...(this.admin?{onUpdated:(updated:Detail)=>{this.detail=updated;this.draw();},onAssigned:(target:Detail)=>this.navigate(this.channel,target.id),onRename:(label:string)=>{edition.label=label;this.draw();}}:{}),onQueue:ids=>void this.run(()=>this.enqueue(ids)),onPlay:(parts,index)=>void this.run(()=>this.player.play(parts.map(part=>({part,title:detail.title+' · '+part.title,video:this.channel==='video'})),index))}:undefined; - const resourceAside=
- ; + ; } private metadataPage(hidden=false){return this.metadataView&&this.detail&&this.admin?:null;} private metadataEditionTools(){ @@ -514,18 +515,18 @@ export class MediaScreen { :<> {this.taskTab==='media'?<> -
扫描所有媒体库并建立索引
{this.scanNotice&&

{this.scanNotice}

} +

媒体库扫描

扫描所有媒体库并建立索引

{this.scanNotice&&

{this.scanNotice}

} {this.scanActionError&&

{this.scanActionError}

}

媒体库扫描历史{this.jobLibraryId?' · '+this.allLibraries.find(l=>l.id===this.jobLibraryId)?.name:''}

{this.jobState==='loading'&&!this.busy&&}{this.jobState==='error'&&

{this.jobError}

}{this.jobState==='ready'&&this.jobs.length===0&&

暂无媒体库扫描历史。

} l.id===this.jobLibraryId)?.name||'媒体库'} busy={this.busy||this.jobState!=='ready'} onRetry={id=>void this.scan(id||this.jobLibraryId)} onCancel={id=>void this.run(async()=>{await this.api.request('jobs/'+id+'/cancel','POST');await this.loadJobs(this.jobLibraryId);})} onDelete={id=>void this.run(async()=>{await this.api.request('scan-jobs/'+id,'DELETE');await this.loadJobs(this.jobLibraryId);})}/> :this.taskTab==='ai'?<> -
AI 识别媒体类型
{this.scanNotice&&

{this.scanNotice}

} +

AI 扫描

识别媒体类型并生成待处理结果

{this.scanNotice&&

{this.scanNotice}

}

AI 扫描历史

void this.loadAiJobs()} onDelete={id=>void this.run(async()=>{await this.api.deleteAiScanJob(id);await this.loadAiJobs();})}/> :<> -
在线匹配媒体信息
+

刮削

在线匹配媒体信息并复核结果

} {this.taskTab!=='scrape'&&

目录不可访问时保留原有资料。扫描与在线匹配分开执行,匹配失败不影响本地播放。

} diff --git a/web/src/media/v2.css b/web/src/media/v2.css index 54d6d36..138c24b 100644 --- a/web/src/media/v2.css +++ b/web/src/media/v2.css @@ -519,3 +519,6 @@ body[data-media-theme] {--surface-muted:var(--media-theme-surface)} .media-screen .media-folder-operation>button:hover{border-color:var(--media-accent)} .media-scrape-manager>.media-task-create{margin-top:0}.media-scrape-manager>.media-task-history{margin-top:28px} @media(max-width:520px){.media-screen .media-folder-operation{align-items:stretch;flex-direction:column}.media-folder-operation>button{align-self:flex-start}} + +.media-resource-hidden{display:none!important}.media-menu-submenu{position:relative}.media-menu-submenu>summary{list-style:none;cursor:pointer}.media-menu-submenu>summary::-webkit-details-marker{display:none}.media-menu-submenu>nav{display:none;position:absolute;right:100%;top:-6px;min-width:150px;padding:6px;border:1px solid var(--border);border-radius:10px;background:var(--surface);box-shadow:0 8px 24px rgba(0,0,0,.12)}.media-menu-submenu[open]>nav{display:grid;gap:3px}.media-menu-submenu>nav button{text-align:left}.media-folder-actions{display:flex;justify-content:flex-end;margin:8px 0}.media-folder-actions>nav{display:grid;gap:4px}.media-task-intro{display:flex;align-items:center;justify-content:space-between;gap:20px;margin:4px 0 20px;padding:18px 20px;border:1px solid var(--border);border-radius:14px;background:var(--surface-muted,#f3f4ef)}.media-task-intro h2{margin:0;font-size:20px;color:var(--text)}.media-task-intro p{margin:6px 0 0;color:var(--muted);font-size:13px}.media-task-intro .media-task-actions{display:flex;flex-wrap:wrap;gap:8px}.media-task-intro button{min-height:40px}.media-screen .media-task-tabs{border-radius:10px;background:var(--surface-muted,#f3f4ef);padding:5px}.media-screen .media-task-tabs button[aria-current=page]{background:var(--media-accent);color:#fff;box-shadow:none}.media-screen .media-task-tabs button{font-weight:650}.media-screen .media-folder-actions>summary{padding:9px 14px;border:1px solid var(--border);border-radius:9px;background:var(--surface);cursor:pointer}.media-screen .media-folder-actions[open]>summary{border-color:var(--media-accent)} +@media(max-width:640px){.media-task-intro{align-items:stretch;flex-direction:column;gap:14px;margin-bottom:16px;padding:15px}.media-task-intro h2{font-size:18px}.media-task-intro .media-task-actions{display:grid;grid-template-columns:1fr}.media-task-intro .media-task-actions button,.media-task-intro>button{width:100%}.media-screen .media-task-tabs{display:grid;grid-template-columns:repeat(3,1fr);width:100%;box-sizing:border-box}.media-screen .media-task-tabs button{padding-inline:5px;font-size:13px}.media-menu-submenu>nav{right:0;top:100%}}