播放页引导下载添加

This commit is contained in:
谢宇宁
2026-10-27 21:45:47 +07:00
parent 93a64beaa0
commit 3c9ab2630f
6 changed files with 31 additions and 12 deletions
+4 -1
View File
@@ -28,7 +28,10 @@ echo "[2/5] 打包 standalone..."
echo "[3/5] 生成 tar.gz 和 SHA-256..."
# 从 dist/production/ 下进入打 tar,避免外层目录名出现在压缩包里;up_next_h5_prod.sh 预期入口在根(start.mjs 等)。
# node_modules 不打进包,远端 npm ci --omit=dev 自己装 Linux 原生依赖;--no-xattrs 去 macOS 扩展属性噪音。
(cd "$DIR/dist/production" && tar --no-xattrs --exclude='./node_modules' --exclude='.DS_Store' -czf "$STAGE/$PACKAGE_NAME" --owner=0 --group=0 .)
# 属主统一为 root:GNU tar 用 --owner/--group,macOS 自带的 bsdtar 用 --uid/--gid/--uname/--gname;COPYFILE_DISABLE 防止打入 ._* 文件。
if tar --version 2>/dev/null | grep -q 'GNU tar'; then OWNER_FLAGS='--owner=0 --group=0'; else OWNER_FLAGS='--uid 0 --gid 0 --uname root --gname root'; fi
# shellcheck disable=SC2086 # OWNER_FLAGS 需要按空格拆成多个参数
(cd "$DIR/dist/production" && COPYFILE_DISABLE=1 tar --no-xattrs --exclude='./node_modules' --exclude='.DS_Store' $OWNER_FLAGS -czf "$STAGE/$PACKAGE_NAME" .)
(cd "$STAGE" && shasum -a 256 "$PACKAGE_NAME" > "$PACKAGE_NAME.sha256")
ls -lh "$STAGE/$PACKAGE_NAME" "$STAGE/$PACKAGE_NAME.sha256"
+2 -2
View File
@@ -1,7 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next-dev/dev/types/routes.d.ts";
import "./.next-dev/dev/types/root-params.d.ts";
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+9
View File
@@ -3142,3 +3142,12 @@ html body .hg-footer-original .hg-footer-original-title .footer-brand-text{displ
.hg-detail-actions .play-detail-actions{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px}
@media(max-width:760px){.hg-detail-actions .play-detail-actions{width:100%}.hg-detail-actions .play-detail-actions>button{flex:1 1 0;min-width:0}.hg-detail-actions .detail-play,.hg-detail-actions .series-download:not([hidden]){flex:1 1 auto;justify-content:center}}
@media(max-width:760px){#drama-play-page .hg-detail-actions .play-detail-actions{flex-wrap:nowrap}#drama-play-page .hg-detail-actions .play-detail-actions>button{flex:1 1 0;width:auto;min-width:0;max-width:none;padding:9px 6px;justify-content:center}}
.play-screen .hg-app-gate:not([hidden]){position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;background:radial-gradient(circle at 50% 40%,#1d1e1ee6,#050505f2);text-align:center}
.hg-app-gate-copy{display:flex;flex-direction:column;gap:6px}
.hg-app-gate-copy strong{color:#fff;font-size:clamp(17px,2vw,22px);font-weight:700}
.hg-app-gate-copy p,.hg-app-gate-fallback{margin:0;color:#c9c9c9;font-size:14px;line-height:1.7}
.hg-app-gate-btn:not([hidden]){display:inline-flex;align-items:center;gap:8px;height:46px;padding:0 26px;border-radius:999px;font-size:15px;font-weight:700;text-decoration:none}
.hg-app-gate-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8}
.hg-app-gate:has(.hg-app-gate-btn:not([hidden])) .hg-app-gate-fallback{display:none}
.hg-app-gate:has(.hg-app-gate-btn[hidden]) .hg-app-gate-copy{display:none}
.play-screen:has(.hg-app-gate:not([hidden]))>:not(.hg-app-gate):not(video){visibility:hidden}
+7 -2
View File
@@ -13,6 +13,11 @@ export function orderedChapters(value:unknown):Chapter[] {
return recordList<Chapter>(value).filter(item=>Number.isSafeInteger(item.id)&&item.id>0).sort((a,b)=>(a.currentEpisode||0)-(b.currentEpisode||0));
}
/** Shown in place of the player when this episode has no playable source: invites the visitor to the App. */
export function appGate(heading:string,visible=false):string {
return `<div class="hg-app-gate"${visible?'':' hidden'}><div class="hg-app-gate-copy"><strong>本集请在 黄果吃瓜 App 观看</strong><p>下载 App 即可继续观看《${escape(heading)}》及全部剧集</p></div><p class="hg-app-gate-fallback">本集视频暂未开放,请稍后再试</p><a class="primary hg-app-gate-btn" data-app-download data-app-download-entry target="_blank" rel="sponsored nofollow noopener noreferrer" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 5h4M11 19h2"/></svg><span>下载 App 继续观看</span></a></div>`;
}
/** Recommended titles replace comments: crawlable internal links with full context for each title. */
export function recommendationsMarkup(raw:DramaDetail):string {
const related=recordList<Drama>(raw.relatedRecommendData).filter(item=>item.id!==raw.id).slice(0,12);
@@ -25,11 +30,11 @@ export function playerMarkup(raw:DramaDetail,ordered:Chapter[],options:PlayerVie
const displayTitle=title(raw),episodes=raw.totalEpisode||ordered.length,actionState=chapter||raw;
const heading=chapter?`${displayTitle} 第${chapter.currentEpisode||1}集`:displayTitle;
const access=isVideo?{allowed:!!playbackUrl(raw.video?.url),gate:playbackUrl(raw.video?.url)?'':'unavailable',source:playbackUrl(raw.video?.url)} as const:chapterAccess(chapter);
const previewSource=isVideo?'':playbackUrl(raw.preview),mode=initialPlayMode(access,raw.preview);
const previewSource=isVideo?'':playbackUrl(raw.preview),mode=initialPlayMode(access,raw.preview),blocked=!!chapter&&mode==='main'&&!access.allowed;
const modeTabs=previewSource?`<div class="channel-tabs play-mode-tabs" role="group" aria-label="播放内容"><button type="button" data-play-mode="main" aria-pressed="${mode==='main'}">正片</button><button type="button" data-play-mode="preview" aria-pressed="${mode==='preview'}">免费预告</button></div>`:'';
const initialEpisodePage=chapter?Math.floor(Math.max(0,ordered.findIndex(item=>item.id===chapter.id))/45):0;
const actions=()=>`<div class="play-detail-actions"><button type="button" data-engage="like" aria-pressed="${actionState.isLike===1}">${detailActionIcon('like')}<span>${actionState.isLike===1?'已点赞':'点赞'}</span></button><button type="button" data-engage="collect" aria-pressed="${actionState.isCollect===1}">${detailActionIcon('save')}<span>${actionState.isCollect===1?'已收藏':'收藏'}</span></button><button type="button" data-share>${detailActionIcon('share')}<span>分享</span></button></div>`;
const screen=()=>`${modeTabs}<div class="play-screen" data-play-mode="${mode}"><video ${INLINE_VIDEO_ATTRIBUTES} preload="metadata" ${imageAttributes(raw.cover||raw.coverImg,undefined,'poster')} aria-label="${escape(heading)}"></video><div class="play-screen-overlay"><button type="button" aria-label="播放">▶</button><p class="play-source-status" role="status">正在准备视频…</p></div><div class="play-screen-controls"><button data-play-action="next" ${!chapter||ordered.at(-1)?.id===chapter.id?'disabled':''}>下一集</button></div></div>`;
const screen=()=>`${modeTabs}<div class="play-screen" data-play-mode="${mode}"><video ${INLINE_VIDEO_ATTRIBUTES} preload="metadata" ${imageAttributes(raw.cover||raw.coverImg,undefined,'poster')} aria-label="${escape(heading)}"></video><div class="play-screen-overlay"><button type="button" aria-label="播放"${blocked?' hidden':''}>▶</button><p class="play-source-status" role="status"${blocked?' hidden':''}>正在准备视频…</p></div>${appGate(heading,blocked)}<div class="play-screen-controls"><button data-play-action="next" ${!chapter||ordered.at(-1)?.id===chapter.id?'disabled':''}>下一集</button></div></div>`;
const applicationAds=promotionRow(raw.advitise?.nineGridList||home.advitise?.nineGridList);
const bannerAds=ads(raw.advitise?.detailbannerList,'play-banner-ad');
const watchInfo=()=>`<section class="hg-watch-info">${chapter?`<span class="play-episode-caption">正在观看 第 ${chapter.currentEpisode||1} 集 / 共 ${episodes} 集</span>`:''}<h1 id="play-detail-title">${escape(heading)}</h1>${detailFacts(raw,episodes,actionState)}${actions()}</section>${synopsisMarkup(raw,`《${displayTitle}》剧情简介`)}`;
+5 -4
View File
@@ -97,11 +97,12 @@ export async function renderPlayer(app:SiteContext,id:number,chapterId?:number,i
page.querySelectorAll<HTMLButtonElement>('[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<HTMLButtonElement>('[data-media="toggle"]');if(center)center.hidden=false;
status.hidden=false;
const center=box.querySelector<HTMLButtonElement>('[data-media="toggle"]');if(center)center.hidden=blocked;
const gate=box.querySelector<HTMLElement>('.hg-app-gate');if(gate)gate.hidden=!blocked;
status.hidden=blocked;
void media!.setSource(mode==='preview'?previewSource:access.allowed?access.source:'');
if(blocked)status.textContent='本集视频暂未开放,请稍后再试';
else if(!(mode==='preview'?previewSource:access.source))status.textContent='该剧集暂未提供视频源';
// A blocked main episode shows the App gate instead of a status line.
if(!blocked&&!(mode==='preview'?previewSource:access.source))status.textContent='该剧集暂未提供视频源';
else if(mode==='preview')status.textContent='免费预告 · 点击播放';
}
page.querySelectorAll<HTMLButtonElement>('button[data-play-mode]').forEach(button=>button.addEventListener('click',()=>{setMode(button.dataset.playMode as PlayMode);if(mode==='preview')void media!.toggle();},{signal}));
+4 -3
View File
@@ -277,7 +277,7 @@ function playerFixture({allowed=false,preview=true,detail={},chapterList,mobile=
mainButton.dataset.playMode='main';previewButton.dataset.playMode='preview';
const node=(parent,key)=>{if(!parent.nodes.has(key))parent.nodes.set(key,new Element());return parent.nodes.get(key);};
page.nodes.set('video',video);page.nodes.set('.play-screen',box);page.nodes.set('.play-source-status',status);
box.nodes.set('.play-screen-overlay',overlay);box.nodes.set('[data-media="toggle"]',new Element('BUTTON'));
box.nodes.set('.play-screen-overlay',overlay);box.nodes.set('[data-media="toggle"]',new Element('BUTTON'));box.nodes.set('.hg-app-gate',new Element());
box.nodes.set('[data-play-action="previous"]',new Element('BUTTON'));box.nodes.set('[data-play-action="next"]',new Element('BUTTON'));
for(const key of ['.play-episode-pages','.play-episode-grid'])node(page,key);
page.lists.set('button[data-play-mode]',preview?[mainButton,previewButton]:[]);page.lists.set('[data-play-mode]',preview?[box,mainButton,previewButton]:[box]);
@@ -377,15 +377,16 @@ test('slug playback links survive client episode rendering and next controls, an
}finally{dispose?.();f.dispose();}
});
test('chapters without a source offer the free preview; main shows a not-yet-available notice and never reports',async()=>{
test('chapters without a source offer the free preview; main shows the App download gate and never reports',async()=>{
const f=playerFixture();let dispose;
try{
dispose=await renderPlayer(f.app,23456,90042);await flush();
assert.equal(f.box.dataset.playMode,'preview');assert.equal(f.video.src,'https://cdn.example/preview.mp4');assert.equal(f.video.plays,0);
await f.controlActions.toggle();assert.equal(f.video.plays,1);assert.equal(f.posts.length,0);
f.mainButton.dispatchEvent(new Event('click'));await flush();
assert.equal(f.video.src,'');assert.equal(f.box.dataset.playMode,'main');assert.equal(f.status.textContent,'本集视频暂未开放,请稍后再试');
assert.equal(f.video.src,'');assert.equal(f.box.dataset.playMode,'main');assert.equal(f.box.querySelector('.hg-app-gate').hidden,false);assert.equal(f.status.hidden,true);assert.equal(f.box.querySelector('[data-media="toggle"]').hidden,true);
await f.controlActions.toggle();await f.controlActions.retry();assert.equal(f.lock.open,false);assert.equal(f.posts.length,0);assert.equal(f.video.plays,1);
f.previewButton.dispatchEvent(new Event('click'));await flush();assert.equal(f.box.querySelector('.hg-app-gate').hidden,true);assert.equal(f.status.hidden,false);
assert.ok(!f.box.querySelector('.play-screen-overlay').innerHTML.includes('data-show-unlock'));
}finally{dispose?.();f.dispose();}
});