Add clipping/highlight planning docs and sync current extension updates

This commit is contained in:
2026-03-01 17:55:20 +09:00
parent b9a4faa5f1
commit 3e1bd55438
24 changed files with 272 additions and 71 deletions

View File

@@ -0,0 +1,115 @@
# Page Clipping + Highlight Implementation Plan
## Goal
- 사용자가 특정 페이지에서 선택한 텍스트를 클리핑(저장)한다.
- 저장된 클립은 원문 위치에 하이라이트로 다시 표시된다.
- 팝업에서 클립 목록을 보고 해당 위치로 다시 이동할 수 있다.
## Scope (v1)
1. 지원 대상
- 텍스트 기반 웹페이지 (`contenteditable` 제외, 일반 DOM 문서 우선)
- 탭 단위 클립 저장/조회
2. 포함 기능
- 선택 텍스트 캡처
- 하이라이트 주입/복원
- 클립 목록 조회/삭제
- 클릭 시 원문 위치로 스크롤
3. 제외 기능(후속)
- PDF/캔버스/이미지 OCR 하이라이트
- 협업 공유/서버 동기화
- 다중 색상 태깅, 폴더 분류
## Data Model (Draft)
```ts
type ClipItem = {
id: string
tabId?: number
pageUrl: string
pageTitle: string
quote: string
createdAt: string
color: 'yellow'
anchor: {
textStart: string
textEnd: string
exact: string
prefix?: string
suffix?: string
xpathStart?: string
xpathEnd?: string
startOffset?: number
endOffset?: number
}
}
```
## Architecture
1. Content Script
- 사용자 선택(`window.getSelection`)에서 `Range` 추출
- `anchor` 생성(텍스트 인용 + DOM 포지션)
- background에 `clip:create` 메시지 전송
- `clip:apply` 이벤트 수신 시 하이라이트 렌더링
2. Background (Service Worker)
- `clip:create`, `clip:list`, `clip:delete`, `clip:reveal` 메시지 처리
- `storage.local` 기반 영속화
- 탭 활성화/페이지 완료 시 `clip:sync` 트리거
3. Popup/History UI
- 현재 탭 URL 기준 클립 목록 요청
- 항목별 `위치로 이동`, `삭제` 버튼
- 상태 메시지(저장 성공/실패) 표시
## Step-by-Step
1. Step 1: Selection Capture + Overlay
- `src/lib/clipAnchor.ts` 생성
- `Range -> anchor` 변환 유틸 구현
- `src/content/index.ts`에 단축키/우클릭 기반 캡처 진입점 추가
- `span[data-gomdown-clip]` 하이라이트 렌더링/해제 로직 구현
2. Step 2: Store + Message Channel
- `src/lib/clipStore.ts` 생성 (`list/upsert/delete/byUrl`)
- background message router에 `clip:*` 타입 추가
- dedupe(`pageUrl + exact + createdAt window`) 정책 추가
3. Step 3: Popup UI
- `src/popup/main.tsx`에 "Clips" 섹션 추가
- 현재 탭 URL 클립 조회 + 목록 렌더링
- `Reveal/Delete` 액션과 오류 상태 처리
4. Step 4: Re-anchoring
- 페이지 로드 시 `clip:list` 후 순차 복원
- 우선순위:
- `exact + prefix/suffix` 텍스트 매칭
- 실패 시 `xpath + offset` 복구
- 둘 다 실패 시 `broken anchor`로 표시
5. Step 5: Export/Import + QA
- JSON export/import 메시지 추가
- 샘플 페이지(뉴스, 블로그, SPA) 수동 테스트
- 회귀 체크리스트 문서화
## QA Checklist
- 같은 페이지 새로고침 후 하이라이트가 유지된다.
- SPA 라우팅(YouTube/블로그) 후에도 복원 시도가 동작한다.
- 원문 DOM이 일부 바뀐 경우, 텍스트 매칭 fallback이 동작한다.
- 클립 삭제 시 화면/저장소에서 모두 제거된다.
- 확장 비활성화 상태에서 캡처가 차단된다.
## Risk & Mitigation
1. DOM 변형으로 앵커 붕괴
- 텍스트 인용 앵커 + XPath 이중 저장
2. 성능 저하(클립 다수)
- URL 단위 lazy apply, viewport 근처 우선 렌더
3. 사이트 충돌(CSS/스크립트)
- 고유 data-attribute와 최소 침습 스타일 사용
## Definition of Done
- 사용자는 텍스트 선택 후 1회 액션으로 클립 저장 가능
- 같은 URL 재방문 시 하이라이트 자동 복원
- 팝업에서 클립 조회/이동/삭제 가능
- 크롬 기준 수동 시나리오 10개 중 9개 이상 성공

View File

@@ -9,3 +9,11 @@
- [ ] Step 2: content script 보조 탐지 - [ ] Step 2: content script 보조 탐지
- [ ] Step 3: 노이즈 필터/품질 그룹핑 - [ ] Step 3: 노이즈 필터/품질 그룹핑
- [ ] Step 4: 고급 분석/진단 UI - [ ] Step 4: 고급 분석/진단 UI
## Page Clipping + Highlight
- [x] 계획서 작성 (`docs/CLIPPING_HIGHLIGHT_PLAN.md`)
- [ ] Step 1: 텍스트 선택 캡처(`Selection` + `Range`)와 하이라이트 렌더러 구현
- [ ] Step 2: 저장소/메시지 채널 추가 (`clipStore`, background relay)
- [ ] Step 3: Popup/History UI에 클립 목록, 재이동(스크롤), 삭제 기능 추가
- [ ] Step 4: 페이지 재진입 시 하이라이트 복원(anchoring) 및 깨진 앵커 fallback 처리
- [ ] Step 5: 내보내기/가져오기(JSON)와 기본 회귀 테스트 시나리오 정리

