Add clipping/highlight planning docs and sync current extension updates
This commit is contained in:
@@ -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개 이상 성공
|
||||
@@ -9,3 +9,11 @@
|
||||
- [ ] Step 2: content script 보조 탐지
|
||||
- [ ] Step 3: 노이즈 필터/품질 그룹핑
|
||||
- [ ] 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)와 기본 회귀 테스트 시나리오 정리
|
||||
|
||||
@@ -1,83 +1,73 @@
|
||||
{
|
||||
"../../../../../@crx/manifest": {
|
||||
"file": "assets/crx-manifest.js-mSo6-ym3.js",
|
||||
"../../../../../../../@crx/manifest": {
|
||||
"file": "assets/crx-manifest.js-CXu7hmTa.js",
|
||||
"name": "crx-manifest.js",
|
||||
"src": "../../../../../@crx/manifest",
|
||||
"src": "../../../../../../../@crx/manifest",
|
||||
"isEntry": true
|
||||
},
|
||||
"_browser-polyfill-CZ_dLIqp.js": {
|
||||
"file": "assets/browser-polyfill-CZ_dLIqp.js",
|
||||
"name": "browser-polyfill"
|
||||
},
|
||||
"_client-CBvt1tWS.js": {
|
||||
"file": "assets/client-CBvt1tWS.js",
|
||||
"_client-BzjyOx7y.js": {
|
||||
"file": "assets/client-BzjyOx7y.js",
|
||||
"name": "client",
|
||||
"imports": [
|
||||
"_browser-polyfill-CZ_dLIqp.js"
|
||||
"_settings-CgBxHrrF.js"
|
||||
]
|
||||
},
|
||||
"_downloadIntent-Dv31jC2S.js": {
|
||||
"file": "assets/downloadIntent-Dv31jC2S.js",
|
||||
"name": "downloadIntent"
|
||||
},
|
||||
"_index.ts-loader-Bju9eGS_.js": {
|
||||
"file": "assets/index.ts-loader-Bju9eGS_.js",
|
||||
"src": "_index.ts-loader-Bju9eGS_.js"
|
||||
"_index.ts-loader-D_eQmgUa.js": {
|
||||
"file": "assets/index.ts-loader-D_eQmgUa.js",
|
||||
"src": "_index.ts-loader-D_eQmgUa.js"
|
||||
},
|
||||
"_settings-Bo6W9Drl.js": {
|
||||
"file": "assets/settings-Bo6W9Drl.js",
|
||||
"name": "settings",
|
||||
"imports": [
|
||||
"_browser-polyfill-CZ_dLIqp.js"
|
||||
]
|
||||
"_settings-CgBxHrrF.js": {
|
||||
"file": "assets/settings-CgBxHrrF.js",
|
||||
"name": "settings"
|
||||
},
|
||||
"src/background/index.ts": {
|
||||
"file": "assets/index.ts-BljhweV3.js",
|
||||
"file": "assets/index.ts-U8lbRRO-.js",
|
||||
"name": "index.ts",
|
||||
"src": "src/background/index.ts",
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"_browser-polyfill-CZ_dLIqp.js",
|
||||
"_downloadIntent-Dv31jC2S.js",
|
||||
"_settings-Bo6W9Drl.js"
|
||||
"_settings-CgBxHrrF.js",
|
||||
"_downloadIntent-Dv31jC2S.js"
|
||||
]
|
||||
},
|
||||
"src/config/index.html": {
|
||||
"file": "assets/index.html-B0Kfv8fq.js",
|
||||
"file": "assets/index.html-B7fMyQPm.js",
|
||||
"name": "index.html",
|
||||
"src": "src/config/index.html",
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"_client-CBvt1tWS.js",
|
||||
"_settings-Bo6W9Drl.js",
|
||||
"_browser-polyfill-CZ_dLIqp.js"
|
||||
"_client-BzjyOx7y.js",
|
||||
"_settings-CgBxHrrF.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/index-B2D5FcJM.css"
|
||||
]
|
||||
},
|
||||
"src/content/index.ts": {
|
||||
"file": "assets/index.ts-C6ePCen1.js",
|
||||
"file": "assets/index.ts-w1ilzv93.js",
|
||||
"name": "index.ts",
|
||||
"src": "src/content/index.ts",
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"_browser-polyfill-CZ_dLIqp.js",
|
||||
"_settings-CgBxHrrF.js",
|
||||
"_downloadIntent-Dv31jC2S.js"
|
||||
]
|
||||
},
|
||||
"src/popup/index.html": {
|
||||
"file": "assets/index.html-BLzIyLM-.js",
|
||||
"file": "assets/index.html-92_ZB8wX.js",
|
||||
"name": "index.html",
|
||||
"src": "src/popup/index.html",
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"_client-CBvt1tWS.js",
|
||||
"_browser-polyfill-CZ_dLIqp.js",
|
||||
"_settings-Bo6W9Drl.js"
|
||||
"_client-BzjyOx7y.js",
|
||||
"_settings-CgBxHrrF.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/index-D6aWDpYY.css"
|
||||
"assets/index-CJaGAyoX.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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,{})}));
|
||||
+2
-2
@@ -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(`
|
||||
`).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,{})}));
|
||||
@@ -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
+1
-1
@@ -5,7 +5,7 @@
|
||||
(async () => {
|
||||
const { onExecute } = await import(
|
||||
/* @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 } });
|
||||
})().catch(console.error);
|
||||
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -28,7 +28,7 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [
|
||||
"assets/index.ts-loader-Bju9eGS_.js"
|
||||
"assets/index.ts-loader-D_eQmgUa.js"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
@@ -57,9 +57,9 @@
|
||||
],
|
||||
"resources": [
|
||||
"images/*",
|
||||
"assets/browser-polyfill-CZ_dLIqp.js",
|
||||
"assets/settings-CgBxHrrF.js",
|
||||
"assets/downloadIntent-Dv31jC2S.js",
|
||||
"assets/index.ts-C6ePCen1.js"
|
||||
"assets/index.ts-w1ilzv93.js"
|
||||
],
|
||||
"use_dynamic_url": false
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
import './assets/index.ts-BljhweV3.js';
|
||||
import './assets/index.ts-U8lbRRO-.js';
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gomdown Helper Settings</title>
|
||||
<script type="module" crossorigin src="/assets/index.html-B0Kfv8fq.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/browser-polyfill-CZ_dLIqp.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/client-CBvt1tWS.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/settings-Bo6W9Drl.js">
|
||||
<script type="module" crossorigin src="/assets/index.html-B7fMyQPm.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/settings-CgBxHrrF.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/client-BzjyOx7y.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B2D5FcJM.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gomdown Helper</title>
|
||||
<script type="module" crossorigin src="/assets/index.html-BLzIyLM-.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/browser-polyfill-CZ_dLIqp.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/client-CBvt1tWS.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/settings-Bo6W9Drl.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D6aWDpYY.css">
|
||||
<script type="module" crossorigin src="/assets/index.html-92_ZB8wX.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/settings-CgBxHrrF.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/client-BzjyOx7y.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CJaGAyoX.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Binary file not shown.
+52
-2
@@ -1,8 +1,10 @@
|
||||
import browser from 'webextension-polyfill'
|
||||
import { isLikelyDownloadUrl, normalizeUrl } from '../lib/downloadIntent'
|
||||
import { getSettings } from '../lib/settings'
|
||||
|
||||
const CAPTURE_TTL_MS = 8000
|
||||
const captureInFlight = new Map<string, number>()
|
||||
let extensionEnabled = false
|
||||
|
||||
function pruneCaptureInFlight(): void {
|
||||
const now = Date.now()
|
||||
@@ -12,6 +14,7 @@ function pruneCaptureInFlight(): void {
|
||||
}
|
||||
|
||||
async function sendCapture(url: string, referer: string): Promise<boolean> {
|
||||
if (!extensionEnabled) return false
|
||||
const normalized = normalizeUrl(url, window.location.href)
|
||||
if (!normalized) return false
|
||||
|
||||
@@ -46,6 +49,7 @@ function shouldIgnoreHotkey(event: MouseEvent | KeyboardEvent): boolean {
|
||||
}
|
||||
|
||||
async function interceptAnchorEvent(event: MouseEvent): Promise<void> {
|
||||
if (!extensionEnabled) return
|
||||
if (event.defaultPrevented) return
|
||||
if (shouldIgnoreHotkey(event)) return
|
||||
|
||||
@@ -61,6 +65,7 @@ async function interceptAnchorEvent(event: MouseEvent): Promise<void> {
|
||||
}
|
||||
|
||||
function interceptMouseLike(event: MouseEvent): void {
|
||||
if (!extensionEnabled) return
|
||||
const anchor = findAnchor(event.target)
|
||||
if (!anchor) return
|
||||
const href = anchor.href || ''
|
||||
@@ -89,6 +94,7 @@ document.addEventListener('click', (event: MouseEvent) => {
|
||||
}, true)
|
||||
|
||||
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (!extensionEnabled) return
|
||||
if (event.key !== 'Enter') return
|
||||
if (event.defaultPrevented) return
|
||||
if (shouldIgnoreHotkey(event)) return
|
||||
@@ -114,7 +120,7 @@ function installProgrammaticInterceptors(): void {
|
||||
const originalOpen = window.open.bind(window)
|
||||
window.open = function gomdownInterceptOpen(url?: string | URL, target?: string, features?: string): Window | null {
|
||||
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)
|
||||
return null
|
||||
}
|
||||
@@ -128,7 +134,7 @@ function installProgrammaticInterceptors(): void {
|
||||
const originalAnchorClick = HTMLAnchorElement.prototype.click
|
||||
HTMLAnchorElement.prototype.click = function gomdownInterceptAnchorClick(): void {
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -196,6 +202,10 @@ function removeYoutubeOverlay(): void {
|
||||
}
|
||||
|
||||
function ensureYoutubeOverlay(): void {
|
||||
if (!extensionEnabled) {
|
||||
removeYoutubeOverlay()
|
||||
return
|
||||
}
|
||||
if (window.top !== window.self) return
|
||||
if (!isYoutubeWatchPage(window.location.href)) {
|
||||
removeYoutubeOverlay()
|
||||
@@ -283,6 +293,15 @@ watchYoutubeRouteChanges()
|
||||
let mediaToastRoot: HTMLDivElement | 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 {
|
||||
if (mediaToastRoot) return mediaToastRoot
|
||||
const root = document.createElement('div')
|
||||
@@ -308,6 +327,7 @@ function ensureMediaToastRoot(): HTMLDivElement {
|
||||
}
|
||||
|
||||
function showMediaCapturedToast(payload: { kind?: string; url?: string; suggestedOut?: string }): void {
|
||||
if (!extensionEnabled) return
|
||||
const root = ensureMediaToastRoot()
|
||||
const kind = String(payload?.kind || 'media').toUpperCase()
|
||||
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()
|
||||
})
|
||||
|
||||
+11
-11
@@ -111,7 +111,18 @@ function App(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="top-row">
|
||||
<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">
|
||||
<label>RPC Secret</label>
|
||||
@@ -132,17 +143,6 @@ function App(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="toggle">
|
||||
Extension Enabled
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extensionStatus}
|
||||
onChange={(e) => {
|
||||
void onToggleExtension(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="toggle">
|
||||
Use Native Host
|
||||
<input
|
||||
|
||||
@@ -16,11 +16,52 @@ body {
|
||||
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: 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 {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
Reference in New Issue
Block a user