diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..56470c6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Extensions/Firefox"] + path = Extensions/Firefox + url = https://github.com/nimbold/Firelink-Extension.git diff --git a/Extensions/Firefox b/Extensions/Firefox new file mode 160000 index 0000000..fcb619e --- /dev/null +++ b/Extensions/Firefox @@ -0,0 +1 @@ +Subproject commit fcb619e73ced0d97194a148c26df593a1d3c6903 diff --git a/Extensions/Firefox/background.js b/Extensions/Firefox/background.js deleted file mode 100644 index d01ca1a..0000000 --- a/Extensions/Firefox/background.js +++ /dev/null @@ -1,110 +0,0 @@ -// background.js -const FIRELINK_SERVER_URL = "http://localhost:6412/download"; - -// Default settings -const defaultSettings = { - globalCapture: false, - siteToggles: {} // hostname -> boolean (true if capture is disabled for this site) -}; - -// Initialize settings -chrome.runtime.onInstalled.addListener(() => { - chrome.storage.local.get(['globalCapture', 'siteToggles'], (result) => { - if (result.globalCapture === undefined) { - chrome.storage.local.set(defaultSettings); - } - }); - - // Create context menus - chrome.contextMenus.create({ - id: "download-with-firelink", - title: "Download with Firelink", - contexts: ["link"] - }); - - chrome.contextMenus.create({ - id: "download-selected-with-firelink", - title: "Download selected with Firelink", - contexts: ["selection"] - }); -}); - -// Function to send URLs to Firelink -async function sendToFirelink(urls, referer = "") { - try { - const payload = { - urls: Array.isArray(urls) ? urls : [urls], - referer: referer - }; - - // Attempt to send to Firelink local server - await fetch(FIRELINK_SERVER_URL, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(payload) - }); - console.log("Successfully sent to Firelink:", payload); - } catch (error) { - console.error("Failed to send to Firelink:", error); - // Fallback logic could go here - } -} - -// Handle context menu clicks -chrome.contextMenus.onClicked.addListener((info, tab) => { - if (info.menuItemId === "download-with-firelink") { - if (info.linkUrl) { - sendToFirelink([info.linkUrl], tab.url); - } - } else if (info.menuItemId === "download-selected-with-firelink") { - // We need to inject a script to get the selected HTML/links - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ["content.js"] - }, () => { - // Send a message to the content script to perform the extraction - chrome.tabs.sendMessage(tab.id, { action: "extractSelectionLinks" }, (response) => { - if (response && response.links && response.links.length > 0) { - sendToFirelink(response.links, tab.url); - } else { - // Fallback: If no links were found by the content script, try to parse the raw text - if (info.selectionText) { - const urlRegex = /(https?:\/\/[^\s]+)/g; - const matches = info.selectionText.match(urlRegex); - if (matches && matches.length > 0) { - sendToFirelink(matches, tab.url); - } - } - } - }); - }); - } -}); - -// Listen for downloads -chrome.downloads.onCreated.addListener((downloadItem) => { - chrome.storage.local.get(['globalCapture', 'siteToggles'], (settings) => { - const globalCapture = settings.globalCapture || false; - const siteToggles = settings.siteToggles || {}; - - let hostname = ""; - try { - hostname = new URL(downloadItem.referrer || downloadItem.url).hostname; - } catch (e) { - // Invalid URL - } - - // Check if capture is disabled for this specific site - const siteCaptureDisabled = siteToggles[hostname] === true; - - if (globalCapture && !siteCaptureDisabled) { - // Cancel the browser download - chrome.downloads.cancel(downloadItem.id, () => { - // Send to Firelink - sendToFirelink([downloadItem.url], downloadItem.referrer); - }); - } - }); -}); diff --git a/Extensions/Firefox/content.js b/Extensions/Firefox/content.js deleted file mode 100644 index 661941a..0000000 --- a/Extensions/Firefox/content.js +++ /dev/null @@ -1,37 +0,0 @@ -// content.js -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === "extractSelectionLinks") { - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - sendResponse({ links: [] }); - return; - } - - const links = new Set(); - - // Extract a tags from the selected range - for (let i = 0; i < selection.rangeCount; i++) { - const range = selection.getRangeAt(i); - const container = document.createElement("div"); - container.appendChild(range.cloneContents()); - - const anchors = container.querySelectorAll("a"); - anchors.forEach(a => { - if (a.href) { - // ensure the link is a full URL by using the current document's base URI if it's relative - try { - const url = new URL(a.getAttribute('href'), document.baseURI).href; - links.add(url); - } catch (e) { - // Invalid URL, fallback to just checking if it looks like a valid protocol - if (a.href.startsWith("http")) { - links.add(a.href); - } - } - } - }); - } - - sendResponse({ links: Array.from(links) }); - } -}); diff --git a/Extensions/Firefox/icons/icon-128.png b/Extensions/Firefox/icons/icon-128.png deleted file mode 100644 index 2e50ae6..0000000 Binary files a/Extensions/Firefox/icons/icon-128.png and /dev/null differ diff --git a/Extensions/Firefox/icons/icon-48.png b/Extensions/Firefox/icons/icon-48.png deleted file mode 100644 index 344542b..0000000 Binary files a/Extensions/Firefox/icons/icon-48.png and /dev/null differ diff --git a/Extensions/Firefox/manifest.json b/Extensions/Firefox/manifest.json deleted file mode 100644 index 5675f68..0000000 --- a/Extensions/Firefox/manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "manifest_version": 3, - "name": "Firelink", - "version": "1.0.0", - "description": "Capture downloads and links directly to Firelink download manager.", - "permissions": [ - "downloads", - "downloads.open", - "contextMenus", - "storage", - "activeTab", - "scripting" - ], - "background": { - "scripts": ["background.js"] - }, - "action": { - "default_popup": "popup/popup.html", - "default_title": "Firelink" - }, - "icons": { - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - }, - "browser_specific_settings": { - "gecko": { - "id": "firelink@nimbold.github.io" - } - } -} diff --git a/Extensions/Firefox/popup/popup.html b/Extensions/Firefox/popup/popup.html deleted file mode 100644 index 08ddfa5..0000000 --- a/Extensions/Firefox/popup/popup.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - -
-
-
- -
-