View File

@@ -1,83 +1,73 @@
{ {
"../../../../../@crx/manifest": { "../../../../../../../@crx/manifest": {
"file": "assets/crx-manifest.js-mSo6-ym3.js", "file": "assets/crx-manifest.js-CXu7hmTa.js",
"name": "crx-manifest.js", "name": "crx-manifest.js",
"src": "../../../../../@crx/manifest", "src": "../../../../../../../@crx/manifest",
"isEntry": true "isEntry": true
}, },
"_browser-polyfill-CZ_dLIqp.js": { "_client-BzjyOx7y.js": {
"file": "assets/browser-polyfill-CZ_dLIqp.js", "file": "assets/client-BzjyOx7y.js",
"name": "browser-polyfill"
},
"_client-CBvt1tWS.js": {
"file": "assets/client-CBvt1tWS.js",
"name": "client", "name": "client",
"imports": [ "imports": [
"_browser-polyfill-CZ_dLIqp.js" "_settings-CgBxHrrF.js"
] ]
}, },
"_downloadIntent-Dv31jC2S.js": { "_downloadIntent-Dv31jC2S.js": {
"file": "assets/downloadIntent-Dv31jC2S.js", "file": "assets/downloadIntent-Dv31jC2S.js",
"name": "downloadIntent" "name": "downloadIntent"
}, },
"_index.ts-loader-Bju9eGS_.js": { "_index.ts-loader-D_eQmgUa.js": {
"file": "assets/index.ts-loader-Bju9eGS_.js", "file": "assets/index.ts-loader-D_eQmgUa.js",
"src": "_index.ts-loader-Bju9eGS_.js" "src": "_index.ts-loader-D_eQmgUa.js"
}, },
"_settings-Bo6W9Drl.js": { "_settings-CgBxHrrF.js": {
"file": "assets/settings-Bo6W9Drl.js", "file": "assets/settings-CgBxHrrF.js",
"name": "settings", "name": "settings"
"imports": [
"_browser-polyfill-CZ_dLIqp.js"
]
}, },
"src/background/index.ts": { "src/background/index.ts": {
"file": "assets/index.ts-BljhweV3.js", "file": "assets/index.ts-U8lbRRO-.js",
"name": "index.ts", "name": "index.ts",
"src": "src/background/index.ts", "src": "src/background/index.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_browser-polyfill-CZ_dLIqp.js", "_settings-CgBxHrrF.js",
"_downloadIntent-Dv31jC2S.js", "_downloadIntent-Dv31jC2S.js"
"_settings-Bo6W9Drl.js"
] ]
}, },
"src/config/index.html": { "src/config/index.html": {
"file": "assets/index.html-B0Kfv8fq.js", "file": "assets/index.html-B7fMyQPm.js",
"name": "index.html", "name": "index.html",
"src": "src/config/index.html", "src": "src/config/index.html",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_client-CBvt1tWS.js", "_client-BzjyOx7y.js",
"_settings-Bo6W9Drl.js", "_settings-CgBxHrrF.js"
"_browser-polyfill-CZ_dLIqp.js"
], ],
"css": [ "css": [
"assets/index-B2D5FcJM.css" "assets/index-B2D5FcJM.css"
] ]
}, },
"src/content/index.ts": { "src/content/index.ts": {
"file": "assets/index.ts-C6ePCen1.js", "file": "assets/index.ts-w1ilzv93.js",
"name": "index.ts", "name": "index.ts",
"src": "src/content/index.ts", "src": "src/content/index.ts",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_browser-polyfill-CZ_dLIqp.js", "_settings-CgBxHrrF.js",
"_downloadIntent-Dv31jC2S.js" "_downloadIntent-Dv31jC2S.js"
] ]
}, },
"src/popup/index.html": { "src/popup/index.html": {
"file": "assets/index.html-BLzIyLM-.js", "file": "assets/index.html-92_ZB8wX.js",
"name": "index.html", "name": "index.html",
"src": "src/popup/index.html", "src": "src/popup/index.html",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_client-CBvt1tWS.js", "_client-BzjyOx7y.js",
"_browser-polyfill-CZ_dLIqp.js", "_settings-CgBxHrrF.js"
"_settings-Bo6W9Drl.js"
], ],
"css": [ "css": [
"assets/index-D6aWDpYY.css" "assets/index-CJaGAyoX.css"
] ]
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
:root{color-scheme:dark;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}body{margin:0;background:#151821;color:#e7ebf7;width:360px}.container{padding:14px;display:grid;gap:10px}.top-row{display:flex;align-items:center;justify-content:space-between;gap:8px}h1{margin:0;font-size:14px}.power-toggle{height:28px;min-width:70px;border-radius:999px;padding:0 12px;display:inline-flex;align-items:center;justify-content:center;gap:6px;font-size:11px;font-weight:800;letter-spacing:.04em}.power-toggle.on{background:#204f37;border-color:#2b7a52;color:#c7ffe3}.power-toggle.off{background:#4a2631;border-color:#75404d;color:#ffd6df}.power-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.9}.field{display:grid;gap:6px}label{font-size:12px;color:#aeb6cc}input[type=text],input[type=number]{height:32px;border:1px solid #3e4658;border-radius:6px;padding:0 10px;background:#202532;color:#e7ebf7}.toggle{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#cdd5ea}button{height:34px;border:1px solid #5562f0;border-radius:8px;background:#5562f0;color:#fff;font-weight:600;cursor:pointer}.media-panel{margin-top:6px;border:1px solid #384255;border-radius:8px;padding:8px;background:#1b202c;display:grid;gap:8px}.media-head{display:flex;align-items:center;justify-content:space-between;font-size:12px}.media-list{display:grid;gap:6px;max-height:150px;overflow:auto}.media-item{display:flex;align-items:center;justify-content:space-between;gap:8px}.media-meta{min-width:0;display:grid;gap:2px}.kind{font-size:11px;color:#8fc0ff}.url{font-size:11px;color:#c6d1e8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:250px}.mini{height:26px;min-width:44px;padding:0 8px;border-radius:6px;font-size:11px}.mini.ghost{background:#2a3140;border-color:#47546c;color:#d4def2}.empty{font-size:11px;color:#96a3bc}.status{font-size:12px;color:#8fe0a6;min-height:14px}

View File

@@ -1 +0,0 @@
:root{color-scheme:dark;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}body{margin:0;background:#151821;color:#e7ebf7;width:360px}.container{padding:14px;display:grid;gap:10px}h1{margin:0;font-size:14px}.field{display:grid;gap:6px}label{font-size:12px;color:#aeb6cc}input[type=text],input[type=number]{height:32px;border:1px solid #3e4658;border-radius:6px;padding:0 10px;background:#202532;color:#e7ebf7}.toggle{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#cdd5ea}button{height:34px;border:1px solid #5562f0;border-radius:8px;background:#5562f0;color:#fff;font-weight:600;cursor:pointer}.media-panel{margin-top:6px;border:1px solid #384255;border-radius:8px;padding:8px;background:#1b202c;display:grid;gap:8px}.media-head{display:flex;align-items:center;justify-content:space-between;font-size:12px}.media-list{display:grid;gap:6px;max-height:150px;overflow:auto}.media-item{display:flex;align-items:center;justify-content:space-between;gap:8px}.media-meta{min-width:0;display:grid;gap:2px}.kind{font-size:11px;color:#8fc0ff}.url{font-size:11px;color:#c6d1e8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:250px}.mini{height:26px;min-width:44px;padding:0 8px;border-radius:6px;font-size:11px}.mini.ghost{background:#2a3140;border-color:#47546c;color:#d4def2}.empty{font-size:11px;color:#96a3bc}.status{font-size:12px;color:#8fe0a6;min-height:14px}

View File

@@ -0,0 +1 @@
import{c as N,j as t,R as r}from"./client-BzjyOx7y.js";import{g as k,b as i,s as m}from"./settings-CgBxHrrF.js";function S(){const[s,d]=r.useState(null),[x,n]=r.useState(""),[l,u]=r.useState([]);r.useEffect(()=>{k().then(d)},[]),r.useEffect(()=>{let e=null;const a=async()=>{const o=await i.runtime.sendMessage({type:"media:list"});o?.ok&&Array.isArray(o.items)&&u(o.items.slice(0,10))};return a(),e=window.setInterval(()=>{a()},2e3),()=>{e!==null&&window.clearInterval(e)}},[]);const c=(e,a)=>{d(o=>o&&{...o,[e]:a})},p=async e=>{if(!s)return;const a={...s,extensionStatus:e};d(a),await m({extensionStatus:e}),n(e?"Extension ON":"Extension OFF"),window.setTimeout(()=>n(""),1200)},g=async()=>{s&&(await m(s),n("Saved"),window.setTimeout(()=>n(""),1200))},h=async()=>{const e=i.runtime.getURL("src/config/index.html");await i.tabs.create({url:e})},j=async()=>{const e=i.runtime.getURL("src/history/index.html");await i.tabs.create({url:e})},v=async()=>{const e=await i.runtime.sendMessage({type:"page:enqueue-ytdlp"});n(e?.ok?"Active tab sent to gdown (yt-dlp)":`Send failed: ${e?.error||"unknown error"}`),window.setTimeout(()=>n(""),1800)},w=async e=>{const a=await i.runtime.sendMessage({type:"media:enqueue",url:e.url,referer:e.referer||"",kind:e.kind,suggestedOut:e.suggestedOut||"",cookie:e.cookie||"",userAgent:e.userAgent||""});n(a?.ok?"Media sent to gdown":`Send failed: ${a?.error||"unknown error"}`),window.setTimeout(()=>n(""),1600)},y=async()=>{await i.runtime.sendMessage({type:"media:clear"}),u([]),n("Captured media cleared"),window.setTimeout(()=>n(""),1200)};return s?t.jsxs("div",{className:"container",children:[t.jsxs("div",{className:"top-row",children:[t.jsx("h1",{children:"Gomdown Helper"}),t.jsxs("button",{className:`power-toggle ${s.extensionStatus?"on":"off"}`,onClick:()=>{p(!s.extensionStatus)},children:[t.jsx("span",{className:"power-dot"}),s.extensionStatus?"ON":"OFF"]})]}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Secret"}),t.jsx("input",{type:"text",value:s.motrixAPIkey,onChange:e=>c("motrixAPIkey",e.target.value),placeholder:"aria2 rpc secret"})]}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Port"}),t.jsx("input",{type:"number",value:s.motrixPort,onChange:e=>c("motrixPort",Number(e.target.value||16800))})]}),t.jsxs("label",{className:"toggle",children:["Use Native Host",t.jsx("input",{type:"checkbox",checked:s.useNativeHost,onChange:e=>c("useNativeHost",e.target.checked)})]}),t.jsxs("label",{className:"toggle",children:["Activate gdown App",t.jsx("input",{type:"checkbox",checked:s.activateAppOnDownload,onChange:e=>c("activateAppOnDownload",e.target.checked)})]}),t.jsx("button",{onClick:g,children:"Save"}),t.jsx("button",{onClick:h,children:"Settings"}),t.jsx("button",{onClick:j,children:"History"}),t.jsx("button",{onClick:()=>{v()},children:"Send Active Tab (yt-dlp)"}),t.jsxs("div",{className:"media-panel",children:[t.jsxs("div",{className:"media-head",children:[t.jsx("strong",{children:"Captured Media"}),t.jsx("button",{className:"mini ghost",onClick:y,children:"Clear"})]}),l.length===0?t.jsx("div",{className:"empty",children:"No media captured yet"}):t.jsx("div",{className:"media-list",children:l.map(e=>t.jsxs("div",{className:"media-item",children:[t.jsxs("div",{className:"media-meta",children:[t.jsx("span",{className:"kind",children:e.kind.toUpperCase()}),e.pageTitle?t.jsx("span",{className:"url",children:e.pageTitle}):null,t.jsx("span",{className:"url",children:e.url})]}),t.jsx("button",{className:"mini",onClick:()=>{w(e)},children:"Add"})]},e.id))})]}),t.jsx("div",{className:"status",children:x})]}):t.jsx("div",{className:"container",children:"Loading..."})}N.createRoot(document.getElementById("root")).render(t.jsx(r.StrictMode,{children:t.jsx(S,{})}));

