feat: add youtube floating quick-download action

This commit is contained in:
tongki078
2026-02-26 12:17:36 +09:00
parent e8b7432594
commit 59226ed6fd
10 changed files with 172 additions and 11 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "gomdown-helper",
"private": true,
"version": "0.0.10",
"version": "0.0.11",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,6 +1,6 @@
{
"../../../../../@crx/manifest": {
"file": "assets/crx-manifest.js--9ZsUvq0.js",
"file": "assets/crx-manifest.js-B0cyAIB1.js",
"name": "crx-manifest.js",
"src": "../../../../../@crx/manifest",
"isEntry": true
@@ -20,9 +20,9 @@
"file": "assets/downloadIntent-Dv31jC2S.js",
"name": "downloadIntent"
},
"_index.ts-loader-DMyyuf2n.js": {
"file": "assets/index.ts-loader-DMyyuf2n.js",
"src": "_index.ts-loader-DMyyuf2n.js"
"_index.ts-loader-BHtfStLc.js": {
"file": "assets/index.ts-loader-BHtfStLc.js",
"src": "_index.ts-loader-BHtfStLc.js"
},
"_settings-Bo6W9Drl.js": {
"file": "assets/settings-Bo6W9Drl.js",
@@ -32,7 +32,7 @@
]
},
"src/background/index.ts": {
"file": "assets/index.ts-U2ACoZ75.js",
"file": "assets/index.ts-BAxKsZ8F.js",
"name": "index.ts",
"src": "src/background/index.ts",
"isEntry": true,
@@ -57,7 +57,7 @@
]
},
"src/content/index.ts": {
"file": "assets/index.ts-BGLNJwsP.js",
"file": "assets/index.ts-CMnPQ13j.js",
"name": "index.ts",
"src": "src/content/index.ts",
"isEntry": true,

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{b as m}from"./browser-polyfill-CZ_dLIqp.js";import{i as a,n as k}from"./downloadIntent-Dv31jC2S.js";const E=8e3,i=new Map;function v(){const e=Date.now();for(const[r,t]of i.entries())t<=e&&i.delete(r)}async function u(e,r){const t=k(e,window.location.href);if(!t)return!1;if(v(),i.has(t))return!0;i.set(t,Date.now()+E);try{if((await m.runtime.sendMessage({type:"capture-link-download",url:t,referer:r||document.referrer||window.location.href}))?.ok)return!0}catch{}return i.delete(t),!1}function p(e){return e?e instanceof HTMLAnchorElement?e:e instanceof Element?e.closest("a[href]"):null:null}function h(e){return!!(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)}async function g(e){if(e.defaultPrevented||h(e))return;const r=p(e.target);if(!r)return;const t=r.href||"";!t||!a(t,window.location.href)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),await u(t,document.referrer||window.location.href))}function b(e){const r=p(e.target);if(!r)return;const t=r.href||"";!t||!a(t,window.location.href)||h(e)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),u(t,document.referrer||window.location.href))}document.addEventListener("pointerdown",e=>{e.button===0&&b(e)},!0);document.addEventListener("mousedown",e=>{e.button===0&&b(e)},!0);document.addEventListener("click",e=>{e.button===0&&g(e)},!0);document.addEventListener("keydown",e=>{if(e.key!=="Enter"||e.defaultPrevented||h(e))return;const r=p(e.target);if(!r)return;const t=r.href||"";!t||!a(t,window.location.href)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),u(t,document.referrer||window.location.href))},!0);document.addEventListener("auxclick",e=>{e.button===1&&g(e)},!0);function C(){try{const e=window.open.bind(window);window.open=function(t,n,x){const d=String(t||"").trim();return d&&a(d,window.location.href)?(u(d,window.location.href),null):e(t,n,x)}}catch{}try{const e=HTMLAnchorElement.prototype.click;HTMLAnchorElement.prototype.click=function(){const t=this.href||this.getAttribute("href")||"";if(t&&a(t,window.location.href)){u(t,document.referrer||window.location.href);return}e.call(this)}}catch{}}C();let l=null,o=null,w=!1,y=null,s=window.location.href;function L(e){try{const r=new URL(e);return r.hostname!=="www.youtube.com"&&r.hostname!=="youtube.com"?!1:r.pathname==="/watch"&&r.searchParams.has("v")}catch{return!1}}function c(e,r="idle"){o&&(o.textContent=e,r==="ok"?o.style.color="#8ff0a4":r==="error"?o.style.color="#ff9b9b":o.style.color="#aeb7d8")}async function P(){if(!w){w=!0,c("gdown으로 전송 중...");try{const e=await m.runtime.sendMessage({type:"page:enqueue-ytdlp-url",url:window.location.href,referer:window.location.href});e?.ok?c("다운로드 모달로 전송됨","ok"):c(`전송 실패: ${e?.error||"unknown error"}`,"error")}catch(e){c(`전송 실패: ${String(e)}`,"error")}finally{w=!1}}}function I(){l&&(l.remove(),l=null,o=null)}function f(){if(window.top!==window.self)return;if(!L(window.location.href)){I();return}if(l)return;const e=document.createElement("div");e.id="gomdown-youtube-overlay",e.style.position="fixed",e.style.right="20px",e.style.bottom="24px",e.style.zIndex="2147483647",e.style.background="rgba(17, 21, 32, 0.94)",e.style.border="1px solid rgba(133, 148, 195, 0.35)",e.style.borderRadius="12px",e.style.padding="10px",e.style.boxShadow="0 8px 24px rgba(0, 0, 0, 0.28)",e.style.backdropFilter="blur(6px)",e.style.width="220px",e.style.fontFamily="ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",e.style.color="#e8edff";const r=document.createElement("div");r.textContent="Gdown Helper",r.style.fontSize="12px",r.style.fontWeight="700",r.style.marginBottom="8px";const t=document.createElement("button");t.type="button",t.textContent="이 영상 다운로드",t.style.width="100%",t.style.height="34px",t.style.border="1px solid #5a69f0",t.style.borderRadius="8px",t.style.background="#5a69f0",t.style.color="#ffffff",t.style.fontSize="12px",t.style.fontWeight="700",t.style.cursor="pointer",t.addEventListener("click",()=>{P()});const n=document.createElement("div");n.textContent="클릭 시 gdown 다운로드 모달로 연결",n.style.fontSize="11px",n.style.marginTop="8px",n.style.lineHeight="1.35",n.style.color="#aeb7d8",e.appendChild(r),e.appendChild(t),e.appendChild(n),document.documentElement.appendChild(e),l=e,o=n}function S(){y===null&&(y=window.setInterval(()=>{const e=window.location.href;e!==s&&(s=e,f())},800),window.addEventListener("popstate",()=>{s=window.location.href,f()}),document.addEventListener("yt-navigate-finish",()=>{s=window.location.href,f()}))}f();S();

