Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions js&css/extension/www.youtube.com/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -733,20 +733,20 @@ html[it-theme=plain] #cinematics {
}

/* Background-color update */
html[it-theme]:not([it-theme=default]) ytd-app,
html[it-theme]:not([it-theme=default]) ytd-browse-view,
html[it-theme]:not([it-theme=default]) ytd-watch-flexy,
html[it-theme]:not([it-theme=default]) #page,
html[it-theme]:not([it-theme=default]) #content { background-color: var(--yt-spec-base-background) !important; };
html[it-theme]:not([it-theme=default]):not([it-theme=none]) ytd-app,
html[it-theme]:not([it-theme=default]):not([it-theme=none]) ytd-browse-view,
html[it-theme]:not([it-theme=default]):not([it-theme=none]) ytd-watch-flexy,
html[it-theme]:not([it-theme=default]):not([it-theme=none]) #page,
html[it-theme]:not([it-theme=default]):not([it-theme=none]) #content { background-color: var(--yt-spec-base-background) !important; };

/*video detail text color. Fix. Themes didnt apply video description text color*/
html[it-theme]:not([it-theme=default]):not([it-theme=dark]) .yt-core-attributed-string--link-inherit-color {color: var(--yt-spec-text-primary) !important}
html[it-theme]:not([it-theme=default]):not([it-theme=dark]) .yt-core-attributed-string__link--call-to-action-color {color: var(--yt-spec-call-to-action)}
html[it-theme]:not([it-theme=default]):not([it-theme=none]):not([it-theme=dark]) .yt-core-attributed-string--link-inherit-color {color: var(--yt-spec-text-primary) !important}
html[it-theme]:not([it-theme=default]):not([it-theme=none]):not([it-theme=dark]) .yt-core-attributed-string__link--call-to-action-color {color: var(--yt-spec-call-to-action)}
/*The next two lines below can be removed if exact theming consistency isn't the goal and we just want to keep it simple.*/
html[it-theme]:not([it-theme=default]):not([it-theme=dark]) a.yt-simple-endpoint.yt-formatted-string {color: var(--yt-spec-call-to-action) !important}
html[it-theme]:not([it-theme=default]):not([it-theme=none]):not([it-theme=dark]) a.yt-simple-endpoint.yt-formatted-string {color: var(--yt-spec-call-to-action) !important}
/* Actually this above line is wrong. Default behavior is to have #tags near the views and upload date have two different colors depending if the video detail is expanded or not, but I don't know how to replicate that.*/
html[it-theme]:not([it-theme=default]):not([it-theme=dark]) yt-formatted-string[has-link-only_]:not([force-default-style]) a.yt-simple-endpoint.yt-formatted-string {color: var(--yt-spec-text-primary) !important}
html[it-theme]:not([it-theme=default]):not([it-theme=dark]) .yt-core-attributed-string--highlight-text-decorator .yt-core-attributed-string__link--display-type {color: var(--yt-spec-text-primary)}
html[it-theme]:not([it-theme=default]):not([it-theme=none]):not([it-theme=dark]) yt-formatted-string[has-link-only_]:not([force-default-style]) a.yt-simple-endpoint.yt-formatted-string {color: var(--yt-spec-text-primary) !important}
html[it-theme]:not([it-theme=default]):not([it-theme=none]):not([it-theme=dark]) .yt-core-attributed-string--highlight-text-decorator .yt-core-attributed-string__link--display-type {color: var(--yt-spec-text-primary)}

/*BLACK*/
html[it-theme=black] [dark],
Expand Down
3 changes: 3 additions & 0 deletions js&css/web-accessible/www.youtube.com/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ ImprovedTube.setTheme = function () {
this.elements.my_colors?.remove();
break

// 'none' leaves YouTube's own light/dark mode alone (no `dark` attribute,
// no PREF f6 cookie) so user styles such as Stylus are not fought.
case 'none':
case 'default':
document.getElementById('cinematics')?.removeAttribute('style');
this.elements.my_colors?.remove();
Expand Down
13 changes: 13 additions & 0 deletions menu/skeleton-parts/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ extension.skeleton.main.layers.section.themes.on.click.section = {
}
}
},
// Leaves YouTube's own styling alone, for people who theme it with
// Stylus or similar and don't want the extension to fight them.
none: {
component: 'label',
variant: 'none-theme',
text: 'none',
tags: 'off,disable,stylus,userstyle,userstyles',
radio: {
component: 'radio',
group: 'theme',
value: 'none'
}
},
custom: {
component: 'label',
variant: 'custom-theme',
Expand Down
7 changes: 7 additions & 0 deletions menu/styles/themes.css
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@
height: 48px !important;
}