View File

@@ -1,3 +1,3 @@
import{c as p,j as e,R as a}from"./client-CBvt1tWS.js";import{g as j,s as m}from"./settings-Bo6W9Drl.js";import"./browser-polyfill-CZ_dLIqp.js";function u(){const[t,i]=a.useState(null),[l,r]=a.useState(""),[h,d]=a.useState("");a.useEffect(()=>{j().then(s=>{i(s),r((s.blacklist||[]).join(` import{c as j,j as e,R as a}from"./client-BzjyOx7y.js";import{g as p,s as m}from"./settings-CgBxHrrF.js";function u(){const[t,i]=a.useState(null),[l,r]=a.useState(""),[h,d]=a.useState("");a.useEffect(()=>{p().then(s=>{i(s),r((s.blacklist||[]).join(`
`))})},[]);const n=(s,c)=>{i(o=>o&&{...o,[s]:c})},x=async()=>{if(!t)return;const s={...t,minFileSize:Number(t.minFileSize||0),motrixPort:Number(t.motrixPort||16800),blacklist:l.split(` `))})},[]);const n=(s,c)=>{i(o=>o&&{...o,[s]:c})},x=async()=>{if(!t)return;const s={...t,minFileSize:Number(t.minFileSize||0),motrixPort:Number(t.motrixPort||16800),blacklist:l.split(`
`).map(c=>c.trim()).filter(Boolean)};await m(s),i(s),d("Saved"),window.setTimeout(()=>d(""),1500)};return t?e.jsxs("div",{className:"wrap",children:[e.jsx("h1",{children:"Gomdown Helper Settings"}),e.jsxs("div",{className:"grid",children:[e.jsxs("section",{className:"card",children:[e.jsx("label",{children:"RPC Secret"}),e.jsx("input",{value:t.motrixAPIkey,onChange:s=>n("motrixAPIkey",s.target.value)}),e.jsx("label",{children:"RPC Port"}),e.jsx("input",{type:"number",value:t.motrixPort,onChange:s=>n("motrixPort",Number(s.target.value||16800))}),e.jsx("label",{children:"Min file size (MB)"}),e.jsx("input",{type:"number",value:t.minFileSize,onChange:s=>n("minFileSize",Number(s.target.value||0))}),e.jsx("label",{children:"Blacklist (one per line)"}),e.jsx("textarea",{rows:8,value:l,onChange:s=>r(s.target.value)})]}),e.jsxs("section",{className:"card",children:[e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Extension enabled"}),e.jsx("input",{type:"checkbox",checked:t.extensionStatus,onChange:s=>n("extensionStatus",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Enable notifications"}),e.jsx("input",{type:"checkbox",checked:t.enableNotifications,onChange:s=>n("enableNotifications",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Use native host"}),e.jsx("input",{type:"checkbox",checked:t.useNativeHost,onChange:s=>n("useNativeHost",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Activate app on download"}),e.jsx("input",{type:"checkbox",checked:t.activateAppOnDownload,onChange:s=>n("activateAppOnDownload",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Hide browser download shelf"}),e.jsx("input",{type:"checkbox",checked:t.hideChromeBar,onChange:s=>n("hideChromeBar",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Show context menu option"}),e.jsx("input",{type:"checkbox",checked:t.showContextOption,onChange:s=>n("showContextOption",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Download fallback"}),e.jsx("input",{type:"checkbox",checked:t.downloadFallback,onChange:s=>n("downloadFallback",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Dark mode"}),e.jsx("input",{type:"checkbox",checked:t.darkMode,onChange:s=>n("darkMode",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Show only aria downloads"}),e.jsx("input",{type:"checkbox",checked:t.showOnlyAria,onChange:s=>n("showOnlyAria",s.target.checked)})]})]})]}),e.jsxs("div",{className:"actions",children:[e.jsx("button",{onClick:x,children:"Save"}),e.jsx("button",{className:"ghost",onClick:()=>window.close(),children:"Close"})]}),e.jsx("div",{children:h})]}):e.jsx("div",{className:"wrap",children:"Loading..."})}p.createRoot(document.getElementById("root")).render(e.jsx(a.StrictMode,{children:e.jsx(u,{})})); `).map(c=>c.trim()).filter(Boolean)};await m(s),i(s),d("Saved"),window.setTimeout(()=>d(""),1500)};return t?e.jsxs("div",{className:"wrap",children:[e.jsx("h1",{children:"Gomdown Helper Settings"}),e.jsxs("div",{className:"grid",children:[e.jsxs("section",{className:"card",children:[e.jsx("label",{children:"RPC Secret"}),e.jsx("input",{value:t.motrixAPIkey,onChange:s=>n("motrixAPIkey",s.target.value)}),e.jsx("label",{children:"RPC Port"}),e.jsx("input",{type:"number",value:t.motrixPort,onChange:s=>n("motrixPort",Number(s.target.value||16800))}),e.jsx("label",{children:"Min file size (MB)"}),e.jsx("input",{type:"number",value:t.minFileSize,onChange:s=>n("minFileSize",Number(s.target.value||0))}),e.jsx("label",{children:"Blacklist (one per line)"}),e.jsx("textarea",{rows:8,value:l,onChange:s=>r(s.target.value)})]}),e.jsxs("section",{className:"card",children:[e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Extension enabled"}),e.jsx("input",{type:"checkbox",checked:t.extensionStatus,onChange:s=>n("extensionStatus",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Enable notifications"}),e.jsx("input",{type:"checkbox",checked:t.enableNotifications,onChange:s=>n("enableNotifications",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Use native host"}),e.jsx("input",{type:"checkbox",checked:t.useNativeHost,onChange:s=>n("useNativeHost",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Activate app on download"}),e.jsx("input",{type:"checkbox",checked:t.activateAppOnDownload,onChange:s=>n("activateAppOnDownload",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Hide browser download shelf"}),e.jsx("input",{type:"checkbox",checked:t.hideChromeBar,onChange:s=>n("hideChromeBar",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Show context menu option"}),e.jsx("input",{type:"checkbox",checked:t.showContextOption,onChange:s=>n("showContextOption",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Download fallback"}),e.jsx("input",{type:"checkbox",checked:t.downloadFallback,onChange:s=>n("downloadFallback",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Dark mode"}),e.jsx("input",{type:"checkbox",checked:t.darkMode,onChange:s=>n("darkMode",s.target.checked)})]}),e.jsxs("div",{className:"row",children:[e.jsx("span",{children:"Show only aria downloads"}),e.jsx("input",{type:"checkbox",checked:t.showOnlyAria,onChange:s=>n("showOnlyAria",s.target.checked)})]})]})]}),e.jsxs("div",{className:"actions",children:[e.jsx("button",{onClick:x,children:"Save"}),e.jsx("button",{className:"ghost",onClick:()=>window.close(),children:"Close"})]}),e.jsx("div",{children:h})]}):e.jsx("div",{className:"wrap",children:"Loading..."})}j.createRoot(document.getElementById("root")).render(e.jsx(a.StrictMode,{children:e.jsx(u,{})}));

View File

@@ -1 +0,0 @@
import{c as k,j as t,R as r}from"./client-CBvt1tWS.js";import{b as i}from"./browser-polyfill-CZ_dLIqp.js";import{g as N,s as m}from"./settings-Bo6W9Drl.js";function b(){const[s,d]=r.useState(null),[x,n]=r.useState(""),[l,u]=r.useState([]);r.useEffect(()=>{N().then(d)},[]),r.useEffect(()=>{let e=null;const a=async()=>{const o=await i.runtime.sendMessage({type:"media:list"});o?.ok&&Array.isArray(o.items)&&u(o.items.slice(0,10))};return a(),e=window.setInterval(()=>{a()},2e3),()=>{e!==null&&window.clearInterval(e)}},[]);const c=(e,a)=>{d(o=>o&&{...o,[e]:a})},h=async e=>{if(!s)return;const a={...s,extensionStatus:e};d(a),await m({extensionStatus:e}),n(e?"Extension ON":"Extension OFF"),window.setTimeout(()=>n(""),1200)},g=async()=>{s&&(await m(s),n("Saved"),window.setTimeout(()=>n(""),1200))},p=async()=>{const e=i.runtime.getURL("src/config/index.html");await i.tabs.create({url:e})},j=async()=>{const e=i.runtime.getURL("src/history/index.html");await i.tabs.create({url:e})},v=async()=>{const e=await i.runtime.sendMessage({type:"page:enqueue-ytdlp"});n(e?.ok?"Active tab sent to gdown (yt-dlp)":`Send failed: ${e?.error||"unknown error"}`),window.setTimeout(()=>n(""),1800)},w=async e=>{const a=await i.runtime.sendMessage({type:"media:enqueue",url:e.url,referer:e.referer||"",kind:e.kind,suggestedOut:e.suggestedOut||"",cookie:e.cookie||"",userAgent:e.userAgent||""});n(a?.ok?"Media sent to gdown":`Send failed: ${a?.error||"unknown error"}`),window.setTimeout(()=>n(""),1600)},y=async()=>{await i.runtime.sendMessage({type:"media:clear"}),u([]),n("Captured media cleared"),window.setTimeout(()=>n(""),1200)};return s?t.jsxs("div",{className:"container",children:[t.jsx("h1",{children:"Gomdown Helper"}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Secret"}),t.jsx("input",{type:"text",value:s.motrixAPIkey,onChange:e=>c("motrixAPIkey",e.target.value),placeholder:"aria2 rpc secret"})]}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Port"}),t.jsx("input",{type:"number",value:s.motrixPort,onChange:e=>c("motrixPort",Number(e.target.value||16800))})]}),t.jsxs("label",{className:"toggle",children:["Extension Enabled",t.jsx("input",{type:"checkbox",checked:s.extensionStatus,onChange:e=>{h(e.target.checked)}})]}),t.jsxs("label",{className:"toggle",children:["Use Native Host",t.jsx("input",{type:"checkbox",checked:s.useNativeHost,onChange:e=>c("useNativeHost",e.target.checked)})]}),t.jsxs("label",{className:"toggle",children:["Activate gdown App",t.jsx("input",{type:"checkbox",checked:s.activateAppOnDownload,onChange:e=>c("activateAppOnDownload",e.target.checked)})]}),t.jsx("button",{onClick:g,children:"Save"}),t.jsx("button",{onClick:p,children:"Settings"}),t.jsx("button",{onClick:j,children:"History"}),t.jsx("button",{onClick:()=>{v()},children:"Send Active Tab (yt-dlp)"}),t.jsxs("div",{className:"media-panel",children:[t.jsxs("div",{className:"media-head",children:[t.jsx("strong",{children:"Captured Media"}),t.jsx("button",{className:"mini ghost",onClick:y,children:"Clear"})]}),l.length===0?t.jsx("div",{className:"empty",children:"No media captured yet"}):t.jsx("div",{className:"media-list",children:l.map(e=>t.jsxs("div",{className:"media-item",children:[t.jsxs("div",{className:"media-meta",children:[t.jsx("span",{className:"kind",children:e.kind.toUpperCase()}),e.pageTitle?t.jsx("span",{className:"url",children:e.pageTitle}):null,t.jsx("span",{className:"url",children:e.url})]}),t.jsx("button",{className:"mini",onClick:()=>{w(e)},children:"Add"})]},e.id))})]}),t.jsx("div",{className:"status",children:x})]}):t.jsx("div",{className:"container",children:"Loading..."})}k.createRoot(document.getElementById("root")).render(t.jsx(r.StrictMode,{children:t.jsx(b,{})}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
(async () => { (async () => {
const { onExecute } = await import( const { onExecute } = await import(
/* @vite-ignore */ /* @vite-ignore */
chrome.runtime.getURL("assets/index.ts-C6ePCen1.js") chrome.runtime.getURL("assets/index.ts-w1ilzv93.js")
); );
onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } }); onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });
})().catch(console.error); })().catch(console.error);

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{b as e}from"./browser-polyfill-CZ_dLIqp.js";const t={extensionStatus:!0,useNativeHost:!0,activateAppOnDownload:!0,enableNotifications:!0,hideChromeBar:!0,showContextOption:!0,downloadFallback:!1,darkMode:!1,showOnlyAria:!1,minFileSize:0,blacklist:[],motrixPort:16800,motrixAPIkey:""};async function n(){const o=await e.storage.sync.get(Object.keys(t));return{extensionStatus:!!(o.extensionStatus??t.extensionStatus),useNativeHost:!!(o.useNativeHost??t.useNativeHost),activateAppOnDownload:!!(o.activateAppOnDownload??t.activateAppOnDownload),enableNotifications:!!(o.enableNotifications??t.enableNotifications),hideChromeBar:!!(o.hideChromeBar??t.hideChromeBar),showContextOption:!!(o.showContextOption??t.showContextOption),downloadFallback:!!(o.downloadFallback??t.downloadFallback),darkMode:!!(o.darkMode??t.darkMode),showOnlyAria:!!(o.showOnlyAria??t.showOnlyAria),minFileSize:Number(o.minFileSize??t.minFileSize),blacklist:Array.isArray(o.blacklist)?o.blacklist.map(a=>String(a)):t.blacklist,motrixPort:Number(o.motrixPort??t.motrixPort),motrixAPIkey:String(o.motrixAPIkey??t.motrixAPIkey)}}async function s(o){await e.storage.sync.set(o)}export{n as g,s};

