├── .gitignore ├── Brisk-Demo.gif ├── README.md ├── app ├── images │ ├── logo128.png │ ├── logo16.png │ ├── logo32.png │ └── logo48.png ├── manifest.json ├── pages │ └── popup.html ├── scripts │ ├── background.js │ ├── common.js │ ├── content-script.js │ └── popup.js └── styles │ └── common.css ├── build-script.sh ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea -------------------------------------------------------------------------------- /Brisk-Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrisklyDev/brisk-browser-extension/17c2804882a7000c3bde406b1bf20b593c82ae1f/Brisk-Demo.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![release](https://img.shields.io/github/v/release/AminBhst/brisk-browser-extension?style=flat-square)](https://github.com/AminBhst/brisk/releases) 2 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/AminBhst/brisk-browser-extension/total?style=flat-square) 3 | Discord Chat 4 | 5 | 6 |

Browser extension for Brisk Download Manager

7 | 8 | 9 | ## :package: Installation 10 | #### Chrome / Edge / Opera 11 | [link-chrome]: https://github.com/AminBhst/brisk-browser-extension/releases/latest 'Version published on Chrome Web Store' 12 | 13 | [Chrome][link-chrome] [Edge][link-chrome] [Opera][link-chrome] 14 | 15 |

For the above browsers, you'll need to manually download and install the latest version from releases. To see the step-by-step installation guide, open Brisk and click on the "Get Extension" button in the top menu.

16 | 17 | #### Firefox 18 | [link-firefox]: https://addons.mozilla.org/en-US/firefox/addon/brisk/ 19 | 20 | [Firefox][link-firefox] 21 | 22 | For Firefox users, the extension is available on the official [Firefox Addons Website](https://addons.mozilla.org/en-US/firefox/addon/brisk/). 23 | 24 | ## :rocket: Features 25 | - Capturing download requests from the browser and directly adding them to Brisk 26 | - Extracting all download links from a selected text and adding them to Brisk all at once 27 | - Capturing m3u8 video streams from the browser 28 | 29 | ## :film_projector: Demo 30 | https://github.com/user-attachments/assets/fc63dcf4-0e69-4610-9c92-f26f12f9ad01 31 | 32 | ## Build From Source 33 | ```shell 34 | yarn install 35 | yarn build {browser-name} 36 | ``` 37 | 38 | ## :busts_in_silhouette: Community 39 | 40 |
cord.nvim
41 | 42 | -------------------------------------------------------------------------------- /app/images/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrisklyDev/brisk-browser-extension/17c2804882a7000c3bde406b1bf20b593c82ae1f/app/images/logo128.png -------------------------------------------------------------------------------- /app/images/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrisklyDev/brisk-browser-extension/17c2804882a7000c3bde406b1bf20b593c82ae1f/app/images/logo16.png -------------------------------------------------------------------------------- /app/images/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrisklyDev/brisk-browser-extension/17c2804882a7000c3bde406b1bf20b593c82ae1f/app/images/logo32.png -------------------------------------------------------------------------------- /app/images/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrisklyDev/brisk-browser-extension/17c2804882a7000c3bde406b1bf20b593c82ae1f/app/images/logo48.png -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Brisk", 3 | "short_name": "Brisk", 4 | "description": "Browser extension for Brisk download manager", 5 | "version": "1.3.0", 6 | "__firefox__browser_specific_settings": { 7 | "gecko": { 8 | "id": "{9ca5a4a8-58d1-4a47-8e0d-5e53a21c9a7b}" 9 | } 10 | }, 11 | "manifest_version": 3, 12 | "homepage_url": "https://github.com/AminBhst/brisk-browser-extension", 13 | "background": { 14 | "service_worker": "scripts/background.js" 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [ 19 | "" 20 | ], 21 | "js": [ 22 | "scripts/content-script.js" 23 | ] 24 | } 25 | ], 26 | "__firefox__background": { 27 | "scripts": [ 28 | "scripts/background.js" 29 | ] 30 | }, 31 | "content_security_policy": { 32 | "extension_pages": "script-src 'self'; object-src 'self'" 33 | }, 34 | "action": { 35 | "default_icon": { 36 | "16": "images/logo16.png", 37 | "32": "images/logo32.png", 38 | "48": "images/logo48.png", 39 | "128": "images/logo128.png" 40 | }, 41 | "default_title": "Brisk", 42 | "default_popup": "pages/popup.html" 43 | }, 44 | "icons": { 45 | "16": "images/logo16.png", 46 | "32": "images/logo32.png", 47 | "128": "images/logo128.png" 48 | }, 49 | "web_accessible_resources": [ 50 | { 51 | "resources": [ 52 | "scripts/*", 53 | "styles/*", 54 | "images/*" 55 | ], 56 | "matches": [ 57 | "" 58 | ] 59 | } 60 | ], 61 | "permissions": [ 62 | "downloads", 63 | "storage", 64 | "cookies", 65 | "contextMenus", 66 | "webRequest", 67 | "tabs" 68 | ], 69 | "host_permissions": [ 70 | "" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /app/pages/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Extension Popup 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 |
    17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/scripts/background.js: -------------------------------------------------------------------------------- 1 | import * as browser from 'webextension-polyfill'; 2 | import {sendRequestToBrisk} from "./common"; 3 | 4 | let m3u8UrlsByTab = {}; 5 | let vttUrlsByTab = {}; 6 | let videoUrlsByTab = {}; 7 | let downloadHrefs; 8 | createContextMenuItem(); 9 | 10 | const isFirefox = navigator.userAgent.includes("Firefox"); 11 | const extraInfoSpec = ["requestHeaders"]; 12 | if (!isFirefox) { 13 | extraInfoSpec.push("extraHeaders"); 14 | } 15 | 16 | browser.runtime.onInstalled.addListener(() => { 17 | browser.storage.sync.set({briskPort: 3020}); 18 | }); 19 | 20 | browser.downloads.onCreated.addListener(sendBriskDownloadAdditionRequest); 21 | 22 | browser.runtime.onMessage.addListener((message) => downloadHrefs = message); 23 | 24 | browser.webRequest.onBeforeSendHeaders.addListener(async (details) => { 25 | handleVideoStreams(details); 26 | }, {urls: ['']}, extraInfoSpec); 27 | 28 | 29 | function handleVideoStreams(details) { 30 | const {tabId, url, requestHeaders} = details; 31 | const refererHeader = requestHeaders.find(h => h.name.toLowerCase() === 'referer'); 32 | const referer = refererHeader?.value || null; 33 | const pathName = new URL(url).pathname; 34 | if (pathName.endsWith('m3u8')) { 35 | addUrlToTab(m3u8UrlsByTab, tabId, url, referer); 36 | } 37 | // For aniplaynow.live 38 | if (url.includes("/m3u8-proxy")) { 39 | const urlObj = new URL(url); 40 | const rawUrl = urlObj.searchParams.get("url"); 41 | const realM3u8Url = rawUrl || null; 42 | const headersParam = urlObj.searchParams.get("headers"); 43 | let realReferer = referer; 44 | if (!headersParam) { 45 | const headers = JSON.parse(headersParam); 46 | realReferer = headers.Referer; 47 | } 48 | console.log(`realM3u8 ${realM3u8Url}`); 49 | console.log(`real header ${realReferer}`); 50 | addUrlToTab(m3u8UrlsByTab, tabId, realM3u8Url, realReferer); 51 | } else if (url.endsWith('.vtt')) { 52 | console.log(`vtt referer ${referer}`); 53 | addUrlToTab(vttUrlsByTab, tabId, url, referer); 54 | } else if (url.endsWith(".mp4") || url.endsWith(".webm")) { 55 | addUrlToTab(videoUrlsByTab, tabId, url, referer); 56 | } 57 | } 58 | 59 | function addUrlToTab(urlsByTab, tabId, url, referer) { 60 | if (!urlsByTab[tabId]) { 61 | urlsByTab[tabId] = []; 62 | } 63 | if (!urlsByTab[tabId].includes(url)) { 64 | urlsByTab[tabId].push({url: url, referer: referer}); 65 | } 66 | } 67 | 68 | // Listen for tab reload events to clear the m3u8 URLs for that tab 69 | browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 70 | if (changeInfo.status === 'complete') { 71 | m3u8UrlsByTab[tabId] = []; 72 | vttUrlsByTab[tabId] = []; 73 | videoUrlsByTab[tabId] = []; 74 | } 75 | }); 76 | 77 | // Listen for requests from popup to get m3u8 URLs for the current tab 78 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 79 | console.log(message); 80 | if (message.type === 'get-m3u8-list') { 81 | let urls = m3u8UrlsByTab[message.tabId] || []; 82 | if (videoUrlsByTab[message.tabId]) { 83 | urls.push(...videoUrlsByTab[message.tabId]); 84 | } 85 | sendResponse({m3u8Urls: urls}); 86 | } 87 | if (message.type === 'get-vtt-list') { 88 | const urls = vttUrlsByTab[message.tabId] || []; 89 | sendResponse({vttUrls: urls}); 90 | } 91 | }); 92 | 93 | browser.contextMenus.onClicked.addListener(async (info, tab) => { 94 | if (info.menuItemId === "brisk-download") { 95 | let body = { 96 | "type": "multi", "data": { 97 | "referer": tab.url, downloadHrefs 98 | }, 99 | }; 100 | try { 101 | await sendRequestToBrisk(body); 102 | } catch (e) { 103 | console.error("Failed to send request to Brisk!"); 104 | } 105 | } 106 | }); 107 | 108 | 109 | async function sendBriskDownloadAdditionRequest(downloadItem) { 110 | let body = { 111 | "type": "single", "data": { 112 | 'url': downloadItem.url, 'referer': downloadItem.referrer 113 | } 114 | }; 115 | let response = await sendRequestToBrisk(body); 116 | let json = await response.json(); 117 | if (response.status === 200 && json["captured"]) { 118 | await removeBrowserDownload(downloadItem.id); 119 | } 120 | } 121 | 122 | async function removeBrowserDownload(id) { 123 | await browser.downloads.cancel(id).then(pass).catch(console.log); 124 | await browser.downloads.erase({id}).then(pass).catch(console.log); 125 | await browser.downloads.removeFile(id).then(pass).catch(pass); 126 | } 127 | 128 | function createContextMenuItem() { 129 | browser.contextMenus.removeAll().then(() => { 130 | browser.contextMenus.create({ 131 | id: "brisk-download", title: "Download selected links with Brisk", contexts: ["selection"] 132 | }, () => null); 133 | }).catch(console.log); 134 | } 135 | 136 | const pass = () => null; -------------------------------------------------------------------------------- /app/scripts/common.js: -------------------------------------------------------------------------------- 1 | import * as browser from "webextension-polyfill"; 2 | 3 | export const defaultPort = 3020; 4 | const extensionVersion = browser.runtime.getManifest().version; 5 | 6 | async function getBriskBaseUrl() { 7 | return "http://localhost:" + await getBriskPort(); 8 | } 9 | 10 | export async function sendRequestToBrisk(body) { 11 | body.extensionVersion = extensionVersion; 12 | return await fetch( 13 | await getBriskBaseUrl(), 14 | {method: 'POST', body: JSON.stringify(body)} 15 | ); 16 | } 17 | 18 | export async function getBriskPort() { 19 | let data = await browser.storage.local.get(['briskPort']); 20 | if (data.briskPort) { 21 | return data.briskPort; 22 | } 23 | return defaultPort; 24 | } 25 | 26 | 27 | export function extractResolution(text) { 28 | const match = text.match(/(?:\b|\D)(\d{3,4})p?\b/i); 29 | return match ? match[1] + (text.includes(match[1] + 'p') ? 'p' : '') : null; 30 | } 31 | -------------------------------------------------------------------------------- /app/scripts/content-script.js: -------------------------------------------------------------------------------- 1 | import * as browser from 'webextension-polyfill'; 2 | 3 | const hostname = window.location.hostname.replace(/^www\./, ''); 4 | 5 | const videoNameResolvers = { 6 | 'aniwatchtv.to': resolve_aniWatchVideoName, 7 | 'hianimez.to': resolve_aniWatchVideoName, 8 | 'aniplaynow.live': resolve_aniPlayVideoName, 9 | 'openani.me': resolve_openAnime, 10 | [atob('aGFuaW1lLnR2')]: resolve_aGFuaW1lLnR2, 11 | [atob('aGVudGFpaGF2ZW4ueHh4')]: resolve_aGVudGFpaGF2ZW4ueHh4, 12 | }; 13 | 14 | function resolve_aGVudGFpaGF2ZW4ueHh4() { 15 | const heading = document.querySelector('#chapter-heading'); 16 | if (!heading) { 17 | return null; 18 | } 19 | const rawText = heading.textContent.trim(); 20 | const parts = rawText.split('-').map(part => part.trim()); 21 | if (parts.length === 2) { 22 | const titlePart = parts[0].replace(/\s+/g, '.'); 23 | const episodePart = parts[1].replace(/\s+/g, '.'); 24 | return `${titlePart}.${episodePart}`; 25 | } 26 | const cleaned = rawText 27 | .replace(/\s+/g, '.') 28 | .replace(/[^\w.-]/g, ''); 29 | if (!cleaned) { 30 | return null; 31 | } 32 | return cleaned; 33 | } 34 | 35 | function resolve_openAnime() { 36 | let title = document.title 37 | .replaceAll("|", "") 38 | .replaceAll("OpenAnime", "") 39 | .trim(); 40 | 41 | return title.endsWith('.') ? title.slice(0, -1) : title; 42 | } 43 | 44 | function resolve_aGFuaW1lLnR2() { 45 | const titleElement = document.querySelector('.tv-title'); 46 | if (titleElement) { 47 | return titleElement.textContent.trim(); 48 | } 49 | } 50 | 51 | function resolve_aniPlayVideoName() { 52 | const titleElement = document.querySelector('a[href*="/anime/info/"] span'); 53 | const animeName = titleElement?.textContent.trim(); 54 | const episodeElement = document.querySelector('span.font-medium.text-sm.md\\:text-white'); 55 | const episodeNumber = episodeElement ? episodeElement.textContent.trim() : null; 56 | const epNum = episodeNumber ? episodeNumber.match(/\d+/)?.[0] : null; 57 | if (epNum == null) return animeName; 58 | const epNumPadded = epNum.toString().padStart(2, '0'); 59 | return `${animeName}.${epNumPadded}`; 60 | } 61 | 62 | function resolve_aniWatchVideoName() { 63 | const animeName = document.querySelector('h2.film-name a')?.textContent?.trim() || null; 64 | const notice = document.querySelector('.server-notice strong b'); 65 | if (!notice || !animeName) return null; 66 | const text = notice.textContent.trim(); // e.g. "Episode 2" 67 | const match = text.match(/\d+/); 68 | let epNum = match ? parseInt(match[0], 10) : null; 69 | if (epNum === null) return animeName; 70 | const epNumPadded = epNum.toString().padStart(2, '0'); 71 | return `${animeName}.${epNumPadded}`; 72 | } 73 | 74 | function getSuggestedVideoName() { 75 | const resolver = videoNameResolvers[hostname]; 76 | if (!resolver) return null; 77 | return resolver().replace(/[\\\/:*?"<>|=]/g, '.').replace(/\s+/g, '.'); 78 | } 79 | 80 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 81 | if (message.type === 'get-suggested-video-name') { 82 | const suggestedName = getSuggestedVideoName(); 83 | sendResponse(suggestedName); 84 | return true; 85 | } 86 | }); 87 | 88 | function debounce(fn, delay) { 89 | let timer = null; 90 | return function () { 91 | let context = this, args = arguments; 92 | clearTimeout(timer); 93 | timer = setTimeout(function () { 94 | fn.apply(context, args); 95 | }, delay); 96 | }; 97 | } 98 | 99 | document.addEventListener("selectionchange", debounce(function (event) { 100 | try { 101 | let extractedHrefs = getHrefOfAllSelectedLinks(); 102 | sendHrefsToBackground(extractedHrefs); 103 | } catch (e) { 104 | console.log(e); 105 | } 106 | }, 250)); 107 | 108 | 109 | function sendHrefsToBackground(hrefs) { 110 | browser.runtime.sendMessage(hrefs); 111 | } 112 | 113 | const getHrefOfAllSelectedLinks = () => getSelectedNodes() 114 | .filter(node => node.tagName === "A") 115 | .map(node => node.href); 116 | 117 | 118 | function getSelectedNodes() { 119 | const selection = document.getSelection(); 120 | const fragment = document.createDocumentFragment(); 121 | const nodeList = []; 122 | 123 | for (let i = 0; i < selection.rangeCount; i++) { 124 | fragment.append(selection.getRangeAt(i).cloneContents()); 125 | } 126 | 127 | const walker = document.createTreeWalker(fragment); 128 | let currentNode = walker.currentNode; 129 | while (currentNode) { 130 | nodeList.push(currentNode); 131 | currentNode = walker.nextNode(); 132 | } 133 | 134 | return nodeList; 135 | } 136 | -------------------------------------------------------------------------------- /app/scripts/popup.js: -------------------------------------------------------------------------------- 1 | import * as browser from 'webextension-polyfill'; 2 | import {sendRequestToBrisk, extractResolution} from "./common"; 3 | 4 | async function createM3u8List(tabId, m3u8Urls, listContainer) { 5 | let suggestedName = await browser.tabs.sendMessage(tabId, {type: 'get-suggested-video-name'}); 6 | m3u8Urls.forEach((obj) => { 7 | const listItem = document.createElement('li'); 8 | const nameSpan = document.createElement('span'); 9 | nameSpan.textContent = resolveSuggestedName(obj, suggestedName, true); 10 | const downloadButton = document.createElement('button'); 11 | downloadButton.textContent = 'Download'; 12 | downloadButton.classList.add('download-btn'); 13 | registerVideoStreamDownloadClickListener(downloadButton, obj); 14 | listItem.appendChild(nameSpan); 15 | listItem.appendChild(downloadButton); 16 | listContainer.appendChild(listItem); 17 | }); 18 | } 19 | 20 | 21 | function resolveSuggestedName(obj, suggestedName, handleMaster) { 22 | let fileNameFromUrl = obj.url.substring(obj.url.lastIndexOf('/') + 1) 23 | if (fileNameFromUrl.includes('.m3u8?')) { 24 | fileNameFromUrl = fileNameFromUrl.substring(0, fileNameFromUrl.indexOf('.m3u8?') + 5) 25 | } 26 | if (fileNameFromUrl.includes('.mp4?')) { 27 | fileNameFromUrl = fileNameFromUrl.substring(0, fileNameFromUrl.indexOf('.mp4?') + 4) 28 | } 29 | if (suggestedName == null || suggestedName === '') { 30 | return fileNameFromUrl; 31 | } 32 | let resolution = extractResolution(fileNameFromUrl); 33 | if (suggestedName.endsWith(".mp4")) { 34 | return resolution 35 | ? `${suggestedName}.${resolution}.mp4` 36 | : `${suggestedName}.mp4`; 37 | } else if (handleMaster && fileNameFromUrl.includes("master")) { 38 | return `${suggestedName}.all.resolutions.ts`; 39 | } else { 40 | return `${suggestedName}.ts`; 41 | } 42 | } 43 | 44 | function registerVideoStreamDownloadClickListener(downloadButton, obj) { 45 | downloadButton.addEventListener('click', async () => { 46 | let tabId = await getCurrentTabId(); 47 | const tab = await browser.tabs.get(tabId); 48 | let suggestedName = await browser.tabs.sendMessage(tabId, {type: 'get-suggested-video-name'}); 49 | suggestedName = resolveSuggestedName(obj, suggestedName, false); 50 | if (obj.url.endsWith(".mp4") || obj.url.endsWith(".webm")) { 51 | let body = { 52 | 'type': 'single', 53 | 'data': { 54 | 'url': obj.url, 55 | 'referer': tab.url, 56 | 'suggestedName': suggestedName, 57 | 'refererHeader': obj.referer, 58 | } 59 | }; 60 | await sendRequestToBrisk(body); 61 | } else { 62 | let vttUrls = await browser.runtime.sendMessage({type: 'get-vtt-list', tabId}); 63 | await sendRequestToBrisk({ 64 | 'type': 'm3u8', 65 | 'm3u8Url': obj.url, 66 | 'vttUrls': vttUrls['vttUrls'], 67 | 'referer': tab.url, 68 | 'suggestedName': suggestedName, 69 | 'refererHeader': obj.referer, 70 | }); 71 | } 72 | }); 73 | } 74 | 75 | async function getCurrentTabId() { 76 | let tabs = await browser.tabs.query({active: true, currentWindow: true}); 77 | return tabs[0].id; 78 | } 79 | 80 | document.addEventListener('DOMContentLoaded', () => { 81 | const portInput = document.getElementById('port'); 82 | const savePortButton = document.getElementById('save-port'); 83 | browser.storage.local.get('briskPort').then((data) => { 84 | if (data.briskPort) { 85 | portInput.value = data.briskPort; 86 | } else { 87 | portInput.value = 3020; 88 | } 89 | }).catch((error) => { 90 | console.error('Failed to load port from storage:', error); 91 | }); 92 | savePortButton.addEventListener('click', () => { 93 | const port = portInput.value; 94 | if (port >= 1 && port <= 65535) { 95 | browser.storage.local.set({briskPort: port}).then(() => { 96 | alert('Port saved successfully!'); 97 | }).catch((error) => { 98 | console.error('Failed to save port:', error); 99 | }); 100 | } else { 101 | alert('Please enter a valid port number (1-65535).'); 102 | } 103 | }); 104 | }); 105 | 106 | document.addEventListener('DOMContentLoaded', () => { 107 | const listContainer = document.getElementById('m3u8-list'); 108 | browser.tabs.query({active: true, currentWindow: true}).then((tabs) => { 109 | const tabId = tabs[0].id; // Get current tab's ID 110 | browser.runtime.sendMessage({type: 'get-m3u8-list', tabId}).then((response) => { 111 | console.log(`m3u8 received ${response.m3u8Urls}`); 112 | createM3u8List(tabId, response.m3u8Urls, listContainer); 113 | }); 114 | }); 115 | }); 116 | 117 | document.querySelectorAll('.tab-button').forEach(button => { 118 | button.addEventListener('click', () => { 119 | const tab = button.dataset.tab; 120 | document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); 121 | button.classList.add('active'); 122 | document.querySelectorAll('.tab-content').forEach(content => { 123 | content.classList.toggle('active', content.id === tab); 124 | }); 125 | }); 126 | }); -------------------------------------------------------------------------------- /app/styles/common.css: -------------------------------------------------------------------------------- 1 | /* Reset HTML and Body Styles */ 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | width: 400px; 6 | height: 400px; 7 | box-sizing: border-box; 8 | overflow: hidden; /* Prevent scrolling */ 9 | background-color: #121212; /* Dark mode background */ 10 | color: #ffffff; /* Light text for dark background */ 11 | font-family: Arial, sans-serif; /* Clean, readable font */ 12 | } 13 | 14 | /* Main Container */ 15 | .container { 16 | display: flex; 17 | flex-direction: column; 18 | width: 400px; 19 | height: 400px; 20 | background-color: #1e1e1e; /* Slightly lighter dark for contrast */ 21 | border: 1px solid #333333; /* Optional border for debugging or aesthetics */ 22 | border-radius: 8px; /* Smooth corners */ 23 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.8); /* Subtle shadow for depth */ 24 | overflow-y: auto; /* Allow scrolling if content overflows */ 25 | } 26 | 27 | /* Tab Buttons */ 28 | .tabs { 29 | display: flex; 30 | justify-content: space-evenly; 31 | background-color: #2c2c2c; /* Darker tab background */ 32 | border-bottom: 2px solid #444444; 33 | padding: 8px 0; 34 | } 35 | 36 | .tab-button { 37 | flex: 1; 38 | padding: 10px; 39 | text-align: center; 40 | color: #cccccc; 41 | background-color: #2c2c2c; 42 | border: none; 43 | outline: none; 44 | cursor: pointer; 45 | transition: background-color 0.2s, color 0.2s; 46 | font-weight: bold; 47 | } 48 | 49 | .tab-button:hover { 50 | background-color: #444444; 51 | color: #ffffff; 52 | } 53 | 54 | .tab-button.active { 55 | background-color: #1e88e5; /* Highlighted active tab in blue */ 56 | color: #ffffff; 57 | } 58 | 59 | /* Tab Content */ 60 | .tab-content { 61 | flex: 1; 62 | padding: 16px; 63 | display: none; /* Hidden by default */ 64 | overflow-y: auto; /* Scroll if content overflows */ 65 | } 66 | 67 | .tab-content.active { 68 | display: block; /* Show active content */ 69 | } 70 | 71 | h2 { 72 | font-size: 1.5em; 73 | margin-bottom: 12px; 74 | color: #ffffff; 75 | } 76 | 77 | /* Video Streams List */ 78 | .streams-list { 79 | list-style: none; 80 | padding: 0; 81 | margin: 0; 82 | } 83 | 84 | .stream-item { 85 | padding: 10px; 86 | margin: 8px 0; 87 | background-color: #333333; 88 | color: #ffffff; 89 | border-radius: 6px; 90 | transition: background-color 0.2s; 91 | } 92 | 93 | .stream-item:hover { 94 | background-color: #444444; 95 | } 96 | 97 | /* Settings Item */ 98 | .settings-item { 99 | margin: 12px 0; 100 | padding: 8px 12px; 101 | background-color: #2c2c2c; 102 | color: #ffffff; 103 | border-radius: 6px; 104 | display: flex; 105 | align-items: center; 106 | } 107 | 108 | .settings-item label { 109 | margin-left: 8px; 110 | font-size: 1em; 111 | color: #cccccc; 112 | } 113 | 114 | h1 { 115 | font-size: 24px; 116 | margin-bottom: 20px; 117 | text-align: center; 118 | color: #0096c7; /* Blue accent */ 119 | font-weight: 600; 120 | } 121 | 122 | /* Main Container Styling */ 123 | #m3u8-list { 124 | list-style: none; 125 | padding: 0; 126 | margin: 0; 127 | max-height: 350px; 128 | overflow-y: auto; 129 | } 130 | 131 | li { 132 | background: linear-gradient(45deg, #4a90e2, #6fa3e3); /* Blue gradient */ 133 | margin: 10px 0; 134 | padding: 12px; 135 | border-radius: 8px; 136 | display: flex; 137 | justify-content: space-between; 138 | align-items: center; 139 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2); 140 | transition: transform 0.3s ease, background 0.3s ease; 141 | } 142 | 143 | li:hover { 144 | transform: translateY(-4px); 145 | background: linear-gradient(45deg, #6fa3e3, #4a90e2); /* Light hover effect */ 146 | } 147 | 148 | /* File Name Styling */ 149 | span { 150 | font-size: 14px; 151 | font-weight: 500; 152 | color: #ffffff; 153 | word-wrap: break-word; 154 | max-width: 220px; 155 | overflow: hidden; 156 | text-overflow: ellipsis; 157 | } 158 | 159 | /* Download Button Styling */ 160 | .download-btn { 161 | background-color: #0096c7; /* Blue button */ 162 | color: white; 163 | border: none; 164 | padding: 8px 15px; 165 | border-radius: 25px; 166 | font-size: 14px; 167 | cursor: pointer; 168 | transition: background 0.3s ease; 169 | } 170 | 171 | .download-btn:hover { 172 | background-color: #08b62d; /* Darker blue on hover */ 173 | } 174 | 175 | /* Empty State Styling */ 176 | p { 177 | color: #bbb; 178 | text-align: center; 179 | font-size: 14px; 180 | } 181 | 182 | .port-setting { 183 | margin-top: 20px; 184 | } 185 | 186 | .port-setting label { 187 | font-size: 14px; 188 | display: block; 189 | margin-bottom: 5px; 190 | } 191 | 192 | .port-setting input[type="number"] { 193 | width: 350px; 194 | padding: 8px; 195 | border: 1px solid #ccc; 196 | border-radius: 4px; 197 | font-size: 14px; 198 | } 199 | 200 | .port-setting button { 201 | width: 368px; 202 | padding: 8px; 203 | font-size: 14px; 204 | background-color: #007bff; 205 | color: white; 206 | border: none; 207 | border-radius: 4px; 208 | cursor: pointer; 209 | margin-top: 10px; 210 | } 211 | 212 | .port-setting button:hover { 213 | background-color: #0056b3; 214 | } -------------------------------------------------------------------------------- /build-script.sh: -------------------------------------------------------------------------------- 1 | yarn install 2 | yarn build firefox -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brisk-browser-extension", 3 | "version": "1.3.0", 4 | "description": "Browser extension for Brisk download manager", 5 | "scripts": { 6 | "dev": "webextension-toolbox dev", 7 | "build": "webextension-toolbox build", 8 | "lint": "eslint --fix app" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/AminBhst/brisk-browser-extension" 13 | }, 14 | "devDependencies": { 15 | "@webextension-toolbox/webextension-toolbox": "^5.2.2", 16 | "eslint": "^7.32.0", 17 | "eslint-config-prettier": "^8.3.0", 18 | "eslint-plugin-prettier": "^4.0.0", 19 | "prettier": "^2.4.1", 20 | "webextension-polyfill": "^0.10.0" 21 | } 22 | } 23 | --------------------------------------------------------------------------------