.satus-label--none-theme {
height: 48px !important;
background: transparent;
outline: 1px dashed currentColor;
outline-offset: -1px;
}

.satus-label--dark-theme {
position: relative;
height: 48px !important; margin-top: -5px !important;
Expand Down
113 changes: 113 additions & 0 deletions tests/unit/theme-none.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const fs = require('fs');
const path = require('path');

/* A "none" theme that leaves YouTube's own styling alone, so user styles
* (Stylus and similar) are not fought (#4329). */

describe('"none" theme option in the menu', () => {
beforeAll(() => {
global.extension = {skeleton: {main: {layers: {section: {}}}}};
global.satus = {storage: {get: jest.fn()}};
jest.isolateModules(() => {
require('../../menu/skeleton-parts/themes.js');
});
});

test('is offered as a theme radio with the value "none"', () => {
const option = extension.skeleton.main.layers.section.themes.on.click.section.none;

// reuses the existing "none" string, already translated in most locales
expect(option.text).toBe('none');
expect(option.radio.group).toBe('theme');
expect(option.radio.value).toBe('none');
});
});

describe('setTheme with the "none" theme', () => {
let html, masthead, cinematics;

beforeEach(() => {
html = {setAttribute: jest.fn(), removeAttribute: jest.fn()};
masthead = {setAttribute: jest.fn(), removeAttribute: jest.fn()};
cinematics = {removeAttribute: jest.fn(), style: {setProperty: jest.fn()}};

global.document = {
documentElement: html,
querySelector: jest.fn(() => masthead),
getElementById: jest.fn(() => cinematics)
};
global.ImprovedTube = {
storage: {theme: 'none'},
elements: {my_colors: {remove: jest.fn()}},
messages: {send: jest.fn()},
setPrefCookieValueByName: jest.fn()
};
jest.isolateModules(() => {
require('../../js&css/web-accessible/www.youtube.com/themes.js');
});
});

test("leaves YouTube's own light/dark mode untouched", () => {
ImprovedTube.setTheme();

// neither the `dark` attribute nor YouTube's PREF f6 dark-mode cookie is touched
expect(html.setAttribute).not.toHaveBeenCalled();
expect(html.removeAttribute).not.toHaveBeenCalled();
expect(masthead.setAttribute).not.toHaveBeenCalled();
expect(masthead.removeAttribute).not.toHaveBeenCalled();
expect(ImprovedTube.setPrefCookieValueByName).not.toHaveBeenCalled();
expect(ImprovedTube.messages.send).not.toHaveBeenCalled();
});

test('drops the custom palette and restores the default cinematics glow', () => {
const palette = ImprovedTube.elements.my_colors;

ImprovedTube.setTheme();

expect(palette.remove).toHaveBeenCalled();
expect(cinematics.removeAttribute).toHaveBeenCalledWith('style');
expect(cinematics.style.setProperty).not.toHaveBeenCalled();
});
});

describe('theme stylesheets with the "none" theme', () => {
// Every stylesheet the extension ships, not just the one that has these rules
// today: a rule added to a sibling file would otherwise keep restyling the
// page under "none" without failing anything.
const cssRoot = path.join(__dirname, '../../js&css');

function stylesheets(dir) {
return fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return stylesheets(full);
return entry.isFile() && entry.name.endsWith('.css') ? [full] : [];
});
}

const files = stylesheets(cssRoot);

test('every shipped stylesheet is checked', () => {
expect(files.length).toBeGreaterThan(1);
});

test('no rule meant for "any theme but default" also matches "none"', () => {
// `html[it-theme]` matches every stored theme, "none" included, so each of
// these rules has to exclude "none" explicitly or it restyles the page anyway.
const offenders = [];
let checked = 0;

for (const file of files) {
const css = fs.readFileSync(file, 'utf8');
const selectors = css.match(/html\[it-theme\]:not\(\[it-theme=default\]\)[^,{]*/g) || [];
for (const selector of selectors) {
checked += 1;
if (!selector.includes(':not([it-theme=none])')) {
offenders.push(`${path.relative(cssRoot, file)}: ${selector.trim()}`);
}
}
}

expect(offenders).toEqual([]);
expect(checked).toBe(10);
});
});