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
This commit is contained in:
commit
e0ce59d3e8
5 changed files with 167 additions and 7 deletions
119
assets/css/carousel.css
Normal file
119
assets/css/carousel.css
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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`"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,6 +101,7 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
|
|||
match_length = 0
|
||||
|
||||
raw_data.as_h.each do |hash_key, value|
|
||||
if text.is_a?(String)
|
||||
if md = text.try &.match(/#{hash_key}/)
|
||||
if md[0].size >= match_length
|
||||
translation = value.as_s
|
||||
|
@ -108,14 +109,20 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
when .as_s?
|
||||
translation = raw_data.as_s
|
||||
else
|
||||
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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue