Add workaround to avoid duplicating theme css

In order to support both a theme toggle and to automatically use the
user's selected theme based on browser/system defaults the stylesheets
has to be duplicated twice since the latter requires the css to be
wrapped around a media query.

This duplication is a painful experience to deal with when adding or
changing existing styles. Even more so when it involves jumping around
the behemoth default.css from and back to whatever sections you were
just working on.

I don't believe the we the Invidious team will be able to agree to a
proper solution anytime soon (eg css post-processor, modern css
light-dark feature, etc) so this commit is here as a stopgap measure.

The workaround is to move the theming styles to two separate files
which are read at runtime and used to generate a combined stylesheet
with the necessary duplication for the media query. This combined
stylesheet is then delivered on a new route added to Invidious,
bypassing the static file handler.
This commit is contained in:
syeopite 2025-05-21 18:29:32 -07:00
parent 2c857b5ab6
commit 60c8c55ff2
No known key found for this signature in database
GPG Key ID: A73C186DA3955A1A
10 changed files with 215 additions and 291 deletions

View File

@ -97,23 +97,3 @@ DEALINGS IN THE SOFTWARE.
width: 50%; width: 50%;
z-index: 1; z-index: 1;
} }
.light-theme .slider-nav {
background-color: #ddd;
}
.dark-theme .slider-nav {
background-color: #0005;
}
@media (prefers-color-scheme: light) {
.no-theme .slider-nav {
background-color: #ddd;
}
}
@media (prefers-color-scheme: dark) {
.no-theme .slider-nav {
background-color: #0005;
}
}

81
assets/css/dark.css Normal file
View File

@ -0,0 +1,81 @@
/* Contains the CSS for the Invidious dark theme
--------
This file should not get loaded by the user as the css here will be used
to generate theme.css as to support user-agent dark/light theme
in addition to the class-based swap method without duplicating any styles.
------
*/
.dark-theme a:hover,
.dark-theme a:active,
.dark-theme summary:hover,
.dark-theme a:focus,
.dark-theme summary:focus {
color: rgb(0, 182, 240);
}
.dark-theme .pure-button-primary:hover,
.dark-theme .pure-button-primary:focus,
.dark-theme .pure-button-secondary:hover,
.dark-theme .pure-button-secondary:focus {
color: #fff !important;
border-color: rgb(0, 182, 240) !important;
background-color: rgba(0, 182, 240, 1) !important;
}
.dark-theme .pure-button-secondary {
background-color: #0002;
color: #ddd;
}
.dark-theme a {
color: #adadad;
text-decoration: none;
}
body.dark-theme {
background-color: rgba(35, 35, 35, 1);
color: #f0f0f0;
}
.dark-theme .pure-form legend,
.dark-theme .pure-menu-heading {
color: #f0f0f0;
}
.dark-theme input,
.dark-theme select,
.dark-theme textarea {
color: rgba(35, 35, 35, 1);
}
.dark-theme .pure-form input[type="file"] {
color: #f0f0f0;
}
.dark-theme .searchbar input {
background-color: inherit;
color: inherit;
}
.dark-theme .error-card {
border: 1px solid #5e5e5e;
}
.dark-theme footer {
color: #adadad;
}
.dark-theme footer a {
color: #adadad !important;
}
.dark-theme #filters-box {
background: #373737;
}
.dark-theme .slider-nav {
background-color: #0005;
}

View File

