New:Socket for Asana Is Now Available.Learn more
Get Started

Yt Focus Mode

Package Overview
Versions
2
Alerts
File Explorer
Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ytfocus@roxu.dev - firefox Package Compare versions

Comparing version
1.0.0
to
1.1.0
+12
.gitignore
node_modules/
.env
.env.*
*.log
.DS_Store
Thumbs.db
*.zip
*.crx
*.xpi
web-ext-artifacts/
dist/
build/

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

MIT License
Copyright (c) 2026 YouTube Focus Mode
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<p align="center">
<img src="icons/icon128.png" width="96" height="96" alt="YouTube Focus Mode Logo" />
</p>
<h1 align="center">YouTube Focus Mode 🎯</h1>
<p align="center">
A professional, high-performance, and privacy-first distraction blocker for YouTube on Firefox. YouTube Focus Mode helps you take back control of your time by selectively hiding Shorts, the homepage feed, recommended videos, and comment sections with absolute zero latency.
</p>
<p align="center">
<img src="screenshot.png" width="40%" alt="YouTube Focus Mode popup" />
<img src="screenshot2.png" width="40%" alt="YouTube Focus Mode settings" />
</p>
---
## Key Features
* **Hide Shorts** — Completely scrubs YouTube Shorts shelves from the homepage grid, sidebar navigation menu, related video recommendations, and search result feeds.
* **Hide Homepage Feed** — Removes the main distraction-heavy video feed from the YouTube home page, replacing it with a clean interface to encourage search-focused intent rather than mindless scrolling.
* **Hide Recommendations** — Hides the right-side related video suggestion panel on watch pages and automatically expands the main video player to fill the layout for an immersive, distraction-free viewing experience.
* **Hide Comments** — Hides the comment section on video watch pages, preventing distraction and keeping your focus purely on learning or content ingestion.
* **Daily Watch Limit Tracker** — Set a daily time allowance (e.g., 30 minutes). A background script actively tracks your YouTube watch time and displays a customizable cyberpunk-style blocker overlay with a snooze function when your focus limit is reached.
* **One-Click Master Toggle** — Instantly enable or disable all focus filters globally with a single tactile switch in the header.
* **Premium Glassmorphism UI** — Built with clean Inter typography, handcrafted modern SVG iconography, micro-interactive hover transitions, and a smart dynamic reset button with instant feedback.
* **Auto-Syncing Settings** — All changes and filters apply instantly to all active YouTube tabs in real-time, saved securely to `browser.storage.sync` with zero manual page refreshes required.
---
## Privacy & Compliance
YouTube Focus Mode is built to be secure, lightweight, and completely transparent:
* **Zero Telemetry / Zero Tracking** — No background tracking scripts, no third-party APIs, and no telemetry data collection. Your browsing habits remain entirely yours.
* **Zero Remote Code** — 100% locally contained code execution to guarantee absolute security and comply with the strictest Mozilla Add-on Store policies.
* **Least Privilege Permissions** — Declares only the minimal `storage` permission for saving toggle states. No intrusive access to downloads, bookmarks, or browser history.
* **Gecko Compliant** — Designed for modern Firefox (128.0+ / MV3) with strict declarations and clean standard host permissions (`*://*.youtube.com/*`).
---
## Architecture
```
[ YouTube DOM Events & SPA Renders ]
┌─────────────────────────┐ ▼ ┌─────────────────────────┐
│ popup.html/.js │ ◄─────────── [ Read / Write ] ──────────► │ content.js (MV3) │
│ (Inter/Glassmorphism UI)│ │ (DOM Mutation Observer) │
└───────────┬─────────────┘ └───────────┬─────────────┘
│ │
┌───────────▼─────────────┐ [ Read ]
│ background.js (MV3) │ │
│ (Alarms API Tracker) │ ▼
└───────────┬─────────────┘ ┌───────────────────────────────────────────────────┐
│ │ browser.storage.local │
[ Write ] │ • secondsWatchedToday • snoozedToday │
│ └───────────────────────────────────────────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ browser.storage.sync │
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ • masterEnabled: true/false • hideHomepageFeed: true/false │ │
│ │ • hideShorts: true/false • watchLimitMinutes: 0-90 │ │
│ │ • hideComments: true/false │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
```
The extension is engineered with high-efficiency frontend paradigms:
1. **Zero-Latency Injected Stylesheet**: Inserts a single `<style>` element with ID `__yfm_styles__` at `document_start`. It applies highly optimized standard CSS selectors to hide elements natively, preventing layout thrashing or pop-in flickers.
2. **SPA-Resilient Mutation Observer**: YouTube uses a modern single-page application framework that constantly destroys and rebuilds the DOM. The injected content script employs a high-performance `MutationObserver` to watch `document.documentElement` reactively, ensuring that our stylesheet is continuously maintained and active without consuming high CPU cycles.
3. **Reactive Settings Synchronizer**: Listens to changes in `browser.storage.onChanged` in the storage area sync, so when you click a filter toggle in the action popup, the changes are dynamically propagated and applied to all running YouTube instances instantly.
4. **Background Alarm Tracker**: Employs the `browser.alarms` API in `background.js` to poll active YouTube tabs every 1 minute. This ensures highly efficient tracking of watch time that naturally pauses when YouTube is not the focused window, resetting automatically at midnight.
---
## Developer Setup & Installation
### Running Locally (Temporary Add-on)
1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`.
2. Click the **"Load Temporary Add-on..."** button.
3. Select `manifest.json` from this repository's folder.
4. The YouTube Focus Mode icon will appear in your browser toolbar/addons list. Click it to open the popup.
### Local Quality Assurance
Verify source code compliance using the official Mozilla Extension Linter:
```bash
# Run the official linter
npx web-ext lint --source-dir .
```
---
## Release & Packaging
The project is configured for rapid packaging and validation via Mozilla's official `web-ext` command-line utility.
### Packaging
Run the packaging command to compile a production-ready `.zip`/`.xpi` file. This command automatically filters out development metadata, secrets, and raw assets that shouldn't be included in the production bundle:
```bash
npx web-ext build --source-dir . --overwrite-dest --ignore-files .gitignore README.md "screenshot*.png"
```
### Self-Distribution Signing
To generate a signed `.xpi` package for self-distribution:
```bash
npx web-ext sign --api-key=$WEB_EXT_API_KEY --api-secret=$WEB_EXT_API_SECRET --ignore-files .gitignore README.md "screenshot*.png"
```
Retrieve your JWT credentials from the [Mozilla Developer Center](https://addons.mozilla.org/en-US/developers/addon/api/key/).
---
## License
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

+90
-11

@@ -1,15 +0,94 @@

// background.js — YouTube Focus Mode Service Worker
/* ============================================================
YouTube Focus Mode — Background Script
Tracks active cumulative watch time on YouTube tabs.
Handles daily resets and message events.
============================================================ */
const DEFAULT_SETTINGS = {
masterEnabled: true,
hideShorts: true,
hideRecommendations: true,
hideComments: true
};
'use strict';
// Set defaults on first install
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
chrome.storage.sync.set(DEFAULT_SETTINGS);
const TIMER_ALARM_NAME = 'yfm-watch-timer';
// Initialize defaults on install/startup
browser.runtime.onInstalled.addListener(() => {
initializeTimer();
});
browser.runtime.onStartup.addListener(() => {
initializeTimer();
});
function initializeTimer() {
const todayStr = new Date().toLocaleDateString('en-CA'); // YYYY-MM-DD format
browser.storage.local.get(['secondsWatchedToday', 'snoozedToday', 'lastResetDate'], (data) => {
const updates = {};
if (data.secondsWatchedToday === undefined) updates.secondsWatchedToday = 0;
if (data.snoozedToday === undefined) updates.snoozedToday = false;
if (!data.lastResetDate) updates.lastResetDate = todayStr;
if (Object.keys(updates).length > 0) {
browser.storage.local.set(updates);
}
});
// Create alarm to poll active tab status every 1 minute
browser.alarms.create(TIMER_ALARM_NAME, { periodInMinutes: 1 });
}
// Check if calendar date has changed and perform midnight reset
function checkDailyReset(callback) {
const todayStr = new Date().toLocaleDateString('en-CA');
browser.storage.local.get(['lastResetDate'], (data) => {
if (data.lastResetDate !== todayStr) {
browser.storage.local.set({
secondsWatchedToday: 0,
snoozedToday: false,
lastResetDate: todayStr
}, () => {
if (callback) callback(true);
});
} else {
if (callback) callback(false);
}
});
}
// Alarm Listener
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === TIMER_ALARM_NAME) {
checkDailyReset((resetPerformed) => {
if (resetPerformed) return; // If reset was just performed, skip watch increment for this cycle
// Query for the actively focused tab in the focused window
browser.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
if (tabs && tabs[0] && tabs[0].url) {
const url = tabs[0].url;
// Verify if it is YouTube
if (
url.includes('youtube.com/watch') ||
url.includes('youtube.com/results') ||
url.includes('youtube.com/channel') ||
url.includes('youtube.com/@') ||
url.match(/^https?:\/\/(www\.)?youtube\.com\/?/)
) {
// Check if user has a limit set
browser.storage.sync.get({ watchLimitMinutes: 0, masterEnabled: true }, (settings) => {
if (settings.masterEnabled && settings.watchLimitMinutes > 0) {
browser.storage.local.get({ secondsWatchedToday: 0 }, (localData) => {
const newSeconds = localData.secondsWatchedToday + 60;
browser.storage.local.set({ secondsWatchedToday: newSeconds });
});
}
});
}
}
});
});
}
});
// Listener for Tab Close from Content Script
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'closeTab' && sender.tab) {
browser.tabs.remove(sender.tab.id);
}
});
+282
-43
/* ============================================================
YouTube Focus Mode — Content Script
Reads masterEnabled + individual toggles from storage.
If masterEnabled is false, all injected CSS is stripped out.
Controls dynamic page hiding, whitelisting, auto-theater mode,
custom CSS injection, and daily watch limit overlays.
============================================================ */
(function () {
'use strict';
"use strict";
const STYLE_ID = '__yfm_styles__';
const STYLE_ID = "__yfm_styles__";
const OVERLAY_ID = "ytfm-limit-overlay";

@@ -15,32 +16,218 @@ const DEFAULTS = {

hideShorts: true,
hideRecommendations: true,
hideComments: true
hideShortsInSearch: true,
hideHomepageFeed: false,
hideComments: false,
hideSidebar: true,
hideEndcards: false,
hideMerch: false,
hideNotificationBell: false,
watchLimitMinutes: 0,
};
// ── Selector banks grouped by feature ─────────────────────
const SELECTORS = {
hideShorts: [
'ytd-guide-entry-renderer a[title="Shorts"]',
'ytd-mini-guide-entry-renderer[aria-label="Shorts"]',
'ytd-rich-shelf-renderer[is-shorts]',
'ytd-guide-entry-renderer a[href="/shorts"]',
'ytd-mini-guide-entry-renderer a[href="/shorts"]',
"ytd-rich-shelf-renderer[is-shorts]",
'ytd-rich-shelf-renderer[feature="SHORTS"]',
'ytd-reel-shelf-renderer',
'ytd-shelf-renderer:has(ytd-reel-item-renderer)',
'#related ytd-reel-shelf-renderer',
'ytd-rich-item-renderer:has(ytd-rich-grid-slim-media[is-short])',
"ytd-reel-shelf-renderer",
"ytd-shelf-renderer:has(ytd-reel-item-renderer)",
"#related ytd-reel-shelf-renderer",
"ytd-rich-item-renderer:has(ytd-rich-grid-slim-media[is-short])",
'ytd-rich-item-renderer:has(a[href*="/shorts/"])',
'yt-chip-cloud-chip-renderer:has(yt-formatted-string[title="Shorts"])'
'yt-chip-cloud-chip-renderer:has(a[href="/shorts"])',
],
hideRecommendations: [
'#secondary',
'#related',
'ytd-watch-next-secondary-results-renderer'
hideShortsInSearch: [
'ytd-video-renderer:has(a[href*="/shorts/"])',
"ytd-reel-item-renderer",
"ytd-reel-shelf-renderer",
"ytd-rich-shelf-renderer[is-shorts]",
'ytd-shelf-renderer:has(a[href*="/shorts/"])',
],
hideComments: [
'#comments',
'ytd-comments#comments'
]
hideHomepageFeed: ["#contents", "ytd-rich-grid-renderer"],
hideComments: ["#comments", "ytd-comments#comments"],
hideSidebar: [
"#secondary",
"#related",
"ytd-watch-next-secondary-results-renderer",
],
hideEndcards: [
".ytp-ce-element",
".ytp-ce-cover-image",
".ytp-ce-video",
".ytp-ce-playlist",
".ytp-ce-channel",
],
hideMerch: [
"ytd-merch-shelf-renderer",
".ytd-merch-shelf-renderer",
"#merch-shelf",
],
hideNotificationBell: [
"ytd-notification-toggle-button-renderer",
"#notification-preference-button",
".ytd-notification-toggle-button-renderer",
],
};
// ── Build CSS from current settings ───────────────────────
let observer = null;
let currentSettings = null;
let lastCheckedVideoId = "";
let enforceLimitInterval = null;
// ─────────────────────────────────────────────────────────────────────────────
// ── Focus Limit Overlay & Active Play Enforcer ──────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
function checkFocusLimit() {
if (
!currentSettings ||
!currentSettings.masterEnabled ||
!currentSettings.watchLimitMinutes
) {
removeLimitOverlay();
return;
}
browser.storage.local.get(
["secondsWatchedToday", "snoozedToday", "snoozeExpiryTime"],
(localData) => {
const limitSeconds = currentSettings.watchLimitMinutes * 60;
const secondsToday = localData.secondsWatchedToday || 0;
const isSnoozed = !!localData.snoozedToday;
const snoozeExpiry = localData.snoozeExpiryTime || 0;
if (secondsToday >= limitSeconds) {
if (isSnoozed) {
if (Date.now() < snoozeExpiry) {
// Under snooze period, do not overlay
removeLimitOverlay();
return;
} else {
// Snooze expired, show strict overlay with no snooze button
injectLimitOverlay(true);
}
} else {
// Limit hit, first time, show overlay with snooze button
injectLimitOverlay(false);
}
} else {
removeLimitOverlay();
}
},
);
}
function injectLimitOverlay(snoozeUsed) {
// Only overlay the video player to keep it non-blocking on the page layout
const playerEl =
document.getElementById("movie_player") ||
document.querySelector(".html5-video-player");
if (!playerEl) return;
// Pause the video actively
const video = playerEl.querySelector("video");
if (video && !video.paused) {
video.pause();
}
if (document.getElementById(OVERLAY_ID)) {
// Overlay exists, update state of snooze button if expired
const snoozeBtn = document.getElementById("ytfm-btn-snooze");
if (snoozeBtn && snoozeUsed) {
snoozeBtn.disabled = true;
snoozeBtn.style.opacity = "0.35";
snoozeBtn.textContent = "Snooze Limit Reached";
}
return;
}
const overlay = document.createElement("div");
overlay.id = OVERLAY_ID;
const style = document.createElement("style");
style.textContent = `#${OVERLAY_ID} { position: absolute; inset: 0; z-index: 2147483647; background: rgba(10, 10, 15, 0.72); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); display: flex; align-items: center; justify-content: center; font-family: "Inter", system-ui, sans-serif; color: #f0f0f5; } .yfm-limit-card { text-align: center; padding: 30px; background: rgba(24, 24, 31, 0.9); border: 1px solid rgba(255, 78, 78, 0.25); box-shadow: 0 0 30px rgba(255, 78, 78, 0.15); border-radius: 16px; max-width: 380px; width: 90%; } .yfm-limit-icon { font-size: 32px; margin-bottom: 12px; } .yfm-limit-card h2 { font-size: 18px; font-weight: 700; margin-bottom: 8px; color: #f0f0f5; letter-spacing: -0.3px; } .yfm-limit-card p { font-size: 13px; color: #9090a8; margin-bottom: 24px; line-height: 1.5; } .yfm-limit-btns { display: flex; gap: 12px; justify-content: center; } .yfm-limit-btn { font-size: 13px; font-weight: 600; padding: 8px 18px; border-radius: 20px; cursor: pointer; transition: 0.2s; border: none; font-family: inherit; } .yfm-btn-break { background: #ff4e4e; color: #ffffff; box-shadow: 0 0 10px rgba(255, 78, 78, 0.25); } .yfm-btn-break:hover { background: #ff3838; box-shadow: 0 0 15px rgba(255, 78, 78, 0.35); } .yfm-btn-snooze { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255,255,255,0.08); color: #9090a8; } .yfm-btn-snooze:hover:not(:disabled) { background: rgba(255, 255, 255, 0.08); color: #f0f0f5; } .yfm-btn-snooze:disabled { cursor: not-allowed; opacity: 0.35; }`;
overlay.appendChild(style);
const card = document.createElement("div");
card.className = "yfm-limit-card";
const icon = document.createElement("div");
icon.className = "yfm-limit-icon";
icon.textContent = "🎯";
const h2 = document.createElement("h2");
h2.textContent = "You've hit your focus limit for today";
const p = document.createElement("p");
p.textContent = "Your scheduled screen limit has passed. Taking active breaks keeps your productivity high.";
const btns = document.createElement("div");
btns.className = "yfm-limit-btns";
const breakBtn = document.createElement("button");
breakBtn.id = "ytfm-btn-break";
breakBtn.className = "yfm-limit-btn yfm-btn-break";
breakBtn.textContent = "Take a break";
const snoozeBtn = document.createElement("button");
snoozeBtn.id = "ytfm-btn-snooze";
snoozeBtn.className = "yfm-limit-btn yfm-btn-snooze";
snoozeBtn.textContent = snoozeUsed ? "Snooze Limit Reached" : "5 more minutes";
if (snoozeUsed) snoozeBtn.disabled = true;
btns.appendChild(breakBtn);
btns.appendChild(snoozeBtn);
card.appendChild(icon);
card.appendChild(h2);
card.appendChild(p);
card.appendChild(btns);
overlay.appendChild(card);
playerEl.appendChild(overlay);
breakBtn.addEventListener("click", () => {
browser.runtime.sendMessage({ action: "closeTab" });
});
if (snoozeBtn) {
snoozeBtn.addEventListener("click", () => {
const fiveMinutesMs = 5 * 60 * 1000;
browser.storage.local.set(
{
snoozedToday: true,
snoozeExpiryTime: Date.now() + fiveMinutesMs,
},
() => {
removeLimitOverlay();
},
);
});
}
// Keep active check interval to enforce pause when overlay is active
if (!enforceLimitInterval) {
enforceLimitInterval = setInterval(() => {
const v = playerEl.querySelector("video");
if (v && !v.paused && document.getElementById(OVERLAY_ID)) {
v.pause();
}
}, 250);
}
}
function removeLimitOverlay() {
const overlay = document.getElementById(OVERLAY_ID);
if (overlay) overlay.remove();
if (enforceLimitInterval) {
clearInterval(enforceLimitInterval);
enforceLimitInterval = null;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// ── CSS Hiding Stylesheet Logic ──────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
function buildCSS(settings) {

@@ -51,7 +238,8 @@ const rules = [];

if (settings[feature]) {
rules.push(`${selectors.join(',\n')} { display: none !important; }`);
rules.push(`${selectors.join(",\n")} { display: none !important; }`);
}
}
if (settings.hideRecommendations) {
// Apply auto-stretching when sidebar recommendations are hidden
if (settings.hideSidebar) {
rules.push(`

@@ -72,17 +260,19 @@ ytd-watch-flexy #primary.ytd-watch-flexy {

return rules.join('\n');
return rules.join("\n");
}
// ── Inject / remove the <style> tag ───────────────────────
function applySettings(settings) {
currentSettings = settings;
let el = document.getElementById(STYLE_ID);
if (!settings.masterEnabled) {
// Extension disabled — strip all rules
if (el) el.remove();
removeLimitOverlay();
stopObserver();
return;
}
// Standard Stylesheet Management
if (!el) {
el = document.createElement('style');
el = document.createElement("style");
el.id = STYLE_ID;

@@ -92,28 +282,77 @@ (document.head || document.documentElement).appendChild(el);

el.textContent = buildCSS(settings);
// Active Feature Checks
checkFocusLimit();
startObserver();
}
// ── MutationObserver: keep style tag alive on SPA navigation
let observer = null;
// ─────────────────────────────────────────────────────────────────────────────
// ── MutationObserver (SPA Navigation Friendly) ───────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
function startObserver() {
if (!currentSettings || !currentSettings.masterEnabled) {
stopObserver();
return;
}
if (observer) return;
observer = new MutationObserver(() => {
if (!document.getElementById(STYLE_ID)) {
chrome.storage.sync.get(DEFAULTS, applySettings);
if (currentSettings) {
if (currentSettings.masterEnabled) {
if (!document.getElementById(STYLE_ID)) {
browser.storage.sync.get(DEFAULTS, applySettings);
}
}
checkFocusLimit();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
// ── Bootstrap ─────────────────────────────────────────────
chrome.storage.sync.get(DEFAULTS, (settings) => {
function stopObserver() {
if (observer) {
observer.disconnect();
observer = null;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// ── Storage and Event Synchronizers ──────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
function handleStorageChange(changes, area) {
if (area === "sync") {
browser.storage.sync.get(DEFAULTS, (settings) => {
// Fetch local settings too for timers
browser.storage.local.get(
["secondsWatchedToday", "snoozedToday", "snoozeExpiryTime"],
(localData) => {
applySettings(settings);
},
);
});
} else if (area === "local") {
// Re-evaluate focus limits immediately on clock updates
checkFocusLimit();
}
}
// YouTube SPA dynamic page navigation triggers
document.addEventListener("yt-navigate-finish", () => {
checkFocusLimit();
if (currentSettings) {
applySettings(currentSettings);
}
});
// Startup Initialization
browser.storage.sync.get(DEFAULTS, (settings) => {
applySettings(settings);
startObserver();
});
// Re-apply whenever any toggle changes in the popup
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') return;
chrome.storage.sync.get(DEFAULTS, applySettings);
});
browser.storage.onChanged.addListener(handleStorageChange);
})();
+1
-61

@@ -1,61 +0,1 @@

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
<defs>
<radialGradient id="glow128" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff4e4e" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#ff4e4e" stop-opacity="0"/>
</radialGradient>
<linearGradient id="bg128" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e1e2e"/>
<stop offset="100%" stop-color="#0f0f13"/>
</linearGradient>
<linearGradient id="iris128" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff6b6b"/>
<stop offset="100%" stop-color="#cc2a2a"/>
</linearGradient>
<filter id="shadow128" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#ff4e4e" flood-opacity="0.4"/>
</filter>
</defs>
<!-- Background -->
<rect width="128" height="128" rx="28" fill="url(#bg128)"/>
<rect x="2" y="2" width="124" height="124" rx="26" stroke="#ff4e4e" stroke-width="1.5" fill="none" opacity="0.2"/>
<!-- Subtle grid lines -->
<line x1="64" y1="0" x2="64" y2="128" stroke="white" stroke-width="0.5" opacity="0.03"/>
<line x1="0" y1="64" x2="128" y2="64" stroke="white" stroke-width="0.5" opacity="0.03"/>
<!-- Outer glow -->
<circle cx="64" cy="64" r="52" fill="url(#glow128)"/>
<!-- Corner brackets – thick -->
<path d="M18 40V18h22" stroke="#ff4e4e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M88 18h22v22" stroke="#ff4e4e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M110 88v22H88" stroke="#ff4e4e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40 110H18V88" stroke="#ff4e4e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Inner brackets (decorative) -->
<path d="M30 52V34h18" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/>
<path d="M80 34h18v18" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/>
<path d="M98 76v18H80" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/>
<path d="M48 94H30V76" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/>
<!-- Eye path -->
<path d="M22 64c9-18 20-24 42-24s33 6 42 24c-9 18-20 24-42 24s-33-6-42-24z"
fill="#ff4e4e" opacity="0.1" stroke="#ff4e4e" stroke-width="2.5"/>
<!-- Iris -->
<circle cx="64" cy="64" r="16" fill="url(#iris128)" filter="url(#shadow128)"/>
<!-- Pupil -->
<circle cx="64" cy="64" r="7" fill="#0d0d15"/>
<!-- Inner reflection ring -->
<circle cx="64" cy="64" r="10" fill="none" stroke="white" stroke-width="0.5" opacity="0.2"/>
<!-- Specular highlights -->
<circle cx="70" cy="58" r="3.5" fill="white" opacity="0.55"/>
<circle cx="57" cy="70" r="1.5" fill="white" opacity="0.2"/>
<!-- Green checkmark / focus-confirmed badge -->
<circle cx="96" cy="96" r="14" fill="#0d0d15" stroke="#34d399" stroke-width="2"/>
<path d="M89 96l4.5 4.5 9-9" stroke="#34d399" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" zoomAndPan="magnify" viewBox="0 0 96 95.999999" height="128" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="8c4607c7e0"><path d="M 0 1.832031 L 93 1.832031 L 93 94.082031 L 0 94.082031 Z M 0 1.832031 " clip-rule="nonzero"/></clipPath></defs><rect x="-9.6" width="115.2" fill="#ffffff" y="-9.6" height="115.199999" fill-opacity="1"/><rect x="-9.6" width="115.2" fill="#ffffff" y="-9.6" height="115.199999" fill-opacity="1"/><g clip-path="url(#8c4607c7e0)"><path fill="#ff3131" d="M 80.945312 28.265625 C 70.699219 18.019531 55.734375 14.394531 41.886719 18.8125 C 39.558594 19.554688 37.296875 20.523438 35.164062 21.691406 L 37.824219 26.558594 C 39.644531 25.5625 41.582031 24.734375 43.574219 24.097656 C 55.433594 20.316406 68.25 23.414062 77.023438 32.1875 C 89.902344 45.066406 89.902344 66.019531 77.023438 78.898438 C 64.144531 91.777344 43.1875 91.777344 30.3125 78.898438 C 24.085938 72.671875 20.652344 64.375 20.652344 55.542969 C 20.652344 47.644531 23.402344 40.179688 28.4375 34.234375 L 32.449219 38.246094 C 33.710938 39.507812 33.933594 41.464844 33.007812 42.988281 C 27.351562 52.292969 28.53125 64.613281 36.566406 72.648438 C 41.28125 77.359375 47.472656 79.71875 53.667969 79.71875 C 59.863281 79.71875 66.054688 77.359375 70.769531 72.648438 C 80.199219 63.214844 80.199219 47.871094 70.769531 38.441406 C 63.890625 31.558594 53.605469 29.476562 44.566406 33.132812 L 46.648438 38.277344 C 53.613281 35.453125 61.542969 37.0625 66.847656 42.363281 C 74.117188 49.628906 74.117188 61.457031 66.847656 68.726562 C 59.578125 75.992188 47.753906 75.988281 40.488281 68.726562 C 35.34375 63.582031 33.851562 56.15625 35.996094 49.683594 C 36.898438 46.964844 40.355469 46.152344 42.378906 48.179688 L 45.066406 50.863281 C 43.046875 54.578125 43.601562 59.332031 46.738281 62.472656 C 48.648438 64.382812 51.160156 65.335938 53.667969 65.335938 C 56.175781 65.335938 58.683594 64.382812 60.59375 62.472656 C 64.414062 58.652344 64.414062 52.4375 60.597656 48.617188 L 60.59375 48.617188 C 58.746094 46.765625 56.285156 45.746094 53.667969 45.746094 C 52.003906 45.746094 50.402344 46.164062 48.984375 46.9375 L 29.558594 27.511719 L 33.46875 18.554688 C 33.925781 17.511719 33.695312 16.292969 32.886719 15.484375 L 20.203125 2.800781 C 19.554688 2.148438 18.628906 1.863281 17.726562 2.035156 C 16.824219 2.203125 16.066406 2.808594 15.699219 3.652344 L 11.46875 13.34375 L 1.777344 17.574219 C 0.933594 17.941406 0.328125 18.699219 0.160156 19.601562 C -0.0117188 20.503906 0.273438 21.429688 0.921875 22.078125 L 13.609375 34.765625 C 14.140625 35.296875 14.851562 35.578125 15.570312 35.578125 C 15.945312 35.578125 16.324219 35.5 16.679688 35.347656 L 22.503906 32.804688 C 17.703125 39.359375 15.109375 47.246094 15.105469 55.542969 C 15.105469 65.859375 19.113281 75.546875 26.390625 82.820312 C 33.910156 90.34375 43.789062 94.101562 53.667969 94.101562 C 63.546875 94.101562 73.425781 90.34375 80.945312 82.820312 C 95.988281 67.78125 95.988281 43.304688 80.945312 28.265625 Z M 19.15625 9.597656 L 27.628906 18.070312 L 25.347656 23.300781 L 16.875 14.828125 Z M 7.722656 21.03125 L 12.949219 18.75 L 21.421875 27.222656 L 16.195312 29.503906 Z M 7.722656 21.03125 " fill-opacity="1" fill-rule="nonzero"/></g></svg>

@@ -1,40 +0,1 @@

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<defs>
<radialGradient id="glow48" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff4e4e" stop-opacity="0.25"/>
<stop offset="100%" stop-color="#ff4e4e" stop-opacity="0"/>
</radialGradient>
<linearGradient id="bg48" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e1e2e"/>
<stop offset="100%" stop-color="#12121a"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="48" height="48" rx="12" fill="url(#bg48)"/>
<rect x="1" y="1" width="46" height="46" rx="11" stroke="#ff4e4e" stroke-width="1" fill="none" opacity="0.3"/>
<!-- Glow -->
<circle cx="24" cy="24" r="20" fill="url(#glow48)"/>
<!-- Corner brackets -->
<path d="M8 16V8h8" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32 8h8v8" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40 32v8h-8" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 40H8v-8" stroke="#ff4e4e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Eye outline -->
<path d="M12 24c3.2-6 6.5-8 12-8s8.8 2 12 8c-3.2 6-6.5 8-12 8s-8.8-2-12-8z"
fill="#ff4e4e" opacity="0.15" stroke="#ff4e4e" stroke-width="1.4"/>
<!-- Iris -->
<circle cx="24" cy="24" r="5" fill="#ff4e4e" opacity="0.9"/>
<!-- Pupil -->
<circle cx="24" cy="24" r="2.2" fill="#12121a"/>
<!-- Specular -->
<circle cx="26" cy="22.5" r="1" fill="white" opacity="0.7"/>
<!-- Small tick mark — "focus confirmed" -->
<path d="M19 36l2.5 2.5L28 32" stroke="#34d399" stroke-width="1.8"
stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" zoomAndPan="magnify" viewBox="0 0 36 36.000001" height="48" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="24ee328933"><path d="M 0.488281 0 L 35 0 L 35 33.566406 L 0.488281 33.566406 Z M 0.488281 0 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#24ee328933)"><path fill="#ff3131" d="M 30.019531 9.640625 C 26.28125 5.902344 20.824219 4.582031 15.769531 6.191406 C 14.917969 6.464844 14.09375 6.816406 13.316406 7.242188 L 14.285156 9.015625 C 14.953125 8.652344 15.65625 8.351562 16.386719 8.121094 C 20.710938 6.742188 25.386719 7.871094 28.589844 11.070312 C 33.289062 15.769531 33.289062 23.417969 28.589844 28.117188 C 23.890625 32.816406 16.246094 32.8125 11.546875 28.117188 C 9.273438 25.84375 8.023438 22.816406 8.023438 19.59375 C 8.023438 16.710938 9.027344 13.988281 10.863281 11.820312 L 12.328125 13.28125 C 12.785156 13.742188 12.867188 14.457031 12.53125 15.011719 C 10.464844 18.40625 10.898438 22.902344 13.828125 25.832031 C 15.546875 27.554688 17.808594 28.414062 20.066406 28.414062 C 22.328125 28.414062 24.585938 27.554688 26.308594 25.832031 C 29.75 22.394531 29.75 16.792969 26.308594 13.351562 C 23.796875 10.84375 20.042969 10.082031 16.746094 11.417969 L 17.507812 13.292969 C 20.046875 12.265625 22.941406 12.851562 24.878906 14.785156 C 27.527344 17.4375 27.527344 21.75 24.878906 24.402344 C 22.222656 27.054688 17.910156 27.054688 15.257812 24.402344 C 13.382812 22.527344 12.839844 19.816406 13.621094 17.453125 C 13.949219 16.464844 15.210938 16.167969 15.949219 16.90625 L 16.929688 17.886719 C 16.191406 19.242188 16.394531 20.976562 17.539062 22.121094 C 18.238281 22.816406 19.152344 23.167969 20.066406 23.167969 C 20.984375 23.167969 21.898438 22.816406 22.59375 22.121094 C 23.988281 20.726562 23.988281 18.460938 22.597656 17.066406 L 22.59375 17.066406 C 21.921875 16.390625 21.023438 16.019531 20.066406 16.019531 C 19.460938 16.019531 18.878906 16.171875 18.359375 16.453125 L 11.273438 9.367188 L 12.699219 6.097656 C 12.863281 5.71875 12.78125 5.273438 12.488281 4.976562 L 7.859375 0.347656 C 7.621094 0.113281 7.285156 0.0078125 6.953125 0.0703125 C 6.625 0.132812 6.347656 0.351562 6.214844 0.660156 L 4.671875 4.195312 L 1.132812 5.742188 C 0.828125 5.875 0.605469 6.152344 0.542969 6.480469 C 0.484375 6.808594 0.585938 7.148438 0.824219 7.382812 L 5.453125 12.011719 C 5.644531 12.207031 5.90625 12.308594 6.167969 12.308594 C 6.304688 12.308594 6.441406 12.28125 6.574219 12.222656 L 8.699219 11.296875 C 6.945312 13.6875 6 16.566406 6 19.59375 C 6 23.359375 7.460938 26.890625 10.113281 29.546875 C 12.859375 32.289062 16.464844 33.664062 20.066406 33.664062 C 23.671875 33.664062 27.277344 32.289062 30.019531 29.546875 C 35.507812 24.058594 35.507812 15.128906 30.019531 9.640625 Z M 7.476562 2.828125 L 10.566406 5.921875 L 9.734375 7.828125 L 6.644531 4.738281 Z M 3.304688 7 L 5.210938 6.167969 L 8.304688 9.261719 L 6.394531 10.09375 Z M 3.304688 7 " fill-opacity="1" fill-rule="nonzero"/></g></svg>
{
"manifest_version": 3,
"name": "YouTube Focus Mode",
"version": "1.0.0",
"version": "1.1.0",
"description": "Stay focused on YouTube \u2014 hide Shorts, recommendations, comments, and the homepage feed.",
"author": "YouTube Focus Mode",
"icons": {
"16": "icons/icon16.svg",
"32": "icons/icon32.svg",
"48": "icons/icon48.svg",
"128": "icons/icon128.svg"
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},

@@ -16,5 +14,4 @@ "action": {

"default_icon": {
"16": "icons/icon16.svg",
"32": "icons/icon32.svg",
"48": "icons/icon48.svg"
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},

@@ -24,4 +21,10 @@ "default_title": "YouTube Focus Mode"

"permissions": [
"storage"
"storage",
"alarms"
],
"background": {
"scripts": [
"background.js"
]
},
"host_permissions": [

@@ -44,8 +47,2 @@ "https://www.youtube.com/*",

],
"background": {
"scripts": [
"background.js"
],
"type": "module"
},
"browser_specific_settings": {

@@ -62,3 +59,6 @@ "gecko": {

}
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'"
}
}
+122
-0

@@ -6,2 +6,35 @@ /* ============================================================

/* ── Local Fonts ───────────────────────────────────────────── */
@font-face {
font-family: 'Inter';
src: local('Inter'), url('fonts/Inter-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: local('Inter Medium'), url('fonts/Inter-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: local('Inter SemiBold'), url('fonts/Inter-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: local('Inter Bold'), url('fonts/Inter-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* ── Reset & Base ─────────────────────────────────────────── */

@@ -384,1 +417,90 @@ *, *::before, *::after {

}
/* ── Scrollable Body Area ───────────────────────────────── */
.popup-scroll-container {
max-height: 410px;
overflow-y: auto;
padding-bottom: 8px;
}
/* Custom Cyberpunk Scrollbar */
.popup-scroll-container::-webkit-scrollbar {
width: 6px;
}
.popup-scroll-container::-webkit-scrollbar-track {
background: transparent;
}
.popup-scroll-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
transition: background var(--transition);
}
.popup-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.18);
}
.main-divider {
height: 1px;
background: var(--border);
margin: 15px 6px;
}
/* ── Daily Watch Limit Dropdown ─────────────────────────── */
.control-group {
padding: 0 6px;
display: flex;
flex-direction: column;
gap: 8px;
}
.select-wrapper {
position: relative;
width: 100%;
}
.select-wrapper select {
width: 100%;
padding: 10px 14px;
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
appearance: none;
cursor: pointer;
outline: none;
transition: border-color var(--transition), background var(--transition);
}
.select-wrapper select:hover {
background: var(--bg-hover);
border-color: rgba(255,255,255,0.12);
}
.select-wrapper select:focus {
border-color: rgba(255, 78, 78, 0.4);
box-shadow: 0 0 10px var(--accent-glow);
}
.select-arrow {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
pointer-events: none;
display: flex;
align-items: center;
}
.timer-status {
font-size: 0.72rem;
color: var(--text-muted);
margin-top: 2px;
font-weight: 500;
}
+164
-64

@@ -7,5 +7,2 @@ <!DOCTYPE html>

<title>YouTube Focus Mode</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="popup.css" />

@@ -36,72 +33,175 @@ </head>

<!-- ── Toggle List ───────────────────────── -->
<main class="popup-main">
<p class="section-label">FOCUS FILTERS</p>
<!-- ── Scrollable Body Area ───────────────── -->
<div class="popup-scroll-container">
<main class="popup-main">
<p class="section-label">FOCUS FILTERS</p>
<ul class="toggle-list" role="list">
<ul class="toggle-list" role="list">
<!-- Hide Shorts -->
<li class="toggle-item" id="item-hideShorts">
<div class="toggle-info">
<span class="toggle-icon">
<!-- Shorts / Reel icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<path d="M17.77 10.32l-1.2-.5L18 9.06c1.84-.96 2.53-3.23 1.56-5.06s-3.23-2.53-5.06-1.56L6 6.94c-1.29.68-2.06 2.03-1.98 3.46.03.6.22 1.18.56 1.69L3.38 13.5C2.09 14.2 1.32 15.57 1.32 17c0 2.21 1.79 4 4 4 .75 0 1.45-.22 2.06-.58L19 13.56c1.29-.68 2.06-2.04 1.98-3.47-.07-1.2-.75-2.26-1.78-2.87zM13 3.5c.83 0 1.5.67 1.5 1.5S13.83 6.5 13 6.5 11.5 5.83 11.5 5s.67-1.5 1.5-1.5zm-7.68 14c-.41.26-.87.4-1.32.4-.83 0-1.5-.67-1.5-1.5 0-.59.33-1.12.85-1.4L14.5 9.46l1.2.5-9.82 5.16c-.2.1-.37.24-.49.41-.32.44-.36 1-.11 1.47.39.75 1.32 1.04 2.07.65L18.5 12l.28.53L7.5 17.21c-.69.36-1.51.22-2.08-.29-.19-.17-.37-.37-.1-.42zm13.5-4.76L7.75 18.26c.48-.25 1-.38 1.53-.38.45 0 .9.1 1.32.31l7.57-3.97 1.2-.63c.77-.4 1.26-1.19 1.26-2.06 0-1.28-1.04-2.32-2.32-2.32-.3 0-.58.06-.84.16l.35-.17z"/>
</svg>
</span>
<div>
<span class="toggle-label">Hide Shorts</span>
<span class="toggle-desc">Removes Shorts from sidebar, home &amp; search</span>
<!-- Hide Shorts -->
<li class="toggle-item" id="item-hideShorts">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M17.77 10.32l-1.2-.5L18 9.06c1.84-.96 2.53-3.23 1.56-5.06s-3.23-2.53-5.06-1.56L6 6.94c-1.29.68-2.06 2.03-1.98 3.46.03.6.22 1.18.56 1.69L3.38 13.5C2.09 14.2 1.32 15.57 1.32 17c0 2.21 1.79 4 4 4 .75 0 1.45-.22 2.06-.58L19 13.56c1.29-.68 2.06-2.04 1.98-3.47-.07-1.2-.75-2.26-1.78-2.87zm-4.77-6.82c.83 0 1.5.67 1.5 1.5S13.83 6.5 13 6.5 11.5 5.83 11.5 5s.67-1.5 1.5-1.5zm-7.68 14c-.41.26-.87.4-1.32.4-.83 0-1.5-.67-1.5-1.5 0-.59.33-1.12.85-1.4L14.5 9.46l1.2.5-9.82 5.16c-.2.1-.37.24-.49.41-.32.44-.36 1-.11 1.47.39.75 1.32 1.04 2.07.65L18.5 12l.28.53L7.5 17.21c-.69.36-1.51.22-2.08-.29-.19-.17-.37-.37-.1-.42zm13.5-4.76L7.75 18.26c.48-.25 1-.38 1.53-.38.45 0 .9.1 1.32.31l7.57-3.97 1.2-.63c.77-.4 1.26-1.19 1.26-2.06 0-1.28-1.04-2.32-2.32-2.32-.3 0-.58.06-.84.16l.35-.17z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Shorts</span>
<span class="toggle-desc">Removes Shorts from sidebar and home</span>
</div>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Shorts">
<input type="checkbox" id="toggle-hideShorts" />
<span class="slider"></span>
</label>
</li>
<label class="switch" aria-label="Toggle Hide Shorts">
<input type="checkbox" id="toggle-hideShorts" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Recommendations -->
<li class="toggle-item" id="item-hideRecommendations">
<div class="toggle-info">
<span class="toggle-icon">
<!-- Sidebar / panel icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<path d="M3 5h2V3c-1.1 0-2 .9-2 2zm0 8h2v-2H3v2zm4 8h2v-2H7v2zM3 9h2V7H3v2zm10-6h-2v2h2V3zm6 0v2h2c0-1.1-.9-2-2-2zM5 21v-2H3c0 1.1.9 2 2 2zm-2-4h2v-2H3v2zM9 3H7v2h2V3zm2 18h2v-2h-2v2zm8-8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V7h-2v2zm0 8h2v-2h-2v2zm-4 4h2v-2h-2v2zm0-16h2V3h-2v2z"/>
</svg>
</span>
<div>
<span class="toggle-label">Hide Recommendations</span>
<span class="toggle-desc">Hides the right-side video suggestion panel</span>
<!-- Hide Shorts in Search -->
<li class="toggle-item" id="item-hideShortsInSearch">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Shorts in Search</span>
<span class="toggle-desc">Removes Shorts shelves and results from Search</span>
</div>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Recommendations">
<input type="checkbox" id="toggle-hideRecommendations" />
<span class="slider"></span>
</label>
</li>
<label class="switch" aria-label="Toggle Hide Shorts in Search">
<input type="checkbox" id="toggle-hideShortsInSearch" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Comments -->
<li class="toggle-item" id="item-hideComments">
<div class="toggle-info">
<span class="toggle-icon">
<!-- Comment icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/>
</svg>
</span>
<div>
<span class="toggle-label">Hide Comments</span>
<span class="toggle-desc">Collapses the comments section on videos</span>
<!-- Hide Homepage Feed -->
<li class="toggle-item" id="item-hideHomepageFeed">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Homepage Feed</span>
<span class="toggle-desc">Removes the main video feed from home</span>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Homepage Feed">
<input type="checkbox" id="toggle-hideHomepageFeed" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Sidebar -->
<li class="toggle-item" id="item-hideSidebar">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 16H5V5h9v14zm5 0h-3V5h3v14z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Sidebar</span>
<span class="toggle-desc">Hides the watch page recommended videos</span>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Sidebar">
<input type="checkbox" id="toggle-hideSidebar" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Comments -->
<li class="toggle-item" id="item-hideComments">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Comments</span>
<span class="toggle-desc">Collapses the comments section on video pages</span>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Comments">
<input type="checkbox" id="toggle-hideComments" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Endcards -->
<li class="toggle-item" id="item-hideEndcards">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.81 9l-3.5-3.5L9.28 10l2.91 2.91L9.28 15.82l1.41 1.41 3.5-3.5a.996.996 0 0 0 0-1.41z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Endcards</span>
<span class="toggle-desc">Hides overlay video cards at the end of videos</span>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Endcards">
<input type="checkbox" id="toggle-hideEndcards" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Merch -->
<li class="toggle-item" id="item-hideMerch">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M17.21 9l-4.38-6.56c-.19-.28-.51-.44-.83-.44-.32 0-.64.16-.83.44L6.79 9H2v2h2v9c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-9h2V9h-4.79zM9 9l3-4.5L15 9H9zm9 11H6v-9h12v9z"/><circle cx="9" cy="14" r="1.5"/><circle cx="15" cy="14" r="1.5"/></svg>
</span>
<div>
<span class="toggle-label">Hide Merch Shelves</span>
<span class="toggle-desc">Hides YouTube merchandise and shop shelves</span>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Merch">
<input type="checkbox" id="toggle-hideMerch" />
<span class="slider"></span>
</label>
</li>
<!-- Hide Notification Bell -->
<li class="toggle-item" id="item-hideNotificationBell">
<div class="toggle-info">
<span class="toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>
</span>
<div>
<span class="toggle-label">Hide Notification Bell</span>
<span class="toggle-desc">Hides subscribe bell indicators on channel pages</span>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Notification Bell">
<input type="checkbox" id="toggle-hideNotificationBell" />
<span class="slider"></span>
</label>
</li>
</ul>
<!-- ── Divider ────────────────────────────── -->
<div class="main-divider"></div>
<!-- ── Session Limit Dropdown ─────────────── -->
<div class="control-group">
<label for="watch-limit" class="section-label">DAILY WATCH LIMIT</label>
<div class="select-wrapper">
<select id="watch-limit">
<option value="0">No limit</option>
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="45">45 minutes</option>
<option value="60">60 minutes</option>
<option value="90">90 minutes</option>
</select>
<div class="select-arrow">
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1l4 4 4-4"/></svg>
</div>
</div>
<label class="switch" aria-label="Toggle Hide Comments">
<input type="checkbox" id="toggle-hideComments" />
<span class="slider"></span>
</label>
</li>
<div id="timer-status" class="timer-status">Tracked watch time: 0m</div>
</div>
</ul>
</main>
</main>
</div>
<!-- ── Divider ────────────────────────────── -->

@@ -112,3 +212,3 @@ <div class="divider"></div>

<footer class="popup-footer">
<span class="footer-tip">💡 Changes apply instantly on YouTube</span>
<span class="footer-tip">Changes apply instantly on YouTube</span>
<button id="btn-reset" class="btn-reset" title="Reset all toggles to defaults">Reset</button>

@@ -119,2 +219,2 @@ </footer>

</body>
</html>
</html>
+112
-36
/* ============================================================
YouTube Focus Mode — Popup Script
Master toggle enables/disables the extension entirely.
Individual toggles control each feature independently.
Manages tactile switch state binds, daily watch limits,
whitelists rendering, and advanced Custom CSS live tests.
============================================================ */

@@ -10,3 +10,12 @@

const FEATURE_KEYS = ['hideShorts', 'hideRecommendations', 'hideComments'];
const FEATURE_KEYS = [
'hideShorts',
'hideShortsInSearch',
'hideHomepageFeed',
'hideSidebar',
'hideComments',
'hideEndcards',
'hideMerch',
'hideNotificationBell'
];

@@ -16,12 +25,22 @@ const DEFAULTS = {

hideShorts: true,
hideRecommendations: true,
hideComments: true
hideShortsInSearch: true,
hideHomepageFeed: false,
hideComments: false,
hideSidebar: true,
hideEndcards: false,
hideMerch: false,
hideNotificationBell: false,
watchLimitMinutes: 0
};
// ── DOM refs ──────────────────────────────────────────────
const masterCheckbox = document.getElementById('master-enabled');
const masterLabel = document.getElementById('master-label');
const toggleList = document.querySelector('.toggle-list');
const btnReset = document.getElementById('btn-reset');
// ── DOM Element Selectors ──────────────────────────────────
const masterCheckbox = document.getElementById('master-enabled');
const masterLabel = document.getElementById('master-label');
const scrollContainer = document.querySelector('.popup-scroll-container');
const btnReset = document.getElementById('btn-reset');
// Watch limit elements
const selectLimit = document.getElementById('watch-limit');
const timerStatus = document.getElementById('timer-status');
const toggleEls = {};

@@ -35,3 +54,6 @@ const itemEls = {};

// ── UI helpers ────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// ── UI State Update Handlers ───────────────────────────────
// ───────────────────────────────────────────────────────────
function setMasterUI(enabled) {

@@ -41,3 +63,3 @@ masterCheckbox.checked = enabled;

masterLabel.classList.toggle('is-off', !enabled);
toggleList.classList.toggle('toggles-disabled', !enabled);
scrollContainer.classList.toggle('toggles-disabled', !enabled);
}

@@ -48,5 +70,11 @@

const val = !!settings[key];
toggleEls[key].checked = val;
itemEls[key].classList.toggle('active', val);
if (toggleEls[key]) toggleEls[key].checked = val;
if (itemEls[key]) itemEls[key].classList.toggle('active', val);
});
// Set watch limit dropdown
if (selectLimit) {
selectLimit.value = settings.watchLimitMinutes !== undefined ? settings.watchLimitMinutes : 0;
}
}

@@ -57,24 +85,49 @@

setFeatureUI(settings);
updateWatchTimeStatus(settings.watchLimitMinutes || 0);
}
// ── Load saved settings on open ───────────────────────────
chrome.storage.sync.get(DEFAULTS, applyFullUI);
// ───────────────────────────────────────────────────────────
// ── Watch Time Limits Tracker ──────────────────────────────
// ───────────────────────────────────────────────────────────
// ── Master toggle ─────────────────────────────────────────
function updateWatchTimeStatus(limitMinutes) {
browser.storage.local.get({ secondsWatchedToday: 0 }, (localData) => {
const minutesToday = Math.floor(localData.secondsWatchedToday / 60);
if (limitMinutes > 0) {
timerStatus.textContent = `Active today: ${minutesToday}m / ${limitMinutes}m limit`;
timerStatus.style.color = minutesToday >= limitMinutes ? '#ff4e4e' : '#9090a8';
} else {
timerStatus.textContent = `Active today: ${minutesToday}m (No limit)`;
timerStatus.style.color = '#9090a8';
}
});
}
// ───────────────────────────────────────────────────────────
// ── Event Listener Binds ───────────────────────────────────
// ───────────────────────────────────────────────────────────
// Load Initial Settings
browser.storage.sync.get(DEFAULTS, applyFullUI);
// Master switch listener
masterCheckbox.addEventListener('change', () => {
const enabled = masterCheckbox.checked;
setMasterUI(enabled);
chrome.storage.sync.set({ masterEnabled: enabled });
browser.storage.sync.set({ masterEnabled: enabled });
});
// ── Individual feature toggles ────────────────────────────
// Feature switch binders
FEATURE_KEYS.forEach(key => {
// Clicking the row also fires the toggle
itemEls[key].addEventListener('click', (e) => {
if (e.target.closest('.switch')) return;
toggleEls[key].checked = !toggleEls[key].checked;
handleFeatureToggle(key);
});
if (itemEls[key] && toggleEls[key]) {
itemEls[key].addEventListener('click', (e) => {
if (e.target.closest('.switch')) return;
toggleEls[key].checked = !toggleEls[key].checked;
handleFeatureToggle(key);
});
toggleEls[key].addEventListener('change', () => handleFeatureToggle(key));
toggleEls[key].addEventListener('change', () => handleFeatureToggle(key));
}
});

@@ -84,15 +137,33 @@

const val = toggleEls[key].checked;
itemEls[key].classList.toggle('active', val);
chrome.storage.sync.set({ [key]: val });
if (itemEls[key]) itemEls[key].classList.toggle('active', val);
console.log(`Saving ${key}:`, val);
browser.storage.sync.set({ [key]: val });
}
// ── Reset button ──────────────────────────────────────────
// Watch limit dropdown change listener
if (selectLimit) {
selectLimit.addEventListener('change', () => {
const limitVal = parseInt(selectLimit.value);
browser.storage.sync.set({ watchLimitMinutes: limitVal }, () => {
updateWatchTimeStatus(limitVal);
});
});
}
// Reset all settings button listener
btnReset.addEventListener('click', () => {
chrome.storage.sync.set(DEFAULTS, () => {
browser.storage.sync.set(DEFAULTS, () => {
applyFullUI(DEFAULTS);
// Reset local counter as well for a completely clean start
browser.storage.local.set({ secondsWatchedToday: 0, snoozedToday: false }, () => {
updateWatchTimeStatus(0);
});
btnReset.textContent = '✓ Done';
btnReset.style.color = '#34d399';
btnReset.style.borderColor = 'rgba(52, 211, 153, 0.4)';
setTimeout(() => {
btnReset.textContent = 'Reset';
btnReset.style.color = '';
btnReset.style.borderColor = '';
}, 1200);

@@ -102,8 +173,13 @@ });

// ── Live-sync if storage changes from another context ─────
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') return;
chrome.storage.sync.get(DEFAULTS, applyFullUI);
// Dynamic storage listener
browser.storage.onChanged.addListener((changes, area) => {
if (area === 'sync') {
browser.storage.sync.get(DEFAULTS, applyFullUI);
} else if (area === 'local') {
browser.storage.sync.get({ watchLimitMinutes: 0 }, (settings) => {
updateWatchTimeStatus(settings.watchLimitMinutes || 0);
});
}
});
})();
})();
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<rect width="16" height="16" rx="4" fill="#1a1a25"/>
<rect x="1" y="1" width="14" height="14" rx="3.5" stroke="#ff4e4e" stroke-width="0.75" fill="none"/>
<!-- Eye with focus ring -->
<ellipse cx="8" cy="8" r="3.5" fill="#ff4e4e" opacity="0.15"/>
<circle cx="8" cy="8" r="2" fill="#ff4e4e"/>
<circle cx="8" cy="8" r="0.8" fill="#1a1a25"/>
<!-- Corner brackets -->
<path d="M3 5V3h2" stroke="#ff4e4e" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 3h2v2" stroke="#ff4e4e" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 11v2h-2" stroke="#ff4e4e" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H3v-2" stroke="#ff4e4e" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#1a1a25"/>
<rect x="1.5" y="1.5" width="29" height="29" rx="6.5" stroke="#ff4e4e" stroke-width="1" fill="none" opacity="0.4"/>
<!-- Glow circle -->
<circle cx="16" cy="16" r="10" fill="#ff4e4e" opacity="0.07"/>
<!-- Focus brackets -->
<path d="M6 11V7h4" stroke="#ff4e4e" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 7h4v4" stroke="#ff4e4e" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 21v4h-4" stroke="#ff4e4e" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 25H6v-4" stroke="#ff4e4e" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Eye shape -->
<path d="M9 16c1.8-3.5 4-5 7-5s5.2 1.5 7 5c-1.8 3.5-4 5-7 5s-5.2-1.5-7-5z"
fill="#ff4e4e" opacity="0.2" stroke="#ff4e4e" stroke-width="1.2"/>
<circle cx="16" cy="16" r="3" fill="#ff4e4e"/>
<circle cx="16" cy="16" r="1.2" fill="#1a1a25"/>
<circle cx="17.2" cy="15" r="0.6" fill="white" opacity="0.6"/>
</svg>

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet