Scaffold WebExtension: manifest, content collector, background aria2 RPC, popup UI, filters, and scripts
This commit is contained in:
		
					parent
					
						
							
								b81ca429d4
							
						
					
				
			
			
				commit
				
					
						8eeb1d3b20
					
				
			
		
					 10 changed files with 427 additions and 0 deletions
				
			
		
							
								
								
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					dist/
 | 
				
			||||||
 | 
					.web-extension-id
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										31
									
								
								manifest.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								manifest.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "manifest_version": 3,
 | 
				
			||||||
 | 
					  "name": "Archive.org Link Grabber",
 | 
				
			||||||
 | 
					  "version": "0.1.0",
 | 
				
			||||||
 | 
					  "description": "Filter and export archive.org /download links; copy or send to aria2 RPC.",
 | 
				
			||||||
 | 
					  "action": {
 | 
				
			||||||
 | 
					    "default_title": "Archive.org Link Grabber",
 | 
				
			||||||
 | 
					    "default_popup": "src/popup/index.html"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "permissions": [
 | 
				
			||||||
 | 
					    "storage",
 | 
				
			||||||
 | 
					    "clipboardWrite",
 | 
				
			||||||
 | 
					    "activeTab"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "host_permissions": [
 | 
				
			||||||
 | 
					    "https://archive.org/*",
 | 
				
			||||||
 | 
					    "http://localhost:6800/*",
 | 
				
			||||||
 | 
					    "https://localhost:6800/*"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "background": {
 | 
				
			||||||
 | 
					    "service_worker": "src/background/index.js",
 | 
				
			||||||
 | 
					    "type": "module"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "content_scripts": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "matches": ["https://archive.org/download/*"],
 | 
				
			||||||
 | 
					      "js": ["src/content/collect.js"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "archive-org-link-grabber",
 | 
				
			||||||
 | 
					  "version": "0.1.0",
 | 
				
			||||||
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "description": "Firefox WebExtension to filter archive.org /download links and copy/send to aria2.",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "dev": "web-ext run --start-url https://archive.org --source-dir .",
 | 
				
			||||||
 | 
					    "build": "web-ext build -o -a dist",
 | 
				
			||||||
 | 
					    "lint": "echo 'Add ESLint config to enable' && exit 0",
 | 
				
			||||||
 | 
					    "format": "echo 'Add Prettier config to enable' && exit 0",
 | 
				
			||||||
 | 
					    "test": "echo 'No tests yet' && exit 0"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/background/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/background/index.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					import { addUrisBatch } from "../lib/aria2.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					browser.runtime.onMessage.addListener(async (msg, sender) => {
 | 
				
			||||||
 | 
					  if (!msg || !msg.type) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (msg.type === 'aria2.send') {
 | 
				
			||||||
 | 
					    const { endpoint, secret, uris, options } = msg.payload || {};
 | 
				
			||||||
 | 
					    if (!endpoint || !Array.isArray(uris) || uris.length === 0) {
 | 
				
			||||||
 | 
					      return { ok: false, error: 'Missing endpoint or URIs' };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const res = await addUrisBatch({ endpoint, secret, uris, options });
 | 
				
			||||||
 | 
					      return { ok: true, result: res };
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      return { ok: false, error: String(err && err.message || err) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/content/collect.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/content/collect.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					(() => {
 | 
				
			||||||
 | 
					  function extFromName(name) {
 | 
				
			||||||
 | 
					    const base = name.split('?')[0];
 | 
				
			||||||
 | 
					    const idx = base.lastIndexOf('.');
 | 
				
			||||||
 | 
					    return idx > -1 ? base.slice(idx + 1).toLowerCase() : '';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function toItem(a) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const url = a.href;
 | 
				
			||||||
 | 
					      const u = new URL(url);
 | 
				
			||||||
 | 
					      const name = decodeURIComponent(u.pathname.split('/').pop() || '');
 | 
				
			||||||
 | 
					      // Attempt to find size/date hints in the DOM (best-effort, optional)
 | 
				
			||||||
 | 
					      let sizeText = '';
 | 
				
			||||||
 | 
					      let dateText = '';
 | 
				
			||||||
 | 
					      const row = a.closest('tr');
 | 
				
			||||||
 | 
					      if (row) {
 | 
				
			||||||
 | 
					        const tds = Array.from(row.querySelectorAll('td'));
 | 
				
			||||||
 | 
					        // Heuristics: look for cells containing size-like patterns or dates
 | 
				
			||||||
 | 
					        for (const td of tds) {
 | 
				
			||||||
 | 
					          const t = td.textContent.trim();
 | 
				
			||||||
 | 
					          if (!sizeText && /\b\d+(?:[\.,]\d+)?\s?(?:[KMG]B|bytes?)\b/i.test(t)) sizeText = t;
 | 
				
			||||||
 | 
					          if (!dateText && /\d{4}-\d{2}-\d{2}/.test(t)) dateText = t.match(/\d{4}-\d{2}-\d{2}/)[0];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        type: extFromName(name),
 | 
				
			||||||
 | 
					        sizeText,
 | 
				
			||||||
 | 
					        dateText
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function collectLinks() {
 | 
				
			||||||
 | 
					    const anchors = Array.from(document.querySelectorAll('a[href]'));
 | 
				
			||||||
 | 
					    const items = [];
 | 
				
			||||||
 | 
					    for (const a of anchors) {
 | 
				
			||||||
 | 
					      const href = a.getAttribute('href') || '';
 | 
				
			||||||
 | 
					      // Only include likely file links under /download/
 | 
				
			||||||
 | 
					      if (href.includes('/download/')) {
 | 
				
			||||||
 | 
					        const item = toItem(a);
 | 
				
			||||||
 | 
					        if (item && item.name) items.push(item);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return items;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Respond to messages from the popup to collect links
 | 
				
			||||||
 | 
					  browser.runtime.onMessage.addListener((msg, sender) => {
 | 
				
			||||||
 | 
					    if (msg && msg.type === 'collectLinks') {
 | 
				
			||||||
 | 
					      return collectLinks();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/lib/aria2.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/lib/aria2.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					export async function addUri({ endpoint, secret, uri, options = {} }) {
 | 
				
			||||||
 | 
					  const body = {
 | 
				
			||||||
 | 
					    jsonrpc: "2.0",
 | 
				
			||||||
 | 
					    id: "archive-org-link-grabber:" + Date.now(),
 | 
				
			||||||
 | 
					    method: "aria2.addUri",
 | 
				
			||||||
 | 
					    params: [
 | 
				
			||||||
 | 
					      secret ? `token:${secret}` : undefined,
 | 
				
			||||||
 | 
					      [uri],
 | 
				
			||||||
 | 
					      options
 | 
				
			||||||
 | 
					    ].filter(v => v !== undefined)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const res = await fetch(endpoint, {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					    body: JSON.stringify(body)
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (!res.ok) {
 | 
				
			||||||
 | 
					    const text = await res.text().catch(() => String(res.status));
 | 
				
			||||||
 | 
					    throw new Error(`aria2 RPC error ${res.status}: ${text}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return res.json();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function addUrisBatch({ endpoint, secret, uris, options = {} }) {
 | 
				
			||||||
 | 
					  const results = [];
 | 
				
			||||||
 | 
					  for (const uri of uris) {
 | 
				
			||||||
 | 
					    // Simple sequential send; keep it obvious and robust
 | 
				
			||||||
 | 
					    // Consider concurrency/batching later if needed
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					    const r = await addUri({ endpoint, secret, uri, options });
 | 
				
			||||||
 | 
					    results.push(r);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return results;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										78
									
								
								src/lib/filters.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/lib/filters.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,78 @@
 | 
				
			||||||
 | 
					export function buildMatcher(input, { useRegex = false, caseSensitive = false } = {}) {
 | 
				
			||||||
 | 
					  const value = (input || '').trim();
 | 
				
			||||||
 | 
					  if (!value) return () => true;
 | 
				
			||||||
 | 
					  if (useRegex || /^\/.+\/[gimsuy]*$/.test(value)) {
 | 
				
			||||||
 | 
					    // Allow /pattern/flags syntax
 | 
				
			||||||
 | 
					    let pattern = value;
 | 
				
			||||||
 | 
					    let flags = caseSensitive ? '' : 'i';
 | 
				
			||||||
 | 
					    const m = value.match(/^\/(.*)\/([gimsuy]*)$/);
 | 
				
			||||||
 | 
					    if (m) { pattern = m[1]; flags = m[2] || flags; }
 | 
				
			||||||
 | 
					    const re = new RegExp(pattern, flags);
 | 
				
			||||||
 | 
					    return (s) => re.test(String(s || ''));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const needle = caseSensitive ? value : value.toLowerCase();
 | 
				
			||||||
 | 
					  return (s) => {
 | 
				
			||||||
 | 
					    const hay = caseSensitive ? String(s || '') : String(s || '').toLowerCase();
 | 
				
			||||||
 | 
					    return hay.includes(needle);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function parseSizeToBytes(v) {
 | 
				
			||||||
 | 
					  if (v == null) return undefined;
 | 
				
			||||||
 | 
					  const s = String(v).trim();
 | 
				
			||||||
 | 
					  if (!s) return undefined;
 | 
				
			||||||
 | 
					  const m = s.match(/^(\d+(?:[\.,]\d+)?)\s*(B|KB|MB|GB|TB)?$/i);
 | 
				
			||||||
 | 
					  if (!m) return Number(s) || undefined;
 | 
				
			||||||
 | 
					  const n = parseFloat(m[1].replace(',', '.'));
 | 
				
			||||||
 | 
					  const unit = (m[2] || 'B').toUpperCase();
 | 
				
			||||||
 | 
					  const mult = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4 }[unit];
 | 
				
			||||||
 | 
					  return Math.round(n * mult);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extFromName(name) {
 | 
				
			||||||
 | 
					  const base = String(name || '').split('?')[0];
 | 
				
			||||||
 | 
					  const idx = base.lastIndexOf('.');
 | 
				
			||||||
 | 
					  return idx > -1 ? base.slice(idx + 1).toLowerCase() : '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function applyFilters(items, filters) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    name = '',
 | 
				
			||||||
 | 
					    type = '',
 | 
				
			||||||
 | 
					    nameRegex = false,
 | 
				
			||||||
 | 
					    typeRegex = false,
 | 
				
			||||||
 | 
					    caseSensitive = false,
 | 
				
			||||||
 | 
					    sizeMin = '',
 | 
				
			||||||
 | 
					    sizeMax = '',
 | 
				
			||||||
 | 
					    dateFrom = '',
 | 
				
			||||||
 | 
					    dateTo = ''
 | 
				
			||||||
 | 
					  } = filters || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const nameMatch = buildMatcher(name, { useRegex: nameRegex, caseSensitive });
 | 
				
			||||||
 | 
					  const typeMatch = buildMatcher(type, { useRegex: typeRegex, caseSensitive });
 | 
				
			||||||
 | 
					  const minBytes = parseSizeToBytes(sizeMin);
 | 
				
			||||||
 | 
					  const maxBytes = parseSizeToBytes(sizeMax);
 | 
				
			||||||
 | 
					  const fromTime = dateFrom ? new Date(dateFrom).getTime() : undefined;
 | 
				
			||||||
 | 
					  const toTime = dateTo ? new Date(dateTo).getTime() : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (items || []).filter((it) => {
 | 
				
			||||||
 | 
					    const okName = nameMatch(it.name);
 | 
				
			||||||
 | 
					    const ext = it.type || extFromName(it.name);
 | 
				
			||||||
 | 
					    const okType = typeMatch(ext);
 | 
				
			||||||
 | 
					    let okSize = true;
 | 
				
			||||||
 | 
					    if (minBytes != null || maxBytes != null) {
 | 
				
			||||||
 | 
					      // Best-effort: parse from sizeText if provided
 | 
				
			||||||
 | 
					      const s = it.sizeBytes != null ? it.sizeBytes : parseSizeToBytes(it.sizeText);
 | 
				
			||||||
 | 
					      if (minBytes != null && (s == null || s < minBytes)) okSize = false;
 | 
				
			||||||
 | 
					      if (maxBytes != null && (s != null && s > maxBytes)) okSize = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let okDate = true;
 | 
				
			||||||
 | 
					    if (fromTime != null || toTime != null) {
 | 
				
			||||||
 | 
					      const t = it.timeMs != null ? it.timeMs : (it.dateText ? new Date(it.dateText).getTime() : undefined);
 | 
				
			||||||
 | 
					      if (fromTime != null && (t == null || t < fromTime)) okDate = false;
 | 
				
			||||||
 | 
					      if (toTime != null && (t != null && t > toTime)) okDate = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return okName && okType && okSize && okDate;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										58
									
								
								src/popup/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/popup/index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,58 @@
 | 
				
			||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8" />
 | 
				
			||||||
 | 
					    <title>Archive.org Link Grabber</title>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="styles.css" />
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <header>
 | 
				
			||||||
 | 
					      <h1>Archive.org Link Grabber</h1>
 | 
				
			||||||
 | 
					    </header>
 | 
				
			||||||
 | 
					    <section class="filters">
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label>Type/Ext <input id="f-type" placeholder="mp4|mkv" /></label>
 | 
				
			||||||
 | 
					        <label class="chk"><input type="checkbox" id="f-type-re" /> Regex</label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label>Name <input id="f-name" placeholder="/^(movie|clip).*\\.mp4$/i" /></label>
 | 
				
			||||||
 | 
					        <label class="chk"><input type="checkbox" id="f-name-re" /> Regex</label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label>Size ≥ <input id="f-size-min" placeholder="100MB" size="8" /></label>
 | 
				
			||||||
 | 
					        <label>≤ <input id="f-size-max" placeholder="2GB" size="8" /></label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label>Date from <input id="f-date-from" type="date" /></label>
 | 
				
			||||||
 | 
					        <label>to <input id="f-date-to" type="date" /></label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label class="chk"><input type="checkbox" id="f-case" /> Case sensitive</label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row actions">
 | 
				
			||||||
 | 
					        <button id="btn-refresh">Load Links</button>
 | 
				
			||||||
 | 
					        <span id="count">0</span> matches
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <section class="aria2">
 | 
				
			||||||
 | 
					      <h2>aria2 RPC</h2>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label>Endpoint <input id="rpc-endpoint" placeholder="http://localhost:6800/jsonrpc" /></label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <label>Secret <input id="rpc-secret" type="password" placeholder="your-token" /></label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row actions">
 | 
				
			||||||
 | 
					        <button id="btn-copy">Copy Links</button>
 | 
				
			||||||
 | 
					        <button id="btn-send">Send to aria2</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="row">
 | 
				
			||||||
 | 
					        <output id="status"></output>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script type="module" src="index.js"></script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  </html>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										117
									
								
								src/popup/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/popup/index.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,117 @@
 | 
				
			||||||
 | 
					import { applyFilters } from "../lib/filters.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const els = {
 | 
				
			||||||
 | 
					  fType: document.getElementById('f-type'),
 | 
				
			||||||
 | 
					  fTypeRe: document.getElementById('f-type-re'),
 | 
				
			||||||
 | 
					  fName: document.getElementById('f-name'),
 | 
				
			||||||
 | 
					  fNameRe: document.getElementById('f-name-re'),
 | 
				
			||||||
 | 
					  fSizeMin: document.getElementById('f-size-min'),
 | 
				
			||||||
 | 
					  fSizeMax: document.getElementById('f-size-max'),
 | 
				
			||||||
 | 
					  fDateFrom: document.getElementById('f-date-from'),
 | 
				
			||||||
 | 
					  fDateTo: document.getElementById('f-date-to'),
 | 
				
			||||||
 | 
					  fCase: document.getElementById('f-case'),
 | 
				
			||||||
 | 
					  btnRefresh: document.getElementById('btn-refresh'),
 | 
				
			||||||
 | 
					  btnCopy: document.getElementById('btn-copy'),
 | 
				
			||||||
 | 
					  btnSend: document.getElementById('btn-send'),
 | 
				
			||||||
 | 
					  rpcEndpoint: document.getElementById('rpc-endpoint'),
 | 
				
			||||||
 | 
					  rpcSecret: document.getElementById('rpc-secret'),
 | 
				
			||||||
 | 
					  count: document.getElementById('count'),
 | 
				
			||||||
 | 
					  status: document.getElementById('status'),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let allItems = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getFilters() {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: els.fType.value,
 | 
				
			||||||
 | 
					    name: els.fName.value,
 | 
				
			||||||
 | 
					    typeRegex: els.fTypeRe.checked,
 | 
				
			||||||
 | 
					    nameRegex: els.fNameRe.checked,
 | 
				
			||||||
 | 
					    sizeMin: els.fSizeMin.value,
 | 
				
			||||||
 | 
					    sizeMax: els.fSizeMax.value,
 | 
				
			||||||
 | 
					    dateFrom: els.fDateFrom.value,
 | 
				
			||||||
 | 
					    dateTo: els.fDateTo.value,
 | 
				
			||||||
 | 
					    caseSensitive: els.fCase.checked,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getActiveTabId() {
 | 
				
			||||||
 | 
					  const tabs = await browser.tabs.query({ active: true, currentWindow: true });
 | 
				
			||||||
 | 
					  return tabs[0]?.id;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function refresh() {
 | 
				
			||||||
 | 
					  els.status.textContent = '';
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const tabId = await getActiveTabId();
 | 
				
			||||||
 | 
					    if (!tabId) throw new Error('No active tab');
 | 
				
			||||||
 | 
					    const items = await browser.tabs.sendMessage(tabId, { type: 'collectLinks' });
 | 
				
			||||||
 | 
					    allItems = items || [];
 | 
				
			||||||
 | 
					    updateCount();
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    els.status.textContent = String(e?.message || e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function filtered() {
 | 
				
			||||||
 | 
					  return applyFilters(allItems, getFilters());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateCount() {
 | 
				
			||||||
 | 
					  els.count.textContent = String(filtered().length);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function copyLinks() {
 | 
				
			||||||
 | 
					  const urls = filtered().map(i => i.url).join('\n');
 | 
				
			||||||
 | 
					  await navigator.clipboard.writeText(urls);
 | 
				
			||||||
 | 
					  els.status.textContent = `Copied ${filtered().length} link(s) to clipboard.`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function sendToAria2() {
 | 
				
			||||||
 | 
					  const endpoint = els.rpcEndpoint.value.trim();
 | 
				
			||||||
 | 
					  const secret = els.rpcSecret.value.trim();
 | 
				
			||||||
 | 
					  const uris = filtered().map(i => i.url);
 | 
				
			||||||
 | 
					  if (!endpoint) { els.status.textContent = 'Please set aria2 endpoint.'; return; }
 | 
				
			||||||
 | 
					  if (uris.length === 0) { els.status.textContent = 'No links to send.'; return; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Persist settings
 | 
				
			||||||
 | 
					  await browser.storage.local.set({ aria2: { endpoint, secret } });
 | 
				
			||||||
 | 
					  els.status.textContent = 'Sending to aria2…';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const res = await browser.runtime.sendMessage({
 | 
				
			||||||
 | 
					    type: 'aria2.send',
 | 
				
			||||||
 | 
					    payload: { endpoint, secret, uris }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (res?.ok) {
 | 
				
			||||||
 | 
					    els.status.textContent = `Sent ${uris.length} link(s) to aria2.`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    els.status.textContent = `Error: ${res?.error || 'unknown'}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function restoreSettings() {
 | 
				
			||||||
 | 
					  const { aria2 } = await browser.storage.local.get('aria2');
 | 
				
			||||||
 | 
					  if (aria2) {
 | 
				
			||||||
 | 
					    if (aria2.endpoint) els.rpcEndpoint.value = aria2.endpoint;
 | 
				
			||||||
 | 
					    if (aria2.secret) els.rpcSecret.value = aria2.secret;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    els.rpcEndpoint.value = 'http://localhost:6800/jsonrpc';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function wire() {
 | 
				
			||||||
 | 
					  els.btnRefresh.addEventListener('click', refresh);
 | 
				
			||||||
 | 
					  els.btnCopy.addEventListener('click', copyLinks);
 | 
				
			||||||
 | 
					  els.btnSend.addEventListener('click', sendToAria2);
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    els.fType, els.fTypeRe, els.fName, els.fNameRe, els.fSizeMin, els.fSizeMax,
 | 
				
			||||||
 | 
					    els.fDateFrom, els.fDateTo, els.fCase
 | 
				
			||||||
 | 
					  ].forEach(el => el.addEventListener('input', updateCount));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener('DOMContentLoaded', async () => {
 | 
				
			||||||
 | 
					  wire();
 | 
				
			||||||
 | 
					  await restoreSettings();
 | 
				
			||||||
 | 
					  await refresh();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/popup/styles.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/popup/styles.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 10px; }
 | 
				
			||||||
 | 
					header h1 { font-size: 14px; margin: 0 0 8px; }
 | 
				
			||||||
 | 
					section { margin-bottom: 10px; }
 | 
				
			||||||
 | 
					.row { display: flex; align-items: center; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
 | 
				
			||||||
 | 
					label { font-size: 12px; }
 | 
				
			||||||
 | 
					label input { margin-left: 6px; }
 | 
				
			||||||
 | 
					.chk { align-items: center; }
 | 
				
			||||||
 | 
					button { font-size: 12px; padding: 4px 10px; }
 | 
				
			||||||
 | 
					#count { font-weight: bold; margin: 0 4px; }
 | 
				
			||||||
 | 
					#status { font-size: 12px; white-space: pre-wrap; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue