├── README.md ├── css ├── popup.css └── style.css ├── js ├── background.js ├── content.js └── popup.js ├── manifest.json └── popup.html /README.md: -------------------------------------------------------------------------------- 1 | # Twice - Think Twice Before Social Media 2 | 3 | A Chrome extension that helps you be more mindful of your social media usage by prompting you to think twice before visiting social media sites. It's designed to reduce mindless scrolling and promote intentional browsing habits. 4 | 5 | image 6 | 7 | 8 | 9 | ## Features 10 | 11 | - Displays a gentle reminder when you attempt to visit social media sites 12 | - Helps you pause and reflect on whether you really need to check social media 13 | - Promotes mindful internet usage and better digital habits 14 | 15 | ## Installation 16 | 17 | 1. Clone this repository or download the source code 18 | 2. Open Chrome and navigate to `chrome://extensions/` 19 | 3. Enable "Developer mode" by toggling the switch in the top right corner 20 | 4. Click "Load unpacked" button 21 | 5. Select the directory containing the extension files (where manifest.json is located) 22 | 23 | The extension icon should now appear in your Chrome toolbar. 24 | 25 | ## How It Works 26 | 27 | When you try to visit a social media site, Twice will show a popup asking you to pause and reflect. This small interruption helps you: 28 | - Be more conscious of your browsing habits 29 | - Reduce impulsive social media checking 30 | - Make intentional decisions about your online time 31 | 32 | ## Development 33 | 34 | The extension consists of: 35 | - `manifest.json`: Extension configuration 36 | - `popup.html`: Main extension interface 37 | - `css/`: Styling files 38 | - `js/`: JavaScript functionality 39 | 40 | To make changes, simply edit the files and reload the extension in Chrome by clicking the refresh icon on the extension card in `chrome://extensions/`. 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /css/popup.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap'); 2 | 3 | body { 4 | width: 280px; 5 | padding: 16px; 6 | margin: 0; 7 | font-family: 'Inter', -apple-system, sans-serif; 8 | font-size: 13px; 9 | color: #fff; 10 | background-color: #1C1C1E; 11 | } 12 | 13 | h2 { 14 | font-size: 14px; 15 | font-weight: 500; 16 | margin: 0 0 12px 0; 17 | letter-spacing: -0.01em; 18 | color: #fff; 19 | } 20 | 21 | h3 { 22 | font-size: 12px; 23 | font-weight: 500; 24 | margin: 0 0 8px 0; 25 | letter-spacing: -0.01em; 26 | color: #999; 27 | } 28 | 29 | .site-list { 30 | margin: 0; 31 | padding: 0; 32 | list-style: none; 33 | max-height: 200px; 34 | overflow-y: auto; 35 | } 36 | 37 | .site-item { 38 | position: relative; 39 | display: flex; 40 | align-items: center; 41 | justify-content: space-between; 42 | padding: 4px; 43 | padding-right: 8px; 44 | } 45 | 46 | .site-item .domain-text { 47 | flex-grow: 1; 48 | } 49 | 50 | .site-item .controls { 51 | display: flex; 52 | align-items: center; 53 | gap: 8px; 54 | } 55 | 56 | .delete-btn { 57 | background: transparent; 58 | border: none; 59 | color: #ff3b30; 60 | cursor: pointer; 61 | font-size: 14px; 62 | font-weight: 400; 63 | line-height: 1; 64 | padding: 4px 8px; 65 | border-radius: 4px; 66 | transition: all 0.2s ease; 67 | } 68 | 69 | .delete-btn:hover { 70 | background: rgba(255, 59, 48, 0.1); 71 | } 72 | 73 | .site-item .toggle { 74 | margin-left: 8px; 75 | } 76 | 77 | .site-item:last-child { 78 | border-bottom: none; 79 | } 80 | 81 | .toggle { 82 | position: relative; 83 | display: inline-block; 84 | width: 32px; 85 | height: 18px; 86 | } 87 | 88 | .toggle input { 89 | opacity: 0; 90 | width: 0; 91 | height: 0; 92 | } 93 | 94 | .slider { 95 | position: absolute; 96 | cursor: pointer; 97 | top: 0; 98 | left: 0; 99 | right: 0; 100 | bottom: 0; 101 | background-color: #3A3A3C; 102 | transition: .2s; 103 | border-radius: 34px; 104 | } 105 | 106 | .slider:before { 107 | position: absolute; 108 | content: ""; 109 | height: 14px; 110 | width: 14px; 111 | left: 2px; 112 | bottom: 2px; 113 | background-color: #fff; 114 | transition: .2s; 115 | border-radius: 50%; 116 | } 117 | 118 | input:checked + .slider { 119 | background-color: #0A84FF; 120 | } 121 | 122 | input:checked + .slider:before { 123 | transform: translateX(14px); 124 | } 125 | 126 | .current-site { 127 | margin-top: 20px; 128 | padding-top: 16px; 129 | border-top: 1px solid rgba(255, 255, 255, 0.1); 130 | } 131 | 132 | .current-site p { 133 | margin: 0 0 8px 0; 134 | color: #999; 135 | font-size: 12px; 136 | } 137 | 138 | .add-button { 139 | display: inline-block; 140 | width: 100%; 141 | padding: 8px 16px; 142 | background-color: #2C2C2E; 143 | color: #fff; 144 | border: 1px solid rgba(255, 255, 255, 0.2); 145 | border-radius: 4px; 146 | cursor: pointer; 147 | text-align: center; 148 | font-size: 13px; 149 | transition: background-color 0.2s; 150 | } 151 | 152 | .add-button:hover { 153 | background-color: #3A3A3C; 154 | } 155 | 156 | .add-button:disabled { 157 | background-color: #eee; 158 | color: #999; 159 | cursor: not-allowed; 160 | } 161 | 162 | .status-message { 163 | margin-top: 8px; 164 | font-size: 12px; 165 | color: #666; 166 | text-align: center; 167 | min-height: 16px; 168 | } 169 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap"); 2 | 3 | .focus-reminder { 4 | all: initial !important; 5 | position: fixed !important; 6 | display: flex !important; 7 | align-items: center !important; 8 | justify-content: center !important; 9 | top: 0% !important; 10 | left: 0% !important; 11 | background: rgba(28, 28, 30, 0.98) !important; 12 | padding: 32px !important; 13 | z-index: 2147483647 !important; 14 | width: 100vw !important; 15 | height: 100vh !important; 16 | font-family: "Inter", -apple-system, sans-serif !important; 17 | box-sizing: border-box !important; 18 | } 19 | 20 | .focus-reminder * { 21 | all: revert !important; 22 | font-family: "Inter", -apple-system, sans-serif !important; 23 | box-sizing: border-box !important; 24 | line-height: normal !important; 25 | } 26 | 27 | .focus-reminder-content { 28 | max-width: 400px !important; 29 | text-align: center !important; 30 | background: rgba(44, 44, 46, 0.5) !important; 31 | padding: 32px !important; 32 | border-radius: 16px !important; 33 | border: 1px solid rgba(255, 255, 255, 0.1) !important; 34 | backdrop-filter: blur(20px) !important; 35 | } 36 | 37 | .focus-reminder h2 { 38 | margin: 0 0 16px 0 !important; 39 | color: #ffffff !important; 40 | font-size: 24px !important; 41 | font-weight: 500 !important; 42 | letter-spacing: -0.01em !important; 43 | } 44 | 45 | .focus-reminder p { 46 | color: #999 !important; 47 | font-size: 14px !important; 48 | line-height: 1.5 !important; 49 | margin: 0 0 8px 0 !important; 50 | font-weight: 300 !important; 51 | } 52 | 53 | .focus-reminder .section { 54 | margin: 24px 0 !important; 55 | padding: 16px !important; 56 | border-radius: 12px !important; 57 | background: rgba(255, 255, 255, 0.05) !important; 58 | } 59 | 60 | .focus-reminder .section-title { 61 | color: #fff !important; 62 | font-size: 16px !important; 63 | font-weight: 500 !important; 64 | margin-bottom: 8px !important; 65 | letter-spacing: -0.01em !important; 66 | } 67 | 68 | .focus-reminder .section-subtitle { 69 | color: #999 !important; 70 | font-size: 14px !important; 71 | font-weight: 300 !important; 72 | margin-bottom: 16px !important; 73 | line-height: 1.6 !important; 74 | } 75 | 76 | .focus-reminder .timer-options { 77 | margin: 0 auto !important; 78 | max-width: 280px !important; 79 | } 80 | 81 | .focus-reminder .schedule { 82 | background-color: rgba(255, 255, 255, 0.1) !important; 83 | color: white !important; 84 | border: 1px solid rgba(255, 255, 255, 0.2) !important; 85 | padding: 12px 24px !important; 86 | font-size: 14px !important; 87 | font-weight: 400 !important; 88 | border-radius: 8px !important; 89 | cursor: pointer !important; 90 | transition: all 0.2s ease !important; 91 | width: 100% !important; 92 | } 93 | 94 | .focus-reminder .schedule:hover { 95 | background-color: rgba(255, 255, 255, 0.15) !important; 96 | transform: translateY(-1px) !important; 97 | } 98 | 99 | .focus-reminder .countdown { 100 | font-size: 32px !important; 101 | font-weight: 500 !important; 102 | color: white !important; 103 | margin: 16px 0 !important; 104 | } 105 | 106 | .focus-reminder-buttons { 107 | margin-top: 32px !important; 108 | text-align: center !important; 109 | } 110 | 111 | @keyframes breathing { 112 | 0% { 113 | transform: scale(1) !important; 114 | } 115 | 50% { 116 | transform: scale(1.02) !important; 117 | } 118 | 100% { 119 | transform: scale(1) !important; 120 | } 121 | } 122 | 123 | .focus-reminder .leave { 124 | background-color: #2563eb !important; 125 | color: white !important; 126 | border: none !important; 127 | padding: 12px 24px !important; 128 | font-size: 16px !important; 129 | font-weight: 500 !important; 130 | border-radius: 8px !important; 131 | cursor: pointer !important; 132 | transition: all 0.2s ease !important; 133 | margin-bottom: 8px !important; 134 | animation: breathing 2s ease-in-out infinite !important; 135 | } 136 | 137 | .focus-reminder .leave:hover { 138 | background-color: #1d4ed8 !important; 139 | transform: translateY(-1px) scale(1.02) !important; 140 | box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2) !important; 141 | animation: none !important; 142 | } 143 | 144 | .focus-reminder .button-hint { 145 | font-size: 13px !important; 146 | color: #94a3b8 !important; 147 | margin: 0 !important; 148 | } 149 | 150 | .focus-reminder button { 151 | all: revert !important; 152 | font-family: "Inter", -apple-system, sans-serif !important; 153 | border: none !important; 154 | cursor: pointer !important; 155 | transition: all 0.2s ease !important; 156 | margin: 0 !important; 157 | padding: 0 !important; 158 | background: none !important; 159 | line-height: normal !important; 160 | text-align: center !important; 161 | width: 100% !important; 162 | } 163 | -------------------------------------------------------------------------------- /js/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 2 | if (message.action === 'closeTab' && sender.tab) { 3 | chrome.tabs.remove(sender.tab.id); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /js/content.js: -------------------------------------------------------------------------------- 1 | const DELAY_TIME = 30 2 | 3 | async function shouldShowReminder() { 4 | try { 5 | const domain = window.location.hostname.replace('www.', ''); 6 | const result = await chrome.storage.sync.get('sites'); 7 | 8 | if (!result.sites || result.sites.length === 0) { 9 | return true; 10 | } 11 | 12 | const sites = result.sites; 13 | const matchingSite = sites.find(site => 14 | domain.includes(site.domain) || site.domain.includes(domain) 15 | ); 16 | 17 | return matchingSite ? matchingSite.enabled : false; 18 | } catch (error) { 19 | return false; 20 | } 21 | } 22 | 23 | async function createReminderDialog() { 24 | if (!document.body || document.documentElement.tagName.toLowerCase() === 'svg') { 25 | return; 26 | } 27 | 28 | if (document.querySelector('.focus-reminder')) { 29 | return; 30 | } 31 | 32 | const shouldShow = await shouldShowReminder(); 33 | 34 | if (!shouldShow) { 35 | return; 36 | } 37 | 38 | const dialog = document.createElement('div'); 39 | dialog.className = 'focus-reminder'; 40 | dialog.innerHTML = ` 41 |
42 |

