mirror of
				https://github.com/iv-org/invidious.git
				synced 2025-10-31 20:51:56 +00:00 
			
		
		
		
	UI: Nicer buttons (#3763)
This commit is contained in:
		| @@ -1,3 +1,7 @@ | ||||
| /* | ||||
|  * Common attributes | ||||
|  */ | ||||
|  | ||||
| html, | ||||
| body { | ||||
|   font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, | ||||
| @@ -11,6 +15,16 @@ body { | ||||
|   min-height: 100vh; | ||||
| } | ||||
|  | ||||
| .h-box { | ||||
|   padding-left: 1em; | ||||
|   padding-right: 1em; | ||||
| } | ||||
|  | ||||
| .v-box { | ||||
|   padding-top: 1em; | ||||
|   padding-bottom: 1em; | ||||
| } | ||||
|  | ||||
| .deleted { | ||||
|   background-color: rgb(255, 0, 0, 0.5); | ||||
| } | ||||
| @@ -20,6 +34,34 @@ body { | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
|  | ||||
| .title { | ||||
|   margin: 0.5em 0 1em 0; | ||||
| } | ||||
|  | ||||
| /* A flex container */ | ||||
| .flexible { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .flex-left { | ||||
|   display: flex; | ||||
|   flex: 1 1 auto; | ||||
|   flex-flow: row wrap; | ||||
|   justify-content: flex-start; | ||||
| } | ||||
| .flex-right { | ||||
|   display: flex; | ||||
|   flex: 2 0 auto; | ||||
|   flex-flow: row nowrap; | ||||
|   justify-content: flex-end; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Channel page | ||||
|  */ | ||||
|  | ||||
| .channel-profile > * { | ||||
|   font-size: 1.17em; | ||||
|   font-weight: bold; | ||||
| @@ -90,16 +132,6 @@ body a.channel-owner { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .h-box { | ||||
|   padding-left: 1em; | ||||
|   padding-right: 1em; | ||||
| } | ||||
|  | ||||
| .v-box { | ||||
|   padding-top: 1em; | ||||
|   padding-bottom: 1em; | ||||
| } | ||||
|  | ||||
| div { | ||||
|   overflow-wrap: break-word; | ||||
|   word-wrap: break-word; | ||||
| @@ -115,6 +147,11 @@ div { | ||||
|   padding-right: 10px; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Buttons | ||||
|  */ | ||||
|  | ||||
| body a.pure-button { | ||||
|   color: rgba(0,0,0,.8); | ||||
| } | ||||
| @@ -127,30 +164,48 @@ body a.pure-button-primary, | ||||
|   color: rgba(35, 35, 35, 1); | ||||
| } | ||||
|  | ||||
| button.pure-button-primary:hover, | ||||
| body a.pure-button-primary:hover, | ||||
| button.pure-button-primary:focus, | ||||
| body a.pure-button-primary:focus { | ||||
|   background-color: rgba(0, 182, 240, 1); | ||||
|   color: #fff; | ||||
| .pure-button-primary, | ||||
| .pure-button-secondary { | ||||
|   border: 1px solid #a0a0a0; | ||||
|   border-radius: 3px; | ||||
|   margin: 0 .4em; | ||||
| } | ||||
|  | ||||
| .pure-button-secondary.low-profile { | ||||
|   padding: 5px 10px; | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| /* Has to be combined with flex-left/right */ | ||||
| .button-container { | ||||
|   flex-flow: wrap; | ||||
|   gap: 0.5em 0.75em; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Video thumbnails | ||||
|  */ | ||||
|  | ||||
| div.thumbnail { | ||||
|   padding: 28.125%; | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| img.thumbnail { | ||||
|   position: absolute; | ||||
|   display: block; /* See: https://stackoverflow.com/a/11635197 */ | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   left: 0; | ||||
|   top: 0; | ||||
|   object-fit: cover; | ||||
| } | ||||
|  | ||||
| .thumbnail-placeholder { | ||||
|   min-height: 50px; | ||||
|   border: 2px dotted; | ||||
| } | ||||
|  | ||||
| div.watched-overlay { | ||||
|   z-index: 50; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
| @@ -168,30 +223,31 @@ div.watched-indicator { | ||||
|   background-color: red; | ||||
| } | ||||
|  | ||||
| .length { | ||||
| div.thumbnail > .top-left-overlay, | ||||
| div.thumbnail > .bottom-right-overlay { | ||||
|   z-index: 100; | ||||
|   position: absolute; | ||||
|   background-color: rgba(35, 35, 35, 0.75); | ||||
|   color: #fff; | ||||
|   border-radius: 2px; | ||||
|   padding: 2px; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
|   font-size: 16px; | ||||
|   right: 0.25em; | ||||
|   bottom: -0.75em; | ||||
| } | ||||
|  | ||||
| .watched { | ||||
|   z-index: 100; | ||||
|   position: absolute; | ||||
|   background-color: rgba(35, 35, 35, 0.75); | ||||
| .top-left-overlay { top: 0.6em; left: 0.6em; } | ||||
| .bottom-right-overlay { bottom: 0.6em; right: 0.6em; } | ||||
|  | ||||
| .length { | ||||
|   padding: 1px; | ||||
|   margin: -2px 0; | ||||
|   color: #fff; | ||||
|   border-radius: 2px; | ||||
|   padding: 4px 8px 4px 8px; | ||||
|   font-size: 16px; | ||||
|   left: 0.2em; | ||||
|   top: -0.7em; | ||||
|   border-radius: 3px; | ||||
| } | ||||
|  | ||||
| .length, .top-left-overlay button { | ||||
|   color: #eee; | ||||
|   background-color: rgba(35, 35, 35, 0.85) !important; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Navbar | ||||
|  */ | ||||
| @@ -267,6 +323,11 @@ input[type="search"]::-webkit-search-cancel-button { | ||||
|   margin-right: 1em; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Responsive rules | ||||
|  */ | ||||
|  | ||||
| @media only screen and (max-aspect-ratio: 16/9) { | ||||
|   .player-dimensions.vjs-fluid { | ||||
|     padding-top: 46.86% !important; | ||||
| @@ -285,20 +346,28 @@ input[type="search"]::-webkit-search-cancel-button { | ||||
|   .navbar > div { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|   } | ||||
|  | ||||
|   .navbar > div:not(:last-child) { | ||||
|     margin-bottom: 1em; | ||||
|     margin-bottom: 25px; | ||||
|   } | ||||
|  | ||||
|   .navbar > .searchbar > form { | ||||
|     width: 60%; | ||||
|     width: 75%; | ||||
|   } | ||||
|  | ||||
|   h1 { | ||||
|     font-size: 1.25em; | ||||
|     margin: 0.42em 0; | ||||
|   } | ||||
|  | ||||
|   /* Space out the subscribe & RSS buttons and align them to the left */ | ||||
|   .title.flexible { display: block; } | ||||
|   .title.flexible > .flex-right { margin: 0.75em 0; justify-content: flex-start; } | ||||
|  | ||||
|   /* Space out buttons to make them easier to tap */ | ||||
|   .user-field { font-size: 125%; } | ||||
|   .user-field > :not(:last-child) { margin-right: 1.75em; } | ||||
|  | ||||
|   .icon-buttons { font-size: 125%; } | ||||
|   .icon-buttons > :not(:last-child) { margin-right: 0.75em; } | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 320px) { | ||||
| @@ -315,10 +384,6 @@ input[type="search"]::-webkit-search-cancel-button { | ||||
|  | ||||
| .video-card-row { margin: 15px 0; } | ||||
|  | ||||
| .flexible { display: flex; } | ||||
| .flex-left  { flex: 1 1 100%; flex-wrap: wrap;   } | ||||
| .flex-right { flex: 1 0 auto; flex-wrap: nowrap; } | ||||
|  | ||||
| p.channel-name { margin: 0; } | ||||
| p.video-data   { margin: 0; font-weight: bold; font-size: 80%; } | ||||
|  | ||||
| @@ -347,6 +412,22 @@ p.video-data   { margin: 0; font-weight: bold; font-size: 80%; } | ||||
|   border: none; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Page navigation | ||||
|  */ | ||||
|  | ||||
| .page-nav-container { margin: 15px 0 30px 0; } | ||||
|  | ||||
| .page-prev-container { text-align: start; } | ||||
| .page-next-container { text-align: end; } | ||||
|  | ||||
| .page-prev-container, | ||||
| .page-next-container { | ||||
|   display: inline-block; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Footer | ||||
|  */ | ||||
| @@ -389,6 +470,7 @@ span > select { | ||||
|   word-wrap: normal; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Light theme | ||||
|  */ | ||||
| @@ -401,9 +483,18 @@ span > select { | ||||
|   color: #075A9E !important; | ||||
| } | ||||
|  | ||||
| .light-theme a.pure-button-primary:hover, | ||||
| .light-theme a.pure-button-primary:focus { | ||||
| .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 { | ||||
| @@ -431,9 +522,18 @@ span > select { | ||||
|     color: #075A9E !important; | ||||
|   } | ||||
|  | ||||
|   .no-theme a.pure-button-primary:hover, | ||||
|   .no-theme a.pure-button-primary:focus { | ||||
|   .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 { | ||||
| @@ -453,6 +553,7 @@ span > select { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Dark theme | ||||
|  */ | ||||
| @@ -465,6 +566,20 @@ span > select { | ||||
|   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: #a0a0a0; | ||||
|   text-decoration: none; | ||||
| @@ -505,6 +620,20 @@ body.dark-theme { | ||||
|     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: #a0a0a0; | ||||
|     text-decoration: none; | ||||
| @@ -539,6 +668,12 @@ body.dark-theme { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Miscellanous | ||||
|  */ | ||||
|  | ||||
|  | ||||
| /*With commit d9528f5 all contents of the page is now within a flexbox. However, | ||||
| the hr element is rendered improperly within one. | ||||
| See https://stackoverflow.com/a/34372979 for more info */ | ||||
| @@ -576,12 +711,7 @@ label[for="music-desc-expansion"]:hover { | ||||
| } | ||||
|  | ||||
| /* Bidi (bidirectional text) support */ | ||||
| h1, | ||||
| h2, | ||||
| h3, | ||||
| h4, | ||||
| h5, | ||||
| p, | ||||
| h1, h2, h3, h4, h5, p, | ||||
| #descriptionWrapper, | ||||
| #description-box, | ||||
| #music-description-box { | ||||
|   | ||||
| @@ -9,6 +9,11 @@ | ||||
|     "generic_subscribers_count_plural": "{{count}} subscribers", | ||||
|     "generic_subscriptions_count": "{{count}} subscription", | ||||
|     "generic_subscriptions_count_plural": "{{count}} subscriptions", | ||||
|     "generic_button_delete": "Delete", | ||||
|     "generic_button_edit": "Edit", | ||||
|     "generic_button_save": "Save", | ||||
|     "generic_button_cancel": "Cancel", | ||||
|     "generic_button_rss": "RSS", | ||||
|     "LIVE": "LIVE", | ||||
|     "Shared `x` ago": "Shared `x` ago", | ||||
|     "Unsubscribe": "Unsubscribe", | ||||
| @@ -170,6 +175,7 @@ | ||||
|     "Title": "Title", | ||||
|     "Playlist privacy": "Playlist privacy", | ||||
|     "Editing playlist `x`": "Editing playlist `x`", | ||||
|     "playlist_button_add_items": "Add videos", | ||||
|     "Show more": "Show more", | ||||
|     "Show less": "Show less", | ||||
|     "Watch on YouTube": "Watch on YouTube", | ||||
|   | ||||
| @@ -9,6 +9,11 @@ | ||||
|     "generic_subscribers_count_plural": "{{count}} abonnés", | ||||
|     "generic_subscriptions_count": "{{count}} abonnement", | ||||
|     "generic_subscriptions_count_plural": "{{count}} abonnements", | ||||
|     "generic_button_delete": "Supprimer", | ||||
|     "generic_button_edit": "Editer", | ||||
|     "generic_button_save": "Enregistrer", | ||||
|     "generic_button_cancel": "Annuler", | ||||
|     "generic_button_rss": "RSS", | ||||
|     "LIVE": "EN DIRECT", | ||||
|     "Shared `x` ago": "Ajoutée il y a `x`", | ||||
|     "Unsubscribe": "Se désabonner", | ||||
| @@ -149,6 +154,7 @@ | ||||
|     "Title": "Titre", | ||||
|     "Playlist privacy": "Paramètres de confidentialité de la liste de lecture", | ||||
|     "Editing playlist `x`": "Modifier la liste de lecture `x`", | ||||
|     "playlist_button_add_items": "Ajouter des vidéos", | ||||
|     "Show more": "Afficher plus", | ||||
|     "Show less": "Afficher moins", | ||||
|     "Watch on YouTube": "Voir la vidéo sur Youtube", | ||||
|   | ||||
							
								
								
									
										97
									
								
								src/invidious/frontend/pagination.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/invidious/frontend/pagination.cr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| require "uri" | ||||
|  | ||||
| module Invidious::Frontend::Pagination | ||||
|   extend self | ||||
|  | ||||
|   private def previous_page(str : String::Builder, locale : String?, url : String) | ||||
|     # Link | ||||
|     str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) | ||||
|  | ||||
|     if locale_is_rtl?(locale) | ||||
|       # Inverted arrow ("previous" points to the right) | ||||
|       str << translate(locale, "Previous page") | ||||
|       str << "  " | ||||
|       str << %(<i class="icon ion-ios-arrow-forward"></i>) | ||||
|     else | ||||
|       # Regular arrow ("previous" points to the left) | ||||
|       str << %(<i class="icon ion-ios-arrow-back"></i>) | ||||
|       str << "  " | ||||
|       str << translate(locale, "Previous page") | ||||
|     end | ||||
|  | ||||
|     str << "</a>" | ||||
|   end | ||||
|  | ||||
|   private def next_page(str : String::Builder, locale : String?, url : String) | ||||
|     # Link | ||||
|     str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) | ||||
|  | ||||
|     if locale_is_rtl?(locale) | ||||
|       # Inverted arrow ("next" points to the left) | ||||
|       str << %(<i class="icon ion-ios-arrow-back"></i>) | ||||
|       str << "  " | ||||
|       str << translate(locale, "Next page") | ||||
|     else | ||||
|       # Regular arrow ("next" points to the right) | ||||
|       str << translate(locale, "Next page") | ||||
|       str << "  " | ||||
|       str << %(<i class="icon ion-ios-arrow-forward"></i>) | ||||
|     end | ||||
|  | ||||
|     str << "</a>" | ||||
|   end | ||||
|  | ||||
|   def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true) | ||||
|     return String.build do |str| | ||||
|       str << %(<div class="h-box">\n) | ||||
|       str << %(<div class="page-nav-container flexible">\n) | ||||
|  | ||||
|       str << %(<div class="page-prev-container flex-left">) | ||||
|  | ||||
|       if current_page > 1 | ||||
|         params_prev = URI::Params{"page" => (current_page - 1).to_s} | ||||
|         url_prev = HttpServer::Utils.add_params_to_url(base_url, params_prev) | ||||
|  | ||||
|         self.previous_page(str, locale, url_prev.to_s) | ||||
|       end | ||||
|  | ||||
|       str << %(</div>\n) | ||||
|       str << %(<div class="page-next-container flex-right">) | ||||
|  | ||||
|       if show_next | ||||
|         params_next = URI::Params{"page" => (current_page + 1).to_s} | ||||
|         url_next = HttpServer::Utils.add_params_to_url(base_url, params_next) | ||||
|  | ||||
|         self.next_page(str, locale, url_next.to_s) | ||||
|       end | ||||
|  | ||||
|       str << %(</div>\n) | ||||
|  | ||||
|       str << %(</div>\n) | ||||
|       str << %(</div>\n\n) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?) | ||||
|     return String.build do |str| | ||||
|       str << %(<div class="h-box">\n) | ||||
|       str << %(<div class="page-nav-container flexible">\n) | ||||
|  | ||||
|       str << %(<div class="page-prev-container flex-left"></div>\n) | ||||
|  | ||||
|       str << %(<div class="page-next-container flex-right">) | ||||
|  | ||||
|       if !ctoken.nil? | ||||
|         params_next = URI::Params{"continuation" => ctoken} | ||||
|         url_next = HttpServer::Utils.add_params_to_url(base_url, params_next) | ||||
|  | ||||
|         self.next_page(str, locale, url_next.to_s) | ||||
|       end | ||||
|  | ||||
|       str << %(</div>\n) | ||||
|  | ||||
|       str << %(</div>\n) | ||||
|       str << %(</div>\n\n) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -165,3 +165,12 @@ def translate_bool(locale : String?, translation : Bool) | ||||
|     return translate(locale, "No") | ||||
|   end | ||||
| end | ||||
|  | ||||
| def locale_is_rtl?(locale : String?) | ||||
|   # Fallback to en-US | ||||
|   return false if locale.nil? | ||||
|  | ||||
|   # Arabic, Persian, Hebrew | ||||
|   # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts | ||||
|   return {"ar", "fa", "he"}.includes? locale | ||||
| end | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| require "uri" | ||||
|  | ||||
| module Invidious::HttpServer | ||||
|   module Utils | ||||
|     extend self | ||||
| @@ -16,5 +18,23 @@ module Invidious::HttpServer | ||||
|         return "#{url.request_target}?#{params}" | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def add_params_to_url(url : String | URI, params : URI::Params) : URI | ||||
|       url = URI.parse(url) if url.is_a?(String) | ||||
|  | ||||
|       url_query = url.query || "" | ||||
|  | ||||
|       # Append the parameters | ||||
|       url.query = String.build do |str| | ||||
|         if !url_query.empty? | ||||
|           str << url_query | ||||
|           str << '&' | ||||
|         end | ||||
|  | ||||
|         str << params | ||||
|       end | ||||
|  | ||||
|       return url | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -102,6 +102,10 @@ module Invidious::Routes::Feeds | ||||
|     end | ||||
|     env.set "user", user | ||||
|  | ||||
|     # Used for pagination links | ||||
|     base_url = "/feed/subscriptions" | ||||
|     base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") | ||||
|  | ||||
|     templated "feeds/subscriptions" | ||||
|   end | ||||
|  | ||||
| @@ -129,6 +133,10 @@ module Invidious::Routes::Feeds | ||||
|     end | ||||
|     watched ||= [] of String | ||||
|  | ||||
|     # Used for pagination links | ||||
|     base_url = "/feed/history" | ||||
|     base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") | ||||
|  | ||||
|     templated "feeds/history" | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -163,13 +163,20 @@ module Invidious::Routes::Playlists | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       videos = get_playlist_videos(playlist, offset: (page - 1) * 100) | ||||
|       items = get_playlist_videos(playlist, offset: (page - 1) * 100) | ||||
|     rescue ex | ||||
|       videos = [] of PlaylistVideo | ||||
|       items = [] of PlaylistVideo | ||||
|     end | ||||
|  | ||||
|     csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY) | ||||
|  | ||||
|     # Pagination | ||||
|     page_nav_html = Frontend::Pagination.nav_numeric(locale, | ||||
|       base_url: "/playlist?list=#{playlist.id}", | ||||
|       current_page: page, | ||||
|       show_next: (items.size == 100) | ||||
|     ) | ||||
|  | ||||
|     templated "edit_playlist" | ||||
|   end | ||||
|  | ||||
| @@ -247,11 +254,19 @@ module Invidious::Routes::Playlists | ||||
|  | ||||
|     begin | ||||
|       query = Invidious::Search::Query.new(env.params.query, :playlist, region) | ||||
|       videos = query.process.select(SearchVideo).map(&.as(SearchVideo)) | ||||
|       items = query.process.select(SearchVideo).map(&.as(SearchVideo)) | ||||
|     rescue ex | ||||
|       videos = [] of SearchVideo | ||||
|       items = [] of SearchVideo | ||||
|     end | ||||
|  | ||||
|     # Pagination | ||||
|     query_encoded = URI.encode_www_form(query.try &.text || "", space_to_plus: true) | ||||
|     page_nav_html = Frontend::Pagination.nav_numeric(locale, | ||||
|       base_url: "/add_playlist_items?list=#{playlist.id}&q=#{query_encoded}", | ||||
|       current_page: page, | ||||
|       show_next: (items.size >= 20) | ||||
|     ) | ||||
|  | ||||
|     env.set "add_playlist_items", plid | ||||
|     templated "add_playlist_items" | ||||
|   end | ||||
| @@ -418,7 +433,7 @@ module Invidious::Routes::Playlists | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       videos = get_playlist_videos(playlist, offset: (page - 1) * 200) | ||||
|       items = get_playlist_videos(playlist, offset: (page - 1) * 200) | ||||
|     rescue ex | ||||
|       return error_template(500, "Error encountered while retrieving playlist videos.<br>#{ex.message}") | ||||
|     end | ||||
| @@ -427,6 +442,13 @@ module Invidious::Routes::Playlists | ||||
|       env.set "remove_playlist_items", plid | ||||
|     end | ||||
|  | ||||
|     # Pagination | ||||
|     page_nav_html = Frontend::Pagination.nav_numeric(locale, | ||||
|       base_url: "/playlist?list=#{playlist.id}", | ||||
|       current_page: page, | ||||
|       show_next: (page_count != 1 && page < page_count) | ||||
|     ) | ||||
|  | ||||
|     templated "playlist" | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -52,24 +52,28 @@ module Invidious::Routes::Search | ||||
|       user = env.get? "user" | ||||
|  | ||||
|       begin | ||||
|         videos = query.process | ||||
|         items = query.process | ||||
|       rescue ex : ChannelSearchException | ||||
|         return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") | ||||
|       rescue ex | ||||
|         return error_template(500, ex) | ||||
|       end | ||||
|  | ||||
|       params = query.to_http_params | ||||
|       url_prev_page = "/search?#{params}&page=#{query.page - 1}" | ||||
|       url_next_page = "/search?#{params}&page=#{query.page + 1}" | ||||
|  | ||||
|       redirect_url = Invidious::Frontend::Misc.redirect_url(env) | ||||
|  | ||||
|       # Pagination | ||||
|       page_nav_html = Frontend::Pagination.nav_numeric(locale, | ||||
|         base_url: "/search?#{query.to_http_params}", | ||||
|         current_page: query.page, | ||||
|         show_next: (items.size >= 20) | ||||
|       ) | ||||
|  | ||||
|       if query.type == Invidious::Search::Query::Type::Channel | ||||
|         env.set "search", "channel:#{query.channel} #{query.text}" | ||||
|       else | ||||
|         env.set "search", query.text | ||||
|       end | ||||
|  | ||||
|       templated "search" | ||||
|     end | ||||
|   end | ||||
| @@ -91,16 +95,18 @@ module Invidious::Routes::Search | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       videos = Invidious::Hashtag.fetch(hashtag, page) | ||||
|       items = Invidious::Hashtag.fetch(hashtag, page) | ||||
|     rescue ex | ||||
|       return error_template(500, ex) | ||||
|     end | ||||
|  | ||||
|     params = env.params.query.empty? ? "" : "&#{env.params.query}" | ||||
|  | ||||
|     # Pagination | ||||
|     hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false) | ||||
|     url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}" | ||||
|     url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}" | ||||
|     page_nav_html = Frontend::Pagination.nav_numeric(locale, | ||||
|       base_url: "/hashtag/#{hashtag_encoded}", | ||||
|       current_page: page, | ||||
|       show_next: (items.size >= 60) | ||||
|     ) | ||||
|  | ||||
|     templated "hashtag" | ||||
|   end | ||||
|   | ||||
| @@ -31,33 +31,5 @@ | ||||
| </script> | ||||
| <script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script> | ||||
|  | ||||
| <div class="pure-g"> | ||||
|     <% videos.each_slice(4) do |slice| %> | ||||
|             <% slice.each do |item| %> | ||||
|                 <%= rendered "components/item" %> | ||||
|             <% end %> | ||||
|     <% end %> | ||||
| </div> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <% if query %> | ||||
|     <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%> | ||||
|     <div class="pure-g h-box"> | ||||
|         <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|             <% if query.page > 1 %> | ||||
|                 <a href="/add_playlist_items?list=<%= plid %>&q=<%= query_encoded %>&page=<%= page - 1 %>"> | ||||
|                     <%= 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 videos.size >= 20 %> | ||||
|                 <a href="/add_playlist_items?list=<%= plid %>&q=<%= query_encoded %>&page=<%= page + 1 %>"> | ||||
|                     <%= translate(locale, "Next page") %> | ||||
|                 </a> | ||||
|             <% end %> | ||||
|         </div> | ||||
|     </div> | ||||
| <% end %> | ||||
| <%= rendered "components/items_paginated" %> | ||||
|   | ||||
| @@ -17,7 +17,12 @@ | ||||
|  | ||||
|   youtube_url = "https://www.youtube.com#{relative_url}" | ||||
|   redirect_url = Invidious::Frontend::Misc.redirect_url(env) | ||||
| -%> | ||||
|  | ||||
|   page_nav_html = IV::Frontend::Pagination.nav_ctoken(locale, | ||||
|     base_url: relative_url, | ||||
|     ctoken: next_continuation | ||||
|   ) | ||||
| %> | ||||
|  | ||||
| <% content_for "header" do %> | ||||
| <%- if selected_tab.videos? -%> | ||||
| @@ -45,21 +50,5 @@ | ||||
|     <hr> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g"> | ||||
| <% items.each do |item| %> | ||||
|     <%= rendered "components/item" %> | ||||
| <% end %> | ||||
| </div> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-md-4-5"></div> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> | ||||
|         <% if next_continuation %> | ||||
|             <a href="<%= relative_url %>?continuation=<%= next_continuation %><% if sort_options.any? sort_by %>&sort_by=<%= sort_by %><% end %>"> | ||||
|                 <%= translate(locale, "Next page") %> | ||||
|             </a> | ||||
|         <% end %> | ||||
|     </div> | ||||
| </div> | ||||
| <%= rendered "components/items_paginated" %> | ||||
|   | ||||
| @@ -8,29 +8,30 @@ | ||||
|     </div> | ||||
| <% end %> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-2-3"> | ||||
| <div class="pure-g h-box flexible title"> | ||||
|     <div class="pure-u-1-2 flex-left flexible"> | ||||
|         <div class="channel-profile"> | ||||
|             <img src="/ggpht<%= channel_profile_pic %>" alt="" /> | ||||
|             <span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="pure-u-1-3"> | ||||
|         <h3 style="text-align:right"> | ||||
|             <a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a> | ||||
|         </h3> | ||||
|  | ||||
|     <div class="pure-u-1-2 flex-right flexible button-container"> | ||||
|         <div class="pure-u"> | ||||
|             <% sub_count_text = number_to_short_text(channel.sub_count) %> | ||||
|             <%= rendered "components/subscribe_widget" %> | ||||
|         </div> | ||||
|  | ||||
|         <div class="pure-u"> | ||||
|             <a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>"> | ||||
|                 <i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %> | ||||
|             </a> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="h-box"> | ||||
|     <div id="descriptionWrapper"> | ||||
|         <p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="h-box"> | ||||
|     <% sub_count_text = number_to_short_text(channel.sub_count) %> | ||||
|     <%= rendered "components/subscribe_widget" %> | ||||
|     <div id="descriptionWrapper"><p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p></div> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|   | ||||
| @@ -1,157 +1,146 @@ | ||||
| <% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %> | ||||
| <%- | ||||
|   thin_mode = env.get("preferences").as(Preferences).thin_mode | ||||
|   item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil | ||||
|   author_verified = item.responds_to?(:author_verified) && item.author_verified | ||||
| -%> | ||||
|  | ||||
| <div class="pure-u-1 pure-u-md-1-4"> | ||||
|     <div class="h-box"> | ||||
|         <% case item when %> | ||||
|         <% when SearchChannel %> | ||||
|             <a href="/channel/<%= item.ucid %>"> | ||||
|                 <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|             <% if !thin_mode %> | ||||
|                 <a tabindex="-1" href="/channel/<%= item.ucid %>"> | ||||
|                     <center> | ||||
|                         <img loading="lazy" tabindex="-1" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>" alt="" /> | ||||
|                         <img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>" alt="" /> | ||||
|                     </center> | ||||
|                 <% end %> | ||||
|                 <p dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></p> | ||||
|             </a> | ||||
|                 </a> | ||||
|             <%- else -%> | ||||
|                 <div class="thumbnail-placeholder" style="width:56.25%"></div> | ||||
|             <% end %> | ||||
|  | ||||
|             <div class="video-card-row flexible"> | ||||
|                 <div class="flex-left"><a href="/channel/<%= item.ucid %>"> | ||||
|                     <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> | ||||
|                         <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> | ||||
|                     </p> | ||||
|                 </a></div> | ||||
|             </div> | ||||
|  | ||||
|             <p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p> | ||||
|             <% if !item.auto_generated %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %> | ||||
|             <h5><%= item.description_html %></h5> | ||||
|         <% when SearchPlaylist, InvidiousPlaylist %> | ||||
|             <% if item.id.starts_with? "RD" %> | ||||
|                 <% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" %> | ||||
|             <% else %> | ||||
|                 <% url = "/playlist?list=#{item.id}" %> | ||||
|             <% end %> | ||||
|             <%- | ||||
|               if item.id.starts_with? "RD" | ||||
|                 link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" | ||||
|               else | ||||
|                 link_url = "/playlist?list=#{item.id}" | ||||
|               end | ||||
|             -%> | ||||
|  | ||||
|             <a style="width:100%" href="<%= url %>"> | ||||
|                 <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                     <div class="thumbnail"> | ||||
|                         <img loading="lazy" tabindex="-1" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>" alt="" /> | ||||
|                         <p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> | ||||
|                     </div> | ||||
|                 <% end %> | ||||
|                 <p dir="auto"><%= HTML.escape(item.title) %></p> | ||||
|             </a> | ||||
|             <a href="/channel/<%= item.ucid %>"> | ||||
|                 <p dir="auto"><b><%= HTML.escape(item.author) %><% if !item.is_a?(InvidiousPlaylist) && !item.author_verified.nil? && item.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></b></p> | ||||
|             </a> | ||||
|         <% when MixVideo %> | ||||
|             <a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>"> | ||||
|                 <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                     <div class="thumbnail"> | ||||
|                         <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> | ||||
|                         <% if item.length_seconds != 0 %> | ||||
|                             <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> | ||||
|                         <% end %> | ||||
|             <div class="thumbnail"> | ||||
|                 <%- if !thin_mode %> | ||||
|                     <a tabindex="-1" href="<%= link_url %>"> | ||||
|                         <img loading="lazy" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>" alt="" /> | ||||
|                     </a> | ||||
|                 <%- else -%> | ||||
|                     <div class="thumbnail-placeholder"></div> | ||||
|                 <%- end -%> | ||||
|  | ||||
|                         <% if item_watched %> | ||||
|                             <div class="watched-overlay"></div> | ||||
|                             <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div> | ||||
|                         <% end %> | ||||
|                     </div> | ||||
|                 <% end %> | ||||
|                 <p dir="auto"><%= HTML.escape(item.title) %></p> | ||||
|             </a> | ||||
|             <a href="/channel/<%= item.ucid %>"> | ||||
|                 <p dir="auto"><b><%= HTML.escape(item.author) %></b></p> | ||||
|             </a> | ||||
|         <% when PlaylistVideo %> | ||||
|             <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>"> | ||||
|                 <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                     <div class="thumbnail"> | ||||
|                         <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> | ||||
|                 <div class="bottom-right-overlay"> | ||||
|                     <p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|                         <% if plid_form = env.get?("remove_playlist_items") %> | ||||
|                             <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                                 <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                                 <p class="watched"> | ||||
|                                     <button type="submit" style="all:unset" data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>"><i class="icon ion-md-trash"></i></button> | ||||
|                                 </p> | ||||
|                             </form> | ||||
|                         <% end %> | ||||
|  | ||||
|                         <% if item.responds_to?(:live_now) && item.live_now %> | ||||
|                             <p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> | ||||
|                         <% elsif item.length_seconds != 0 %> | ||||
|                             <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> | ||||
|                         <% end %> | ||||
|  | ||||
|                         <% if item_watched %> | ||||
|                             <div class="watched-overlay"></div> | ||||
|                             <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div> | ||||
|                         <% end %> | ||||
|                     </div> | ||||
|                 <% end %> | ||||
|                 <p dir="auto"><%= HTML.escape(item.title) %></p> | ||||
|             </a> | ||||
|  | ||||
|             <div class="video-card-row flexible"> | ||||
|                 <div class="flex-left"><a href="/channel/<%= item.ucid %>"> | ||||
|                     <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %></p> | ||||
|                 </a></div> | ||||
|                 <% endpoint_params = "?v=#{item.id}&list=#{item.plid}" %> | ||||
|                 <%= rendered "components/video-context-buttons" %> | ||||
|             <div class="video-card-row"> | ||||
|                 <a href="<%= link_url %>"><p dir="auto"><%= HTML.escape(item.title) %></p></a> | ||||
|             </div> | ||||
|  | ||||
|             <div class="video-card-row flexible"> | ||||
|                 <div class="flex-left"> | ||||
|                     <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> | ||||
|                         <p dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p> | ||||
|                     <% elsif Time.utc - item.published > 1.minute %> | ||||
|                         <p dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p> | ||||
|                     <% end %> | ||||
|                 </div> | ||||
|  | ||||
|                 <% if item.responds_to?(:views) && item.views %> | ||||
|                 <div class="flex-right"> | ||||
|                     <p dir="auto"><%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %></p> | ||||
|                 </div> | ||||
|                 <% end %> | ||||
|                 <div class="flex-left"><a href="/channel/<%= item.ucid %>"> | ||||
|                     <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> | ||||
|                         <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> | ||||
|                     </p> | ||||
|                 </a></div> | ||||
|             </div> | ||||
|         <% when Category %> | ||||
|         <% else %> | ||||
|             <a style="width:100%" href="/watch?v=<%= item.id %>"> | ||||
|                 <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                     <div class="thumbnail"> | ||||
|                         <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> | ||||
|                         <% if env.get? "show_watched" %> | ||||
|                             <form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                                 <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                                 <p class="watched"> | ||||
|                                     <button type="submit" style="all:unset" data-onclick="mark_watched" data-id="<%= item.id %>"> | ||||
|                                         <i data-mouse="switch_classes" data-switch-classes="ion-ios-eye-off,ion-ios-eye" class="icon ion-ios-eye"></i> | ||||
|                                     </button> | ||||
|                                 </p> | ||||
|                             </form> | ||||
|                         <% elsif plid_form = env.get? "add_playlist_items" %> | ||||
|                             <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                                 <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                                 <p class="watched"> | ||||
|                                     <button type="submit" style="all:unset" data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button> | ||||
|                                 </p> | ||||
|                             </form> | ||||
|                         <% end %> | ||||
|             <%- | ||||
|               # `endpoint_params` is used for the "video-context-buttons" component | ||||
|               if item.is_a?(PlaylistVideo) | ||||
|                 link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" | ||||
|                 endpoint_params = "?v=#{item.id}&list=#{item.plid}" | ||||
|               elsif item.is_a?(MixVideo) | ||||
|                 link_url = "/watch?v=#{item.id}&list=#{item.rdid}" | ||||
|                 endpoint_params = "?v=#{item.id}&list=#{item.rdid}" | ||||
|               else | ||||
|                 link_url = "/watch?v=#{item.id}" | ||||
|                 endpoint_params = "?v=#{item.id}" | ||||
|               end | ||||
|             -%> | ||||
|  | ||||
|                         <% if item.responds_to?(:live_now) && item.live_now %> | ||||
|                             <p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> | ||||
|                         <% elsif item.length_seconds != 0 %> | ||||
|                             <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> | ||||
|                         <% end %> | ||||
|             <div class="thumbnail"> | ||||
|                 <%- if !thin_mode -%> | ||||
|                     <a tabindex="-1" href="<%= link_url %>"> | ||||
|                         <img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg" alt="" /> | ||||
|  | ||||
|                         <% if item_watched %> | ||||
|                             <div class="watched-overlay"></div> | ||||
|                             <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div> | ||||
|                         <% end %> | ||||
|                     </div> | ||||
|                 <% end %> | ||||
|                 <p dir="auto"><%= HTML.escape(item.title) %></p> | ||||
|             </a> | ||||
|                     </a> | ||||
|                 <%- else -%> | ||||
|                     <div class="thumbnail-placeholder"></div> | ||||
|                 <%- end -%> | ||||
|  | ||||
|                 <div class="top-left-overlay"> | ||||
|                     <%- if env.get? "show_watched" -%> | ||||
|                         <form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                             <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                             <button type="submit" class="pure-button pure-button-secondary low-profile" | ||||
|                                     data-onclick="mark_watched" data-id="<%= item.id %>"> | ||||
|                                 <i data-mouse="switch_classes" data-switch-classes="ion-ios-eye-off,ion-ios-eye" class="icon ion-ios-eye"></i> | ||||
|                             </button> | ||||
|                         </form> | ||||
|                     <%- end -%> | ||||
|  | ||||
|                     <%- if plid_form = env.get?("add_playlist_items") -%> | ||||
|                         <%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> | ||||
|                         <form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post"> | ||||
|                             <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                             <button type="submit" class="pure-button pure-button-secondary low-profile" | ||||
|                                     data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button> | ||||
|                         </form> | ||||
|                     <%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%> | ||||
|                         <%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> | ||||
|                         <form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post"> | ||||
|                             <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                             <button type="submit" class="pure-button pure-button-secondary low-profile" | ||||
|                                     data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>"><i class="icon ion-md-trash"></i></button> | ||||
|                         </form> | ||||
|                     <%- end -%> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="bottom-right-overlay"> | ||||
|                     <%- if item.responds_to?(:live_now) && item.live_now -%> | ||||
|                         <p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> | ||||
|                     <%- elsif item.length_seconds != 0 -%> | ||||
|                         <p class="length"><%= recode_length_seconds(item.length_seconds) %></p> | ||||
|                     <%- end -%> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="video-card-row"> | ||||
|                 <a href="<%= link_url %>"><p dir="auto"><%= HTML.escape(item.title) %></p></a> | ||||
|             </div> | ||||
|  | ||||
|             <div class="video-card-row flexible"> | ||||
|                 <div class="flex-left"><a href="/channel/<%= item.ucid %>"> | ||||
|                     <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %><% if !item.is_a?(ChannelVideo) && !item.author_verified.nil? && item.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></p> | ||||
|                     <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> | ||||
|                         <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> | ||||
|                     </p> | ||||
|                 </a></div> | ||||
|  | ||||
|                 <% endpoint_params = "?v=#{item.id}" %> | ||||
|                 <%= rendered "components/video-context-buttons" %> | ||||
|             </div> | ||||
|  | ||||
| @@ -159,7 +148,7 @@ | ||||
|                 <div class="flex-left"> | ||||
|                     <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> | ||||
|                         <p class="video-data" dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p> | ||||
|                     <% elsif Time.utc - item.published > 1.minute %> | ||||
|                     <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %> | ||||
|                         <p class="video-data" dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p> | ||||
|                     <% end %> | ||||
|                 </div> | ||||
|   | ||||
							
								
								
									
										11
									
								
								src/invidious/views/components/items_paginated.ecr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/invidious/views/components/items_paginated.ecr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <%= page_nav_html %> | ||||
|  | ||||
| <div class="pure-g"> | ||||
|     <%- items.each do |item| -%> | ||||
|         <%= rendered "components/item" %> | ||||
|     <%- end -%> | ||||
| </div> | ||||
|  | ||||
| <%= page_nav_html %> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
| @@ -1,22 +1,18 @@ | ||||
| <% if user %> | ||||
|     <% if subscriptions.includes? ucid %> | ||||
|         <p> | ||||
|             <form action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                 <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                 <button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary"> | ||||
|                     <b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b> | ||||
|                 </button> | ||||
|             </form> | ||||
|         </p> | ||||
|     <% else %> | ||||
|         <p> | ||||
|             <form action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                 <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                 <button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary"> | ||||
|                     <b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b> | ||||
|                 </button> | ||||
|             </form> | ||||
|         </p> | ||||
|     <% end %> | ||||
|  | ||||
|     <script id="subscribe_data" type="application/json"> | ||||
| @@ -33,10 +29,8 @@ | ||||
|     </script> | ||||
|     <script src="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>"></script> | ||||
| <% else %> | ||||
|     <p> | ||||
|         <a id="subscribe" class="pure-button pure-button-primary" | ||||
|             href="/login?referer=<%= env.get("current_page") %>"> | ||||
|             <b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b> | ||||
|         </a> | ||||
|     </p> | ||||
| <% end %> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <div class="flex-right"> | ||||
| <div class="flex-right flexible"> | ||||
|     <div class="icon-buttons"> | ||||
|         <a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" href="https://www.youtube.com/watch<%=endpoint_params%>"> | ||||
|             <i class="icon ion-logo-youtube"></i> | ||||
| @@ -6,7 +6,7 @@ | ||||
|         <a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1"> | ||||
|             <i class="icon ion-md-headset"></i> | ||||
|         </a> | ||||
|          | ||||
|  | ||||
|         <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> | ||||
|             <a title="<%=translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>"> | ||||
|                 <i class="icon ion-md-jet"></i> | ||||
|   | ||||
| @@ -6,35 +6,43 @@ | ||||
| <% end %> | ||||
|  | ||||
| <form class="pure-form" action="/edit_playlist?list=<%= plid %>" method="post"> | ||||
|     <div class="pure-g h-box"> | ||||
|         <div class="pure-u-2-3"> | ||||
|     <div class="h-box flexible"> | ||||
|         <div class="flex-right button-container"> | ||||
|             <div class="pure-u"> | ||||
|                 <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>"> | ||||
|                     <i class="icon ion-md-close"></i> <%= translate(locale, "generic_button_cancel") %> | ||||
|                 </a> | ||||
|             </div> | ||||
|             <div class="pure-u"> | ||||
|                 <button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit"> | ||||
|                     <i class="icon ion-md-save"></i> <%= translate(locale, "generic_button_save") %> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="pure-u"> | ||||
|                 <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> | ||||
|                     <i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %> | ||||
|                 </a> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="h-box flexible title"> | ||||
|         <div> | ||||
|             <h3><input class="pure-input-1" maxlength="150" name="title" type="text" value="<%= title %>"></h3> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="h-box"> | ||||
|         <div class="pure-u-1-1"> | ||||
|             <b> | ||||
|                 <%= HTML.escape(playlist.author) %> | | ||||
|                 <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | | ||||
|                 <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | | ||||
|                 <i class="icon <%= {"ion-md-globe", "ion-ios-unlock", "ion-ios-lock"}[playlist.privacy.value] %>"></i> | ||||
|                 <select name="privacy"> | ||||
|                 <% {"Public", "Unlisted", "Private"}.each do |option| %> | ||||
|                     <option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option> | ||||
|                 <% end %> | ||||
|                 </select> | ||||
|             </b> | ||||
|         </div> | ||||
|         <div class="pure-u-1-3" style="text-align:right"> | ||||
|             <h3> | ||||
|                 <div class="pure-g user-field"> | ||||
|                     <div class="pure-u-1-3"> | ||||
|                         <a href="javascript:void(0)"> | ||||
|                             <button type="submit" style="all:unset"> | ||||
|                                 <i class="icon ion-md-save"></i> | ||||
|                             </button> | ||||
|                         </a> | ||||
|                     </div> | ||||
|                     <div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div> | ||||
|                     <div class="pure-u-1-3"><a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a></div> | ||||
|                 </div> | ||||
|             </h3> | ||||
|             <select name="privacy"> | ||||
|             <%- {"Public", "Unlisted", "Private"}.each do |option| -%> | ||||
|                 <option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option> | ||||
|             <%- end -%> | ||||
|             </select> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
| @@ -44,40 +52,9 @@ | ||||
|     <input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>"> | ||||
| </form> | ||||
|  | ||||
| <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> | ||||
| <div class="h-box" style="text-align:right"> | ||||
|     <h3> | ||||
|         <a href="/add_playlist_items?list=<%= plid %>"><i class="icon ion-md-add"></i></a> | ||||
|     </h3> | ||||
| </div> | ||||
| <% end %> | ||||
|  | ||||
| <div class="h-box"> | ||||
|     <hr> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g"> | ||||
| <% videos.each do |item| %> | ||||
|     <%= rendered "components/item" %> | ||||
| <% end %> | ||||
| </div> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <% if page > 1 %> | ||||
|             <a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>"> | ||||
|                 <%= 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 videos.size == 100 %> | ||||
|             <a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>"> | ||||
|                 <%= translate(locale, "Next page") %> | ||||
|             </a> | ||||
|         <% end %> | ||||
|     </div> | ||||
| </div> | ||||
| <%= rendered "components/items_paginated" %> | ||||
|   | ||||
| @@ -31,39 +31,29 @@ | ||||
|     <% watched.each do |item| %> | ||||
|         <div class="pure-u-1 pure-u-md-1-4"> | ||||
|             <div class="h-box"> | ||||
|                 <a style="width:100%" href="/watch?v=<%= item %>"> | ||||
|                     <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                         <div class="thumbnail"> | ||||
|                             <img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg" alt="" /> | ||||
|                             <form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                                 <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                                 <p class="watched"> | ||||
|                                     <button type="submit" style="all:unset" data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button> | ||||
|                                 </p> | ||||
|                             </form> | ||||
|                         </div> | ||||
|                         <p></p> | ||||
|                     <% end %> | ||||
|                 </a> | ||||
|                 <div class="thumbnail"> | ||||
|                     <a style="width:100%" href="/watch?v=<%= item %>"> | ||||
|                         <img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg" alt="" /> | ||||
|                     </a> | ||||
|  | ||||
|                     <div class="top-left-overlay"><div class="watched"> | ||||
|                     <form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post"> | ||||
|                         <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> | ||||
|                         <button type="submit" class="pure-button pure-button-secondary low-profile" | ||||
|                                 data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button> | ||||
|                     </form> | ||||
|                     </div></div> | ||||
|                 </div> | ||||
|                 <p></p> | ||||
|             </div> | ||||
|         </div> | ||||
|     <% end %> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <% if page > 1 %> | ||||
|             <a href="/feed/history?page=<%= page - 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> | ||||
|                 <%= 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 watched.size >= max_results %> | ||||
|             <a href="/feed/history?page=<%= page + 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> | ||||
|                 <%= translate(locale, "Next page") %> | ||||
|             </a> | ||||
|         <% end %> | ||||
|     </div> | ||||
| </div> | ||||
| <%= | ||||
|   IV::Frontend::Pagination.nav_numeric(locale, | ||||
|     base_url: base_url, | ||||
|     current_page: page, | ||||
|     show_next: (watched.size >= max_results) | ||||
|   ) | ||||
| %> | ||||
|   | ||||
| @@ -56,6 +56,7 @@ | ||||
| </script> | ||||
| <script src="/js/watched_widget.js"></script> | ||||
|  | ||||
|  | ||||
| <div class="pure-g"> | ||||
| <% videos.each do |item| %> | ||||
|     <%= rendered "components/item" %> | ||||
| @@ -64,20 +65,10 @@ | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <% if page > 1 %> | ||||
|             <a href="/feed/subscriptions?page=<%= page - 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> | ||||
|                 <%= 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 (videos.size + notifications.size) == max_results %> | ||||
|             <a href="/feed/subscriptions?page=<%= page + 1 %><% if env.params.query["max_results"]? %>&max_results=<%= max_results %><% end %>"> | ||||
|                 <%= translate(locale, "Next page") %> | ||||
|             </a> | ||||
|         <% end %> | ||||
|     </div> | ||||
| </div> | ||||
| <%= | ||||
|   IV::Frontend::Pagination.nav_numeric(locale, | ||||
|     base_url: base_url, | ||||
|     current_page: page, | ||||
|     show_next: ((videos.size + notifications.size) == max_results) | ||||
|   ) | ||||
| %> | ||||
|   | ||||
| @@ -4,38 +4,5 @@ | ||||
|  | ||||
| <hr/> | ||||
|  | ||||
| <div class="pure-g h-box v-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <%- if page > 1 -%> | ||||
|             <a href="<%= url_prev_page %>"><%= 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 videos.size >= 60 -%> | ||||
|             <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g"> | ||||
|     <%- videos.each do |item| -%> | ||||
|         <%= rendered "components/item" %> | ||||
|     <%- end -%> | ||||
| </div> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <%- if page > 1 -%> | ||||
|             <a href="<%= url_prev_page %>"><%= 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 videos.size >= 60 -%> | ||||
|             <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
| </div> | ||||
| <%= rendered "components/items_paginated" %> | ||||
|   | ||||
| @@ -6,9 +6,50 @@ | ||||
| <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/playlist/<%= plid %>" /> | ||||
| <% end %> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-2-3"> | ||||
|         <h3><%= title %></h3> | ||||
| <div class="h-box flexible title"> | ||||
|     <div class="flex-left"><h3><%= title %></h3></div> | ||||
|  | ||||
|     <div class="flex-right button-container"> | ||||
|         <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> | ||||
|             <div class="pure-u"> | ||||
|                 <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>"> | ||||
|                     <i class="icon ion-md-add"></i> <%= translate(locale, "playlist_button_add_items") %> | ||||
|                 </a> | ||||
|             </div> | ||||
|             <div class="pure-u"> | ||||
|                 <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>"> | ||||
|                     <i class="icon ion-md-create"></i> <%= translate(locale, "generic_button_edit") %> | ||||
|                 </a> | ||||
|             </div> | ||||
|             <div class="pure-u"> | ||||
|                 <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> | ||||
|                     <i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %> | ||||
|                 </a> | ||||
|             </div> | ||||
|         <%- else -%> | ||||
|             <div class="pure-u"> | ||||
|                 <%- if IV::Database::Playlists.exists?(playlist.id) -%> | ||||
|                     <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>"> | ||||
|                         <i class="icon ion-md-add"></i> <%= translate(locale, "Subscribe") %> | ||||
|                     </a> | ||||
|                 <%- else -%> | ||||
|                     <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> | ||||
|                         <i class="icon ion-md-trash"></i> <%= translate(locale, "Unsubscribe") %> | ||||
|                     </a> | ||||
|                 <%- end -%> | ||||
|             </div> | ||||
|         <%- end -%> | ||||
|  | ||||
|         <div class="pure-u"> | ||||
|             <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>"> | ||||
|                 <i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %> | ||||
|             </a> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="h-box"> | ||||
|     <div class="pure-u-1-1"> | ||||
|         <% if playlist.is_a? InvidiousPlaylist %> | ||||
|             <b> | ||||
|                 <% if playlist.author == user.try &.email %> | ||||
| @@ -54,37 +95,12 @@ | ||||
|             </div> | ||||
|         <% end %> | ||||
|     </div> | ||||
|     <div class="pure-u-1-3" style="text-align:right"> | ||||
|         <h3> | ||||
|             <div class="pure-g user-field"> | ||||
|                 <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> | ||||
|                     <div class="pure-u-1-3"><a href="/edit_playlist?list=<%= plid %>"><i class="icon ion-md-create"></i></a></div> | ||||
|                     <div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div> | ||||
|                 <% else %> | ||||
|                     <% if Invidious::Database::Playlists.exists?(playlist.id) %> | ||||
|                         <div class="pure-u-1-3"><a href="/subscribe_playlist?list=<%= plid %>"><i class="icon ion-md-add"></i></a></div> | ||||
|                     <% else %> | ||||
|                         <div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div> | ||||
|                     <% end %> | ||||
|                 <% end %> | ||||
|                 <div class="pure-u-1-3"><a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a></div> | ||||
|             </div> | ||||
|         </h3> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="h-box"> | ||||
|     <div id="descriptionWrapper"><%= playlist.description_html %></div> | ||||
| </div> | ||||
|  | ||||
| <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> | ||||
| <div class="h-box" style="text-align:right"> | ||||
|     <h3> | ||||
|         <a href="/add_playlist_items?list=<%= plid %>"><i class="icon ion-md-add"></i></a> | ||||
|     </h3> | ||||
| </div> | ||||
| <% end %> | ||||
|  | ||||
| <div class="h-box"> | ||||
|     <hr> | ||||
| </div> | ||||
| @@ -100,28 +116,5 @@ | ||||
| <script src="/js/playlist_widget.js?v=<%= ASSET_COMMIT %>"></script> | ||||
| <% end %> | ||||
|  | ||||
| <div class="pure-g"> | ||||
| <% videos.each do |item| %> | ||||
|     <%= rendered "components/item" %> | ||||
| <% end %> | ||||
| </div> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <% if page > 1 %> | ||||
|             <a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>"> | ||||
|                 <%= 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 page_count != 1 && page < page_count %> | ||||
|             <a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>"> | ||||
|                 <%= translate(locale, "Next page") %> | ||||
|             </a> | ||||
|         <% end %> | ||||
|     </div> | ||||
| </div> | ||||
| <%= rendered "components/items_paginated" %> | ||||
|   | ||||
| @@ -7,21 +7,8 @@ | ||||
| <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> | ||||
| <hr/> | ||||
|  | ||||
| <div class="pure-g h-box v-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <%- if query.page > 1 -%> | ||||
|             <a href="<%= url_prev_page %>"><%= 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 videos.size >= 20 -%> | ||||
|             <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <%- if videos.empty? -%> | ||||
| <%- if items.empty? -%> | ||||
| <div class="h-box no-results-error"> | ||||
|     <div> | ||||
|         <%= translate(locale, "search_message_no_results") %><br/><br/> | ||||
| @@ -30,25 +17,5 @@ | ||||
|     </div> | ||||
| </div> | ||||
| <%- else -%> | ||||
| <div class="pure-g"> | ||||
|     <%- videos.each do |item| -%> | ||||
|         <%= rendered "components/item" %> | ||||
|     <%- end -%> | ||||
| </div> | ||||
|     <%= rendered "components/items_paginated" %> | ||||
| <%- end -%> | ||||
|  | ||||
| <script src="/js/watched_indicator.js"></script> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <%- if query.page > 1 -%> | ||||
|             <a href="<%= url_prev_page %>"><%= 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 videos.size >= 20 -%> | ||||
|             <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
| </div> | ||||
|   | ||||
| @@ -204,19 +204,28 @@ we're going to need to do it here in order to allow for translations. | ||||
|     </div> | ||||
|  | ||||
|     <div class="pure-u-1 <% if params.related_videos || plid %>pure-u-lg-3-5<% else %>pure-u-md-4-5<% end %>"> | ||||
|         <div class="h-box"> | ||||
|             <a href="/channel/<%= video.ucid %>" style="display:block;width:fit-content;width:-moz-fit-content"> | ||||
|                 <div class="channel-profile"> | ||||
|                     <% if !video.author_thumbnail.empty? %> | ||||
|                         <img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>" alt="" /> | ||||
|                     <% end %> | ||||
|                     <span id="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></span> | ||||
|  | ||||
|         <div class="pure-g h-box flexible title"> | ||||
|             <div class="pure-u-1-2 flex-left flexible"> | ||||
|                 <a href="/channel/<%= video.ucid %>"> | ||||
|                     <div class="channel-profile"> | ||||
|                         <% if !video.author_thumbnail.empty? %> | ||||
|                             <img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>" alt="" /> | ||||
|                         <% end %> | ||||
|                         <span id="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></span> | ||||
|                     </div> | ||||
|                 </a> | ||||
|             </div> | ||||
|  | ||||
|             <div class="pure-u-1-2 flex-right flexible button-container"> | ||||
|                 <div class="pure-u"> | ||||
|                     <% sub_count_text = video.sub_count_text %> | ||||
|                     <%= rendered "components/subscribe_widget" %> | ||||
|                 </div> | ||||
|             </a> | ||||
|  | ||||
|             <% sub_count_text = video.sub_count_text %> | ||||
|             <%= rendered "components/subscribe_widget" %> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="h-box"> | ||||
|             <p id="published-date"> | ||||
|                 <% if video.premiere_timestamp.try &.> Time.utc %> | ||||
|                     <b><%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b> | ||||
| @@ -295,15 +304,28 @@ we're going to need to do it here in order to allow for translations. | ||||
|  | ||||
|                     <% video.related_videos.each do |rv| %> | ||||
|                         <% if rv["id"]? %> | ||||
|                             <a href="/watch?v=<%= rv["id"] %>&listen=<%= params.listen %>"> | ||||
|                                 <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                                     <div class="thumbnail"> | ||||
|                             <div class="pure-u-1"> | ||||
|  | ||||
|                             <div class="thumbnail"> | ||||
|                                 <%- if !env.get("preferences").as(Preferences).thin_mode -%> | ||||
|                                     <a tabindex="-1" href="/watch?v=<%= rv["id"] %>&listen=<%= params.listen %>"> | ||||
|                                         <img loading="lazy" class="thumbnail" src="/vi/<%= rv["id"] %>/mqdefault.jpg" alt="" /> | ||||
|                                         <p class="length"><%= recode_length_seconds(rv["length_seconds"]?.try &.to_i? || 0) %></p> | ||||
|                                     </div> | ||||
|                                 <% end %> | ||||
|                                 <p style="width:100%"><%= rv["title"] %></p> | ||||
|                             </a> | ||||
|                                     </a> | ||||
|                                 <%- else -%> | ||||
|                                     <div class="thumbnail-placeholder"></div> | ||||
|                                 <%- end -%> | ||||
|  | ||||
|                                 <div class="bottom-right-overlay"> | ||||
|                                     <%- if (length_seconds = rv["length_seconds"]?.try &.to_i?) && length_seconds != 0 -%> | ||||
|                                         <p class="length"><%= recode_length_seconds(length_seconds) %></p> | ||||
|                                     <%- end -%> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <div class="video-card-row"> | ||||
|                                 <a href="/watch?v=<%= rv["id"] %>&listen=<%= params.listen %>"><p dir="auto"><%= HTML.escape(rv["title"]) %></p></a> | ||||
|                             </div> | ||||
|  | ||||
|                             <h5 class="pure-g"> | ||||
|                                 <div class="pure-u-14-24"> | ||||
|                                     <% if rv["ucid"]? %> | ||||
| @@ -321,6 +343,8 @@ we're going to need to do it here in order to allow for translations. | ||||
|                                     %></b> | ||||
|                                 </div> | ||||
|                             </h5> | ||||
|  | ||||
|                             </div> | ||||
|                         <% end %> | ||||
|                     <% end %> | ||||
|                 </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Samantaz Fox
					Samantaz Fox