From 431ccf89df71dac78803b17b58985f33abef04f9 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Sun, 23 Aug 2026 23:12:06 +0800 Subject: [PATCH] feat: add contribution center and segment revisions --- .github/workflows/ci.yml | 2 + .github/workflows/release.yml | 4 +- CHANGELOG.md | 9 ++ PRIVACY.md | 6 +- README.md | 7 +- content.js | 60 ++++++--- docs/API.md | 28 ++++- docs/ARCHITECTURE.md | 10 +- docs/DATA_MODEL.md | 3 + docs/ROADMAP.md | 6 +- manifest.json | 4 +- options.css | 33 +++++ options.html | 26 +++- options.js | 112 ++++++++++++++++- player-adapter.js | 41 +++++++ scripts/test-extension-logic.mjs | 9 ++ scripts/test-player-adapter.mjs | 34 +++++ scripts/validate-extension.mjs | 4 +- server/README.md | 4 +- server/migrations/0008_segment_revisions.sql | 16 +++ server/src/index.ts | 123 ++++++++++++++++--- server/src/validation.ts | 51 ++++++++ server/tests/validation.test.ts | 22 +++- 23 files changed, 560 insertions(+), 54 deletions(-) create mode 100644 player-adapter.js create mode 100644 scripts/test-player-adapter.mjs create mode 100644 server/migrations/0008_segment_revisions.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3827eb..039417c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,12 @@ jobs: - name: Validate extension run: | node --check content.js + node --check player-adapter.js node --check popup.js node --check options.js node scripts/validate-extension.mjs node scripts/test-extension-logic.mjs + node scripts/test-player-adapter.mjs - name: Install Worker dependencies working-directory: server run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b7578e..e96f608 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,10 +24,12 @@ jobs: git fetch origin main git merge-base --is-ancestor "$GITHUB_SHA" origin/main node --check content.js + node --check player-adapter.js node --check popup.js node --check options.js node scripts/validate-extension.mjs node scripts/test-extension-logic.mjs + node scripts/test-player-adapter.mjs node -e "const v=require('./manifest.json').version;if(process.env.GITHUB_REF_NAME!=='v'+v)throw new Error('Tag 与 manifest 版本不一致')" - name: Test Worker working-directory: server @@ -35,7 +37,7 @@ jobs: - name: Build Chrome ZIP run: | mkdir -p dist/douyin-ad-skipper - cp manifest.json content.js content.css popup.html popup.js popup.css options.html options.js options.css LICENSE PRIVACY.md SECURITY.md dist/douyin-ad-skipper/ + cp manifest.json player-adapter.js content.js content.css popup.html popup.js popup.css options.html options.js options.css LICENSE PRIVACY.md SECURITY.md dist/douyin-ad-skipper/ mkdir -p dist/douyin-ad-skipper/icons cp icons/icon-16.png icons/icon-32.png icons/icon-48.png icons/icon-128.png dist/douyin-ad-skipper/icons/ cd dist/douyin-ad-skipper diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ff942..6329c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 更新日志 +## 0.8.0 - 2026-08-23 + +- 播放器控制栏适配从单一选择器升级为多候选策略,新增切换作品、ID 提取和控制栏回退的自动回归测试。 +- 选项页新增脱敏播放器诊断,一键复制扩展版本、挂载状态和页面结构摘要,不包含作品 ID、Cookie 或账号信息。 +- 社区查询会聚合时间高度重叠或边界接近的同类投稿,避免进度条出现多条几乎相同的片段。 +- 新增社区贡献中心,集中展示有效投稿、帮助人数、节省时间、收到的赞成/反对和争议状态。 +- 投稿者可以修改自己的云端片段或主动撤回;修改后旧投票与帮助统计会重置,撤回后立即停止分发。 +- D1 新增片段修订历史表,Worker API 升级至 v4,并补充云端修改、撤回和聚合算法测试。 + ## 0.7.0 - 2026-08-15 - 产品统一为“抖音网页版社区片段助手”,移除页面广告标签识别和整条视频自动翻页,只处理普通作品时间轴中的用户标记片段。 diff --git a/PRIVACY.md b/PRIVACY.md index 371568a..d0b2722 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,14 +1,15 @@ # 隐私说明 -最后更新:2026-08-15 +最后更新:2026-08-23 -## 当前版本(v0.7.0) +## 当前版本(v0.8.0) 扩展只在 `https://www.douyin.com/*` 页面运行,并使用 Chrome `storage` 权限保存: - 功能开关; - 累计跳过次数; - 尚未提交的本地草稿(作品 ID、片段起止时间、标题、作者、作品链接和创建时间)。 +- 最近一次播放器适配诊断(扩展版本、挂载结果、页面路径形状、可见播放器数量和视口尺寸,不含作品 ID、Cookie 或账号资料)。 功能开关、累计跳过次数和未提交草稿保存在 Chrome 扩展本地存储中。首次使用时,用户必须明确选择是否启用社区共享;选择仅本地或撤回同意时不会查询社区 API。只有同意社区共享后,扩展才会生成匿名贡献者 ID 并保存到 Chrome 同步存储,以便同一 Chrome 账号在其他电脑识别自己的社区贡献。已提交片段存放在社区服务器,不再重复保存在本地。 @@ -34,6 +35,7 @@ - 社区片段成功跳过且未立即撤销时,会上传片段 ID;服务器按匿名贡献者哈希、片段和日期去重,用于统计实际帮助人数、跳过次数和节省时间; - 提供完全关闭远程查询与提交的选项; - 社区片段、投票、举报和去重后的帮助统计会在公共服务运营期间保存,或在合法删除请求处理后移除;应用层限流记录会抽样清理超过 48 小时的桶。 +- 投稿者修改或撤回云端片段时,服务器保存原时间、原分类、操作类型和操作时间作为修订记录;不会在修订记录中保存明文匿名贡献者 ID。 作品 ID 的 SHA-256 哈希用于减少查询时直接暴露原始 ID,但作品 ID 的取值空间有限,哈希不应被理解为完全匿名或不可反查。公共社区 API 会对写入请求同时按匿名身份哈希与来源 IP 哈希限流;限流记录仅用于防滥用,应用数据库会抽样清理超过 48 小时的限流桶。Cloudflare 基础设施日志的保存与处理还受 Cloudflare 服务配置和政策约束。 diff --git a/README.md b/README.md index 0d30513..94cdce7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ - 再次播放同一作品时自动跳过本地草稿或社区片段。 - 可从播放器内的提交菜单把待提交片段发送到项目公共社区服务。 - 在选项页查看本地草稿、云端片段和累计贡献时长。 +- 在贡献中心查看帮助人数、社区反馈和争议状态,并修改或撤回自己的云端投稿。 +- 自动聚合时间高度相近的同类投稿,减少重复色块。 +- 播放器适配失败时可复制不含作品 ID 和账号信息的脱敏诊断。 - 导入、导出本地数据。 - 未提交草稿留在本地;已提交片段以社区服务器为准,不上传完整浏览记录。 @@ -28,7 +31,7 @@ 5. 选择本仓库根目录。 6. 打开或刷新 `https://www.douyin.com/`。 -也可以从 GitHub Releases 下载自动生成的 Chrome ZIP,解压后按上述方式加载。Release 标签必须与 `manifest.json` 版本一致,例如版本 `0.7.0` 使用标签 `v0.7.0`。 +也可以从 GitHub Releases 下载自动生成的 Chrome ZIP,解压后按上述方式加载。Release 标签必须与 `manifest.json` 版本一致,例如版本 `0.8.0` 使用标签 `v0.8.0`。 ## 创建片段 @@ -65,7 +68,7 @@ 错误片段被降权或隐藏 ``` -服务端实现位于 `server/`,包含幂等提交、重复片段检测、投票、举报与限流。公共扩展固定连接项目公共 API;需要自建服务的开发者可以在自己的源码分支中替换地址和清单权限。详见 [服务端说明](server/README.md)、[路线图](docs/ROADMAP.md) 和 [API 草案](docs/API.md)。 +服务端实现位于 `server/`,包含幂等提交、相似片段聚合、投稿修改/撤回、投票、举报与限流。公共扩展固定连接项目公共 API;需要自建服务的开发者可以在自己的源码分支中替换地址和清单权限。详见 [服务端说明](server/README.md)、[路线图](docs/ROADMAP.md) 和 [API 草案](docs/API.md)。 ## 项目结构 diff --git a/content.js b/content.js index dee3567..7bbca7c 100644 --- a/content.js +++ b/content.js @@ -2,6 +2,7 @@ 'use strict'; const DEFAULT_COMMUNITY_API = 'https://douyin-ad-skipper-api.douyin-skip-community.workers.dev'; + const adapter = globalThis.DouyinSegmentAdapter; const DEFAULTS = { enabled: true, skipLocalSegments: true, @@ -28,6 +29,8 @@ let lastSegmentSkipKey = ''; let mountScheduled = false; let manualNoticeKey = ''; + let diagnosticFingerprint = ''; + let diagnosticWrittenAt = 0; const skipSuppressedUntil = new Map(); const communityCache = new Map(); const COMMUNITY_CACHE_MS = 10 * 60 * 1000; @@ -75,12 +78,10 @@ } function extractVideoId(video) { - const urlMatches = location.pathname.match(/\/video\/(\d+)/); - if (urlMatches) return urlMatches[1]; - - // 精选、推荐等页面会以弹层打开视频,ID 位于查询参数中。 - const queryId = new URLSearchParams(location.search).get('modal_id'); - if (/^\d{10,}$/.test(queryId || '')) return queryId; + const locationId = adapter?.idFromLocation(location.pathname, location.search); + if (locationId) return locationId; + const nodeId = adapter?.idFromNode(video); + if (nodeId) return nodeId; // 播放器节点常见形式:data-e2e="feed-active-video" class="video_123..."。 let node = video; @@ -111,6 +112,30 @@ return null; } + function writeAdapterDiagnostic(reason, video = null, strategy = '') { + const route = location.pathname.startsWith('/video/') ? '/video/:id' + : location.pathname.startsWith('/user/') ? '/user/:redacted' + : /^\/(recommend|discover|follow|hot|channel)(?:\/|$)/.test(location.pathname) ? `/${location.pathname.split('/')[1]}` + : '/other'; + const snapshot = { + recordedAt: new Date().toISOString(), + extensionVersion: chrome.runtime.getManifest().version, + reason, + strategy: strategy || 'none', + pathShape: route, + modalRoute: new URLSearchParams(location.search).has('modal_id'), + visibleVideoCount: [...document.querySelectorAll('video')].filter(isVisible).length, + videoIdDetected: Boolean(video && extractVideoId(video)), + controlsMounted: Boolean(document.querySelector('.das-player-controls')), + viewport: `${innerWidth}x${innerHeight}`, + }; + const fingerprint = JSON.stringify({ ...snapshot, recordedAt: '' }); + if (fingerprint === diagnosticFingerprint && Date.now() - diagnosticWrittenAt < 30000) return; + diagnosticFingerprint = fingerprint; + diagnosticWrittenAt = Date.now(); + void chrome.storage.local.set({ lastAdapterDiagnostic: snapshot }); + } + function formatTime(seconds) { if (!Number.isFinite(seconds)) return '--:--.-'; const minutes = Math.floor(seconds / 60); @@ -196,7 +221,7 @@ const payload = await response.json(); const segments = Array.isArray(payload.segments) ? payload.segments .filter((item) => CATEGORY_LABELS[item.category] && Number.isFinite(item.start) && Number.isFinite(item.end) && item.end > item.start) - .map((item) => ({ start: item.start, end: item.end, category:item.category, source: 'community', id: item.id, status: item.status, ownedByMe: item.ownedByMe === true })) : []; + .map((item) => ({ start: item.start, end: item.end, category:item.category, source: 'community', id: item.id, status: item.status, ownedByMe: item.ownedByMe === true, clusterSize:Math.max(1,Number(item.clusterSize||1)) })) : []; setCommunityCache(videoId, { loadedAt: Date.now(), retryAt: 0, failureCount: 0, segments }); renderPreviewBar(); log('已加载社区片段', videoId, segments.length); @@ -353,13 +378,19 @@ function ensurePlayerControls() { const video = getActiveVideo(); - if (!video) return null; + if (!video) { + writeAdapterDiagnostic('no-visible-video'); + return null; + } loadCommunitySegments(video); if (draftStart !== null && draftVideoId !== extractVideoId(video)) cancelDraft(); - let player = video.parentElement; - while (player && !player.querySelector?.('xg-right-grid')) player = player.parentElement; - const rightGrid = player?.querySelector('xg-right-grid'); - if (!rightGrid) return null; + const resolved = adapter?.findControlsHost(video); + const player = resolved?.player; + const rightGrid = resolved?.host; + if (!player || !rightGrid) { + writeAdapterDiagnostic('controls-host-missing', video); + return null; + } // 抖音会预加载前后多个播放器,只在当前活动播放器保留一组按钮。 document.querySelectorAll('.das-player-controls').forEach((element) => { @@ -371,13 +402,14 @@ controls.className = 'das-player-controls'; controls.addEventListener('pointerdown', (event) => event.stopPropagation()); controls.addEventListener('click', handlePlayerControl); - const clarity = rightGrid.querySelector('.xgplayer-playclarity-setting'); + const clarity = rightGrid.querySelector('.xgplayer-playclarity-setting,[class*="playclarity"],[class*="clarity-setting"]'); // 抖音右侧控制栏使用 row-reverse;放在清晰度节点之后,视觉上才位于其左侧。 if (clarity) rightGrid.insertBefore(controls, clarity.nextSibling); else rightGrid.appendChild(controls); renderPlayerControls(); log('标记按钮已嵌入播放器控制栏'); } + writeAdapterDiagnostic('mounted', video, resolved.strategy); ensurePreviewBar(video, player); return controls; } @@ -851,7 +883,7 @@ { label:'举报', run:()=>showReportChoices(segment) }, ); } - const ownership = segment.ownedByMe ? ' · 你的投稿' : ''; + const ownership = segment.ownedByMe ? ' · 你的投稿' : segment.clusterSize > 1 ? ` · ${segment.clusterSize} 人相近投稿` : ''; showToast(`已跳过片段 ${formatTime(segment.start)}–${formatTime(segment.end)}${ownership}`, actions); log(segment.source === 'community' ? '跳过社区可信片段' : '跳过本地标记片段', videoId, segment); } diff --git a/docs/API.md b/docs/API.md index 5bb74d1..20e8e18 100644 --- a/docs/API.md +++ b/docs/API.md @@ -20,7 +20,8 @@ GET /v1/videos/{videoId}/segments "end": 31.2, "category": "sponsor", "status": "trusted", - "score": 0.92 + "score": 0.92, + "clusterSize": 2 } ] } @@ -35,6 +36,8 @@ X-Client-ID: 匿名贡献者 UUID(已启用社区时) 带合法匿名贡献者 ID 时,每个结果额外返回 `ownedByMe`,供客户端隐藏对自己投稿的投票和举报入口;服务端不会返回提交者哈希。原始作品 ID 查询仅保留用于旧客户端兼容和迁移。 +同一作品、同一分类中时间高度重叠或起止边界接近的投稿会聚合为一个代表片段;`clusterSize` 大于 1 时表示该结果包含多条相近投稿。 + ## 提交片段 ```http @@ -69,6 +72,29 @@ X-Client-ID: 匿名贡献者 UUID - `skipCount`:这些片段实际被其他匿名用户跳过的次数; - `helpedPeople`:去重后的匿名用户数; - `secondsSaved`:实际累计节省秒数。 +- `receivedUpvotes` / `receivedDownvotes`:投稿收到的赞成与反对总数; +- `disputedCount`:当前处于争议状态的投稿数量。 + +## 修改或撤回自己的投稿 + +```http +PATCH /v1/me/segments/{segmentId} +X-Client-ID: 匿名贡献者 UUID +Content-Type: application/json +``` + +```json +{ "start": 19.2, "end": 30.8, "category": "sponsor" } +``` + +修改成功后会清除该片段原有的投票、举报和帮助统计,并记录修改前后的时间与分类。 + +```http +DELETE /v1/me/segments/{segmentId} +X-Client-ID: 匿名贡献者 UUID +``` + +撤回会把片段标记为 `rejected` 并停止分发,不直接删除修订记录。 ## 记录一次有效跳过 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index eda0fa2..bfa821b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,7 +4,8 @@ ```text 抖音页面 - ├─ content.js:活动视频、作品 ID、控制栏按钮、时间片段跳过 + ├─ player-adapter.js:可测试的作品 ID 与控制栏适配策略 + ├─ content.js:活动视频、控制栏按钮、脱敏诊断、时间片段跳过 ├─ content.css:播放器按钮和提示 └─ chrome.storage.local ├─ 设置 @@ -15,7 +16,7 @@ popup.* → 快速开关和入口 options.* → 片段管理、设置和备份 ``` -内容脚本优先通过 `data-e2e="feed-active-video"` 获取当前播放器,并兼容详情页路径、`modal_id`、`data-aweme-id` 和 `video_作品ID` 类名。 +内容脚本优先通过 `data-e2e="feed-active-video"` 获取当前播放器,并兼容详情页路径、`modal_id`、`data-aweme-id` 和 `video_作品ID` 类名。控制栏适配支持 `xg-right-grid` 及多个结构候选;最近一次挂载结果以脱敏摘要保存在本地,便于问题反馈。 ## 当前社区架构 @@ -29,7 +30,8 @@ Chrome 扩展 边缘 API / Serverless ├─ Schema 校验 ├─ 限流与匿名客户端身份 - ├─ 片段合并 + ├─ 相似片段聚合 + ├─ 投稿修改、撤回与修订历史 ├─ 投票与信誉 └─ 审核操作日志 │ @@ -39,6 +41,8 @@ Chrome 扩展 远程查询失败时必须静默回退到本地片段,不能阻塞视频播放。 +查询响应会把时间高度重叠或边界接近的同类投稿聚合为一个代表片段。投稿者修改片段时,旧投票、举报和帮助统计会重置;撤回通过 `rejected` 状态停止分发,并保留修订记录。 + ## 设计约束 - 扩展不依赖抖音私有接口获取视频内容。 diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 7b9268f..1b1abfd 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -32,6 +32,7 @@ "upvotes": 2, "downvotes": 0, "ownedByMe": false, + "clusterSize": 2, "createdAt": "2026-08-05T12:00:00Z" } ``` @@ -48,3 +49,5 @@ 相同作品中高度重叠的片段应复用已有记录,而不是无限创建重复记录。 服务端还保存只用于幂等的 `client_request_id`,并对 `(submitter_hash, client_request_id)` 建立唯一索引。该字段不会在公共查询中返回。 + +`segment_revisions` 保存投稿者对云端片段执行的修改或撤回操作,包括修改前后的时间和分类。撤回采用 `rejected` 状态而不是直接删除主记录;修改会清理旧投票、举报和帮助统计,使反馈只对应最新边界。 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 65b7b4f..bf2c258 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -7,7 +7,7 @@ - [x] 本地自动跳过 - [x] 片段管理与备份 - [x] 开源协作文档 -- [ ] 自动化 DOM 适配测试 +- [x] 自动化 DOM 适配测试 - [x] 进度条显示本地片段 ## 阶段 2:只读社区试验 @@ -24,7 +24,9 @@ - [x] 片段提交与幂等校验 - [x] 速率限制 - [x] 点赞、点踩与举报 -- [ ] 相似片段聚类 +- [x] 相似片段聚类 +- [x] 投稿者修改和撤回自己的云端片段 +- [x] 社区贡献中心与反馈状态 - [ ] 管理员审核工具 ## 阶段 4:社区治理 diff --git a/manifest.json b/manifest.json index b95a94a..c20cf1e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "抖音网页版社区片段助手", - "version": "0.7.0", + "version": "0.8.0", "description": "标记、预览和跳过抖音网页版普通作品中的社区时间片段。", "icons": { "16": "icons/icon-16.png", @@ -30,7 +30,7 @@ "content_scripts": [ { "matches": ["https://www.douyin.com/*"], - "js": ["content.js"], + "js": ["player-adapter.js", "content.js"], "css": ["content.css"], "run_at": "document_idle" } diff --git a/options.css b/options.css index bd9cd4e..03d05f1 100644 --- a/options.css +++ b/options.css @@ -34,6 +34,23 @@ nav button i { min-width:22px; padding:1px 7px; border-radius:20px; color:#e1e4e .community-impact-card span,.community-impact-card small { display:block; color:var(--muted); } .community-impact-card strong { display:block; margin:2px 0; font-size:17px; } .community-impact-card b { color:var(--cyan); font-size:22px; font-variant-numeric:tabular-nums; } +.compact-button { margin-top:0; } +.contribution-summary { margin-bottom:16px; padding:16px 20px; } +.contribution-summary span { color:var(--cyan); font-weight:700; } +.contribution-summary p { margin:4px 0 0; color:var(--muted); } +.contribution-list { display:flex; flex-direction:column; gap:12px; } +.contribution-item { padding:18px 20px; border:1px solid var(--line); border-radius:14px; background:var(--card); } +.contribution-item.is-disputed { border-color:#744052; background:linear-gradient(120deg,#1c1a21,#24171d); } +.contribution-main { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; } +.contribution-main strong,.contribution-main span { display:block; } +.contribution-main span { color:var(--muted); font-size:12px; } +.contribution-main em { padding:4px 9px; border-radius:99px; color:var(--cyan); background:rgba(37,244,238,.09); font-size:11px; font-style:normal; white-space:nowrap; } +.is-disputed .contribution-main em { color:#ff8aa1; background:rgba(254,44,85,.12); } +.contribution-details { display:flex; flex-wrap:wrap; gap:10px 18px; margin-top:14px; padding-top:13px; border-top:1px solid var(--line); color:#aeb4c0; } +.contribution-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:14px; } +.contribution-actions a,.contribution-actions button { padding:7px 10px; border:1px solid #3b4350; border-radius:8px; color:#cbd0da; background:#252932; cursor:pointer; text-decoration:none; } +.contribution-actions a:hover,.contribution-actions button:hover { color:var(--cyan); border-color:rgba(37,244,238,.4); } +.contribution-actions .withdraw-cloud-button { color:#ff8098; border-color:#59303b; background:#2a1920; } .panel,.settings-card { border:1px solid var(--line); border-radius:15px; background:var(--card); } .panel { padding:22px; } .panel-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; } @@ -89,6 +106,8 @@ h2 { margin:0; font-size:17px; } .info-card { margin-top:16px; padding:18px 20px; border:1px solid rgba(37,244,238,.2); border-radius:12px; background:rgba(37,244,238,.06); } .info-card span { color:var(--cyan); font-weight:700; } .info-card p { margin:5px 0 0; color:#b8bdc8; } +.diagnostic-card { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-top:16px; padding:18px 20px; border:1px solid var(--line); border-radius:12px; background:var(--card); } +.diagnostic-card p { margin:4px 0 0; color:var(--muted); } .consent-card { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:16px; margin-bottom:16px; padding:18px 20px; border:1px solid rgba(37,244,238,.25); border-radius:14px; background:linear-gradient(120deg,rgba(37,244,238,.08),rgba(254,44,85,.05)); } .consent-icon { display:grid; place-items:center; width:42px; height:42px; border-radius:12px; color:#101216; background:var(--cyan); font-size:23px; font-weight:800; } .consent-copy strong { display:block; font-size:16px; } @@ -118,6 +137,20 @@ h2 { margin:0; font-size:17px; } .danger-zone button { padding:9px 13px; border:1px solid #7c3344; border-radius:8px; color:#ff8aa1; background:#351923; cursor:pointer; } .about-card { display:flex; gap:23px; align-items:center; padding:28px; border:1px solid var(--line); border-radius:16px; background:var(--card); } .about-card img { width:92px; height:92px; border-radius:22px; } +.cloud-edit-dialog { width:min(520px,calc(100vw - 36px)); padding:0; border:1px solid #3b404b; border-radius:16px; color:var(--text); background:#171a21; box-shadow:0 26px 90px #000b; } +.cloud-edit-dialog::backdrop { background:#05070aab; backdrop-filter:blur(4px); } +.cloud-edit-dialog form { display:grid; gap:14px; padding:22px; } +.cloud-edit-dialog header { display:flex; align-items:flex-start; justify-content:space-between; } +.cloud-edit-dialog header strong,.cloud-edit-dialog header span { display:block; } +.cloud-edit-dialog header strong { font-size:19px; } +.cloud-edit-dialog header span,.cloud-edit-dialog p { color:var(--muted); } +.cloud-edit-dialog header button { border:0; color:#aeb4c0; background:transparent; cursor:pointer; font-size:26px; } +.cloud-edit-dialog p { margin:0; } +.cloud-edit-dialog label { display:grid; gap:6px; color:#cbd0da; } +.cloud-edit-dialog input,.cloud-edit-dialog select { width:100%; padding:10px 12px; border:1px solid #3a3f4a; border-radius:8px; outline:0; color:var(--text); background:#101217; } +.cloud-edit-dialog input:focus,.cloud-edit-dialog select:focus { border-color:var(--cyan); } +.dialog-actions { display:flex; justify-content:flex-end; gap:9px; } +.dialog-actions .primary-button,.dialog-actions .secondary-button { margin-top:4px; } #optionsToast { position:fixed; right:24px; bottom:24px; padding:11px 15px; border:1px solid var(--line); border-radius:9px; color:#fff; background:#242832; box-shadow:0 10px 30px #0008; opacity:0; transform:translateY(8px); pointer-events:none; transition:.18s; } #optionsToast.visible { opacity:1; transform:none; } @media(max-width:950px){.metric-grid{grid-template-columns:1fr 1fr}.app-shell{grid-template-columns:220px 1fr}.content{padding:36px 28px}.brand strong{font-size:14px}.consent-card{grid-template-columns:auto 1fr}.consent-actions{grid-column:1 / -1}} diff --git a/options.html b/options.html index 9967e3c..123e49b 100644 --- a/options.html +++ b/options.html @@ -13,6 +13,7 @@