Mindful Moment

43 |

Are you spending your time intentionally?

44 | 45 |
46 |

If necessary...

47 |

Look outside the window
and come back in a minute

48 |
49 | 50 |
51 |
52 | 53 | 57 | 58 |
59 | 60 |

and focus on what matters

61 |
62 |
63 | `; 64 | 65 | let countdownInterval; 66 | const timerDisplay = dialog.querySelector('#timer-display'); 67 | const countdownElement = dialog.querySelector('#countdown'); 68 | const timerSelection = dialog.querySelector('#timer-selection'); 69 | 70 | dialog.querySelector('.schedule').addEventListener('click', () => { 71 | if (countdownInterval) clearInterval(countdownInterval); 72 | 73 | let timeLeft = DELAY_TIME; 74 | timerSelection.style.display = 'none'; 75 | timerDisplay.style.display = 'block'; 76 | 77 | countdownInterval = setInterval(() => { 78 | timeLeft--; 79 | const mins = Math.floor(timeLeft / DELAY_TIME); 80 | const secs = timeLeft % DELAY_TIME; 81 | countdownElement.textContent = `${mins}:${secs.toString().padStart(2, '0')}`; 82 | 83 | if (timeLeft <= 0) { 84 | clearInterval(countdownInterval); 85 | dialog.style.opacity = '0'; 86 | dialog.style.transform = 'translate(-50%, -50%) scale(0.95)'; 87 | setTimeout(() => dialog.remove(), 200); 88 | } 89 | }, 1000); 90 | }); 91 | 92 | dialog.querySelector('.leave').addEventListener('click', () => { 93 | if (countdownInterval) { 94 | clearInterval(countdownInterval); 95 | } 96 | dialog.style.opacity = '0'; 97 | dialog.style.transform = 'translate(-50%, -50%) scale(0.95)'; 98 | setTimeout(() => { 99 | chrome.runtime.sendMessage({ action: 'closeTab' }); 100 | }, 200); 101 | }); 102 | 103 | dialog.style.opacity = '0'; 104 | dialog.style.transition = 'all 0.2s ease'; 105 | document.body.appendChild(dialog); 106 | 107 | setTimeout(() => { 108 | dialog.style.opacity = '1'; 109 | }, 50); 110 | } 111 | 112 | function initializeReminder() { 113 | if (!document.body || document.documentElement.tagName.toLowerCase() === 'svg') { 114 | return; 115 | } 116 | createReminderDialog(); 117 | } 118 | 119 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 120 | if (message.action === 'checkReminder') { 121 | createReminderDialog(); 122 | } 123 | }); 124 | 125 | if (document.readyState === 'complete') { 126 | initializeReminder(); 127 | } else { 128 | window.addEventListener('load', initializeReminder); 129 | } 130 | -------------------------------------------------------------------------------- /js/popup.js: -------------------------------------------------------------------------------- 1 | // Default sites that are monitored 2 | const DEFAULT_SITES = [ 3 | 'youtube.com', 4 | 'tiktok.com', 5 | 'twitter.com', 6 | 'x.com', 7 | 'instagram.com', 8 | 'facebook.com' 9 | ]; 10 | 11 | // Load sites from storage or use defaults 12 | async function loadSites() { 13 | const result = await chrome.storage.sync.get('sites'); 14 | if (!result.sites) { 15 | // First time setup: store default sites 16 | const sites = DEFAULT_SITES.map(domain => ({ 17 | domain, 18 | enabled: true 19 | })); 20 | await chrome.storage.sync.set({ sites }); 21 | return sites; 22 | } 23 | return result.sites; 24 | } 25 | 26 | // Show status message 27 | function showStatus(message, isError = false) { 28 | const statusEl = document.getElementById('statusMessage'); 29 | statusEl.textContent = message; 30 | statusEl.style.color = isError ? '#ff3b30' : '#666'; 31 | setTimeout(() => { 32 | statusEl.textContent = ''; 33 | }, 2000); 34 | } 35 | 36 | // Render the site list 37 | async function renderSiteList() { 38 | const sites = await loadSites(); 39 | const siteList = document.getElementById('siteList'); 40 | siteList.innerHTML = ''; 41 | 42 | sites.forEach(site => { 43 | const li = document.createElement('li'); 44 | li.className = 'site-item'; 45 | li.innerHTML = ` 46 | 47 | ${site.domain} 48 |
49 | 53 |
54 | `; 55 | 56 | // Setup delete button handler 57 | const deleteBtn = li.querySelector('.delete-btn'); 58 | deleteBtn.addEventListener('click', async () => { 59 | const updatedSites = sites.filter(s => s.domain !== site.domain); 60 | await chrome.storage.sync.set({ sites: updatedSites }); 61 | showStatus('Site removed'); 62 | renderSiteList(); // Refresh the list 63 | setupAddButton(); // Refresh add button state 64 | }); 65 | 66 | const checkbox = li.querySelector('input'); 67 | checkbox.addEventListener('change', async () => { 68 | const updatedSites = sites.map(s => 69 | s.domain === site.domain ? { ...s, enabled: checkbox.checked } : s 70 | ); 71 | await chrome.storage.sync.set({ sites: updatedSites }); 72 | showStatus(checkbox.checked ? 'Site enabled' : 'Site disabled'); 73 | 74 | // Reload current tab if it matches the toggled site 75 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 76 | const url = new URL(tab.url); 77 | const currentDomain = url.hostname.replace('www.', ''); 78 | if (currentDomain.includes(site.domain) || site.domain.includes(currentDomain)) { 79 | chrome.tabs.reload(tab.id); 80 | } 81 | }); 82 | 83 | siteList.appendChild(li); 84 | }); 85 | } 86 | 87 | // Setup add button for current site 88 | async function setupAddButton() { 89 | const addButton = document.getElementById('addButton'); 90 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 91 | const url = new URL(tab.url); 92 | const domain = url.hostname.replace('www.', ''); 93 | 94 | // Update button state based on whether site is already in list 95 | const sites = await loadSites(); 96 | const siteExists = sites.some(site => 97 | site.domain === domain || domain.includes(site.domain) || site.domain.includes(domain) 98 | ); 99 | 100 | if (siteExists) { 101 | addButton.textContent = 'Already Added'; 102 | addButton.disabled = true; 103 | } else { 104 | addButton.textContent = `Add ${domain}`; 105 | addButton.disabled = false; 106 | } 107 | 108 | addButton.addEventListener('click', async () => { 109 | const sites = await loadSites(); 110 | if (!sites.some(site => site.domain === domain)) { 111 | sites.push({ domain, enabled: true }); 112 | await chrome.storage.sync.set({ sites }); 113 | showStatus('Site added successfully'); 114 | addButton.textContent = 'Added!'; 115 | addButton.disabled = true; 116 | 117 | // Refresh the list 118 | renderSiteList(); 119 | 120 | // Reload the current tab to trigger the reminder 121 | chrome.tabs.reload(tab.id); 122 | } 123 | }); 124 | } 125 | 126 | // Initialize popup 127 | document.addEventListener('DOMContentLoaded', () => { 128 | renderSiteList(); 129 | setupAddButton(); 130 | }); 131 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Twice", 4 | "version": "1.0", 5 | "description": "Reminds you to stay focused when visiting social media sites", 6 | "permissions": [ 7 | "activeTab", 8 | "storage", 9 | "tabs", 10 | "scripting" 11 | ], 12 | "host_permissions": [ 13 | "" 14 | ], 15 | "action": { 16 | "default_popup": "popup.html" 17 | }, 18 | "background": { 19 | "service_worker": "js/background.js" 20 | }, 21 | "content_scripts": [ 22 | { 23 | "matches": [""], 24 | "js": ["js/content.js"], 25 | "css": ["css/style.css"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Think twich when visiting social media sites

8 | 9 | 10 |
11 |

Current website:

12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------