diff --git a/assets/css/carousel.css b/assets/css/carousel.css index 4bae92e5..e34edc65 100644 --- a/assets/css/carousel.css +++ b/assets/css/carousel.css @@ -97,23 +97,3 @@ DEALINGS IN THE SOFTWARE. width: 50%; 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; - } -} diff --git a/assets/css/dark.css b/assets/css/dark.css new file mode 100644 index 00000000..debe135f --- /dev/null +++ b/assets/css/dark.css @@ -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; +} diff --git a/assets/css/default.css b/assets/css/default.css index 01d4b736..8411df1d 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -466,22 +466,6 @@ footer { 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 { margin: 4px 0; 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 */ diff --git a/assets/css/light.css b/assets/css/light.css new file mode 100644 index 00000000..ccef24b3 --- /dev/null +++ b/assets/css/light.css @@ -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; +} diff --git a/assets/css/search.css b/assets/css/search.css index 833ec7e9..0c03a45e 100644 --- a/assets/css/search.css +++ b/assets/css/search.css @@ -95,27 +95,3 @@ fieldset, legend { 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; - } -} diff --git a/src/ext/kemal_static_file_handler.cr b/src/ext/kemal_static_file_handler.cr index a5f42261..db18592b 100644 --- a/src/ext/kemal_static_file_handler.cr +++ b/src/ext/kemal_static_file_handler.cr @@ -159,7 +159,9 @@ module Kemal redirect_to context, expanded_path + '/' 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 config.is_a?(Hash) && config["dir_listing"] == true diff --git a/src/invidious/routes/get_theme_css_file.cr b/src/invidious/routes/get_theme_css_file.cr new file mode 100644 index 00000000..52cc322b --- /dev/null +++ b/src/invidious/routes/get_theme_css_file.cr @@ -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 diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 46b71f1f..3588db4f 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -21,6 +21,7 @@ module Invidious::Routing get "/privacy", Routes::Misc, :privacy get "/licenses", Routes::Misc, :licenses get "/redirect", Routes::Misc, :cross_instance_redirect + get "/css/theme.css", Routes::Misc, :theme_css self.register_channel_routes self.register_watch_routes diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 9904b4fc..855496c4 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -21,6 +21,7 @@ + diff --git a/src/invidious/views/theme.css.ecr b/src/invidious/views/theme.css.ecr new file mode 100644 index 00000000..cb378ee3 --- /dev/null +++ b/src/invidious/views/theme.css.ecr @@ -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%>}