From b345d64337eabb0296c65d78a4c9a065e5499d18 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Wed, 12 Aug 2026 22:33:30 +0800 Subject: [PATCH 1/2] docs: clarify project purpose and usage boundaries --- README.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2230ed9..a990334 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -# 抖音广告跳过(Douyin Ad Skipper) +# 抖音网页版社区片段助手(Douyin Ad Skipper) -一个非官方、社区驱动的 Chrome 扩展,用于跳过抖音网页版中带有明确广告标识的视频,以及用户手动标记的视频内部广告片段。 +一个非官方、开源、社区驱动的浏览器扩展,用于在抖音网页版中标记视频内部的赞助、自我推广、互动提醒等可选片段,并按照用户选择显示或跳过这些片段。 + +这个项目源于个人浏览需求,主要通过 AI 辅助完成设计、开发、测试与文档整理。它仍处于持续完善阶段,不代表成熟的商业产品;欢迎用户在 GitHub 上提出问题、分享使用体验并共同讨论改进方案。 > [!IMPORTANT] > 本项目与抖音、字节跳动、SponsorBlock 或 BilibiliSponsorBlock 没有隶属、授权或背书关系。“抖音”仅用于说明兼容的网站。 +> +> 本项目不是平台广告拦截器:不拦截抖音的广告请求,不隐藏或破解平台官方广告,不伪造播放、点击或曝光数据,也不绕过付费内容。它只处理用户正在观看的普通作品时间轴中的社区标记片段,所有跳过行为均可由用户关闭或撤销。 ## 当前能力 -- 识别“广告 / 商业推广 / 广告推广 / 推广”等明确页面标识并切换到下一条。 -- 在抖音播放器控制栏内创建广告片段起止时间。 +- 在抖音播放器控制栏内创建视频片段的起止时间。 - 在播放器进度条中显示已标记片段:待提交为深绿色,已提交/可信片段为亮绿色。 - 再次播放同一作品时自动跳过本地草稿或社区片段。 - 可从播放器内的提交菜单把待提交片段发送到自建社区服务。 @@ -31,7 +34,7 @@ 播放器底部清晰度按钮左侧会显示片段标记图标: -1. 广告开始时点击“片段从当前开始”。 +1. 可选片段开始时点击“片段从当前开始”。 2. 图标切换为红色取消按钮和青色结束按钮。 3. 广告结束时点击“片段现在结束”,片段先作为本地草稿保存,并以深绿色显示在进度条上。 4. 此时控制栏会出现“提交”和“删除最后一个未提交片段”按钮;点击提交会打开播放器内菜单。 @@ -40,7 +43,7 @@ 自动跳过时间片段后,提示条会短暂提供“撤销”按钮;社区片段还可直接赞成、反对或举报。 -片段管理页可以修改本地草稿的开始和结束时间。修改会清除预览状态,必须返回对应视频重新预览;社区片段跳过后可赞成、反对或按“时间错误、不是广告、视频不符、滥用”举报。 +片段管理页可以修改本地草稿的开始和结束时间。修改会清除预览状态,必须返回对应视频重新预览;社区片段跳过后可赞成、反对或按“时间错误、分类错误、视频不符、滥用”举报。 社区片段支持“自动跳过、显示提示手动跳过、不处理”三种模式。播放器快捷键默认为:`Alt+Z` 开始/结束片段、`Alt+X` 取消创建、`Alt+Enter` 打开提交菜单;可在选项页的“快捷键”中重新录制,在输入框中不会触发。 @@ -81,9 +84,19 @@ 社区查询默认启用并连接项目公共 API,也可随时关闭或改用自建服务。扩展只查询当前作品 ID,并且仅在你主动点击提交时上传片段起止时间。完整说明见 [PRIVACY.md](PRIVACY.md)。 +## 使用边界 + +- 只标记普通作品内部具有明确起止时间的可选片段,不处理平台插入的信息流广告或其他官方广告位。 +- 不得因为不喜欢某位创作者而标记正常内容,也不得把整部作品恶意标记为可跳过片段。 +- 不得使用本项目骚扰创作者、操纵播放数据、绕过付费服务或干扰平台正常运行。 +- 社区数据可能存在错误;用户可以关闭自动跳过、立即撤销跳过,并通过投票或举报参与纠错。 +- 网页结构和平台规则可能随时变化,请在遵守所在地法律、平台规则和创作者权益的前提下使用。 + ## 参与贡献 -欢迎提交问题、页面适配、交互改进和共享服务设计。开始前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 和 [SECURITY.md](SECURITY.md)。 +欢迎提交问题、页面适配、交互改进和共享服务设计。无论是否会编程,都可以通过 [GitHub Issues](https://github.com/bbnotcode/douyin-ad-skipper/issues) 描述遇到的问题、讨论产品边界或提出建议;代码改动也欢迎通过 Pull Request 共同完善。 + +本项目的大部分代码与文档由维护者在 AI 辅助下完成,并经过人工确认与测试。AI 生成内容仍可能存在疏漏,因此问题报告、代码审查和真实使用反馈尤其重要。提交代码前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md),安全或隐私问题请按照 [SECURITY.md](SECURITY.md) 私下反馈,不要公开披露敏感细节。 每次推送和 Pull Request 都会运行扩展语法、清单约束、Worker 测试和 TypeScript 检查;推送 `v版本号` 标签后会自动生成不含服务器配置的 Chrome ZIP,并发布到 GitHub Releases。 From a188b965947131f587f5fb0a967a9dac6ae31549 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Wed, 12 Aug 2026 23:28:31 +0800 Subject: [PATCH 2/2] feat: improve segment editor and refresh consistency --- CHANGELOG.md | 9 +++ PRIVACY.md | 2 +- README.md | 6 +- content.css | 75 ++++++++++++++++++------ content.js | 136 ++++++++++++++++++++++++++++++++++++++++++-- manifest.json | 2 +- server/src/index.ts | 4 +- 7 files changed, 206 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d72801e..c8e8a34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 更新日志 +## 0.6.4 - 2026-08-12 + +- 重构播放器提交窗口,按片段信息、分类、主要定位、精确编辑和提交操作分层布局,改善窄播放器中的拥挤和遮挡。 +- 每个待提交片段新增“删除此片段”“预览片段”“片段开头”“片段结尾”和“精确编辑”操作。 +- 精确编辑支持起点、终点 ±0.1 秒调整,以及可点击的“上一帧 / 下一帧”按钮和 `,` / `.` 快捷键。 +- 微调时间时改为原地更新弹窗,保持编辑区域展开,不再返回收起页面;修改后自动要求重新预览。 +- “片段开头”进入临时校准播放模式,从起点正常播放而不触发当前片段自动跳过;“片段结尾”暂停并定位到终点。 +- 社区片段查询改为强制获取最新结果,服务端禁止缓存片段查询响应,修复提交后刷新页面可能暂时无法跳过的问题。 + ## 0.6.2 - 2026-08-12 - 新增 GitHub Actions 持续集成,自动检查扩展脚本、清单、Worker 测试与类型。 diff --git a/PRIVACY.md b/PRIVACY.md index 71a06a9..3478585 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -2,7 +2,7 @@ 最后更新:2026-08-12 -## 当前版本(v0.6.2) +## 当前版本(v0.6.4) 扩展只在 `https://www.douyin.com/*` 页面运行,并使用 Chrome `storage` 权限保存: diff --git a/README.md b/README.md index a990334..f73e755 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 5. 选择本仓库根目录。 6. 打开或刷新 `https://www.douyin.com/`。 -也可以从 GitHub Releases 下载自动生成的 Chrome ZIP,解压后按上述方式加载。Release 标签必须与 `manifest.json` 版本一致,例如版本 `0.6.2` 使用标签 `v0.6.2`。 +也可以从 GitHub Releases 下载自动生成的 Chrome ZIP,解压后按上述方式加载。Release 标签必须与 `manifest.json` 版本一致,例如版本 `0.6.4` 使用标签 `v0.6.4`。 ## 创建片段 @@ -38,7 +38,7 @@ 2. 图标切换为红色取消按钮和青色结束按钮。 3. 广告结束时点击“片段现在结束”,片段先作为本地草稿保存,并以深绿色显示在进度条上。 4. 此时控制栏会出现“提交”和“删除最后一个未提交片段”按钮;点击提交会打开播放器内菜单。 -5. 在提交菜单逐段点击“预览”;播放器实际越过片段末尾后才允许提交。 +5. 在提交菜单使用“片段开头”“片段结尾”和“精确编辑”校准边界,再逐段点击“预览”;播放器实际越过片段末尾后才允许提交。 6. 提交成功后本地草稿会删除,亮绿色片段和管理列表从社区服务读取。 自动跳过时间片段后,提示条会短暂提供“撤销”按钮;社区片段还可直接赞成、反对或举报。 @@ -47,7 +47,7 @@ 社区片段支持“自动跳过、显示提示手动跳过、不处理”三种模式。播放器快捷键默认为:`Alt+Z` 开始/结束片段、`Alt+X` 取消创建、`Alt+Enter` 打开提交菜单;可在选项页的“快捷键”中重新录制,在输入框中不会触发。 -片段可标记为“赞助/广告”“自我推广”或“互动提醒”,进度条会使用不同颜色。片段管理页支持对起点和终点进行 ±0.1 秒微调;时间变化后需要重新预览才能上传。 +片段可标记为“赞助/广告”“自我推广”或“互动提醒”,进度条会使用不同颜色。播放器提交菜单和片段管理页均支持对起点和终点进行 ±0.1 秒微调;提交菜单还可逐帧前进或后退。时间变化后需要重新预览才能上传。 和 BilibiliSponsorBlock 的默认行为一致:赞助/广告自动跳过,自我推广与互动提醒先显示提示,也可以在“社区共享”页分别改成自动跳过或不处理。 diff --git a/content.css b/content.css index e9d4e21..f0410a2 100644 --- a/content.css +++ b/content.css @@ -109,34 +109,75 @@ .das-submission-menu { position: absolute; - right: 86px; - bottom: 64px; + right: clamp(16px, 5vw, 86px); + bottom: 68px; z-index: 10020; - width: min(360px, calc(100% - 32px)); - padding: 16px; + width: min(440px, calc(100% - 32px)); + max-height: min(560px, calc(100% - 92px)); + padding: 0; + overflow: hidden; border: 1px solid rgba(255,255,255,.14); - border-radius: 12px; + border-radius: 16px; color: #fff; - background: rgba(22,24,29,.96); + background: rgba(18,20,25,.97); + backdrop-filter: blur(18px); box-shadow: 0 16px 50px rgba(0,0,0,.45); font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } -.das-submission-menu header { display: flex; align-items: center; justify-content: space-between; font-size: 17px; } -.das-submission-menu header button { border: 0; color: #aaa; background: transparent; font-size: 24px; cursor: pointer; } -.das-submission-menu p { margin: 10px 0; color: #b8bac1; } -.das-submission-menu ol { max-height: 180px; margin: 0; padding: 0; overflow: auto; list-style: none; } -.das-submission-menu li { display: flex; align-items:center; justify-content: space-between; gap:12px; padding: 8px 0; border-top: 1px solid rgba(255,255,255,.08); } -.das-submission-menu li span { display:flex; flex-direction:column; } -.das-submission-menu li small { margin-top:2px; color:#8f939d; } -.das-submission-menu li button { padding:5px 10px; border:1px solid rgba(37,244,238,.3); border-radius:6px; color:#25f4ee; background:rgba(37,244,238,.08); cursor:pointer; } -.das-submission-menu li select { max-width:105px; padding:5px 6px; border:1px solid rgba(255,255,255,.18); border-radius:6px; color:#fff; background:#292c33; } +.das-submission-menu header { display:flex; align-items:center; justify-content:space-between; padding:16px 18px 11px; } +.das-submission-menu header div { display:flex; min-width:0; flex-direction:column; } +.das-submission-menu header strong { font-size:18px; } +.das-submission-menu header small { margin-top:2px; color:#858a95; font-size:11px; font-weight:500; } +.das-submission-menu header button { display:grid; flex:none; place-items:center; width:32px; height:32px; padding:0; border:0; border-radius:8px; color:#aaa; background:transparent; font-size:25px; cursor:pointer; } +.das-submission-menu header button:hover { color:#fff; background:rgba(255,255,255,.08); } +.das-submission-menu > p { margin:0; padding:0 18px 13px; color:#aeb2bc; font-size:12px; } +.das-submission-menu ol { max-height:min(380px, calc(100vh - 286px)); margin:0; padding:0 10px; overflow:auto; list-style:none; scrollbar-width:thin; } +.das-submission-menu li { display:grid; grid-template-columns:minmax(112px,auto) minmax(0,1fr); gap:12px 14px; padding:15px 8px; border-top:1px solid rgba(255,255,255,.09); } +.das-segment-summary { display:flex; align-self:start; flex-direction:column; padding-top:2px; font-variant-numeric:tabular-nums; white-space:nowrap; } +.das-segment-category { min-width:0; } +.das-submission-menu li small { margin-top:3px; color:#8f939d; font-size:11px; } +.das-submission-menu li button { min-height:32px; padding:5px 10px; border:1px solid rgba(37,244,238,.3); border-radius:7px; color:#25f4ee; background:rgba(37,244,238,.08); cursor:pointer; white-space:nowrap; } +.das-submission-menu li button:hover { background:rgba(37,244,238,.15); } +.das-submission-menu li button.das-active { color:#161823; background:#25f4ee; } +.das-submission-menu li select { min-width:0; width:100%; padding:6px 8px; border:1px solid rgba(255,255,255,.18); border-radius:7px; color:#fff; background:#292c33; } .das-submission-menu li em { color: #9da0a9; font-style: normal; } -.das-submission-actions { display: flex; gap: 8px; margin-top: 14px; } -.das-submission-actions button { flex: 1; padding: 9px 10px; border: 0; border-radius: 7px; color: #fff; background: #34363d; cursor: pointer; } +.das-segment-primary-actions { grid-column:1 / -1; display:grid; grid-template-columns:1.2fr 1fr 1fr; gap:7px; } +.das-segment-primary-actions button { display:flex; min-width:0; height:48px; align-items:center; justify-content:center; gap:7px; padding:6px 8px !important; color:#e7e9ee !important; border-color:rgba(255,255,255,.13) !important; background:rgba(255,255,255,.045) !important; } +.das-segment-primary-actions button:first-child { color:#25f4ee !important; border-color:rgba(37,244,238,.28) !important; background:rgba(37,244,238,.09) !important; } +.das-segment-primary-actions button:hover { border-color:rgba(37,244,238,.42) !important; background:rgba(37,244,238,.13) !important; } +.das-segment-primary-actions button b { flex:none; font-size:15px; font-weight:500; } +.das-segment-primary-actions button span { overflow:hidden; text-overflow:ellipsis; } +.das-segment-secondary-actions { grid-column:1 / -1; display:flex; align-items:center; justify-content:space-between; gap:12px; } +.das-segment-secondary-actions button { min-height:30px !important; padding:4px 2px !important; border:0 !important; color:#aeb2bc !important; background:transparent !important; } +.das-segment-secondary-actions button:hover { color:#25f4ee !important; background:transparent !important; } +.das-segment-secondary-actions button[data-menu-action="toggle-editor"] { display:flex; align-items:center; gap:7px; } +.das-segment-secondary-actions button[data-menu-action="toggle-editor"] b { transition:transform .16s ease; } +.das-segment-secondary-actions button[data-menu-action="toggle-editor"].das-active b { transform:rotate(180deg); } +.das-segment-secondary-actions button[data-menu-action="delete-segment"] { color:#ff718d !important; } +.das-segment-editor { grid-column:1 / -1; display:grid; grid-template-columns:36px 32px minmax(84px,1fr) 32px; gap:5px; align-items:center; } +.das-segment-editor[hidden] { display:none; } +.das-segment-editor > span { color:#8f939d; font-size:11px; } +.das-segment-editor button[data-menu-action="seek-boundary"] { color:#fff; border-color:rgba(255,255,255,.14); background:#292c33; font-variant-numeric:tabular-nums; } +.das-segment-editor button[data-menu-action="adjust"] { padding:0; font-size:17px; } +.das-frame-controls { grid-column:1 / -1; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:7px; padding-top:11px; border-top:1px solid rgba(255,255,255,.08); } +.das-frame-controls button { display:flex; height:42px; align-items:center; justify-content:center; gap:8px; color:#dfe2e8 !important; border-color:rgba(255,255,255,.14) !important; background:rgba(255,255,255,.045) !important; } +.das-frame-controls button:hover { color:#25f4ee !important; border-color:rgba(37,244,238,.4) !important; background:rgba(37,244,238,.1) !important; } +.das-frame-controls button b { color:#25f4ee; font-size:22px; font-weight:400; line-height:1; } +.das-frame-controls kbd { min-width:24px; padding:2px 6px; border:1px solid #4a4f5b; border-bottom-width:2px; border-radius:5px; color:#b9bdc6; background:#292c33; font:11px/1.4 inherit; text-align:center; } +.das-submission-actions { display:grid; grid-template-columns:1.15fr .85fr; gap:10px; padding:0 18px 18px; } +.das-submission-actions button { flex:1; min-width:0; padding:10px; border:0; border-radius:8px; overflow:hidden; color:#fff; background:#34363d; cursor:pointer; text-overflow:ellipsis; white-space:nowrap; } .das-submission-actions button:first-child { color: #161823; background: #25f4ee; font-weight: 700; } .das-submission-actions button:disabled { opacity: .55; cursor: wait; } +@media (max-width: 520px) { + .das-submission-menu { right:8px; bottom:56px; width:calc(100% - 16px); max-height:calc(100% - 68px); } + .das-submission-menu li { grid-template-columns:1fr; } + .das-segment-primary-actions { grid-template-columns:1fr 1fr; } + .das-segment-primary-actions button:first-child { grid-column:1 / -1; } + .das-submission-actions { grid-template-columns:1fr; } +} + .das-toast-actions { display:flex; gap:6px; margin-left:12px; pointer-events:auto; } .das-toast-actions button { padding:4px 8px; border:1px solid rgba(255,255,255,.24); border-radius:6px; color:#fff; background:rgba(255,255,255,.1); cursor:pointer; } .das-toast-actions button:hover { border-color:#25f4ee; color:#25f4ee; } diff --git a/content.js b/content.js index 303c59a..4305986 100644 --- a/content.js +++ b/content.js @@ -187,6 +187,7 @@ const videoHash = await sha256Hex(videoId); const response = await fetch(`${apiBase}/v1/videos/by-hash/${videoHash}/segments`, { headers: { Accept: 'application/json' }, + cache: 'no-store', signal: controller.signal, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -449,7 +450,7 @@ document.querySelectorAll('.das-submission-menu').forEach((menu) => menu.remove()); } - function openSubmissionMenu(video, videoId) { + function openSubmissionMenu(video, videoId, expandedSegmentId = null) { const player = getVideoPlayer(video); if (!player) return; const existing = player.querySelector('.das-submission-menu'); @@ -463,9 +464,26 @@ const menu = document.createElement('section'); menu.className = 'das-submission-menu'; const allPreviewed = pending.every((item) => item.previewed === true); - menu.innerHTML = `
提交广告片段
-