View File

@@ -0,0 +1,13 @@
(function () {
'use strict';
const injectTime = performance.now();
(async () => {
const { onExecute } = await import(
/* @vite-ignore */
chrome.runtime.getURL("assets/index.ts-CMnPQ13j.js")
);
onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });
})().catch(console.error);
})();

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Gomdown Helper",
"description": "Send browser downloads to gdown",
"version": "0.0.10",
"version": "0.0.11",
"default_locale": "en",
"icons": {
"16": "images/16.png",
@@ -28,7 +28,7 @@
"content_scripts": [
{
"js": [
"assets/index.ts-loader-DMyyuf2n.js"
"assets/index.ts-loader-BHtfStLc.js"
],
"matches": [
"<all_urls>"
@@ -59,7 +59,7 @@
"images/*",
"assets/browser-polyfill-CZ_dLIqp.js",
"assets/downloadIntent-Dv31jC2S.js",
"assets/index.ts-BGLNJwsP.js"
"assets/index.ts-CMnPQ13j.js"
],
"use_dynamic_url": false
}

View File

@@ -1 +1 @@
import './assets/index.ts-U2ACoZ75.js';
import './assets/index.ts-BAxKsZ8F.js';

Binary file not shown.

View File

@@ -419,6 +419,13 @@ browser.runtime.onMessage.addListener((message: any, sender: any) => {
})
}
if (message?.type === 'page:enqueue-ytdlp-url') {
const url = String(message?.url || '').trim()
const referer = String(message?.referer || url).trim()
if (!url) return Promise.resolve({ ok: false, error: 'url is empty' })
return transferUrlToGdown(url, referer || url, 'yt-dlp')
}
return undefined
})

View File

