summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamantaz Fox <coding@samantaz.fr>2024-02-19 00:01:14 +0100
committerSamantaz Fox <coding@samantaz.fr>2024-02-19 00:16:17 +0100
commite0ce59d3e8c1230096e680985edac2fa3274e8f1 (patch)
tree129e7814c90692aa1781cb8127967f5ffe475d2d
parentc5a3112e497a528b2786ee3b2d944860c275f8d8 (diff)
parenta957b0fb7c517193dc9b20e7724feb46fe23912e (diff)
downloadinvidious-e0ce59d3e8c1230096e680985edac2fa3274e8f1.tar.gz
invidious-e0ce59d3e8c1230096e680985edac2fa3274e8f1.tar.bz2
invidious-e0ce59d3e8c1230096e680985edac2fa3274e8f1.zip
Channels: Add support for multi-image community posts (#4412)
This PR adds a CSS-only image carousel for community posts with more than one image attached. Closes issue 3522
-rw-r--r--assets/css/carousel.css119
-rw-r--r--locales/en-US.json5
-rw-r--r--src/invidious/frontend/comments_youtube.cr30
-rw-r--r--src/invidious/helpers/i18n.cr19
-rw-r--r--src/invidious/views/template.ecr1
5 files changed, 167 insertions, 7 deletions
diff --git a/assets/css/carousel.css b/assets/css/carousel.css
new file mode 100644
index 00000000..4bae92e5
--- /dev/null
+++ b/assets/css/carousel.css
@@ -0,0 +1,119 @@
+/*
+Copyright (c) 2024 by Jennifer (https://codepen.io/jwjertzoch/pen/JjyGeRy)
+
+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.
+*/
+
+.carousel {
+ margin: 0 auto;
+ overflow: hidden;
+ text-align: center;
+}
+
+.slides {
+ width: 100%;
+ display: flex;
+ overflow-x: scroll;
+ scrollbar-width: none;
+ scroll-snap-type: x mandatory;
+ scroll-behavior: smooth;
+}
+
+.slides::-webkit-scrollbar {
+ display: none;
+}
+
+.slides-item {
+ align-items: center;
+ border-radius: 10px;
+ display: flex;
+ flex-shrink: 0;
+ font-size: 100px;
+ height: 600px;
+ justify-content: center;
+ margin: 0 1rem;
+ position: relative;
+ scroll-snap-align: start;
+ transform: scale(1);
+ transform-origin: center center;
+ transition: transform .5s;
+ width: 100%;
+}
+
+.carousel__nav {
+ padding: 1.25rem .5rem;
+}
+
+.slider-nav {
+ align-items: center;
+ background-color: #ddd;
+ border-radius: 50%;
+ color: #000;
+ display: inline-flex;
+ height: 1.5rem;
+ justify-content: center;
+ padding: .5rem;
+ position: relative;
+ text-decoration: none;
+ width: 1.5rem;
+}
+
+.skip-link {
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ top: auto;
+ width: 1px;
+}
+
+.skip-link:focus {
+ align-items: center;
+ background-color: #000;
+ color: #fff;
+ display: flex;
+ font-size: 30px;
+ height: 30px;
+ justify-content: center;
+ opacity: .8;
+ text-decoration: none;
+ 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/locales/en-US.json b/locales/en-US.json
index da83767c..10887612 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -493,5 +493,8 @@
"channel_tab_playlists_label": "Playlists",
"channel_tab_community_label": "Community",
"channel_tab_channels_label": "Channels",
- "toggle_theme": "Toggle Theme"
+ "toggle_theme": "Toggle Theme",
+ "carousel_slide": "Slide {{current}} of {{total}}",
+ "carousel_skip": "Skip the Carousel",
+ "carousel_go_to": "Go to slide `x`"
}
diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr
index ecc0bc1b..aecac87f 100644
--- a/src/invidious/frontend/comments_youtube.cr
+++ b/src/invidious/frontend/comments_youtube.cr
@@ -107,6 +107,36 @@ module Invidious::Frontend::Comments
</div>
END_HTML
end
+ when "multiImage"
+ html << <<-END_HTML
+ <section class="carousel">
+ <a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a>
+ <div class="slides">
+ END_HTML
+ image_array = attachment["images"].as_a
+
+ image_array.each_index do |i|
+ html << <<-END_HTML
+ <div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
+ <img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
+ </div>
+ END_HTML
+ end
+
+ html << <<-END_HTML
+ </div>
+ <div class="carousel__nav">
+ END_HTML
+ attachment["images"].as_a.each_index do |i|
+ html << <<-END_HTML
+ <a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
+ END_HTML
+ end
+ html << <<-END_HTML
+ </div>
+ <div id="skip-#{child["commentId"]}"></div>
+ </section>
+ END_HTML
else nil # Ignore
end
end
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
index 76e477a4..23a1aafc 100644
--- a/src/invidious/helpers/i18n.cr
+++ b/src/invidious/helpers/i18n.cr
@@ -78,7 +78,7 @@ def load_all_locales
return locales
end
-def translate(locale : String?, key : String, text : String | Nil = nil) : String
+def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
# Log a warning if "key" doesn't exist in en-US locale and return
# that key as the text, so this is more or less transparent to the user.
if !LOCALES["en-US"].has_key?(key)
@@ -101,10 +101,12 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
match_length = 0
raw_data.as_h.each do |hash_key, value|
- if md = text.try &.match(/#{hash_key}/)
- if md[0].size >= match_length
- translation = value.as_s
- match_length = md[0].size
+ if text.is_a?(String)
+ if md = text.try &.match(/#{hash_key}/)
+ if md[0].size >= match_length
+ translation = value.as_s
+ match_length = md[0].size
+ end
end
end
end
@@ -114,8 +116,13 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
raise "Invalid translation \"#{raw_data}\""
end
- if text
+ if text.is_a?(String)
translation = translation.gsub("`x`", text)
+ elsif text.is_a?(Hash(String, String))
+ # adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
+ text.each_key do |hash_key|
+ translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
+ end
end
return translation
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index 5623bf77..9904b4fc 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -21,6 +21,7 @@
<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/default.css?v=<%= ASSET_COMMIT %>">
+ <link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head>