Firelink

-
- -
- -
-
-
- Capture All Downloads - Intercept browser downloads -
- -
- -
-
- Disable on site - ... -
- -
-
- -
- - - diff --git a/Extensions/Firefox/popup/popup.js b/Extensions/Firefox/popup/popup.js deleted file mode 100644 index d7b814c..0000000 --- a/Extensions/Firefox/popup/popup.js +++ /dev/null @@ -1,88 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const globalToggle = document.getElementById('global-toggle'); - const siteToggle = document.getElementById('site-toggle'); - const hostnameSpan = document.getElementById('current-hostname'); - const themeToggleBtn = document.getElementById('theme-toggle'); - const sunIcon = document.getElementById('sun-icon'); - const moonIcon = document.getElementById('moon-icon'); - - // Apply theme function - const applyTheme = (theme) => { - document.documentElement.setAttribute('data-theme', theme); - if (theme === 'light') { - sunIcon.style.display = 'none'; - moonIcon.style.display = 'block'; - } else { - sunIcon.style.display = 'block'; - moonIcon.style.display = 'none'; - } - }; - - // Load current settings including theme - chrome.storage.local.get(['globalCapture', 'siteToggles', 'theme'], (result) => { - globalToggle.checked = result.globalCapture || false; - - // Determine initial theme - let currentTheme = result.theme; - if (!currentTheme) { - currentTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; - } - applyTheme(currentTheme); - - // Get current active tab - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs.length > 0 && tabs[0].url) { - try { - const url = new URL(tabs[0].url); - // Only show site toggle for valid http/https URLs - if (url.protocol.startsWith('http')) { - const hostname = url.hostname; - hostnameSpan.textContent = hostname; - - const siteToggles = result.siteToggles || {}; - // If the site is in siteToggles and value is true, it means capture is DISABLED for this site - siteToggle.checked = siteToggles[hostname] === true; - } else { - document.getElementById('site-setting-row').style.display = 'none'; - } - } catch (e) { - document.getElementById('site-setting-row').style.display = 'none'; - } - } - }); - }); - - // Handle global toggle change - globalToggle.addEventListener('change', (e) => { - chrome.storage.local.set({ globalCapture: e.target.checked }); - }); - - // Handle theme toggle change - themeToggleBtn.addEventListener('click', () => { - const currentTheme = document.documentElement.getAttribute('data-theme') || - (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - applyTheme(newTheme); - chrome.storage.local.set({ theme: newTheme }); - }); - - // Handle site toggle change - siteToggle.addEventListener('change', (e) => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs.length > 0 && tabs[0].url) { - try { - const url = new URL(tabs[0].url); - const hostname = url.hostname; - - chrome.storage.local.get(['siteToggles'], (result) => { - const siteToggles = result.siteToggles || {}; - siteToggles[hostname] = e.target.checked; - chrome.storage.local.set({ siteToggles: siteToggles }); - }); - } catch (error) { - console.error("Invalid URL"); - } - } - }); - }); -}); diff --git a/Extensions/Firefox/popup/styles.css b/Extensions/Firefox/popup/styles.css deleted file mode 100644 index 07d8815..0000000 --- a/Extensions/Firefox/popup/styles.css +++ /dev/null @@ -1,282 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); - -:root { - /* Dark Theme Variables (Default fallback) */ - --bg-color: #0d0d0f; - --text-primary: #f5f5f7; - --text-secondary: #8e8e93; - --text-hover: #a1a1a6; - --row-bg: rgba(255, 255, 255, 0.03); - --row-border: rgba(255, 255, 255, 0.06); - --row-bg-hover: rgba(255, 255, 255, 0.05); - --row-border-hover: rgba(255, 255, 255, 0.1); - --header-border: rgba(255, 255, 255, 0.08); - --blob-color-1: rgba(255, 45, 85, 0.4); - --blob-color-2: rgba(255, 149, 0, 0.1); - --toggle-bg: rgba(255, 255, 255, 0.15); - --toggle-border: rgba(255, 255, 255, 0.05); - --toggle-knob: #ffffff; - --shadow-color: rgba(0, 0, 0, 0.2); - --icon-hover: rgba(255, 255, 255, 0.1); -} - -[data-theme="light"] { - --bg-color: #f2f2f7; - --text-primary: #1c1c1e; - --text-secondary: #636366; - --text-hover: #3a3a3c; - --row-bg: rgba(255, 255, 255, 0.6); - --row-border: rgba(0, 0, 0, 0.05); - --row-bg-hover: rgba(255, 255, 255, 0.9); - --row-border-hover: rgba(0, 0, 0, 0.1); - --header-border: rgba(0, 0, 0, 0.08); - --blob-color-1: rgba(255, 45, 85, 0.2); - --blob-color-2: rgba(255, 149, 0, 0.05); - --toggle-bg: rgba(0, 0, 0, 0.1); - --toggle-border: rgba(0, 0, 0, 0.05); - --toggle-knob: #ffffff; - --shadow-color: rgba(0, 0, 0, 0.05); - --icon-hover: rgba(0, 0, 0, 0.05); -} - -@media (prefers-color-scheme: light) { - :root:not([data-theme="dark"]) { - --bg-color: #f2f2f7; - --text-primary: #1c1c1e; - --text-secondary: #636366; - --text-hover: #3a3a3c; - --row-bg: rgba(255, 255, 255, 0.6); - --row-border: rgba(0, 0, 0, 0.05); - --row-bg-hover: rgba(255, 255, 255, 0.9); - --row-border-hover: rgba(0, 0, 0, 0.1); - --header-border: rgba(0, 0, 0, 0.08); - --blob-color-1: rgba(255, 45, 85, 0.2); - --blob-color-2: rgba(255, 149, 0, 0.05); - --toggle-bg: rgba(0, 0, 0, 0.1); - --toggle-border: rgba(0, 0, 0, 0.05); - --toggle-knob: #ffffff; - --shadow-color: rgba(0, 0, 0, 0.05); - --icon-hover: rgba(0, 0, 0, 0.05); - } -} - -body { - width: 340px; - margin: 0; - padding: 0; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background-color: var(--bg-color); - color: var(--text-primary); - overflow: hidden; - position: relative; - transition: background-color 0.3s ease, color 0.3s ease; -} - -/* Glassmorphic background blob animation */ -.background-blob { - position: absolute; - top: -50px; - right: -50px; - width: 200px; - height: 200px; - background: radial-gradient(circle, var(--blob-color-1) 0%, var(--blob-color-2) 50%, transparent 70%); - border-radius: 50%; - filter: blur(30px); - z-index: 0; - animation: pulse 6s infinite alternate cubic-bezier(0.4, 0, 0.2, 1); - transition: background 0.3s ease; -} - -@keyframes pulse { - 0% { transform: scale(1) translate(0, 0); opacity: 0.6; } - 100% { transform: scale(1.1) translate(-10px, 10px); opacity: 0.9; } -} - -.container { - position: relative; - z-index: 1; - padding: 20px; -} - -.header { - display: flex; - align-items: center; - margin-bottom: 24px; - padding-bottom: 16px; - border-bottom: 1px solid var(--header-border); - transition: border-color 0.3s ease; -} - -.logo { - width: 28px; - height: 28px; - margin-right: 12px; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(255, 45, 85, 0.2); - transition: transform 0.3s ease; -} - -.logo:hover { - transform: scale(1.05) rotate(2deg); -} - -.header-title-container { - flex: 1; -} - -h2 { - margin: 0; - font-size: 20px; - font-weight: 600; - letter-spacing: -0.5px; - background: linear-gradient(90deg, #ff9500, #ff2d55); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.theme-toggle-btn { - background: transparent; - border: none; - cursor: pointer; - padding: 6px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - transition: background-color 0.2s ease, color 0.2s ease; -} - -.theme-toggle-btn:hover { - background-color: var(--icon-hover); - color: var(--text-primary); -} - -.theme-toggle-btn svg { - width: 20px; - height: 20px; - fill: currentColor; -} - -.settings { - display: flex; - flex-direction: column; - gap: 16px; -} - -.setting-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 14px 16px; - background: var(--row-bg); - border: 1px solid var(--row-border); - border-radius: 14px; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - transition: all 0.2s ease; -} - -.setting-row:hover { - background: var(--row-bg-hover); - border-color: var(--row-border-hover); - transform: translateY(-1px); - box-shadow: 0 4px 12px var(--shadow-color); -} - -.setting-info { - display: flex; - flex-direction: column; - flex: 1; - padding-right: 16px; -} - -.setting-title { - font-size: 14px; - font-weight: 500; - color: var(--text-primary); - margin-bottom: 4px; - letter-spacing: -0.2px; - transition: color 0.3s ease; -} - -.setting-desc { - font-size: 12px; - color: var(--text-secondary); - line-height: 1.4; - word-break: break-all; - transition: color 0.2s ease; -} - -.setting-row:hover .setting-desc { - color: var(--text-hover); -} - -#current-hostname { - color: #ff9500; - font-weight: 500; -} - -/* Modern Toggle Switch Styling */ -.switch { - position: relative; - display: inline-block; - width: 48px; - height: 28px; - flex-shrink: 0; -} - -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--toggle-bg); - transition: .4s cubic-bezier(0.4, 0, 0.2, 1); - border: 1px solid var(--toggle-border); - box-sizing: border-box; -} - -.slider:before { - position: absolute; - content: ""; - height: 22px; - width: 22px; - left: 2px; - bottom: 2px; - background-color: var(--toggle-knob); - transition: .4s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 2px 5px rgba(0,0,0,0.3); -} - -input:checked + .slider { - background-color: #ff2d55; - border-color: #ff2d55; - box-shadow: 0 0 12px rgba(255, 45, 85, 0.4); -} - -input:focus + .slider { - outline: 2px solid rgba(255, 45, 85, 0.5); - outline-offset: 2px; -} - -input:checked + .slider:before { - transform: translateX(20px); -} - -.slider.round { - border-radius: 28px; -} - -.slider.round:before { - border-radius: 50%; -} diff --git a/README.md b/README.md index 8ed06c0..c484b12 100644 --- a/README.md +++ b/README.md @@ -98,12 +98,22 @@ git push origin v0.4.1 --- +## 🧩 Browser Extension + +The browser extension for Firelink is developed in a separate repository. You can find the extension source code, track its development, and download releases at: + +πŸ‘‰ **[nimbold/Firelink-Extension](https://github.com/nimbold/Firelink-Extension)** + +For local development parity, this repository tracks the extension via a Git Submodule located in the `Extensions/` directory. + +--- + ## πŸ—ΊοΈ Roadmap - [x] **Data Persistence:** Store history, column layout preferences, and active queues across restarts. - [x] **Zero-Config Setup:** Automatically bundle and configure `aria2c` inside the `.app` bundle. - [x] **Bandwidth Limits:** Add global and per-download speed caps and calendar schedules. -- [ ] **Browser Extensions:** Capture links directly from Safari, Chrome, and Firefox. +- [x] **Browser Extensions:** Capture links directly from Safari, Chrome, and Firefox. (See [Firelink-Extension](https://github.com/nimbold/Firelink-Extension) repository) - [x] **Advanced Transfer Features:** Checksum validation, cookie/header ingestion, and smart mirror failovers. - [x] **Updates & Releases:** GitHub Actions DMG release pipeline and built-in update checker. - [ ] **Distribution:** Notarized `.app` releases and Homebrew formulae.