提交前请逐段预览,确认开始和结束时间准确。

-
    ${pending.map((item,index) => `
  1. ${formatTime(item.start)} – ${formatTime(item.end)}${item.previewed?'✓ 已预览':'尚未预览'}
  2. `).join('')}
+ menu.innerHTML = `
提交社区片段${pending.length} 个待提交片段
+

先微调时间并完整预览,确认片段边界准确后再提交。

+
    ${pending.map((item,index) => `
  1. +
    ${formatTime(item.start)} – ${formatTime(item.end)}${item.previewed?'✓ 已预览':'尚未预览'}
    +
    +
    + + + +
    +
    + + +
    +
    + 起点 + 终点 +
    +
    +
  2. `).join('')}
`; menu.addEventListener('pointerdown', (event) => event.stopPropagation()); menu.addEventListener('click', async (event) => { @@ -473,6 +491,32 @@ const action = event.target.closest('button')?.dataset.menuAction; if (action === 'close' || action === 'keep') closeSubmissionMenu(); if (action === 'preview') await previewPendingSegment(video, videoId, pending[Number(event.target.closest('button').dataset.previewIndex)]); + if (action === 'toggle-editor') { + const button = event.target.closest('button'); + const editor = menu.querySelector(`[data-editor-index="${button.dataset.previewIndex}"]`); + if (editor) { + editor.hidden = !editor.hidden; + button.setAttribute('aria-expanded', String(!editor.hidden)); + button.classList.toggle('das-active', !editor.hidden); + } + } + if (action === 'seek-boundary') { + const button = event.target.closest('button'); + const target = pending[Number(button.dataset.previewIndex)]; + if (target) await inspectSegmentBoundary(video, videoId, target, button.dataset.field); + } + if (action === 'adjust') { + const button = event.target.closest('button'); + await adjustPendingSegment(video, videoId, pending[Number(button.dataset.previewIndex)], button.dataset.field, Number(button.dataset.delta)); + } + if (action === 'frame-step') { + const button = event.target.closest('button'); + stepVideoFrame(video, Number(button.dataset.delta)); + } + if (action === 'delete-segment') { + const button = event.target.closest('button'); + await deletePendingSegment(video, videoId, pending[Number(button.dataset.previewIndex)]); + } if (action === 'submit') await submitPendingSegments(video, videoId, menu); }); menu.addEventListener('change', async (event) => { @@ -482,7 +526,91 @@ if(stored)stored.category=select.value;target.category=select.value; settings.localSegments={...(settings.localSegments||{}),[videoId]:segments};await chrome.storage.local.set({localSegments:settings.localSegments});renderPreviewBar(); }); + menu.addEventListener('keydown', (event) => { + if (event.key !== ',' && event.key !== '.') return; + event.preventDefault(); + stepVideoFrame(video, event.key === ',' ? -1 : 1); + }); + menu.tabIndex = -1; player.appendChild(menu); + menu.focus({ preventScroll: true }); + } + + function stepVideoFrame(video, direction) { + if (!video || !Number.isFinite(direction) || !direction) return; + video.pause(); + video.currentTime = Math.max(0, Math.min(video.duration || Infinity, video.currentTime + Math.sign(direction) / 30)); + } + + async function inspectSegmentBoundary(video, videoId, segment, field) { + if (!segment || !['start', 'end'].includes(field)) return; + const key = `${videoId}:${segment.start}:${segment.end}`; + lastSegmentSkipKey = ''; + if (field === 'start') { + // 校准起点时允许用户连续观看整段,不触发本地或社区自动跳过。 + const inspectMs = Math.max(5000, (segment.end - segment.start + 3) * 1000); + skipSuppressedUntil.set(key, Date.now() + inspectMs); + video.currentTime = Math.max(0, segment.start); + try { await video.play(); } catch {} + showToast(`从片段开头播放 · ${formatTime(segment.start)}`); + return; + } + skipSuppressedUntil.set(key, Date.now() + 5000); + video.pause(); + video.currentTime = Math.min(segment.end, video.duration || segment.end); + showToast(`已定位片段结尾 · ${formatTime(segment.end)}`); + } + + async function deletePendingSegment(video, videoId, segment) { + if (!segment) return; + const segments = [...(settings.localSegments?.[videoId] || [])]; + const index = segments.findIndex((item) => item.createdAt === segment.createdAt); + if (index < 0) return; + const [removed] = segments.splice(index, 1); + const localSegments = { ...(settings.localSegments || {}) }; + if (segments.length) localSegments[videoId] = segments; + else delete localSegments[videoId]; + settings.localSegments = localSegments; + await chrome.storage.local.set({ localSegments }); + closeSubmissionMenu(); + if (segments.some((item) => (item.submissionStatus || 'pending') === 'pending')) openSubmissionMenu(video, videoId); + renderPlayerControls(); + renderPreviewBar(); + showToast(`已删除片段 · ${formatTime(removed.start)}–${formatTime(removed.end)}`); + } + + async function adjustPendingSegment(video, videoId, segment, field, delta) { + if (!segment || !['start', 'end'].includes(field) || !Number.isFinite(delta)) return; + const segments = [...(settings.localSegments?.[videoId] || [])]; + const target = segments.find((item) => item.createdAt === segment.createdAt); + if (!target) return; + const nextValue = Math.round((Number(target[field]) + delta) * 10) / 10; + const nextStart = field === 'start' ? nextValue : Number(target.start); + const nextEnd = field === 'end' ? nextValue : Number(target.end); + if (nextStart < 0 || nextEnd <= nextStart + 0.2 || nextEnd > (video.duration || Infinity)) { + showToast('调整后的片段范围无效'); + return; + } + target[field] = nextValue; + target.previewed = false; + settings.localSegments = { ...(settings.localSegments || {}), [videoId]: segments }; + await chrome.storage.local.set({ localSegments: settings.localSegments }); + const menu = getVideoPlayer(video)?.querySelector('.das-submission-menu'); + const pending = currentSegments(video).filter((item) => (item.submissionStatus || 'pending') === 'pending'); + const pendingIndex = pending.findIndex((item) => item.createdAt === target.createdAt); + if (menu && pendingIndex >= 0) { + const editor = menu.querySelector(`[data-editor-index="${pendingIndex}"]`); + const summary = editor?.closest('li')?.querySelector('.das-segment-summary'); + const times = editor?.querySelectorAll('[data-menu-action="seek-boundary"]'); + if (summary) summary.innerHTML = `${formatTime(target.start)} – ${formatTime(target.end)}尚未预览`; + if (times?.[0]) times[0].textContent = formatTime(target.start); + if (times?.[1]) times[1].textContent = formatTime(target.end); + const submitButton = menu.querySelector('[data-menu-action="submit"]'); + if (submitButton) { submitButton.disabled = true; submitButton.textContent = '请先预览全部'; } + } + video.currentTime = nextValue; + renderPreviewBar(); + showToast(`${field === 'start' ? '起点' : '终点'}已调整 0.1 秒,请重新预览`); } async function previewPendingSegment(video, videoId, segment) { diff --git a/manifest.json b/manifest.json index a90a8d8..35a1070 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "抖音广告跳过", - "version": "0.6.2", + "version": "0.6.4", "description": "跳过抖音网页版广告,并与社区共享手动标记的广告片段。", "icons": { "16": "icons/icon-16.png", diff --git a/server/src/index.ts b/server/src/index.ts index ad7c869..e1c453f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -118,7 +118,7 @@ async function getSegments(videoId: string, env: Env): Promise { SELECT id, video_id, start_ms, end_ms, category, status, upvotes, downvotes, created_at FROM segments WHERE video_id = ? AND status IN ('candidate', 'trusted') ORDER BY start_ms ASC LIMIT 200 `).bind(videoId).all(); - return json({ videoId, segments: result.results.map(segmentJson) }, 200, { 'Cache-Control': 'public, max-age=60' }); + return json({ videoId, segments: result.results.map(segmentJson) }); } async function getSegmentsByHash(videoHash: string, env: Env): Promise { @@ -131,7 +131,7 @@ async function getSegmentsByHash(videoHash: string, env: Env): Promise const { videoId: _videoId, ...segment } = segmentJson(row); return segment; }); - return json({ segments }, 200, { 'Cache-Control': 'public, max-age=60' }); + return json({ segments }); } async function getMySegments(request: Request, env: Env): Promise {