@ -466,22 +466,6 @@ footer {
max-height: 30vh; max-height: 30vh;
} }
.light-theme footer {
color: #7c7c7c;
}
.dark-theme footer {
color: #adadad;
}
.light-theme footer a {
color: #7c7c7c !important;
}
.dark-theme footer a {
color: #adadad !important;
}
footer span { footer span {
margin: 4px 0; margin: 4px 0;
display: block; display: block;
@ -508,236 +492,6 @@ span > select {
} }
/*
* Light theme
*/
.light-theme a:hover,
.light-theme a:active,
.light-theme summary:hover,
.light-theme a:focus,
.light-theme summary:focus {
color: #075A9E !important;
}
.light-theme .pure-button-primary:hover,
.light-theme .pure-button-primary:focus,
.light-theme .pure-button-secondary:hover,
.light-theme .pure-button-secondary:focus {
color: #fff !important;
border-color: rgba(0, 182, 240, 0.75) !important;
background-color: rgba(0, 182, 240, 0.75) !important;
}
.light-theme .pure-button-secondary:not(.low-profile) {
color: #335d7a;
background-color: #fff2;
}
.light-theme a {
color: #335d7a;
text-decoration: none;
}
/* All links that do not fit with the default color goes here */
.light-theme a:not([data-id]) > .icon,
.light-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"],
.light-theme .playlist-restricted > ol > li > a {
color: #303030;
}
.light-theme .pure-menu-heading {
color: #565d64;
}
.light-theme .error-card {
border: 1px solid black;
}
@media (prefers-color-scheme: light) {
.no-theme a:hover,
.no-theme a:active,
.no-theme summary:hover,
.no-theme a:focus,
.no-theme summary:focus {
color: #075A9E !important;
}
.no-theme .pure-button-primary:hover,
.no-theme .pure-button-primary:focus,
.no-theme .pure-button-secondary:hover,
.no-theme .pure-button-secondary:focus {
color: #fff !important;
border-color: rgba(0, 182, 240, 0.75) !important;
background-color: rgba(0, 182, 240, 0.75) !important;
}
.no-theme .pure-button-secondary:not(.low-profile) {
color: #335d7a;
background-color: #fff2;
}
.no-theme a {
color: #335d7a;
text-decoration: none;
}
/* All links that do not fit with the default color goes here */
.no-theme a:not([data-id]) > .icon,
.no-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"],
.no-theme .playlist-restricted > ol > li > a {
color: #303030;
}
.no-theme footer {
color: #7c7c7c;
}
.no-theme footer a {
color: #7c7c7c !important;
}
.light-theme .pure-menu-heading {
color: #565d64;
}
.no-theme .error-card {
border: 1px solid black;
}
}
/*
* Dark theme
*/
.dark-theme a:hover,
.dark-theme a:active,
.dark-theme summary:hover,
.dark-theme a:focus,
.dark-theme summary:focus {
color: rgb(0, 182, 240);
}
.dark-theme .pure-button-primary:hover,
.dark-theme .pure-button-primary:focus,
.dark-theme .pure-button-secondary:hover,
.dark-theme .pure-button-secondary:focus {
color: #fff !important;
border-color: rgb(0, 182, 240) !important;
background-color: rgba(0, 182, 240, 1) !important;
}
.dark-theme .pure-button-secondary {
background-color: #0002;
color: #ddd;
}
.dark-theme a {
color: #adadad;
text-decoration: none;
}
body.dark-theme {
background-color: rgba(35, 35, 35, 1);
color: #f0f0f0;
}
.dark-theme .pure-form legend {
color: #f0f0f0;
}
.dark-theme .pure-menu-heading {
color: #f0f0f0;
}
.dark-theme input,
.dark-theme select,
.dark-theme textarea {
color: rgba(35, 35, 35, 1);
}
.dark-theme .pure-form input[type="file"] {
color: #f0f0f0;
}
.dark-theme .searchbar input {
background-color: inherit;
color: inherit;
}
.dark-theme .error-card {
border: 1px solid #5e5e5e;
}
@media (prefers-color-scheme: dark) {
.no-theme a:hover,
.no-theme a:active,
.no-theme a:focus {
color: rgb(0, 182, 240);
}
.no-theme .pure-button-primary:hover,
.no-theme .pure-button-primary:focus,
.no-theme .pure-button-secondary:hover,
.no-theme .pure-button-secondary:focus {
color: #fff !important;
border-color: rgb(0, 182, 240) !important;
background-color: rgba(0, 182, 240, 1) !important;
}
.no-theme .pure-button-secondary {
background-color: #0002;
color: #ddd;
}
.no-theme a {
color: #adadad;
text-decoration: none;
}
body.no-theme {
background-color: rgba(35, 35, 35, 1);
color: #f0f0f0;
}
.no-theme .pure-form legend {
color: #f0f0f0;
}
.no-theme .pure-menu-heading {
color: #f0f0f0;
}
.no-theme input,
.no-theme select,
.no-theme textarea {
color: rgba(35, 35, 35, 1);
}
.no-theme .pure-form input[type="file"] {
color: #f0f0f0;
}
.no-theme .searchbar input {
background-color: inherit;
color: inherit;
}
.no-theme footer {
color: #adadad;
}
.no-theme footer a {
color: #adadad !important;
}
.no-theme .error-card {
border: 1px solid #5e5e5e;
}
}
/* /*
* Miscellanous * Miscellanous
*/ */

67
assets/css/light.css Normal file
View File

@ -0,0 +1,67 @@
/* Contains the CSS for the Invidious light theme
--------
This file should not get loaded by the user as the css here will be used
to generate theme.css as to support user-agent dark/light theme
in addition to the class-based swap method without duplicating any styles.
------
*/
.light-theme a:hover,
.light-theme a:active,
.light-theme summary:hover,
.light-theme a:focus,
.light-theme summary:focus {
color: #075A9E !important;
}
.light-theme .pure-button-primary:hover,
.light-theme .pure-button-primary:focus,
.light-theme .pure-button-secondary:hover,
.light-theme .pure-button-secondary:focus {
color: #fff !important;
border-color: rgba(0, 182, 240, 0.75) !important;
background-color: rgba(0, 182, 240, 0.75) !important;
}
.light-theme .pure-button-secondary:not(.low-profile) {
color: #335d7a;
background-color: #fff2;
}
.light-theme a {
color: #335d7a;
text-decoration: none;
}
/* All links that do not fit with the default color goes here */
.light-theme a:not([data-id]) > .icon,
.light-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"],
.light-theme .playlist-restricted > ol > li > a {
color: #303030;
}
.light-theme .pure-menu-heading {
color: #565d64;
}
.light-theme footer {
color: #7c7c7c;
}
.light-theme footer a {
color: #7c7c7c !important;
}
.light-theme .error-card {
border: 1px solid black;
}
.light-theme #filters-box {
background: #dfdfdf;
}
.light-theme .slider-nav {
background-color: #ddd;
}

