import { contentPath } from '../routing'; import type { Chapter, Drama, DramaDetail, PageData } from "../api/types"; import type { SiteContext } from "./context"; import { configureDownloadLinks } from "./download"; import { bindSeriesDescription } from "./detail"; import { chapterAccess, initialPlayMode, MediaPlayback, playbackUrl, type PlayMode, type PlayerActions } from './playback'; import { dramaSeoContent } from '../seo'; import type { PlayerInitialData } from '../page-data'; import { episodeItems, episodeTabs, orderedChapters, playerMarkup } from './player-markup'; import { formatCount } from '../api/protocol'; import { applyEngagementSuccess, beginButtonRequest, type EngagementKind } from './request-feedback'; declare global { interface Window { installPlayerControls?: (screen: HTMLElement, episode?: number, actions?: PlayerActions)=>void } } interface PlayerPagination {initial?:PlayerInitialData;preserveMarkup?:boolean} export async function renderPlayer(app:SiteContext,id:number,chapterId?:number,isVideo=false,paging:PlayerPagination={}):Promise<()=>void> { if(isVideo)throw new Error('当前接口暂未提供剧集内容'); const signal=app.signal; const page=document.getElementById('drama-play-page')!; const raw = paging.initial?.detail || await app.api.get('playlet/detail',{id},signal); if(signal.aborted)return ()=>{}; if(!raw?.id||raw.id!==id)throw new Error('该剧集不存在或已下架'); const [chapters,related]=await Promise.all([ paging.initial?Promise.resolve(paging.initial.chapters):app.api.get<{list?:Chapter[]}>('playlet/chapter/list',{playletId:id},signal).then(data=>data?.list).catch(error=>{if(signal.aborted)throw error;return raw.playletChapterInfo;}), paging.initial?Promise.resolve(undefined):app.api.get>('playlet/guess-like/list',{playletId:id,pageNum:1,pageSize:12},signal).catch(error=>{if(signal.aborted)throw error;return undefined;}), ]); if(!paging.initial)raw.relatedRecommendData=related?.list||[]; if(signal.aborted)return ()=>{}; const ordered=orderedChapters(chapters ?? raw.playletChapterInfo); const first=ordered.find(item=>item.currentEpisode===1)||ordered[0]; let chapter: Chapter | null = null; if(chapterId){ if(ordered.length&&!ordered.some(item=>item.id===chapterId))throw new Error('该章节不存在或已下架'); chapter=paging.initial?.chapter || await app.api.get('playlet/chapter/detail',{playletId:id,chapterId},signal); if(!signal.aborted&&(!chapter||chapter.id!==chapterId||chapter.playletId&&chapter.playletId!==id))throw new Error('章节信息不匹配,请重新选择'); } if(signal.aborted)return ()=>{}; const overview=!chapterId&&!isVideo; const source=isVideo?playbackUrl(raw.video?.url):''; const access=isVideo?{allowed:!!source,gate:source?'':'unavailable',source} as const:chapterAccess(chapter); const previewSource=isVideo?'':playbackUrl(raw.preview); let mode:PlayMode=initialPlayMode(access,raw.preview); page.classList.toggle('is-overview',overview); app.updateSeo?.({...dramaSeoContent(raw,chapter||undefined),failed:false,unavailable:isVideo&&!source}); const objectId=isVideo?id:chapter?.id||id; const actionPrefix=isVideo?'video':chapter?'playlet/chapter':'playlet'; const actionState=chapter||raw; const pendingEngagements=new Set(); function syncEngagement(kind:EngagementKind,buttons:HTMLButtonElement[]){ const active=kind==='like'?actionState.isLike===1:actionState.isCollect===1; buttons.forEach(button=>{ button.setAttribute('aria-pressed',String(active)); const label=button.querySelector('span');if(label)label.textContent=kind==='like'?(active?'已点赞':'点赞'):(active?'已收藏':'收藏'); }); const value=kind==='like'?actionState.countLike:actionState.countCollect; page.querySelectorAll(`[data-engage-count="${kind}"]`).forEach(node=>{node.textContent=`${formatCount(value)}${kind==='like'?'点赞':'收藏'}`;}); } if(!paging.preserveMarkup)page.innerHTML=playerMarkup(raw,ordered,{chapter,isVideo,home:app.home}); configureDownloadLinks(app.config, page); const disposeDescription=bindSeriesDescription(page,signal); let episodeSize=matchMedia('(max-width:760px)').matches?15:45; let episodePage=chapter?Math.floor(Math.max(0,ordered.findIndex(item=>item.id===chapter!.id))/episodeSize):0; function renderEpisodes(){ const size=matchMedia('(max-width:760px)').matches?15:45; if(size!==episodeSize){episodePage=Math.floor(episodePage*episodeSize/size);episodeSize=size;} const pages=Math.ceil(ordered.length/size); episodePage=Math.min(Math.max(0,episodePage),Math.max(0,pages-1)); const segment=page.querySelector('.play-episode-pages'),grid=page.querySelector('.play-episode-grid'); if(!grid||!segment)return; segment.innerHTML=episodeTabs(ordered,size,episodePage); const track=segment.querySelector('.episode-page-track'),selected=segment.querySelector('[aria-pressed="true"]'); if(track&&selected){ const bounds=track.getBoundingClientRect(),tab=selected.getBoundingClientRect(); if(tab.leftbounds.right)track.scrollLeft+=tab.right-bounds.right; } grid.innerHTML=episodeItems(id,ordered,chapter,size,episodePage,raw.slug,raw.title||raw.playName||''); } renderEpisodes(); window.addEventListener('resize',renderEpisodes,{signal}); const video=page.querySelector('video'); let media:MediaPlayback|undefined; let reported=false; if(video){ const box=page.querySelector('.play-screen')!; const status=page.querySelector('.play-source-status')!; media=new MediaPlayback(video,{signal,status:message=>{status.textContent=message;},error:()=>{box.dispatchEvent(new Event('playback:error'));}}); const toggle=async()=>{if(mode==='main'&&!access.allowed)return;await media?.toggle();}; window.installPlayerControls?.(box,chapter?.currentEpisode||1,{toggle,retry:async()=>{if(mode==='main'&&!access.allowed)return;await media?.retry();}}); const position=ordered.findIndex(item=>item.id===chapter?.id); const previous=box.querySelector('[data-play-action="previous"]'); const next=box.querySelector('[data-play-action="next"]'); if(previous)previous.disabled=position<=0;if(next)next.disabled=position<0||position>=ordered.length-1; function setMode(nextMode:PlayMode){ mode=nextMode;box.dataset.playMode=mode; page.querySelectorAll('[data-play-mode]').forEach(button=>{if(button.tagName==='BUTTON')button.setAttribute('aria-pressed',String(button.dataset.playMode===mode));}); const blocked=mode==='main'&&!access.allowed; box.querySelector('.ui-empty-state')?.remove(); const center=box.querySelector('[data-media="toggle"]');if(center)center.hidden=false; status.hidden=false; void media!.setSource(mode==='preview'?previewSource:access.allowed?access.source:''); if(blocked)status.textContent='本集视频暂未开放,请稍后再试'; else if(!(mode==='preview'?previewSource:access.source))status.textContent='该剧集暂未提供视频源'; else if(mode==='preview')status.textContent='免费预告 · 点击播放'; } page.querySelectorAll('button[data-play-mode]').forEach(button=>button.addEventListener('click',()=>{setMode(button.dataset.playMode as PlayMode);if(mode==='preview')void media!.toggle();},{signal})); setMode(mode); document.addEventListener('eighteen:closed',()=>{if(mode==='preview'&&video.paused)void media!.toggle();},{once:true,signal}); video.addEventListener('playing',()=>{ if(reported||mode!=='main'||!access.allowed||!chapter)return; reported=true; void app.api.post('playlet/chapter/watch/report',{playletId:id,chapterId:chapter.id}).catch(()=>{reported=false;}); },{signal}); } page.addEventListener('click',event=>{void (async()=>{ const element=(event.target as Element).closest('button');if(!element||element.disabled)return; if(element.dataset.episodePage){episodePage=Number(element.dataset.episodePage);renderEpisodes();} if(element.dataset.episodePage==='0'){episodePage=0;renderEpisodes();} if(element.dataset.playAction){const index=ordered.findIndex(item=>item.id===chapter?.id);const next=ordered[index+(element.dataset.playAction==='next'?1:-1)];if(next){app.navigate(contentPath('play',raw,next.id));}} if(element.dataset.engage){ const kind=element.dataset.engage;if(kind!=='like'&&kind!=='collect'||pendingEngagements.has(kind))return; const active=kind==='like'?actionState.isLike===1:actionState.isCollect===1; const buttons=Array.from(page.querySelectorAll(`[data-engage="${kind}"]`)); if(!buttons.length)buttons.push(element); const finish=buttons.map(button=>beginButtonRequest(button)); pendingEngagements.add(kind); try{ const result=await app.api.post(`${actionPrefix}/${kind}/set`,{[chapter?'chapterId':'id']:objectId,[kind==='like'?'isLike':'isCollect']:!active}); if(signal.aborted)return; applyEngagementSuccess(actionState,kind,active,result);syncEngagement(kind,buttons); }finally{pendingEngagements.delete(kind);finish.forEach(done=>done());} return; } if(element.hasAttribute('data-share')){const finish=beginButtonRequest(element,'复制中');try{await navigator.clipboard.writeText(location.href);if(!signal.aborted)app.notify('页面链接已复制');}finally{finish();}return;} })().catch(error=>app.notify(error instanceof Error?error.message:'操作失败'));},{signal}); return ()=>{disposeDescription();media?.dispose();}; }