View File

@@ -28,7 +28,7 @@
"content_scripts": [ "content_scripts": [
{ {
"js": [ "js": [
"assets/index.ts-loader-Bju9eGS_.js" "assets/index.ts-loader-D_eQmgUa.js"
], ],
"matches": [ "matches": [
"<all_urls>" "<all_urls>"
@@ -57,9 +57,9 @@
], ],
"resources": [ "resources": [
"images/*", "images/*",
"assets/browser-polyfill-CZ_dLIqp.js", "assets/settings-CgBxHrrF.js",
"assets/downloadIntent-Dv31jC2S.js", "assets/downloadIntent-Dv31jC2S.js",
"assets/index.ts-C6ePCen1.js" "assets/index.ts-w1ilzv93.js"
], ],
"use_dynamic_url": false "use_dynamic_url": false
} }

View File

@@ -1 +1 @@
import './assets/index.ts-BljhweV3.js'; import './assets/index.ts-U8lbRRO-.js';

View File

@@ -4,10 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gomdown Helper Settings</title> <title>Gomdown Helper Settings</title>
<script type="module" crossorigin src="/assets/index.html-B0Kfv8fq.js"></script> <script type="module" crossorigin src="/assets/index.html-B7fMyQPm.js"></script>
<link rel="modulepreload" crossorigin href="/assets/browser-polyfill-CZ_dLIqp.js"> <link rel="modulepreload" crossorigin href="/assets/settings-CgBxHrrF.js">
<link rel="modulepreload" crossorigin href="/assets/client-CBvt1tWS.js"> <link rel="modulepreload" crossorigin href="/assets/client-BzjyOx7y.js">
<link rel="modulepreload" crossorigin href="/assets/settings-Bo6W9Drl.js">
<link rel="stylesheet" crossorigin href="/assets/index-B2D5FcJM.css"> <link rel="stylesheet" crossorigin href="/assets/index-B2D5FcJM.css">
</head> </head>
<body> <body>

View File

@@ -4,11 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gomdown Helper</title> <title>Gomdown Helper</title>
<script type="module" crossorigin src="/assets/index.html-BLzIyLM-.js"></script> <script type="module" crossorigin src="/assets/index.html-92_ZB8wX.js"></script>
<link rel="modulepreload" crossorigin href="/assets/browser-polyfill-CZ_dLIqp.js"> <link rel="modulepreload" crossorigin href="/assets/settings-CgBxHrrF.js">
<link rel="modulepreload" crossorigin href="/assets/client-CBvt1tWS.js"> <link rel="modulepreload" crossorigin href="/assets/client-BzjyOx7y.js">
<link rel="modulepreload" crossorigin href="/assets/settings-Bo6W9Drl.js"> <link rel="stylesheet" crossorigin href="/assets/index-CJaGAyoX.css">
<link rel="stylesheet" crossorigin href="/assets/index-D6aWDpYY.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,8 +1,10 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import { isLikelyDownloadUrl, normalizeUrl } from '../lib/downloadIntent' import { isLikelyDownloadUrl, normalizeUrl } from '../lib/downloadIntent'
import { getSettings } from '../lib/settings'
const CAPTURE_TTL_MS = 8000 const CAPTURE_TTL_MS = 8000
const captureInFlight = new Map<string, number>() const captureInFlight = new Map<string, number>()
let extensionEnabled = false
function pruneCaptureInFlight(): void { function pruneCaptureInFlight(): void {
const now = Date.now() const now = Date.now()
@@ -12,6 +14,7 @@ function pruneCaptureInFlight(): void {
} }
async function sendCapture(url: string, referer: string): Promise<boolean> { async function sendCapture(url: string, referer: string): Promise<boolean> {
if (!extensionEnabled) return false
const normalized = normalizeUrl(url, window.location.href) const normalized = normalizeUrl(url, window.location.href)
if (!normalized) return false if (!normalized) return false
@@ -46,6 +49,7 @@ function shouldIgnoreHotkey(event: MouseEvent | KeyboardEvent): boolean {
} }
async function interceptAnchorEvent(event: MouseEvent): Promise<void> { async function interceptAnchorEvent(event: MouseEvent): Promise<void> {
if (!extensionEnabled) return
if (event.defaultPrevented) return if (event.defaultPrevented) return
if (shouldIgnoreHotkey(event)) return if (shouldIgnoreHotkey(event)) return
@@ -61,6 +65,7 @@ async function interceptAnchorEvent(event: MouseEvent): Promise<void> {
} }
function interceptMouseLike(event: MouseEvent): void { function interceptMouseLike(event: MouseEvent): void {
if (!extensionEnabled) return
const anchor = findAnchor(event.target) const anchor = findAnchor(event.target)
if (!anchor) return if (!anchor) return
const href = anchor.href || '' const href = anchor.href || ''
@@ -89,6 +94,7 @@ document.addEventListener('click', (event: MouseEvent) => {
}, true) }, true)
document.addEventListener('keydown', (event: KeyboardEvent) => { document.addEventListener('keydown', (event: KeyboardEvent) => {
if (!extensionEnabled) return
if (event.key !== 'Enter') return if (event.key !== 'Enter') return
if (event.defaultPrevented) return if (event.defaultPrevented) return
if (shouldIgnoreHotkey(event)) return if (shouldIgnoreHotkey(event)) return
@@ -114,7 +120,7 @@ function installProgrammaticInterceptors(): void {
const originalOpen = window.open.bind(window) const originalOpen = window.open.bind(window)
window.open = function gomdownInterceptOpen(url?: string | URL, target?: string, features?: string): Window | null { window.open = function gomdownInterceptOpen(url?: string | URL, target?: string, features?: string): Window | null {
const raw = String(url || '').trim() const raw = String(url || '').trim()
if (raw && isLikelyDownloadUrl(raw, window.location.href)) { if (extensionEnabled && raw && isLikelyDownloadUrl(raw, window.location.href)) {
void sendCapture(raw, window.location.href) void sendCapture(raw, window.location.href)
return null return null
} }
@@ -128,7 +134,7 @@ function installProgrammaticInterceptors(): void {
const originalAnchorClick = HTMLAnchorElement.prototype.click const originalAnchorClick = HTMLAnchorElement.prototype.click
HTMLAnchorElement.prototype.click = function gomdownInterceptAnchorClick(): void { HTMLAnchorElement.prototype.click = function gomdownInterceptAnchorClick(): void {
const href = this.href || this.getAttribute('href') || '' const href = this.href || this.getAttribute('href') || ''
if (href && isLikelyDownloadUrl(href, window.location.href)) { if (extensionEnabled && href && isLikelyDownloadUrl(href, window.location.href)) {
void sendCapture(href, document.referrer || window.location.href) void sendCapture(href, document.referrer || window.location.href)
return return
} }
@@ -196,6 +202,10 @@ function removeYoutubeOverlay(): void {
} }
function ensureYoutubeOverlay(): void { function ensureYoutubeOverlay(): void {
if (!extensionEnabled) {
removeYoutubeOverlay()
return
}
if (window.top !== window.self) return if (window.top !== window.self) return
if (!isYoutubeWatchPage(window.location.href)) { if (!isYoutubeWatchPage(window.location.href)) {
removeYoutubeOverlay() removeYoutubeOverlay()
@@ -283,6 +293,15 @@ watchYoutubeRouteChanges()
let mediaToastRoot: HTMLDivElement | null = null let mediaToastRoot: HTMLDivElement | null = null
let mediaToastTimer: number | null = null let mediaToastTimer: number | null = null
function hideMediaCapturedToast(): void {
if (!mediaToastRoot) return
mediaToastRoot.style.display = 'none'
if (mediaToastTimer !== null) {
window.clearTimeout(mediaToastTimer)
mediaToastTimer = null
}
}
function ensureMediaToastRoot(): HTMLDivElement { function ensureMediaToastRoot(): HTMLDivElement {
if (mediaToastRoot) return mediaToastRoot if (mediaToastRoot) return mediaToastRoot
const root = document.createElement('div') const root = document.createElement('div')
@@ -308,6 +327,7 @@ function ensureMediaToastRoot(): HTMLDivElement {
} }
function showMediaCapturedToast(payload: { kind?: string; url?: string; suggestedOut?: string }): void { function showMediaCapturedToast(payload: { kind?: string; url?: string; suggestedOut?: string }): void {
if (!extensionEnabled) return
const root = ensureMediaToastRoot() const root = ensureMediaToastRoot()
const kind = String(payload?.kind || 'media').toUpperCase() const kind = String(payload?.kind || 'media').toUpperCase()
const out = String(payload?.suggestedOut || '').trim() const out = String(payload?.suggestedOut || '').trim()
@@ -332,3 +352,33 @@ browser.runtime.onMessage.addListener((message: any) => {
}) })
} }
}) })
async function syncExtensionEnabled(): Promise<void> {
try {
const settings = await getSettings()
extensionEnabled = Boolean(settings.extensionStatus)
} catch {
extensionEnabled = false
}
if (!extensionEnabled) {
removeYoutubeOverlay()
hideMediaCapturedToast()
} else {
ensureYoutubeOverlay()
}
}
void syncExtensionEnabled()
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'sync') return
if (!changes.extensionStatus) return
extensionEnabled = Boolean(changes.extensionStatus.newValue)
if (!extensionEnabled) {
removeYoutubeOverlay()
hideMediaCapturedToast()
return
}
ensureYoutubeOverlay()
})

View File

@@ -111,7 +111,18 @@ function App(): JSX.Element {
return ( return (
<div className="container"> <div className="container">
<div className="top-row">
<h1>Gomdown Helper</h1> <h1>Gomdown Helper</h1>
<button
className={`power-toggle ${settings.extensionStatus ? 'on' : 'off'}`}
onClick={() => {
void onToggleExtension(!settings.extensionStatus)
}}
>
<span className="power-dot" />
{settings.extensionStatus ? 'ON' : 'OFF'}
</button>
</div>
<div className="field"> <div className="field">
<label>RPC Secret</label> <label>RPC Secret</label>
@@ -132,17 +143,6 @@ function App(): JSX.Element {
/> />
</div> </div>
<label className="toggle">
Extension Enabled
<input
type="checkbox"
checked={settings.extensionStatus}
onChange={(e) => {
void onToggleExtension(e.target.checked)
}}
/>
</label>
<label className="toggle"> <label className="toggle">
Use Native Host Use Native Host
<input <input

View File

@@ -16,11 +16,52 @@ body {
gap: 10px; gap: 10px;
} }
.top-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
h1 { h1 {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
} }
.power-toggle {
height: 28px;
min-width: 70px;
border-radius: 999px;
padding: 0 12px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.04em;
}
.power-toggle.on {
background: #204f37;
border-color: #2b7a52;
color: #c7ffe3;
}
.power-toggle.off {
background: #4a2631;
border-color: #75404d;
color: #ffd6df;
}
.power-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
opacity: 0.9;
}
.field { .field {
display: grid; display: grid;
gap: 6px; gap: 6px;