mirror of
https://github.com/lawrencehook/remove-youtube-suggestions.git
synced 2026-07-25 06:54:31 +00:00
version 4.3.79
This commit is contained in:
@@ -8,11 +8,14 @@ if (typeof browser === 'undefined') {
|
||||
const uninstallUrl = "http://lawrencehook.com/rys/👋";
|
||||
browser.runtime.setUninstallURL(uninstallUrl);
|
||||
|
||||
browser.runtime.onInstalled.addListener(object => {
|
||||
browser.runtime.onInstalled.addListener(async object => {
|
||||
const url = "http://lawrencehook.com/rys/welcome";
|
||||
if (object.reason === browser.runtime.OnInstalledReason.INSTALL) {
|
||||
browser.tabs.create({ url }, tab => {});
|
||||
}
|
||||
if (object.reason !== browser.runtime.OnInstalledReason.INSTALL) return;
|
||||
// management.getSelf() returns a Promise in both Chrome MV3 and Firefox
|
||||
// (Firefox ignores callbacks), so use await for cross-browser consistency.
|
||||
const info = await browser.management.getSelf();
|
||||
if (info && info.installType === 'development') return; // skip on unpacked/dev loads
|
||||
browser.tabs.create({ url });
|
||||
});
|
||||
|
||||
// Change the browserAction icon if the extension is disabled
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Spend less time on YouTube. Customize YouTube's user interface to be less engaging.",
|
||||
"homepage_url": "https://github.com/lawrencehook/remove-youtube-suggestions",
|
||||
"manifest_version": 3,
|
||||
"version": "4.3.78",
|
||||
"version": "4.3.79",
|
||||
|
||||
"icons": {
|
||||
"16": "/images/16.png",
|
||||
@@ -20,6 +20,7 @@
|
||||
{
|
||||
"js": [
|
||||
"/shared/main.js",
|
||||
"/shared/config.js",
|
||||
"/shared/https.js",
|
||||
"/shared/utils.js",
|
||||
"/shared/banners.js",
|
||||
|
||||
+29
-18
@@ -53,11 +53,6 @@ let lastRedirect;
|
||||
const redirectInterval = 1_000; // 1 second
|
||||
|
||||
|
||||
// Get list of premium feature IDs
|
||||
const PREMIUM_FEATURE_IDS = SECTIONS.flatMap(section =>
|
||||
section.options.filter(opt => opt.premium).map(opt => opt.id)
|
||||
);
|
||||
|
||||
// Decode license token to check premium status (no signature verification - we trust our server)
|
||||
function decodeLicenseToken(token) {
|
||||
if (!token) return null;
|
||||
@@ -77,21 +72,35 @@ function isPremiumFromToken(token) {
|
||||
return decoded.premium === true && decoded.exp > now;
|
||||
}
|
||||
|
||||
function getTier(licenseToken, sessionToken) {
|
||||
if (isPremiumFromToken(licenseToken)) return TIER.PREMIUM;
|
||||
if (sessionToken) return TIER.FREE_SIGNED_IN;
|
||||
return TIER.FREE;
|
||||
}
|
||||
|
||||
// Respond to changes in settings
|
||||
function logStorageChange(changes, area) {
|
||||
if (area !== 'local') return;
|
||||
|
||||
// Update premium status in cache if license token changed
|
||||
if ('license_token' in changes) {
|
||||
cache['_isPremium'] = isPremiumFromToken(changes['license_token'].newValue);
|
||||
if ('license_token' in changes) cache['_licenseToken'] = changes['license_token'].newValue;
|
||||
if ('session_token' in changes) cache['_sessionToken'] = changes['session_token'].newValue;
|
||||
if ('license_token' in changes || 'session_token' in changes) {
|
||||
cache['_tier'] = getTier(cache['_licenseToken'], cache['_sessionToken']);
|
||||
}
|
||||
|
||||
Object.entries(changes).forEach(([id, { oldValue, newValue }]) => {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
// Enforce premium features are false for non-premium users
|
||||
if (PREMIUM_FEATURE_IDS.includes(id) && cache['_isPremium'] !== true) {
|
||||
newValue = false;
|
||||
if (PREMIUM_FEATURE_ID_SET.has(id)) {
|
||||
const tier = cache['_tier'];
|
||||
if (tier === TIER.FREE) {
|
||||
newValue = false;
|
||||
} else if (tier === TIER.FREE_SIGNED_IN && newValue === true) {
|
||||
const alreadyOn = cache[id] === true;
|
||||
if (!alreadyOn && countActivePremium(cache) >= PREMIUM_CONFIG.FREE_PREMIUM_SLOTS) {
|
||||
newValue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HTML.setAttribute(id, newValue);
|
||||
@@ -113,13 +122,15 @@ browser.storage.local.get(settings => {
|
||||
browser.storage.local.set(revealUpdates);
|
||||
}
|
||||
|
||||
// Enforce premium features are false for non-premium users
|
||||
const isPremium = isPremiumFromToken(settings['license_token']);
|
||||
cache['_isPremium'] = isPremium;
|
||||
if (!isPremium) {
|
||||
PREMIUM_FEATURE_IDS.forEach(id => {
|
||||
settings[id] = false;
|
||||
});
|
||||
const tier = getTier(settings['license_token'], settings['session_token']);
|
||||
cache['_tier'] = tier;
|
||||
cache['_licenseToken'] = settings['license_token'];
|
||||
cache['_sessionToken'] = settings['session_token'];
|
||||
if (tier === TIER.FREE) {
|
||||
clearAllPremium(settings);
|
||||
} else if (tier === TIER.FREE_SIGNED_IN) {
|
||||
const writeBack = enforceSlotBudget(settings, PREMIUM_CONFIG.FREE_PREMIUM_SLOTS);
|
||||
if (Object.keys(writeBack).length) browser.storage.local.set(writeBack);
|
||||
}
|
||||
|
||||
Object.entries({ ...DEFAULT_SETTINGS, ...settings}).forEach(([ id, value ]) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Spend less time on YouTube. Customize YouTube's user interface to be less engaging.",
|
||||
"homepage_url": "https://github.com/lawrencehook/remove-youtube-suggestions",
|
||||
"manifest_version": 2,
|
||||
"version": "4.3.78",
|
||||
"version": "4.3.79",
|
||||
|
||||
"icons": {
|
||||
"16": "/images/16.png",
|
||||
@@ -23,6 +23,7 @@
|
||||
{
|
||||
"js": [
|
||||
"/shared/main.js",
|
||||
"/shared/config.js",
|
||||
"/shared/https.js",
|
||||
"/shared/utils.js",
|
||||
"/shared/banners.js",
|
||||
|
||||
+86
-35
@@ -318,7 +318,21 @@ html[foo=bar] {
|
||||
}
|
||||
|
||||
#password_container_background button {
|
||||
color: var(--body-color);
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
background-color: var(--blue-color);
|
||||
color: white;
|
||||
}
|
||||
#password_container_background button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
#password_container_background button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#enable_password_container, #enable_password_container * {
|
||||
@@ -381,10 +395,6 @@ html[foo=bar] {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
#password-button button {
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
||||
#disable_password_container a:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -592,6 +602,23 @@ html[dark_mode='true'] #active_sidebar .active_count {
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
#current_selection_indicator {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: flex-end;
|
||||
margin-bottom: -28px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--label-color);
|
||||
opacity: 0.75;
|
||||
padding: 4px 10px;
|
||||
background-color: var(--body-background-color);
|
||||
border-left: 1px solid var(--box-shadow-color);
|
||||
border-bottom: 1px solid var(--box-shadow-color);
|
||||
border-bottom-left-radius: 6px;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
|
||||
/*************************************
|
||||
* SIGN-IN / ACCOUNT / UPGRADE MODALS
|
||||
@@ -926,6 +953,7 @@ html[dark_mode='true'] #active_sidebar .active_count {
|
||||
/* Upgrade Modal */
|
||||
#upgrade_container {
|
||||
width: auto;
|
||||
max-width: min(90vw, 380px);
|
||||
padding: 24px;
|
||||
gap: 18px;
|
||||
align-items: flex-start;
|
||||
@@ -940,8 +968,29 @@ html[dark_mode='true'] #active_sidebar .active_count {
|
||||
}
|
||||
|
||||
#premium_required_description {
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#upgrade_slot_limit_note {
|
||||
color: var(--body-color);
|
||||
text-align: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.premium-note-main,
|
||||
.premium-note-sub {
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.premium-note-main {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.premium-note-sub {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.55;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#upgrade_plans {
|
||||
@@ -1026,53 +1075,55 @@ html[dark_mode='true'] .upgrade-plan[selected] {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
/* Premium lock icon for options */
|
||||
html:not([is_premium='true']) .option[data-premium='true'] {
|
||||
opacity: 0.25;
|
||||
position: relative;
|
||||
/* Premium options: dim slightly for non-premium users, full opacity for premium */
|
||||
html:not([tier='premium']) .option[data-premium='true'] {
|
||||
opacity: 0.65;
|
||||
}
|
||||
html[tier='premium'] .option[data-premium='true'],
|
||||
#active_section .option[data-premium='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html:not([is_premium='true']) .option[data-premium='true']::after {
|
||||
/* Blue dot at the right edge of premium options (only shown for non-premium users). */
|
||||
.option[data-premium='true'] {
|
||||
position: relative;
|
||||
}
|
||||
html:not([tier='premium']) .option[data-premium='true']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--blue-color, #4a6cf7);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Premium options unlocked */
|
||||
html[is_premium='true'] .option[data-premium='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Premium lock icon for settings menu items */
|
||||
html:not([is_premium='true']) #settings-menu div[data-premium='true'],
|
||||
html:not([is_premium='true']) #power-options div[data-premium='true'] {
|
||||
opacity: 0.25;
|
||||
/* Premium marker for settings-menu / power-options items (same dot as main list) */
|
||||
html:not([tier='premium']) #settings-menu div[data-premium='true'],
|
||||
html:not([tier='premium']) #power-options div[data-premium='true'] {
|
||||
opacity: 0.65;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html:not([is_premium='true']) #settings-menu div[data-premium='true']::after,
|
||||
html:not([is_premium='true']) #power-options div[data-premium='true']::after {
|
||||
html:not([tier='premium']) #settings-menu div[data-premium='true']::after,
|
||||
html:not([tier='premium']) #power-options div[data-premium='true']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--blue-color, #4a6cf7);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
html[is_premium='true'] #settings-menu div[data-premium='true'],
|
||||
html[is_premium='true'] #power-options div[data-premium='true'] {
|
||||
html[tier='premium'] #settings-menu div[data-premium='true'],
|
||||
html[tier='premium'] #power-options div[data-premium='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,23 @@ html[dark_mode='false'] #header-light { display: none }
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
#header-slot-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
#header-slot-indicator .slot-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--label-color);
|
||||
opacity: 0.25;
|
||||
}
|
||||
#header-slot-indicator .slot-dot[filled] {
|
||||
background-color: var(--blue-color, #4a6cf7);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#search_bar {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
+10
-1
@@ -190,7 +190,10 @@
|
||||
<span id="premium_required_header_premium">Premium</span>
|
||||
</div>
|
||||
</div>
|
||||
<p id="premium_required_description">Premium required. Sign in to upgrade.</p>
|
||||
<p id="premium_required_description">
|
||||
<span class="premium-note-main">Sign in to enable any 2 premium features.</span>
|
||||
<span class="premium-note-sub">Upgrade to unlock them all.</span>
|
||||
</p>
|
||||
<div id="premium_required_buttons">
|
||||
<button id="premium-required-cancel-button">Cancel</button>
|
||||
<button id="premium-required-signin-button">Sign In</button>
|
||||
@@ -208,6 +211,10 @@
|
||||
</div>
|
||||
<span id="upgrade_header_action">Upgrade</span>
|
||||
</div>
|
||||
<div id="upgrade_slot_limit_note" hidden>
|
||||
<span class="premium-note-main">Free tier allows 2 premium features.</span>
|
||||
<span class="premium-note-sub">Upgrade or deselect one.</span>
|
||||
</div>
|
||||
<div id="upgrade_plans">
|
||||
<div class="upgrade-plan" data-plan="monthly">
|
||||
<div class="plan-name">Monthly</div>
|
||||
@@ -292,6 +299,8 @@
|
||||
<input type=search placeholder='Search' autofocus tabindex="0"></input>
|
||||
</div>
|
||||
|
||||
<div id='header-slot-indicator' hidden title='Free premium features used'></div>
|
||||
|
||||
<a title='On/Off' id="power-icon">
|
||||
<div id='power-pop-up'>Button is disabled because the schedule is active.</div>
|
||||
<svg id="header-toggle" class='global_enable header-toggle' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||
|
||||
+88
-45
@@ -24,14 +24,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
const settings = { ...DEFAULT_SETTINGS, ...localSettings };
|
||||
|
||||
// Enforce premium features are disabled for non-premium users
|
||||
const isPremium = License.isPremiumSync(localSettings['license_token']);
|
||||
if (!isPremium) {
|
||||
SECTIONS.forEach(section => {
|
||||
section.options.forEach(opt => {
|
||||
if (opt.premium) settings[opt.id] = false;
|
||||
});
|
||||
});
|
||||
const tier = License.getTierSync(localSettings['license_token'], localSettings['session_token']);
|
||||
HTML.setAttribute('tier', tier);
|
||||
if (tier === TIER.FREE) {
|
||||
clearAllPremium(settings);
|
||||
} else if (tier === TIER.FREE_SIGNED_IN) {
|
||||
const writeBack = enforceSlotBudget(settings, PREMIUM_CONFIG.FREE_PREMIUM_SLOTS);
|
||||
if (Object.keys(writeBack).length) browser.storage.local.set(writeBack);
|
||||
}
|
||||
|
||||
const headerSettings = Object.entries(OTHER_SETTINGS).reduce((acc, [id, value]) => {
|
||||
@@ -88,28 +87,13 @@ function refreshActiveSection() {
|
||||
optionNode.classList.remove('removed');
|
||||
optionNode.removeAttribute('id');
|
||||
optionNode.setAttribute('name', name);
|
||||
if (premium) optionNode.setAttribute('data-premium', 'true');
|
||||
optionNode.querySelector('.option_label').innerText = name;
|
||||
|
||||
const svg = optionNode.querySelector('svg');
|
||||
svg.toggleAttribute('active', true);
|
||||
|
||||
optionNode.addEventListener('click', async e => {
|
||||
if (premium && HTML.getAttribute('is_premium') !== 'true') {
|
||||
const signedIn = await Auth.isSignedIn();
|
||||
if (!signedIn) {
|
||||
if (typeof openPremiumRequiredModal === 'function') {
|
||||
openPremiumRequiredModal();
|
||||
} else {
|
||||
displayStatus('Premium feature - sign in to upgrade');
|
||||
}
|
||||
} else if (typeof openUpgradeModal === 'function') {
|
||||
openUpgradeModal();
|
||||
} else {
|
||||
displayStatus('Premium feature - upgrade to access');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
updateSetting(id, false, { manual: true });
|
||||
// Sync the original toggle
|
||||
const originalSvg = document.querySelector(`div#${id} svg`);
|
||||
@@ -144,6 +128,10 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) {
|
||||
SIDEBAR.innerHTML = '';
|
||||
let allTags = [];
|
||||
|
||||
const selectionIndicator = document.createElement('div');
|
||||
selectionIndicator.id = 'current_selection_indicator';
|
||||
OPTIONS_LIST.append(selectionIndicator);
|
||||
|
||||
// Create the Active Options section (hidden until sidebar selected)
|
||||
const activeSection = TEMPLATE_SECTION.cloneNode(true);
|
||||
activeSection.id = 'active_section';
|
||||
@@ -191,24 +179,16 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) {
|
||||
svg.toggleAttribute('active', value);
|
||||
|
||||
optionNode.addEventListener('click', async e => {
|
||||
// Check if premium-locked
|
||||
if (premium && HTML.getAttribute('is_premium') !== 'true') {
|
||||
// Check if signed in first
|
||||
const signedIn = await Auth.isSignedIn();
|
||||
if (!signedIn) {
|
||||
// Not signed in - show premium required modal
|
||||
if (typeof openPremiumRequiredModal === 'function') {
|
||||
openPremiumRequiredModal();
|
||||
} else {
|
||||
displayStatus('Premium feature - sign in to upgrade');
|
||||
}
|
||||
} else if (typeof openUpgradeModal === 'function') {
|
||||
// Signed in but not premium - open upgrade modal
|
||||
openUpgradeModal();
|
||||
} else {
|
||||
displayStatus('Premium feature - upgrade to access');
|
||||
const tier = HTML.getAttribute('tier');
|
||||
if (premium && tier !== TIER.PREMIUM) {
|
||||
const togglingOn = cache[id] !== true;
|
||||
const hasSlot = tier === TIER.FREE_SIGNED_IN &&
|
||||
countActivePremium(cache) < PREMIUM_CONFIG.FREE_PREMIUM_SLOTS;
|
||||
if (togglingOn && !hasSlot) {
|
||||
if (tier === TIER.FREE_SIGNED_IN) openUpgradeModal({ reason: 'slot_limit' });
|
||||
else openPremiumRequiredModal();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const value = svg.toggleAttribute('active');
|
||||
@@ -229,10 +209,16 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) {
|
||||
OPTIONS_LIST.append(sectionNode);
|
||||
});
|
||||
|
||||
// Add "All" and "Active" sidebar items at the top
|
||||
// Add "Free", "All", and "Active" sidebar items at the top
|
||||
const sidebarTopRow = document.createElement('div');
|
||||
sidebarTopRow.id = 'sidebar_top_row';
|
||||
|
||||
const freeSidebar = document.createElement('div');
|
||||
freeSidebar.id = 'free_sidebar';
|
||||
freeSidebar.className = 'sidebar_section';
|
||||
freeSidebar.innerText = 'free';
|
||||
freeSidebar.addEventListener('click', freeSidebarListener);
|
||||
|
||||
const allSidebar = document.createElement('div');
|
||||
allSidebar.id = 'all_sidebar';
|
||||
allSidebar.className = 'sidebar_section';
|
||||
@@ -245,7 +231,7 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) {
|
||||
activeSidebar.innerHTML = '<span>active</span><span class="active_count"></span>';
|
||||
activeSidebar.addEventListener('click', activeSidebarListener);
|
||||
|
||||
sidebarTopRow.append(allSidebar, activeSidebar);
|
||||
sidebarTopRow.append(freeSidebar, allSidebar, activeSidebar);
|
||||
SIDEBAR.append(sidebarTopRow);
|
||||
|
||||
// Add sections to the sidebar
|
||||
@@ -276,8 +262,11 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) {
|
||||
});
|
||||
}
|
||||
|
||||
// Pre-select sidebar option based on current window. Default to 'Basic'
|
||||
if (resultsPageRegex.test(currentUrl)) {
|
||||
// Pre-select sidebar. Non-premium users default to "free"; others use URL-based section.
|
||||
const tier = License.getTierSync(SETTING_VALUES['license_token'], SETTING_VALUES['session_token']);
|
||||
if (tier !== TIER.PREMIUM) {
|
||||
qs('#free_sidebar').click();
|
||||
} else if (resultsPageRegex.test(currentUrl)) {
|
||||
qs('.sidebar_section[tag="Search"]').click();
|
||||
} else if (videoPageRegex.test(currentUrl)) {
|
||||
qs('.sidebar_section[tag="Video Player"]').click();
|
||||
@@ -291,6 +280,8 @@ function populateOptions(SECTIONS, headerSettings, SETTING_VALUES) {
|
||||
qs('.sidebar_section[tag="Basic"]').click();
|
||||
}
|
||||
|
||||
updateSlotIndicator();
|
||||
|
||||
const searchBar = document.getElementById('search_bar');
|
||||
searchBar.addEventListener('input', onSearchInput);
|
||||
const searchInput = searchBar.querySelector('input');
|
||||
@@ -388,6 +379,18 @@ function updateTimeInfo() {
|
||||
|
||||
function updateSetting(id, value, { write=true, manual=false }={}) {
|
||||
|
||||
// Clamp premium writes that would exceed the free-tier slot budget, so chained
|
||||
// effects behave the same as direct toggles.
|
||||
if (PREMIUM_FEATURE_ID_SET.has(id) && value === true) {
|
||||
const tier = HTML.getAttribute('tier');
|
||||
if (tier === TIER.FREE) {
|
||||
value = false;
|
||||
} else if (tier === TIER.FREE_SIGNED_IN && cache[id] !== true &&
|
||||
countActivePremium(cache) >= PREMIUM_CONFIG.FREE_PREMIUM_SLOTS) {
|
||||
value = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (manual) recordEvent('Setting changed', { id, value });
|
||||
|
||||
HTML.setAttribute(id, value);
|
||||
@@ -414,6 +417,8 @@ function updateSetting(id, value, { write=true, manual=false }={}) {
|
||||
}
|
||||
|
||||
refreshActiveSection();
|
||||
|
||||
if (PREMIUM_FEATURE_ID_SET.has(id)) updateSlotIndicator();
|
||||
}
|
||||
|
||||
|
||||
@@ -426,6 +431,7 @@ function onSearchInput(e) {
|
||||
// Deselect "all" during active search
|
||||
const allSidebar = document.getElementById('all_sidebar');
|
||||
if (allSidebar) allSidebar.removeAttribute('selected');
|
||||
setSelectionLabel(`"${value}"`);
|
||||
|
||||
const sections = qsa('.section_container:not(#template_section):not(#active_section)');
|
||||
const searchTerms = value.toLowerCase().split(' ');
|
||||
@@ -457,6 +463,12 @@ function onSearchInput(e) {
|
||||
}
|
||||
|
||||
|
||||
function setSelectionLabel(text) {
|
||||
const el = document.getElementById('current_selection_indicator');
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
|
||||
function showAll() {
|
||||
const sidebarSections = qsa('.sidebar_section');
|
||||
const sections = qsa('.section_container:not(#template_section):not(#active_section)');
|
||||
@@ -470,6 +482,35 @@ function showAll() {
|
||||
section.querySelectorAll('div.option').forEach(opt => opt.classList.remove('removed'));
|
||||
});
|
||||
if (activeSection) activeSection.classList.add('removed');
|
||||
setSelectionLabel('all');
|
||||
}
|
||||
|
||||
|
||||
function freeSidebarListener(e) {
|
||||
const target = e.currentTarget;
|
||||
qsa('.sidebar_section').forEach(s => {
|
||||
if (s !== target) s.removeAttribute('selected');
|
||||
});
|
||||
target.setAttribute('selected', '');
|
||||
|
||||
const activeSection = document.getElementById('active_section');
|
||||
if (activeSection) activeSection.classList.add('removed');
|
||||
|
||||
const sections = qsa('.section_container:not(#template_section):not(#active_section)');
|
||||
sections.forEach(section => {
|
||||
section.classList.remove('removed');
|
||||
let hasFreeOption = false;
|
||||
section.querySelectorAll('div.option').forEach(opt => {
|
||||
if (opt.getAttribute('data-premium') === 'true') {
|
||||
opt.classList.add('removed');
|
||||
} else {
|
||||
opt.classList.remove('removed');
|
||||
hasFreeOption = true;
|
||||
}
|
||||
});
|
||||
if (!hasFreeOption) section.classList.add('removed');
|
||||
});
|
||||
setSelectionLabel('free');
|
||||
}
|
||||
|
||||
|
||||
@@ -484,6 +525,7 @@ function activeSidebarListener(e) {
|
||||
});
|
||||
qsa('.section_container:not(#template_section):not(#active_section)').forEach(s => s.classList.add('removed'));
|
||||
refreshActiveSection();
|
||||
setSelectionLabel('active');
|
||||
} else {
|
||||
showAll();
|
||||
}
|
||||
@@ -526,6 +568,7 @@ function sidebarSectionListener(e) {
|
||||
section.classList.add('removed');
|
||||
}
|
||||
});
|
||||
setSelectionLabel(tag);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -82,18 +82,12 @@ function openScheduleModal() {
|
||||
updateDays(scheduleDays);
|
||||
});
|
||||
}
|
||||
SCHEDULING_OPTION.addEventListener('click', async e => {
|
||||
if (HTML.getAttribute('is_premium') !== 'true') {
|
||||
await handlePremiumFeatureClick();
|
||||
return;
|
||||
}
|
||||
SCHEDULING_OPTION.addEventListener('click', e => {
|
||||
if (!canUsePremiumFeature('schedule')) return;
|
||||
openScheduleModal();
|
||||
});
|
||||
OPEN_SCHEDULE_OPTION.addEventListener('click', async e => {
|
||||
if (HTML.getAttribute('is_premium') !== 'true') {
|
||||
await handlePremiumFeatureClick();
|
||||
return;
|
||||
}
|
||||
OPEN_SCHEDULE_OPTION.addEventListener('click', e => {
|
||||
if (!canUsePremiumFeature('schedule')) return;
|
||||
openScheduleModal();
|
||||
});
|
||||
RESUME_SCHEDULE_OPTION.addEventListener('click', e => {
|
||||
@@ -192,11 +186,8 @@ function openPasswordModal() {
|
||||
DISABLE_PASSWORD_CONTAINER.toggleAttribute('hidden', !password);
|
||||
});
|
||||
}
|
||||
PASSWORD_OPTION.addEventListener('click', async e => {
|
||||
if (HTML.getAttribute('is_premium') !== 'true') {
|
||||
await handlePremiumFeatureClick();
|
||||
return;
|
||||
}
|
||||
PASSWORD_OPTION.addEventListener('click', e => {
|
||||
if (!canUsePremiumFeature('password')) return;
|
||||
openPasswordModal();
|
||||
});
|
||||
|
||||
@@ -396,6 +387,28 @@ const ACCOUNT_OPTION = qs('#settings-account');
|
||||
const DONATE_LINK = qs('#settings-donate');
|
||||
const DONATE_URL = 'https://www.paypal.com/donate/?cmd=_donations&business=FF9K9YD6K6SWG&Z3JncnB0=';
|
||||
const HEADER_PREMIUM_BADGE = qs('#header-premium-badge');
|
||||
const HEADER_SLOT_INDICATOR = qs('#header-slot-indicator');
|
||||
if (HEADER_SLOT_INDICATOR) {
|
||||
for (let i = 0; i < PREMIUM_CONFIG.FREE_PREMIUM_SLOTS; i++) {
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'slot-dot';
|
||||
HEADER_SLOT_INDICATOR.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSlotIndicator() {
|
||||
if (!HEADER_SLOT_INDICATOR) return;
|
||||
if (HTML.getAttribute('tier') !== TIER.FREE_SIGNED_IN) {
|
||||
HEADER_SLOT_INDICATOR.setAttribute('hidden', '');
|
||||
return;
|
||||
}
|
||||
const used = countActivePremium(cache);
|
||||
HEADER_SLOT_INDICATOR.querySelectorAll('.slot-dot').forEach((dot, i) => {
|
||||
dot.toggleAttribute('filled', i < used);
|
||||
});
|
||||
HEADER_SLOT_INDICATOR.title = `${used}/${PREMIUM_CONFIG.FREE_PREMIUM_SLOTS} free premium features used`;
|
||||
HEADER_SLOT_INDICATOR.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
// Sign-in modal elements
|
||||
const signinModalContainer = qs('#signin_container_background');
|
||||
@@ -475,10 +488,15 @@ async function initAccountState() {
|
||||
const signedIn = await Auth.isSignedIn();
|
||||
if (signedIn) {
|
||||
ACCOUNT_OPTION.textContent = 'Account';
|
||||
// Provisionally treat as free_signed_in until license check returns
|
||||
HTML.setAttribute('tier', TIER.FREE_SIGNED_IN);
|
||||
updateSlotIndicator();
|
||||
// Check license in background
|
||||
refreshLicense(true);
|
||||
} else {
|
||||
ACCOUNT_OPTION.textContent = 'Sign In';
|
||||
HTML.setAttribute('tier', TIER.FREE);
|
||||
updateSlotIndicator();
|
||||
// Auto-open sign-in modal if ?signin=1 param is present
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('signin') === '1') {
|
||||
@@ -488,27 +506,45 @@ async function initAccountState() {
|
||||
}
|
||||
initAccountState();
|
||||
|
||||
// Turn off every premium feature (including menu-level ones like schedule/password).
|
||||
// Used when dropping to the `free` tier via sign-out or license revocation.
|
||||
function disableAllPremiumFeatures() {
|
||||
PREMIUM_FEATURE_IDS.forEach(id => updateSetting(id, false));
|
||||
}
|
||||
|
||||
// When transitioning into `free_signed_in` (e.g. subscription lapsed while the
|
||||
// options page is open), disable any premium features beyond FREE_PREMIUM_SLOTS
|
||||
// so current state matches the new tier. Reuses the shared enforceSlotBudget
|
||||
// helper and routes writes through updateSetting so the full UI stays in sync.
|
||||
function pruneToSlotBudget() {
|
||||
const overBudget = enforceSlotBudget(cache, PREMIUM_CONFIG.FREE_PREMIUM_SLOTS);
|
||||
Object.keys(overBudget).forEach(id => updateSetting(id, false));
|
||||
}
|
||||
|
||||
function updatePremiumUI(licenseData) {
|
||||
if (licenseData && licenseData.signedOut) {
|
||||
ACCOUNT_OPTION.textContent = 'Sign In';
|
||||
HTML.setAttribute('is_premium', 'false');
|
||||
HTML.setAttribute('tier', TIER.FREE);
|
||||
disableAllPremiumFeatures();
|
||||
if (DONATE_LINK) {
|
||||
DONATE_LINK.hidden = false;
|
||||
DONATE_LINK.textContent = 'Donate';
|
||||
DONATE_LINK.setAttribute('href', DONATE_URL);
|
||||
}
|
||||
if (HEADER_PREMIUM_BADGE) HEADER_PREMIUM_BADGE.setAttribute('hidden', '');
|
||||
updateSlotIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
if (licenseData && licenseData.isPremium) {
|
||||
HTML.setAttribute('is_premium', 'true');
|
||||
HTML.setAttribute('tier', TIER.PREMIUM);
|
||||
if (DONATE_LINK) {
|
||||
DONATE_LINK.hidden = true;
|
||||
}
|
||||
if (HEADER_PREMIUM_BADGE) HEADER_PREMIUM_BADGE.removeAttribute('hidden');
|
||||
} else {
|
||||
HTML.setAttribute('is_premium', 'false');
|
||||
HTML.setAttribute('tier', TIER.FREE_SIGNED_IN);
|
||||
pruneToSlotBudget();
|
||||
if (DONATE_LINK) {
|
||||
DONATE_LINK.hidden = false;
|
||||
DONATE_LINK.textContent = 'Donate';
|
||||
@@ -516,6 +552,7 @@ function updatePremiumUI(licenseData) {
|
||||
}
|
||||
if (HEADER_PREMIUM_BADGE) HEADER_PREMIUM_BADGE.setAttribute('hidden', '');
|
||||
}
|
||||
updateSlotIndicator();
|
||||
}
|
||||
|
||||
// Account option click handler
|
||||
@@ -674,13 +711,9 @@ accountModalContainer.addEventListener('click', e => {
|
||||
accountSignoutButton.addEventListener('click', async () => {
|
||||
await Auth.signOut();
|
||||
ACCOUNT_OPTION.textContent = 'Sign In';
|
||||
HTML.setAttribute('is_premium', 'false');
|
||||
// Disable all premium options
|
||||
SECTIONS.forEach(section => {
|
||||
section.options.forEach(option => {
|
||||
if (option.premium) updateSetting(option.id, false);
|
||||
});
|
||||
});
|
||||
HTML.setAttribute('tier', TIER.FREE);
|
||||
updateSlotIndicator();
|
||||
disableAllPremiumFeatures();
|
||||
if (DONATE_LINK) {
|
||||
DONATE_LINK.textContent = 'Donate';
|
||||
DONATE_LINK.setAttribute('href', DONATE_URL);
|
||||
@@ -713,24 +746,30 @@ accountBillingButton.addEventListener('click', async () => {
|
||||
});
|
||||
|
||||
// Upgrade modal
|
||||
function openUpgradeModal() {
|
||||
recordEvent('Upgrade Modal Opened');
|
||||
function openUpgradeModal(options = {}) {
|
||||
const { reason } = options;
|
||||
recordEvent('Upgrade Modal Opened', { reason: reason || 'default' });
|
||||
const slotNote = document.getElementById('upgrade_slot_limit_note');
|
||||
if (slotNote) {
|
||||
if (reason === 'slot_limit') slotNote.removeAttribute('hidden');
|
||||
else slotNote.setAttribute('hidden', '');
|
||||
}
|
||||
upgradeModalContainer.removeAttribute('hidden');
|
||||
selectPlan('monthly');
|
||||
}
|
||||
|
||||
// Premium feature click handler - checks sign-in first
|
||||
async function handlePremiumFeatureClick() {
|
||||
const signedIn = await Auth.isSignedIn();
|
||||
if (!signedIn) {
|
||||
// Not signed in - show premium required modal
|
||||
recordEvent('Premium Feature Click', { signedIn: false });
|
||||
openPremiumRequiredModal();
|
||||
return;
|
||||
// Gate a menu-level premium feature by tier + slot budget.
|
||||
// Returns true when the caller should proceed (open its modal).
|
||||
function canUsePremiumFeature(settingId) {
|
||||
const tier = HTML.getAttribute('tier');
|
||||
if (tier === TIER.PREMIUM) return true;
|
||||
if (cache[settingId] === true) return true; // already active — managing is fine
|
||||
if (tier === TIER.FREE_SIGNED_IN && countActivePremium(cache) < PREMIUM_CONFIG.FREE_PREMIUM_SLOTS) {
|
||||
return true;
|
||||
}
|
||||
// Signed in but not premium - open upgrade modal
|
||||
recordEvent('Premium Feature Click', { signedIn: true });
|
||||
openUpgradeModal();
|
||||
if (tier === TIER.FREE_SIGNED_IN) openUpgradeModal({ reason: 'slot_limit' });
|
||||
else openPremiumRequiredModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Premium required modal (for non-signed-in users)
|
||||
|
||||
@@ -10,6 +10,9 @@ const PREMIUM_CONFIG = {
|
||||
// License token refresh threshold (refresh if expiring within this window)
|
||||
LICENSE_REFRESH_THRESHOLD_MS: 24 * 60 * 60 * 1000, // 24 hours
|
||||
|
||||
// Number of premium features a signed-in non-premium user can activate for free
|
||||
FREE_PREMIUM_SLOTS: 2,
|
||||
|
||||
// Storage keys
|
||||
STORAGE_KEYS: {
|
||||
SESSION_TOKEN: 'session_token',
|
||||
@@ -17,3 +20,10 @@ const PREMIUM_CONFIG = {
|
||||
USER_EMAIL: 'user_email',
|
||||
},
|
||||
};
|
||||
|
||||
// Tier enum for License.getTierSync() and the html[tier] attribute.
|
||||
const TIER = Object.freeze({
|
||||
PREMIUM: 'premium',
|
||||
FREE_SIGNED_IN: 'free_signed_in',
|
||||
FREE: 'free',
|
||||
});
|
||||
|
||||
@@ -117,6 +117,12 @@ const License = {
|
||||
return decoded.premium === true && decoded.exp > now;
|
||||
},
|
||||
|
||||
getTierSync(licenseToken, sessionToken) {
|
||||
if (this.isPremiumSync(licenseToken)) return TIER.PREMIUM;
|
||||
if (sessionToken) return TIER.FREE_SIGNED_IN;
|
||||
return TIER.FREE;
|
||||
},
|
||||
|
||||
// Create checkout session for subscription
|
||||
async createCheckoutSession(plan) {
|
||||
const token = await Auth.getSessionToken();
|
||||
|
||||
@@ -842,3 +842,47 @@ function sectionNameToUrl(name) {
|
||||
|
||||
return HOST + 'features/' + sectionPath + '/';
|
||||
}
|
||||
|
||||
|
||||
// Premium features in SECTIONS plus menu-level premium toggles (schedule, password).
|
||||
// Together these form the pool of features a slot-budgeted free user can activate.
|
||||
const PREMIUM_FEATURE_IDS = [
|
||||
...SECTIONS.flatMap(section =>
|
||||
section.options.filter(opt => opt.premium).map(opt => opt.id)
|
||||
),
|
||||
'schedule',
|
||||
'password',
|
||||
];
|
||||
const PREMIUM_FEATURE_ID_SET = new Set(PREMIUM_FEATURE_IDS);
|
||||
|
||||
function countActivePremium(cache) {
|
||||
let n = 0;
|
||||
PREMIUM_FEATURE_IDS.forEach(id => { if (cache[id] === true) n++; });
|
||||
return n;
|
||||
}
|
||||
|
||||
// Mutates `settings` to force every premium feature off. In-memory only —
|
||||
// does not return a write-back map because callers intentionally leave stored
|
||||
// values alone so the user's premium state returns on re-upgrade.
|
||||
function clearAllPremium(settings) {
|
||||
PREMIUM_FEATURE_IDS.forEach(id => { settings[id] = false; });
|
||||
}
|
||||
|
||||
// Mutates `settings` so at most `slotLimit` premium features stay true (by
|
||||
// iteration order). Returns a write-back map of the cleared settings — callers
|
||||
// should persist these to storage so over-budget premium settings don't
|
||||
// re-surface on the next load.
|
||||
function enforceSlotBudget(settings, slotLimit) {
|
||||
const writeBack = {};
|
||||
let kept = 0;
|
||||
PREMIUM_FEATURE_IDS.forEach(id => {
|
||||
if (settings[id] !== true) return;
|
||||
if (kept < slotLimit) {
|
||||
kept++;
|
||||
} else {
|
||||
settings[id] = false;
|
||||
writeBack[id] = false;
|
||||
}
|
||||
});
|
||||
return writeBack;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user