View File

@ -95,27 +95,3 @@ fieldset, legend {
padding: 15px; padding: 15px;
} }
} }
/* Light theme */
.light-theme #filters-box {
background: #dfdfdf;
}
@media (prefers-color-scheme: light) {
.no-theme #filters-box {
background: #dfdfdf;
}
}
/* Dark theme */
.dark-theme #filters-box {
background: #373737;
}
@media (prefers-color-scheme: dark) {
.no-theme #filters-box {
background: #373737;
}
}

View File

@ -159,7 +159,9 @@ module Kemal
redirect_to context, expanded_path + '/' redirect_to context, expanded_path + '/'
end end
return call_next(context) if file_info.nil? return call_next(context) if file_info.nil? ||
file_path.ends_with?("dark.css") ||
file_path.ends_with?("light.css")
if is_dir if is_dir
if config.is_a?(Hash) && config["dir_listing"] == true if config.is_a?(Hash) && config["dir_listing"] == true

View File

@ -0,0 +1,52 @@
# No need to initialize yet another namespace
#
# :nodoc:
module Invidious::Routes::Misc
private CSS_THEME_LIGHT = File.read("assets/css/light.css")
private CSS_THEME_UA_LIGHT = CSS_THEME_LIGHT.gsub(".light-theme", ".no-theme")
private CSS_THEME_DARK = File.read("assets/css/dark.css")
private CSS_THEME_UA_DARK = CSS_THEME_DARK.gsub(".dark-theme", ".no-theme")
private THEME_LAST_MODIFIED = {
File.info("assets/css/light.css").modification_time,
File.info("assets/css/dark.css").modification_time,
}.min
def self.theme_css(env)
env.response.headers["Content-Type"] = "text/css; charset=utf-8"
# Replicate cache header behavior of static file handler
env.response.headers["Etag"] = %{W/"#{THEME_LAST_MODIFIED.to_unix}"}
env.response.headers["Last-Modified"] = HTTP.format_time(THEME_LAST_MODIFIED)
if cache_request?(env, THEME_LAST_MODIFIED)
env.response.status = HTTP::Status::NOT_MODIFIED
return
end
# Usually added in `send_file` when static_headers proc is called
env.response.headers["Cache-Control"] = "max-age=2629800"
rendered "theme.css"
end
# Taken from https://github.com/crystal-lang/crystal/blob/1.16.3/src/http/server/handlers/static_file_handler.cr#L236
private def self.cache_request?(context : HTTP::Server::Context, last_modified : Time) : Bool
# According to RFC 7232:
# A recipient must ignore If-Modified-Since if the request contains an If-None-Match header field
if if_none_match = context.request.if_none_match
match = {"*", context.response.headers["Etag"]}
if_none_match.any? { |etag| match.includes?(etag) }
elsif if_modified_since = context.request.headers["If-Modified-Since"]?
header_time = HTTP.parse_time(if_modified_since)
# File mtime probably has a higher resolution than the header value.
# An exact comparison might be slightly off, so we add 1s padding.
# Static files should generally not be modified in subsecond intervals, so this is perfectly safe.
# This might be replaced by a more sophisticated time comparison when it becomes available.
!!(header_time && last_modified <= header_time + 1.second)
else
false
end
end
end

View File

@ -21,6 +21,7 @@ module Invidious::Routing
get "/privacy", Routes::Misc, :privacy get "/privacy", Routes::Misc, :privacy
get "/licenses", Routes::Misc, :licenses get "/licenses", Routes::Misc, :licenses
get "/redirect", Routes::Misc, :cross_instance_redirect get "/redirect", Routes::Misc, :cross_instance_redirect
get "/css/theme.css", Routes::Misc, :theme_css
self.register_channel_routes self.register_channel_routes
self.register_watch_routes self.register_watch_routes

View File

@ -21,6 +21,7 @@
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/theme.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head> </head>

View File

@ -0,0 +1,10 @@
/*
This files contains an automatically generated CSS file that contains the styles
for the dark and light themes of Invidious. Automatically generating this file allows
supporting both the user-agent color scheme preference and class-based theme preference
without duplicating the defined styles twice for each theme.
*/
<%=CSS_THEME_LIGHT-%>
@media (prefers-color-scheme: light) {<%= CSS_THEME_UA_LIGHT %>}
<%=CSS_THEME_DARK%>
@media (prefers-color-scheme: dark) {<%=CSS_THEME_UA_DARK%>}