@@ -140,3 +140,142 @@ function installProgrammaticInterceptors(): void {
}
installProgrammaticInterceptors()
let ytOverlayRoot: HTMLDivElement | null = null
let ytOverlayStatus: HTMLDivElement | null = null
let ytOverlayBusy = false
let ytUrlWatcherTimer: number | null = null
let lastObservedUrl = window.location.href
function isYoutubeWatchPage(url: string): boolean {
try {
const parsed = new URL(url)
if (parsed.hostname !== 'www.youtube.com' && parsed.hostname !== 'youtube.com') return false
return parsed.pathname === '/watch' && parsed.searchParams.has('v')
} catch {
return false
}
}
function setYtOverlayStatus(message: string, tone: 'ok' | 'error' | 'idle' = 'idle'): void {
if (!ytOverlayStatus) return
ytOverlayStatus.textContent = message
if (tone === 'ok') ytOverlayStatus.style.color = '#8ff0a4'
else if (tone === 'error') ytOverlayStatus.style.color = '#ff9b9b'
else ytOverlayStatus.style.color = '#aeb7d8'
}
async function enqueueCurrentYoutubePage(): Promise<void> {
if (ytOverlayBusy) return
ytOverlayBusy = true
setYtOverlayStatus('gdown으로 전송 중...')
try {
const result = (await browser.runtime.sendMessage({
type: 'page:enqueue-ytdlp-url',
url: window.location.href,
referer: window.location.href,
})) as { ok?: boolean; error?: string }
if (result?.ok) {
setYtOverlayStatus('다운로드 모달로 전송됨', 'ok')
} else {
setYtOverlayStatus(`전송 실패: ${result?.error || 'unknown error'}`, 'error')
}
} catch (error) {
setYtOverlayStatus(`전송 실패: ${String(error)}`, 'error')
} finally {
ytOverlayBusy = false
}
}
function removeYoutubeOverlay(): void {
if (ytOverlayRoot) {
ytOverlayRoot.remove()
ytOverlayRoot = null
ytOverlayStatus = null
}
}
function ensureYoutubeOverlay(): void {
if (window.top !== window.self) return
if (!isYoutubeWatchPage(window.location.href)) {
removeYoutubeOverlay()
return
}
if (ytOverlayRoot) return
const root = document.createElement('div')
root.id = 'gomdown-youtube-overlay'
root.style.position = 'fixed'
root.style.right = '20px'
root.style.bottom = '24px'
root.style.zIndex = '2147483647'
root.style.background = 'rgba(17, 21, 32, 0.94)'
root.style.border = '1px solid rgba(133, 148, 195, 0.35)'
root.style.borderRadius = '12px'
root.style.padding = '10px'
root.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.28)'
root.style.backdropFilter = 'blur(6px)'
root.style.width = '220px'
root.style.fontFamily = 'ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif'
root.style.color = '#e8edff'
const title = document.createElement('div')
title.textContent = 'Gdown Helper'
title.style.fontSize = '12px'
title.style.fontWeight = '700'
title.style.marginBottom = '8px'
const action = document.createElement('button')
action.type = 'button'
action.textContent = '이 영상 다운로드'
action.style.width = '100%'
action.style.height = '34px'
action.style.border = '1px solid #5a69f0'
action.style.borderRadius = '8px'
action.style.background = '#5a69f0'
action.style.color = '#ffffff'
action.style.fontSize = '12px'
action.style.fontWeight = '700'
action.style.cursor = 'pointer'
action.addEventListener('click', () => {
void enqueueCurrentYoutubePage()
})
const status = document.createElement('div')
status.textContent = '클릭 시 gdown 다운로드 모달로 연결'
status.style.fontSize = '11px'
status.style.marginTop = '8px'
status.style.lineHeight = '1.35'
status.style.color = '#aeb7d8'
root.appendChild(title)
root.appendChild(action)
root.appendChild(status)
document.documentElement.appendChild(root)
ytOverlayRoot = root
ytOverlayStatus = status
}
function watchYoutubeRouteChanges(): void {
if (ytUrlWatcherTimer !== null) return
ytUrlWatcherTimer = window.setInterval(() => {
const current = window.location.href
if (current === lastObservedUrl) return
lastObservedUrl = current
ensureYoutubeOverlay()
}, 800)
window.addEventListener('popstate', () => {
lastObservedUrl = window.location.href
ensureYoutubeOverlay()
})
document.addEventListener('yt-navigate-finish', () => {
lastObservedUrl = window.location.href
ensureYoutubeOverlay()
})
}
ensureYoutubeOverlay()
watchYoutubeRouteChanges()