├── src ├── popup.html ├── img │ ├── 16x16.png │ ├── 32x32.png │ ├── github-mark-white.svg │ └── kofi_symbol.svg ├── js │ ├── background.js │ ├── contentscript.js │ └── popup.js ├── css │ ├── block.css │ └── style.css └── html │ ├── page.html │ └── popup.html ├── tools ├── make-chrome.sh └── make-firefox.sh ├── Makefile ├── platform ├── chrome │ └── manifest.json └── firefox │ └── manifest.json ├── LICENSE └── README.md /src/popup.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bsodoge/Focus-Mode/HEAD/src/img/16x16.png -------------------------------------------------------------------------------- /src/img/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bsodoge/Focus-Mode/HEAD/src/img/32x32.png -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | if(typeof browser === "undefined") browser = chrome; 2 | 3 | browser.runtime.onMessage.addListener(async (message, sender) => { 4 | browser.tabs.update(sender.tab.id, message); 5 | }) -------------------------------------------------------------------------------- /src/css/block.css: -------------------------------------------------------------------------------- 1 | *{ 2 | font-family: "IBM Plex Mono", monospace; 3 | background-color: #181a19; 4 | color: #fff; 5 | text-align: center; 6 | } 7 | 8 | h1{ 9 | font-size: 5rem; 10 | } 11 | 12 | .logo{ 13 | font-size: 1rem; 14 | } 15 | -------------------------------------------------------------------------------- /tools/make-chrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | BLDIR=dist/chrome 5 | rm -rf $BLDIR/* 6 | 7 | echo "Starting build" 8 | 9 | cp -R src/* $BLDIR # Seperate this later 10 | cp platform/chrome/*.json $BLDIR 11 | 12 | pushd $BLDIR 13 | 14 | zip -r chrome.zip ./* 15 | 16 | popd -------------------------------------------------------------------------------- /tools/make-firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | BLDIR=dist/firefox 5 | rm -rf $BLDIR/* 6 | 7 | echo "Starting build" 8 | 9 | cp -R src/* $BLDIR # Seperate this later 10 | cp platform/firefox/*.json $BLDIR 11 | 12 | pushd $BLDIR 13 | 14 | zip -r firefox.zip ./* 15 | 16 | popd -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | src := ${wildcard src/* src/*/*} 2 | platform := ${wildcard platform/* platform/*/*} 3 | 4 | all: firefox chrome 5 | 6 | firefox: dist/firefox/firefox.zip 7 | echo "Firefox build complete" 8 | 9 | dist/firefox/firefox.zip: ${src} ${platform} 10 | tools/make-firefox.sh 11 | 12 | chrome: dist/chrome/chrome.zip 13 | echo "Chrome build complete" 14 | 15 | dist/chrome/chrome.zip: ${src} ${platform} 16 | tools/make-chrome.sh -------------------------------------------------------------------------------- /platform/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version" : 3, 3 | "name": "Focus Mode", 4 | "version": "1.2", 5 | 6 | "description": "A browser extension that allows you to block distractions and stay focused.", 7 | "homepage_url": "https://github.com/Bsodoge/Focus-Mode", 8 | "icons": { 9 | "16": "/img/16x16.png", 10 | "32": "/img/32x32.png" 11 | }, 12 | "content_scripts": [ 13 | { 14 | "matches": [""], 15 | "js": ["/js/contentscript.js"] 16 | } 17 | ], 18 | "background": { 19 | "service_worker": "/js/background.js" 20 | }, 21 | "permissions": [ 22 | "storage", 23 | "tabs" 24 | ], 25 | "action": { 26 | "default_title": "Focus Mode", 27 | "default_popup": "/html/popup.html" 28 | } 29 | } -------------------------------------------------------------------------------- /src/img/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /platform/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version" : 3, 3 | "name": "Focus Mode", 4 | "version": "1.2", 5 | 6 | "description": "A browser extension that allows you to block distractions and stay focused.", 7 | "homepage_url": "https://github.com/Bsodoge/Focus-Mode", 8 | "icons": { 9 | "16": "/img/16x16.png", 10 | "32": "/img/32x32.png" 11 | }, 12 | "content_scripts": [ 13 | { 14 | "matches": ["*://*/*"], 15 | "js": ["/js/contentscript.js"] 16 | } 17 | ], 18 | "browser_specific_settings": { 19 | "gecko": { 20 | "id": "Focus-Mode@bsodoge" 21 | } 22 | }, 23 | "background": { 24 | "scripts": ["/js/background.js"] 25 | }, 26 | "permissions": [ 27 | "storage", 28 | "tabs" 29 | ], 30 | "action": { 31 | "browser_style": true, 32 | "default_title": "Focus Mode", 33 | "default_popup": "/html/popup.html" 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Bsodoge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/js/contentscript.js: -------------------------------------------------------------------------------- 1 | if(typeof browser === "undefined") browser = chrome; 2 | 3 | let isToggled = false; 4 | let optionsToggled = false; 5 | let links = []; 6 | let displayLinks = []; 7 | let days = []; 8 | let startTime = new Date().toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); 9 | let endTime = new Date().toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); 10 | let settings = { 11 | isToggled, 12 | links, 13 | days, 14 | startTime, 15 | displayLinks, 16 | endTime 17 | } 18 | 19 | const applySettings = (storageSettings) => { 20 | settings = storageSettings; 21 | if(settings.isToggled) blockSite(); 22 | } 23 | 24 | const blockSite = () => { 25 | const day = new Date().getDay(); 26 | const time = new Date().toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); 27 | settings.links.forEach(link => { 28 | let reg = new RegExp(link.url); 29 | if(reg.exec(window.location.href) && settings.days.includes(day)) { 30 | setTimeout(blockSite, 5000) 31 | if((time >= settings.startTime) && (time <= settings.endTime) && settings.isToggled) { 32 | browser.runtime.sendMessage({url: browser.runtime.getURL("html/page.html")}); 33 | } 34 | } 35 | }) 36 | } 37 | 38 | const onLoad = async () => { 39 | const storageSettings = await browser.storage.local.get(settings); 40 | applySettings(storageSettings); 41 | } 42 | 43 | browser.runtime.onMessage.addListener(applySettings); 44 | 45 | onLoad(); -------------------------------------------------------------------------------- /src/html/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blocked 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Blocked by

14 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Focus Mode

2 | 3 |

4 | Get Focus Mode for Firefox 5 | Get Focus Mode for Chromium 6 | Focus Mode - Block distracting sites at certain times and certain days. | Product Hunt 7 |

8 | 9 | *** 10 | 11 | Focus Mode is a browser extension designed to keep you focused and productive while browsing the web. 12 | 13 | Features: 14 | - Ability to block multiple sites. 15 | - Ability to block at certain times and days. 16 | - Ability to use wildcard to mass block certain sites e.g reddit.com/* will block all reddit links. 17 | 18 | This is an open-source project and all contributions are welcome. 19 | 20 |

License

21 | MIT License 22 | 23 | Copyright (c) 2025 Bsodoge 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in all 33 | copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | SOFTWARE. 42 | -------------------------------------------------------------------------------- /src/img/kofi_symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Focus Mode 8 | 9 | 10 | 11 | 12 | 13 |
14 | 31 |
32 | 33 |
34 |
35 |
36 | Block sites from 37 | 38 | to 39 | 40 |
41 |
42 |

On days:

43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 58 | 60 |
61 |
62 | 72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | overflow-x: hidden; 33 | min-height: 450px; 34 | width: 450px; 35 | } 36 | ol, ul { 37 | list-style: none; 38 | } 39 | blockquote, q { 40 | quotes: none; 41 | } 42 | blockquote:before, blockquote:after, 43 | q:before, q:after { 44 | content: ''; 45 | content: none; 46 | } 47 | table { 48 | border-collapse: collapse; 49 | border-spacing: 0; 50 | } 51 | 52 | /* Extension specific styling */ 53 | 54 | *{ 55 | color: #fff; 56 | font-family: "IBM Plex Mono", monospace; 57 | } 58 | 59 | button{ 60 | background-color: #484854; 61 | color: #fff; 62 | border: none; 63 | border: 1px solid #484854; 64 | font-size: 0.9rem; 65 | } 66 | 67 | button:hover{ 68 | background-color: #5c5c6a; 69 | } 70 | 71 | button:active{ 72 | background-color: #1b1b21; 73 | } 74 | 75 | input{ 76 | color: #000; 77 | } 78 | 79 | .main{ 80 | width: fit-content; 81 | background-color: #181a19; 82 | padding: 10px; 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: center; 86 | align-items: center; 87 | } 88 | 89 | .logo{ 90 | text-align: center; 91 | font-size: 0.6rem; 92 | } 93 | 94 | .time{ 95 | padding-top: 10px; 96 | display: flex; 97 | align-items: center; 98 | gap: 5px; 99 | } 100 | 101 | .time > input { 102 | background-color: #23222b; 103 | color: #fff; 104 | border: 1px solid #4e4d54; 105 | } 106 | 107 | .time > input:hover { 108 | cursor: text; 109 | } 110 | 111 | button{ 112 | padding: 5px 2.5px; 113 | } 114 | 115 | button:hover{ 116 | cursor: pointer; 117 | } 118 | 119 | .options{ 120 | display: flex; 121 | flex-direction: column; 122 | justify-content: center; 123 | } 124 | 125 | .days-list{ 126 | display: flex; 127 | gap: 4px; 128 | } 129 | 130 | .days{ 131 | padding-top: 10px; 132 | display: flex; 133 | flex-direction: column; 134 | gap: 5px; 135 | } 136 | 137 | .error{ 138 | color: #800000; 139 | } 140 | 141 | .submit-link-container{ 142 | display: flex; 143 | justify-content: center; 144 | align-items: center; 145 | gap: 5px; 146 | font-size: 1rem; 147 | padding: 5px 0px; 148 | } 149 | 150 | .submit-link-container button{ 151 | flex: 0.5; 152 | } 153 | 154 | .submit-link-container input{ 155 | flex: 2; 156 | padding: 5px 2.5px; 157 | } 158 | 159 | .line{ 160 | border-top: 1px solid #ffff; 161 | } 162 | 163 | .link-list{ 164 | height: 130px; 165 | width: 435px; 166 | overflow-y: auto; 167 | line-height: normal; 168 | display: flex; 169 | flex-direction: column; 170 | gap: 5px; 171 | font-size: 0.9rem; 172 | } 173 | 174 | .link-list > * { 175 | display: flex; 176 | justify-content: center; 177 | align-items: center; 178 | gap: 5px; 179 | } 180 | 181 | .link-container{ 182 | flex: 2; 183 | display: inline-block; 184 | text-overflow: ellipsis; 185 | overflow: hidden; 186 | } 187 | 188 | .remove-button{ 189 | flex: 0.5; 190 | } 191 | 192 | .bottom{ 193 | padding-top: 10px; 194 | width: 100%; 195 | } 196 | 197 | .active{ 198 | background-color: #17161b; 199 | border: 1px solid #0085f2; 200 | } 201 | 202 | .activate-button{ 203 | padding: 10px 5px; 204 | font-size: 1.5rem; 205 | } 206 | 207 | .activate-button-container{ 208 | padding-top: 10px; 209 | } 210 | 211 | .days-container{ 212 | display: flex; 213 | flex-wrap: wrap; 214 | gap: 10px; 215 | justify-content: center; 216 | } 217 | 218 | .external-link{ 219 | display: flex; 220 | align-items: center; 221 | justify-content: center; 222 | gap: 4px; 223 | } 224 | 225 | .external-link > img { 226 | width: 25px; 227 | height: 25px; 228 | } 229 | 230 | .external-links{ 231 | display: flex; 232 | justify-content: end; 233 | padding-top: 10px; 234 | width: 100%; 235 | gap: 8px; 236 | } -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | if(typeof browser === "undefined") browser = chrome; 2 | 3 | let isToggled = false; 4 | let optionsToggled = false; 5 | let links = []; 6 | let displayLinks = []; 7 | let days = []; 8 | let startTime = new Date().toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); 9 | let endTime = new Date().toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); 10 | let settings = { 11 | isToggled, 12 | links, 13 | days, 14 | startTime, 15 | displayLinks, 16 | endTime 17 | } 18 | const specialChars = { 19 | ".": "\.", 20 | "+": "\+", 21 | "*": ".*", 22 | "?": "\?", 23 | "^": "\^", 24 | "$": "\$", 25 | "(": "\(", 26 | ")": "\)", 27 | "[": "\[", 28 | "]": "\]", 29 | "{": "\{", 30 | "}": "\}", 31 | "|": "\|", 32 | "/": "\/" 33 | } 34 | 35 | const buttonToggle = document.getElementById("button"); 36 | const linkInputButton = document.getElementById("link-submit"); 37 | const linkInput = document.getElementById("input-link") 38 | const listContainer = document.getElementById("link-list"); 39 | const daysContainer = document.getElementById("days-container"); 40 | const startInput = document.getElementById("start"); 41 | const endInput = document.getElementById("end"); 42 | 43 | const addLink = (link) => { 44 | if (validateLink(link)){ 45 | return; 46 | } 47 | let id = self.crypto.randomUUID(); 48 | displayLinks.push( 49 | { 50 | id: id, 51 | url: link 52 | } 53 | ); 54 | link = convertToRegex(link); 55 | if (!links.includes(link)) { 56 | links.push( 57 | { 58 | id: id, 59 | url: link 60 | } 61 | ); 62 | listLinks(); 63 | setSettings(); 64 | } 65 | linkInput.value = ""; 66 | } 67 | 68 | const validateLink = (givenLink) => { 69 | return displayLinks.some((link) => link.url === givenLink) || givenLink == null || givenLink.match(/^ *$/) !== null; 70 | } 71 | 72 | const changeButtonText = () => { 73 | if (!isToggled) { 74 | buttonToggle.innerText = "Activate"; 75 | buttonToggle.classList.remove('active'); 76 | return; 77 | } 78 | buttonToggle.innerText = "Deactivate"; 79 | buttonToggle.classList.add('active') 80 | } 81 | 82 | const removeLink = (givenId, container) => { 83 | links = links.filter((link) => link.id != givenId); 84 | displayLinks = displayLinks.filter((link) => link.id != givenId); 85 | container.remove(); 86 | setSettings(); 87 | } 88 | 89 | const listLinks = () => { 90 | while(listContainer.hasChildNodes()){ 91 | listContainer.removeChild(listContainer.firstChild); 92 | } 93 | displayLinks.forEach((link, index) => { 94 | const container = document.createElement("div"); 95 | const span = document.createElement("span"); 96 | const button = document.createElement("button"); 97 | button.innerText = "Remove"; 98 | span.classList.add("link-container"); 99 | button.classList.add("remove-button"); 100 | button.addEventListener("click", () => removeLink(link.id, container)); 101 | span.innerText = link.url; 102 | container.append(span); 103 | container.append(button); 104 | listContainer.append(container); 105 | }); 106 | } 107 | 108 | const setSettings = () => { 109 | settings = { 110 | isToggled, 111 | links, 112 | days, 113 | startTime, 114 | displayLinks, 115 | endTime 116 | } 117 | browser.storage.local.set(settings); 118 | } 119 | 120 | const onOpen = () => { 121 | browser.storage.local.get(settings).then((storageSettings) => { 122 | isToggled = storageSettings.isToggled; 123 | links = storageSettings.links; 124 | days = storageSettings.days; 125 | startTime = storageSettings.startTime; 126 | endTime = storageSettings.endTime; 127 | displayLinks = storageSettings.displayLinks; 128 | changeButtonText(); 129 | listLinks(); 130 | changeTimeText(); 131 | changeButtonState(); 132 | settings = storageSettings; 133 | browser.tabs.query({ active: true, currentWindow: true }, sendMessage); 134 | }); 135 | } 136 | 137 | const sendMessage = (tabs) => { 138 | browser.tabs.sendMessage(tabs[0].id, settings); 139 | } 140 | 141 | const toggleExtension = (tabs) => { 142 | isToggled = !isToggled; 143 | changeButtonText(); 144 | setSettings(); 145 | sendMessage(tabs); 146 | } 147 | 148 | const changeButtonState = () => { 149 | daysContainer.childNodes.forEach((child) => { 150 | if(days.includes(parseInt(child.id))) child.classList.add("active"); 151 | }) 152 | } 153 | 154 | const handleDay = (button) => { 155 | givenDay = parseInt(button.target.id); 156 | if(days.includes(givenDay)){ 157 | days = days.filter((day) => day != givenDay); 158 | button.target.classList.remove("active"); 159 | setSettings(); 160 | return; 161 | } 162 | button.target.classList.add("active"); 163 | days.push(givenDay); 164 | setSettings(); 165 | } 166 | 167 | const handleTime = (input) => { 168 | if(input.target.id === "start") startTime = input.target.value; 169 | if(input.target.id === "end") endTime = input.target.value; 170 | setSettings(); 171 | } 172 | 173 | const changeTimeText = () => { 174 | startInput.value = startTime; 175 | endInput.value = endTime; 176 | } 177 | 178 | const convertToRegex = (url) => { 179 | for (const key in specialChars) { 180 | if(url.includes(key)) url = url.replaceAll(key, specialChars[key]); 181 | } 182 | return url; 183 | } 184 | 185 | buttonToggle.addEventListener("click", () => browser.tabs.query({active: true, currentWindow: true}, toggleExtension)); 186 | linkInputButton.addEventListener("click", () => addLink(linkInput.value)); 187 | document.body.addEventListener("keydown", (e) => { if(e.key === "Enter") linkInputButton.click() }); 188 | daysContainer.childNodes.forEach((child) => child.addEventListener("click", handleDay)); 189 | startInput.addEventListener("change", handleTime); 190 | endInput.addEventListener("change", handleTime); 191 | 192 | onOpen(); --------------------------------------------------------------------------------