mirror of
				https://gitea.invidious.io/iv-org/invidious-copy-2023-06-08.git
				synced 2024-08-15 00:53:38 +00:00 
			
		
		
		
	Add support for featured channels page
This commit is contained in:
		
							parent
							
								
									a777eda66a
								
							
						
					
					
						commit
						e9dcac9bd4
					
				
					 46 changed files with 543 additions and 48 deletions
				
			
		|  | @ -50,3 +50,70 @@ | ||||||
| #link-widget-primary a:hover { | #link-widget-primary a:hover { | ||||||
|   color: #e1e1e1 !important; |   color: #e1e1e1 !important; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /* Featured channels page */ | ||||||
|  | 
 | ||||||
|  | .channel-section details { | ||||||
|  |   margin: 20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .channel-section details summary { | ||||||
|  |   margin-bottom: 20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .channel-section details summary::marker { | ||||||
|  |   font-size: 1.3em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .category-heading { | ||||||
|  |   font-size: 1.2em; | ||||||
|  |   user-select: none; | ||||||
|  |   display: inline; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .section-contents .channel-profile { | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Due to space constraints we'll make the special large featured channel display | ||||||
|  | only show up when the screen is wide enough */ | ||||||
|  | 
 | ||||||
|  | @media screen and (min-width: 600px) { | ||||||
|  |   .large-featured-channel.channel-profile { | ||||||
|  |     /* We don't want the following attribute for large featured channels*/ | ||||||
|  |     text-align: initial; | ||||||
|  |     margin: initial; | ||||||
|  | 
 | ||||||
|  |     display: flex; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .large-featured-channel.channel-profile img { | ||||||
|  |     margin: 20% 0; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .large-featured-channel .featured-channel-about { | ||||||
|  |     margin-left: 2em; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .large-featured-channel .featured-channel-title { | ||||||
|  |     font-size: 1.2em; | ||||||
|  |     margin-bottom: 10px | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .large-featured-channel .featured-channel-description { | ||||||
|  |     margin-top: 10px; | ||||||
|  |     font-weight: normal; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Replicate the look for a normal featured channel */ | ||||||
|  | @media screen and (max-width: 600px) { | ||||||
|  |   .large-featured-channel .seperator, .large-featured-channel .featured-channel-description { | ||||||
|  |     display: none | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .large-featured-channel .featured-channel-metadata:last-child { | ||||||
|  |     display: block; | ||||||
|  |     margin-top: 1em; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -344,7 +344,7 @@ span > select { | ||||||
| 
 | 
 | ||||||
| .light-theme a:hover, | .light-theme a:hover, | ||||||
| .light-theme a:active, | .light-theme a:active, | ||||||
| .light-theme summary:hover { | .light-theme .simulated_a:hover { | ||||||
|   color: #075A9E !important; |   color: #075A9E !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -371,7 +371,7 @@ span > select { | ||||||
| @media (prefers-color-scheme: light) { | @media (prefers-color-scheme: light) { | ||||||
|   .no-theme a:hover, |   .no-theme a:hover, | ||||||
|   .no-theme a:active, |   .no-theme a:active, | ||||||
|   .no-theme summary:hover  { |   .no-theme .simulated_a:hover  { | ||||||
|     color: #075A9E !important; |     color: #075A9E !important; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -402,7 +402,7 @@ span > select { | ||||||
| 
 | 
 | ||||||
| .dark-theme a:hover, | .dark-theme a:hover, | ||||||
| .dark-theme a:active, | .dark-theme a:active, | ||||||
| .dark-theme summary:hover { | .dark-theme .simulated_a:hover { | ||||||
|   color: rgb(0, 182, 240); |   color: rgb(0, 182, 240); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -367,5 +367,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -428,5 +428,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  | @ -428,5 +428,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "Country: ", |     "Country: ": "Country: ", | ||||||
|     "Stats": "Stats", |     "Stats": "Stats", | ||||||
|     "Joined": "Joined", |     "Joined": "Joined", | ||||||
|     "Links": "Links" |     "Links": "Links", | ||||||
|  |     "This channel doesn't feature any other channels.": "This channel doesn't feature any other channels." | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -350,5 +350,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -349,5 +349,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -428,5 +428,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -367,5 +367,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -426,5 +426,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -350,5 +350,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -429,5 +429,6 @@ | ||||||
|     "Country: ": "", |     "Country: ": "", | ||||||
|     "Stats": "", |     "Stats": "", | ||||||
|     "Joined": "", |     "Joined": "", | ||||||
|     "Links": "" |     "Links": "", | ||||||
|  |     "This channel doesn't feature any other channels.": "" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -312,6 +312,8 @@ Invidious::Routing.get "/channel/:ucid", Invidious::Routes::Channels, :home | ||||||
| Invidious::Routing.get "/channel/:ucid/videos", Invidious::Routes::Channels, :videos | Invidious::Routing.get "/channel/:ucid/videos", Invidious::Routes::Channels, :videos | ||||||
| Invidious::Routing.get "/channel/:ucid/playlists", Invidious::Routes::Channels, :playlists | Invidious::Routing.get "/channel/:ucid/playlists", Invidious::Routes::Channels, :playlists | ||||||
| Invidious::Routing.get "/channel/:ucid/community", Invidious::Routes::Channels, :community | Invidious::Routing.get "/channel/:ucid/community", Invidious::Routes::Channels, :community | ||||||
|  | Invidious::Routing.get "/channel/:ucid/channels", Invidious::Routes::Channels, :channels | ||||||
|  | Invidious::Routing.get "/channel/:ucid/channels/:param", Invidious::Routes::Channels, :featured_channel_category | ||||||
| Invidious::Routing.get "/channel/:ucid/about", Invidious::Routes::Channels, :about | Invidious::Routing.get "/channel/:ucid/about", Invidious::Routes::Channels, :about | ||||||
| 
 | 
 | ||||||
| Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle | Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle | ||||||
|  |  | ||||||
|  | @ -135,7 +135,7 @@ struct AboutChannel | ||||||
|   property is_family_friendly : Bool |   property is_family_friendly : Bool | ||||||
|   property allowed_regions : Array(String) |   property allowed_regions : Array(String) | ||||||
|   property related_channels : Array(AboutRelatedChannel) |   property related_channels : Array(AboutRelatedChannel) | ||||||
|   property tabs : Hash(String, String) |   property tabs : Hash(String, Tuple(Int32, String)) # TabName => {TabiZZndex, browseEndpoint params} | ||||||
|   property links : Array(Tuple(String, String, String)) |   property links : Array(Tuple(String, String, String)) | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
|  | @ -380,6 +380,27 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by) | ||||||
|   return items, continuation |   return items, continuation | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
|  | def fetch_channel_featured_channels(ucid, tab_data, params = nil, continuation = nil, title = nil ) | ||||||
|  |   if continuation.is_a?(String) | ||||||
|  |      initial_data = request_youtube_api_browse(continuation) | ||||||
|  |      channels_tab_content = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"] | ||||||
|  | 
 | ||||||
|  |      return process_featured_channels([channels_tab_content,], nil, title, continuation_items=true) | ||||||
|  |   else | ||||||
|  |     if params.is_a?(String) | ||||||
|  |       initial_data = request_youtube_api_browse(ucid, params) | ||||||
|  |     else | ||||||
|  |       initial_data = request_youtube_api_browse(ucid, tab_data[1]) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     channels_tab = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][tab_data[0]]["tabRenderer"] | ||||||
|  |     channels_tab_content = channels_tab["content"]["sectionListRenderer"]["contents"].as_a | ||||||
|  |     submenu_data = channels_tab["content"]["sectionListRenderer"]["subMenu"]?.try &.["channelSubMenuRenderer"]["contentTypeSubMenuItems"] || false | ||||||
|  | 
 | ||||||
|  |     return process_featured_channels(channels_tab_content, submenu_data) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
| def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) | def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) | ||||||
|   object = { |   object = { | ||||||
|     "80226972:embedded" => { |     "80226972:embedded" => { | ||||||
|  | @ -887,13 +908,16 @@ def get_about_info(ucid, locale) | ||||||
|   country = "" |   country = "" | ||||||
|   total_views = 0_i64 |   total_views = 0_i64 | ||||||
|   joined = Time.unix(0) |   joined = Time.unix(0) | ||||||
|   tabs = {} of String => String # TabName => browseEndpoint params |   tabs = {} of String => Tuple(Int32, String)  # TabName => {TabiZZndex, browseEndpoint params} | ||||||
|   links = [] of {String, String, String} |   links = [] of {String, String, String} | ||||||
| 
 | 
 | ||||||
|   tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? |   tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? | ||||||
|  |   tab_names = [] of String | ||||||
|  |   tab_data = [] of Tuple(Int32, String) | ||||||
|  | 
 | ||||||
|   if !tabs_json.nil? |   if !tabs_json.nil? | ||||||
|     # Retrieve information from the tabs array. The index we are looking for varies between channels. |     # Retrieve information from the tabs array. The index we are looking for varies between channels. | ||||||
|     tabs_json.each do |node| |     tabs_json.each_with_index do |node, i| | ||||||
|       # Try to find the about section which is located in only one of the tabs. |       # Try to find the about section which is located in only one of the tabs. | ||||||
|       channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]? |       channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]? | ||||||
|         .try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]? |         .try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]? | ||||||
|  | @ -935,10 +959,14 @@ def get_about_info(ucid, locale) | ||||||
|           auto_generated = true |           auto_generated = true | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | 
 | ||||||
|  |       if node["tabRenderer"]? | ||||||
|  |         tab_names << node["tabRenderer"]["title"].as_s.downcase | ||||||
|  |         tab_data << {i, node["tabRenderer"]["endpoint"]["browseEndpoint"]["params"].as_s} | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|     end |     end | ||||||
|     tab_names = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["title"].as_s.downcase } |     tabs = Hash.zip(tab_names, tab_data) | ||||||
|     browse_endpoint_param = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["endpoint"]["browseEndpoint"]["params"].as_s } |  | ||||||
|     tabs = Hash.zip(tab_names, browse_endpoint_param) |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? |   sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? | ||||||
|  |  | ||||||
							
								
								
									
										170
									
								
								src/invidious/featured_channels.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/invidious/featured_channels.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,170 @@ | ||||||
|  | struct FeaturedChannel | ||||||
|  |   include DB::Serializable | ||||||
|  | 
 | ||||||
|  |   property author : String | ||||||
|  |   property ucid : String | ||||||
|  |   property author_thumbnail : String | ||||||
|  |   property subscriber_count : Int32 | ||||||
|  |   property video_count : Int32 | ||||||
|  |   property description_html : String? | ||||||
|  | 
 | ||||||
|  |   def to_json(locale, json : JSON::Builder) | ||||||
|  |     json.object do | ||||||
|  |       json.field "author", self.author | ||||||
|  |       json.field "authorId", self.ucid | ||||||
|  |       json.field "authorUrl", "/channel/#{self.ucid}" | ||||||
|  |       json.field "authorThumbnails" do | ||||||
|  |         json.array do | ||||||
|  |           qualities = {32, 48, 76, 100, 176, 512} | ||||||
|  | 
 | ||||||
|  |           qualities.each do |quality| | ||||||
|  |             json.object do | ||||||
|  |               json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}") | ||||||
|  |               json.field "width", quality | ||||||
|  |               json.field "height", quality | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       json.field "description", html_to_content(self.description_html) | ||||||
|  |       json.field "descriptionHtml", self.description_html | ||||||
|  |       json.field "subCount", self.subscriber_count | ||||||
|  |       json.field "videoCount", self.video_count | ||||||
|  |       json.field "badges", self.badges | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def to_json(locale, json : JSON::Builder | Nil = nil) | ||||||
|  |     if json | ||||||
|  |       to_json(locale, json) | ||||||
|  |     else | ||||||
|  |       JSON.build do |json| | ||||||
|  |         to_json(locale, json) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | struct Category | ||||||
|  |   include DB::Serializable | ||||||
|  | 
 | ||||||
|  |   property title : String | ||||||
|  |   property contents : Array(FeaturedChannel) | FeaturedChannel | ||||||
|  |   property browse_endpoint_param : String? | ||||||
|  |   property continuation_token : String? | ||||||
|  | 
 | ||||||
|  |   def to_json(locale, json : JSON::Builder) | ||||||
|  |     json.object do | ||||||
|  |       json.field "title", self.title | ||||||
|  |       json.field "contents", self.contents | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def to_json(locale, json : JSON::Builder | Nil = nil) | ||||||
|  |     if json | ||||||
|  |       to_json(locale, json) | ||||||
|  |     else | ||||||
|  |       JSON.build do |json| | ||||||
|  |         to_json(locale, json) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | def _extract_channel_data(channel) | ||||||
|  |   ucid = channel["channelId"].as_s | ||||||
|  |   author = channel["title"]["simpleText"].as_s | ||||||
|  |   author_thumbnail = channel["thumbnail"]["thumbnails"].as_a[0]["url"].as_s | ||||||
|  |   subscriber_count = channel["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? | ||||||
|  |     .try { |text| short_text_to_number(text.split(" ")[0]) } || 0 | ||||||
|  | 
 | ||||||
|  |   video_count = channel["videoCountText"]?.try &.["runs"][0]["text"].as_s.gsub(/\D/, "").to_i || 0 | ||||||
|  | 
 | ||||||
|  |   if channel["descriptionSnippet"]? | ||||||
|  |     description = channel["descriptionSnippet"]["runs"][0]["text"].as_s | ||||||
|  |     description_html = HTML.escape(description).gsub("\n", "") | ||||||
|  |   else | ||||||
|  |     description_html = nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   FeaturedChannel.new({ | ||||||
|  |     author: author, | ||||||
|  |     ucid: ucid, | ||||||
|  |     author_thumbnail: author_thumbnail, | ||||||
|  |     subscriber_count: subscriber_count, | ||||||
|  |     video_count: video_count, | ||||||
|  |     description_html: description_html | ||||||
|  |   }) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | def process_featured_channels(data, submenu_data, title=nil, continuation_items=false) | ||||||
|  |   all_categories = [] of Category | ||||||
|  | 
 | ||||||
|  |   if submenu_data.is_a?(Bool) | ||||||
|  |     return all_categories | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   # Extraction process differs when there's more than one category | ||||||
|  |   if data.size > 1 | ||||||
|  |     data.each do |raw_category| | ||||||
|  |       raw_category = raw_category["itemSectionRenderer"]["contents"].as_a[0]["shelfRenderer"] | ||||||
|  | 
 | ||||||
|  |       category_title = raw_category["title"]["runs"][0]["text"].as_s | ||||||
|  |       browse_endpoint_param = raw_category["endpoint"]["browseEndpoint"]["params"].as_s | ||||||
|  | 
 | ||||||
|  |       # Category has multiple channels | ||||||
|  |       if raw_category["content"].as_h.has_key?("horizontalListRenderer") | ||||||
|  |         contents = [] of FeaturedChannel | ||||||
|  |         raw_category["content"]["horizontalListRenderer"]["items"].as_a.each do |channel| | ||||||
|  |           contents << _extract_channel_data(channel["gridChannelRenderer"]) | ||||||
|  |         end | ||||||
|  |       # Single channel | ||||||
|  |       else | ||||||
|  |         channel = raw_category["content"]["expandedShelfContentsRenderer"]["items"][0]["channelRenderer"] | ||||||
|  |         contents = _extract_channel_data(channel) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       all_categories << Category.new({ | ||||||
|  |         title: category_title, | ||||||
|  |         contents: contents, | ||||||
|  |         browse_endpoint_param: browse_endpoint_param, | ||||||
|  |         continuation_token: nil | ||||||
|  |       }) | ||||||
|  |     end | ||||||
|  |   else | ||||||
|  |     if !continuation_items | ||||||
|  |       raw_category_contents = data[0]["itemSectionRenderer"]["contents"].as_a[0]["gridRenderer"]["items"].as_a | ||||||
|  |     else | ||||||
|  |       raw_category_contents = data[0].as_a | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     category_title = submenu_data.try &.[0]["title"].as_s || title || "" | ||||||
|  | 
 | ||||||
|  |     browse_endpoint_param = nil # Not needed | ||||||
|  |     continuation_token = nil | ||||||
|  | 
 | ||||||
|  |     # If a continuation token is needed it'll always be after at least twelve channels | ||||||
|  |     if raw_category_contents.size > 12 | ||||||
|  |       continuation_token = raw_category_contents[-1]["continuationItemRenderer"]?.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s || nil | ||||||
|  | 
 | ||||||
|  |       if !continuation_token.nil? | ||||||
|  |         raw_category_contents = raw_category_contents[0..-2] | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     contents = [] of FeaturedChannel | ||||||
|  |     raw_category_contents.each do |channel| | ||||||
|  |       contents << _extract_channel_data(channel["gridChannelRenderer"]) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     all_categories << Category.new({ | ||||||
|  |       title: category_title, | ||||||
|  |       contents: contents, | ||||||
|  |       browse_endpoint_param: browse_endpoint_param, | ||||||
|  |       continuation_token: continuation_token | ||||||
|  |     }) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   return all_categories | ||||||
|  | end | ||||||
|  | @ -91,6 +91,69 @@ class Invidious::Routes::Channels < Invidious::Routes::BaseRoute | ||||||
|     templated "community" |     templated "community" | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def channels(env) | ||||||
|  |     data = self.fetch_basic_information(env) | ||||||
|  |     if !data.is_a?(Tuple) | ||||||
|  |       return data | ||||||
|  |     end | ||||||
|  |     locale, user, subscriptions, continuation, ucid, channel = data | ||||||
|  | 
 | ||||||
|  |     if !channel.tabs.has_key?("channels") | ||||||
|  |       return env.redirect "/channel/#{channel.ucid}" | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     # When a channel only has a single category it lacks the category param option so we'll handle it here. | ||||||
|  |     if continuation | ||||||
|  |       offset = env.params.query["offset"]? | ||||||
|  |       if offset | ||||||
|  |         offset = offset.to_i | ||||||
|  |       else | ||||||
|  |         offset = 0 | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       # Previous continuation | ||||||
|  |       previous_continuation = env.params.query["previous"]? | ||||||
|  |       # Category title is not returned when using a continuation token. | ||||||
|  |       title = env.params.query["title"]? | ||||||
|  | 
 | ||||||
|  |       featured_channel_categories = fetch_channel_featured_channels(ucid, channel.tabs["channels"], nil, continuation, title).not_nil! | ||||||
|  |     else | ||||||
|  |       previous_continuation = nil | ||||||
|  |       category_param = nil | ||||||
|  |       offset = 0 | ||||||
|  |       title = nil | ||||||
|  | 
 | ||||||
|  |       featured_channel_categories = fetch_channel_featured_channels(ucid, channel.tabs["channels"], nil, nil).not_nil! | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     templated "channels" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def featured_channel_category(env) | ||||||
|  |     # Used to check when the initial page is reached and redirect to /channel/:ucid/channels/:param when zero | ||||||
|  |     offset = env.params.query["offset"]? | ||||||
|  |     category_param = env.params.url["param"] | ||||||
|  |     if offset | ||||||
|  |       offset = offset.to_i | ||||||
|  |     else | ||||||
|  |       offset = 0 | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     data = self.fetch_basic_information(env) | ||||||
|  |     if !data.is_a?(Tuple) | ||||||
|  |       return data | ||||||
|  |     end | ||||||
|  |     locale, user, subscriptions, continuation, ucid, channel = data | ||||||
|  | 
 | ||||||
|  |     # Previous continuation | ||||||
|  |     previous_continuation = env.params.query["previous"]? | ||||||
|  |     # Category title is not returned when using a continuation token. | ||||||
|  |     title = env.params.query["title"]? | ||||||
|  | 
 | ||||||
|  |     featured_channel_categories = fetch_channel_featured_channels(ucid, channel.tabs["channels"], category_param, continuation, title).not_nil! | ||||||
|  |     templated "channels" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def about(env) |   def about(env) | ||||||
|     data = self.fetch_basic_information(env) |     data = self.fetch_basic_information(env) | ||||||
|     if !data.is_a?(Tuple) |     if !data.is_a?(Tuple) | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| <link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>"> | <link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>"> | ||||||
| <% end %> | <% end %> | ||||||
| 
 | 
 | ||||||
| <% content_type =  4 %> | <% content_type =  5 %> | ||||||
| <% sort_options = Tuple.new %> | <% sort_options = Tuple.new %> | ||||||
| <%= rendered "components/channel-information" %> | <%= rendered "components/channel-information" %> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										115
									
								
								src/invidious/views/channels.ecr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/invidious/views/channels.ecr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | ||||||
|  | <% content_for "header" do %> | ||||||
|  | <title><%= channel.author %> - Invidious</title> | ||||||
|  | <link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>"> | ||||||
|  | <% end %> | ||||||
|  | 
 | ||||||
|  | <% content_type =  4 %> | ||||||
|  | <% sort_options = Tuple.new %> | ||||||
|  | <%= rendered "components/channel-information" %> | ||||||
|  | 
 | ||||||
|  | <div class="pure-g h-box"> | ||||||
|  |     <% if !featured_channel_categories.empty? %> | ||||||
|  |         <% featured_channel_categories.each do | category | %> | ||||||
|  |             <div class="channel-section pure-u-1"> | ||||||
|  |                 <details open=""> | ||||||
|  |                     <summary style="display: revert;"> | ||||||
|  |                         <h3 class="category-heading"> | ||||||
|  |                             <% if (category_request_param = category.browse_endpoint_param).is_a?(String) %> | ||||||
|  |                                 <a href="/channel/<%=channel.ucid%>/channels/<%=HTML.escape(category_request_param)%>"> | ||||||
|  |                                     <%= category.title %> | ||||||
|  |                                 </a> | ||||||
|  |                             <%else%> | ||||||
|  |                                 <%= category.title %> | ||||||
|  |                             <%end%> | ||||||
|  |                         </h3> | ||||||
|  |                     </summary> | ||||||
|  |                     <% contents = category.contents%> | ||||||
|  |                     <div class="pure-g section-contents"> | ||||||
|  |                         <% if contents.is_a?(Array(FeaturedChannel)) %> | ||||||
|  |                             <% contents.each do |item|%> | ||||||
|  |                                 <div class="channel-profile pure-u-1 pure-u-sm-1-2 pure-u-md-1-3 pure-u-lg-1-4 pure-u-xl-1-5"> | ||||||
|  |                                     <a class="featured-channel-icon" href="/channel/<%= item.ucid %>"> | ||||||
|  |                                         <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||||
|  |                                               <img src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/> | ||||||
|  |                                         <% end %> | ||||||
|  |                                     </a> | ||||||
|  |                                     <div class="featured-channel-about"> | ||||||
|  |                                         <p class="featured-channel-title"><a href="/channel/<%= item.ucid %>"><%= item.author %></a></p> | ||||||
|  |                                         <div class="featured-channel-metadata"> | ||||||
|  |                                             <p><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></p> | ||||||
|  |                                             <p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p> | ||||||
|  |                                         </div> | ||||||
|  | 
 | ||||||
|  |                                         <% ucid = item.ucid %> | ||||||
|  |                                         <% author = item.author %> | ||||||
|  |                                         <% sub_count_text = number_to_short_text(item.subscriber_count) %> | ||||||
|  |                                         <%= rendered "components/subscribe_widget" %> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             <%end%> | ||||||
|  |                         <% elsif contents.is_a?(FeaturedChannel) %> | ||||||
|  |                             <%item = contents %> | ||||||
|  |                             <div class="channel-profile large-featured-channel pure-u-1"> | ||||||
|  |                                 <a class="featured-channel-icon" href="/channel/<%= item.ucid %>"> | ||||||
|  |                                     <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||||
|  |                                           <img src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/> | ||||||
|  |                                     <% end %> | ||||||
|  |                                 </a> | ||||||
|  |                                 <div class="featured-channel-about"> | ||||||
|  |                                     <p class="featured-channel-title"><a href="/channel/<%= item.ucid %>"><%= item.author %></a></p> | ||||||
|  |                                     <div class="featured-channel-metadata"> | ||||||
|  |                                         <span><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></span> | ||||||
|  |                                         <span class="seperator"> | </span> | ||||||
|  |                                         <span><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></span> | ||||||
|  |                                     </div> | ||||||
|  |                                     <p class="featured-channel-description"><%= item.description_html %></p> | ||||||
|  | 
 | ||||||
|  |                                     <% ucid = contents.ucid %> | ||||||
|  |                                     <% author = contents.author %> | ||||||
|  |                                     <% sub_count_text = number_to_short_text(contents.subscriber_count) %> | ||||||
|  |                                     <%= rendered "components/subscribe_widget" %> | ||||||
|  |                             </div> | ||||||
|  |                         <%end%> | ||||||
|  |                     </div> | ||||||
|  |                 </details> | ||||||
|  |             </div> | ||||||
|  |         <% end %> | ||||||
|  |     <% else %> | ||||||
|  |         <h3 class="pure-u-1"> | ||||||
|  |             <%= translate(locale, "This channel doesn't feature any other channels.")%> | ||||||
|  |         </h3> | ||||||
|  |     <% end %> | ||||||
|  | 
 | ||||||
|  |     <!-- #<div class="channel-section pure-u-1 pure-u-md-1-4 pure-u-lg-1-6"> --> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <div class="pure-g h-box"> | ||||||
|  |     <div class="pure-u-1 pure-u-lg-1-5"> | ||||||
|  |         <% if previous_continuation %> | ||||||
|  |             <a href="/channel/<%=channel.ucid%>/channels/<%=category_param%>?continuation=<%=HTML.escape(previous_continuation)%>&offset=<%=offset.not_nil!-1%>&title=<%=HTML.escape(title.not_nil!)%>"> | ||||||
|  |                 <%= translate(locale, "Previous page") %> | ||||||
|  |             </a> | ||||||
|  |         <% elsif (offset - 1) == 0 %> | ||||||
|  |             <a href="/channel/<%=channel.ucid%>/channels/<%=category_param%>"> | ||||||
|  |                 <%= translate(locale, "Previous page") %> | ||||||
|  |             </a> | ||||||
|  |         <% end %> | ||||||
|  |     </div> | ||||||
|  |     <div class="pure-u-1 pure-u-lg-3-5"></div> | ||||||
|  |     <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> | ||||||
|  |         <% if (next_cont_token = featured_channel_categories[0].continuation_token) %> | ||||||
|  |             <% additional_url_param = ""%> | ||||||
|  |             <% if continuation %> | ||||||
|  |                 <% additional_url_param = "&previous=#{HTML.escape(continuation)}"%> | ||||||
|  |             <%end %> | ||||||
|  |             <% if !title %> | ||||||
|  |                 <% title = featured_channel_categories[0].title %> | ||||||
|  |             <%end %> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             <a href="/channel/<%=channel.ucid%>/channels/<%=category_param%>?continuation=<%=HTML.escape(next_cont_token)%>&offset=<%=offset.not_nil!+1%>&title=<%=HTML.escape(title)%><%=additional_url_param%>"> | ||||||
|  |                 <%= translate(locale, "Next page") %> | ||||||
|  |             </a> | ||||||
|  |         <% end %> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | @ -107,6 +107,20 @@ | ||||||
|               <% end %> |               <% end %> | ||||||
| 
 | 
 | ||||||
|               <% if content_type == 4 %> |               <% if content_type == 4 %> | ||||||
|  |                   <li class="pure-menu-item pure-menu-selected"> | ||||||
|  |                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/channels"> | ||||||
|  |                         <b> <%= translate(locale, "Channels") %> </b> | ||||||
|  |                       </a> | ||||||
|  |                   </li> | ||||||
|  |               <% else %> | ||||||
|  |                   <li class="pure-menu-item"> | ||||||
|  |                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/channels"> | ||||||
|  |                         <%= translate(locale, "Channels") %> | ||||||
|  |                       </a> | ||||||
|  |                   </li> | ||||||
|  |               <% end %> | ||||||
|  | 
 | ||||||
|  |               <% if content_type == 5 %> | ||||||
|                   <li class="pure-menu-item pure-menu-selected"> |                   <li class="pure-menu-item pure-menu-selected"> | ||||||
|                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/about"> |                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/about"> | ||||||
|                         <b> <%= translate(locale, "About") %> </b> |                         <b> <%= translate(locale, "About") %> </b> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
|     </h3> |     </h3> | ||||||
| <% else %> | <% else %> | ||||||
|     <details id="filters"> |     <details id="filters"> | ||||||
|         <summary> |         <summary class="simulated_a"> | ||||||
|             <h3 style="display:inline"> <%= translate(locale, "filter") %> </h3> |             <h3 style="display:inline"> <%= translate(locale, "filter") %> </h3> | ||||||
|         </summary> |         </summary> | ||||||
|         <div id="filters" class="pure-g h-box"> |         <div id="filters" class="pure-g h-box"> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue