From 18ce902aa07e761b8ac9830c583302a4c33eb719 Mon Sep 17 00:00:00 2001 From: "Dave Lage (rockerBOO)" Date: Fri, 25 Nov 2022 02:28:51 -0500 Subject: [PATCH 01/10] update local ports for docker --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index afda8726..905dff50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ +--- + version: "3" services: @@ -11,9 +13,10 @@ services: build: context: . dockerfile: docker/Dockerfile + image: rockerboo/invidious restart: unless-stopped ports: - - "127.0.0.1:3000:3000" + - "127.0.0.1:9999:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: From 71a30512c9891b7e7ac923b45228a12dc9e76a2e Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Wed, 30 Apr 2025 20:04:21 -0400 Subject: [PATCH 02/10] Updated styling, formatting, structure of frontend --- Makefile | 9 +- assets/css/animation.css | 9 + assets/css/default.css | 1657 +++++++++++------ assets/css/embed.css | 20 +- assets/css/empty.css | 36 +- assets/css/player.css | 499 +++-- assets/css/pure-fix.css | 73 + assets/css/search.css | 200 +- assets/css/theme-catppuccin-latte.css | 16 + assets/css/theme-catppuccin-macchiato.css | 16 + assets/css/theme-dracula.css | 16 + assets/css/theme-tokyonight.css | 16 + assets/css/theme.css | 59 + assets/js/comments.js | 68 +- assets/js/handlers.js | 6 +- assets/js/notifications.js | 250 ++- assets/js/playlist_widget.js | 33 +- assets/js/subscribe_widget.js | 40 +- assets/js/theme.js | 44 + assets/js/themes.js | 4 +- assets/js/watch.js | 2 + docker-compose.yml | 65 +- locales/en-US.json | 1 + src/invidious.cr | 3 + src/invidious/config.cr | 3 + src/invidious/frontend/channel_page.cr | 23 +- src/invidious/frontend/comments_youtube.cr | 171 +- src/invidious/frontend/pagination.cr | 44 +- src/invidious/frontend/search_filters.cr | 24 +- src/invidious/frontend/watch_page.cr | 14 +- src/invidious/mixes.cr | 6 +- src/invidious/playlists.cr | 32 +- src/invidious/routes/playlists.cr | 2 + src/invidious/routes/search.cr | 4 + src/invidious/views/add_playlist_items.ecr | 48 +- src/invidious/views/channel.ecr | 5 - src/invidious/views/community.ecr | 14 +- .../views/components/channel_info.ecr | 88 +- src/invidious/views/components/feed_menu.ecr | 10 +- src/invidious/views/components/item.ecr | 205 +- .../views/components/items_paginated.ecr | 2 +- src/invidious/views/components/search_box.ecr | 14 +- .../views/components/subscribe_widget.ecr | 12 +- .../components/video-context-buttons.ecr | 16 +- src/invidious/views/create_playlist.ecr | 55 +- src/invidious/views/edit_playlist.ecr | 80 +- src/invidious/views/feeds/history.ecr | 59 +- src/invidious/views/feeds/playlists.ecr | 41 +- src/invidious/views/feeds/popular.ecr | 4 +- src/invidious/views/feeds/subscriptions.ecr | 96 +- src/invidious/views/feeds/trending.ecr | 42 +- src/invidious/views/playlist.ecr | 188 +- src/invidious/views/search.ecr | 14 +- src/invidious/views/search_homepage.ecr | 16 +- src/invidious/views/template.ecr | 321 ++-- src/invidious/views/user/data_control.ecr | 34 +- src/invidious/views/user/login.ecr | 48 +- src/invidious/views/user/preferences.ecr | 622 ++++--- .../views/user/subscription_manager.ecr | 73 +- src/invidious/views/watch.ecr | 562 +++--- 60 files changed, 3683 insertions(+), 2451 deletions(-) create mode 100644 assets/css/animation.css create mode 100644 assets/css/pure-fix.css create mode 100644 assets/css/theme-catppuccin-latte.css create mode 100644 assets/css/theme-catppuccin-macchiato.css create mode 100644 assets/css/theme-dracula.css create mode 100644 assets/css/theme-tokyonight.css create mode 100644 assets/css/theme.css create mode 100644 assets/js/theme.js diff --git a/Makefile b/Makefile index ec22a0de..70a5de5e 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,6 @@ get-libs: invidious: get-libs crystal build src/invidious.cr $(FLAGS) --progress --stats --error-trace - run: invidious ./invidious @@ -73,6 +72,12 @@ verify: crystal build src/invidious.cr -Dskip_videojs_download \ --no-codegen --progress --stats --error-trace +dev: + crystal build src/invidious.cr -Dskip_videojs_download \ + --no-codegen --progress --stats --error-trace -- --disable-static-cache + +dev-reload: + tree -fiA --prune --noreport src | entr -rd make dev # ----------------------- # (Un)Install @@ -125,4 +130,4 @@ help: # No targets generates an output named after themselves .PHONY: all get-libs build amd64 run -.PHONY: format test verify clean distclean help +.PHONY: format test verify dev dev-reload clean distclean help diff --git a/assets/css/animation.css b/assets/css/animation.css new file mode 100644 index 00000000..aa5eda62 --- /dev/null +++ b/assets/css/animation.css @@ -0,0 +1,9 @@ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/assets/css/default.css b/assets/css/default.css index 2cedcf0c..03b97d12 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,42 +1,386 @@ /* - * Common attributes +f* Common attributes */ +:root { + --sans-serif: ui-rounded, "Hiragino Maru Gothic ProN", "Quicksand", + "Comfortaa", "Manjari", "Arial Rounded MT", "Arial Rounded MT Bold", + "Calibri", source-sans-pro, sans-serif; + --monospace: ui-monospace, "Cascadia Code", "Source Code Pro", "Menlo", + "Consolas", "DejaVu Sans Mono", monospace; + + /** Gap between sections */ + --gap: 2rem; + + /** Gap between parts in a section */ + --secondary-gap: 0.6rem; + + /** Radius for rounded corners */ + --radius: 0.3rem; + + /** Border size */ + --border-width: 0.2rem; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} html, body { - font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, - Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, - Arial, sans-serif; + font-family: var(--sans-serif); + background-color: var(--bg-color); + color: var(--fg-color); + overflow-wrap: break-word; + word-wrap: break-word; + max-width: 100%; } -#contents { +body, +input, +textarea { + font-size: 1rem; +} + +p { + line-height: 1.6; + max-width: 50em; +} + +h1, +h2, +h3, +h4, +button, +input, +textarea, +label { + line-height: 1; +} + +h1, +h2, +h3, +h4, +h5, +p { + unicode-bidi: plaintext; + text-align: start; +} + +@media (max-width: 30em) { + h1 { + font-size: 1.4em; + } + + h2 { + font-size: 1.3em; + } + + h2 { + font-size: 1.2em; + } + + .container h1 { + padding: 0 var(--secondary-gap); + } +} + +a { + text-decoration: none; + color: currentcolor; + transition: + opacity 96ms ease-in-out, + color 96ms ease-in-out, + text-decoration-color 96ms ease-in-out, + outline-width 96ms ease-in-out; +} + +p a { + text-decoration-line: underline; + text-decoration-color: var(--secondary-bg-color); + text-decoration-thickness: var(--border-width); +} + +body > footer a { + text-decoration-line: underline; + text-decoration-color: var(--secondary-bg-color); + text-decoration-thickness: var(--border-width); +} + +nav ul li > a { + padding: var(--secondary-gap); + border-radius: var(--radius); +} + +.menu a { + padding: var(--secondary-gap); + width: 100%; + height: 100%; + display: block; +} + +a:hover, +a:focus { + color: var(--accent-color); +} + +a:hover, +a:active { + text-decoration-color: var(--accent-bg-color); +} + +a:focus { + outline: var(--border-width) solid var(--secondary-bg-color); + outline-offset: calc(var(--secondary-gap) / 3); + border-radius: calc(var(--radius) / 2); + text-decoration: none; +} + +a:active { + outline: var(--border-width) solid var(--accent-bg-color); + outline-offset: calc(var(--secondary-gap) / 3); +} + +body > footer a:hover, +body > footer a:focus { + color: var(--accent-bg-color); + text-decoration-color: var(--accent-bg-color); +} + +nav ul li > a:hover, +nav ul li > a:focus { + outline: var(--border-width) solid var(--secondary-bg-color); + outline-offset: calc(var(--secondary-gap) / 3); +} + +nav ul li > a:active { + outline: var(--border-width) solid var(--accent-bg-color); + outline-offset: calc(var(--secondary-gap) / 3); +} + +textarea { + background-color: var(--secondary-bg-color); + border: var(--border-width) solid var(--secondary-bg-color); + border-radius: var(--radius); +} + +/* Make images easier to work with */ +img, +picture { + max-width: 100%; + display: block; +} + +hr { + border: none; + border-top: 1px solid var(--secondary-color); + border-bottom: 1px solid black; +} + +select, +input[type="text"], +input[type="search"], +input[type="password"], +input[type="number"], +textarea { + font-size: inherit; + padding: var(--secondary-gap); + color: var(--fg-color); + background-color: var(--secondary-bg-color); + border: 1px solid var(--secondary-bg-color); + box-shadow: unset; + border-radius: var(--radius); +} + +/** TODO reduce this selector */ +fieldset div:has(input[type="checkbox"]):not(.control-group), +fieldset div:has(input[type="radio"]):not(.control-group) { + padding: var(--secondary-gap); + border-radius: var(--radius); display: flex; + align-items: center; +} + +fieldset div:has(input[type="checkbox"]:checked + label), +fieldset div:has(input[type="radio"]:checked + label) { + background-color: var(--secondary-bg-color); +} + +input[type="checkbox"], +input[type="radio"] { + min-width: 1em; + min-height: 1em; +} + +fieldset { + border: var(--border-width) solid var(--secondary-bg-color); + border-radius: var(--radius); + margin: var(--gap) 0; + padding: var(--secondary-gap); +} + +textarea:not([cols]) { + min-width: 20em; +} + +textarea:not([rows]) { + min-height: 10em; +} + +label { + cursor: pointer; +} + +legend { + font-size: 1em; + background-color: var(--secondary-bg-color); + border-radius: var(--radius); + padding: var(--secondary-gap) calc(var(--secondary-gap) * 2); + text-transform: uppercase; +} + +/* + * Footer + */ + +body > footer { + padding: calc(var(--gap) * 2) 0; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr; + grid-gap: var(--gap); +} + +@media (max-width: 30em) { + body > footer { + grid-template-columns: 1fr; + padding: var(--secondary-gap); + grid-gap: var(--secondary-gap); + } +} + +nav { + display: flex; + justify-content: space-between; + align-items: center; +} + +nav ul { + list-style: none; + display: flex; + gap: var(--secondary-gap); + padding: 0; + margin: 0; + align-items: flex-start; +} + +nav ul li { + border-radius: var(--radius); + min-width: 1.5em; + min-height: 1.5em; + display: flex; + align-items: center; + justify-content: center; +} + +nav ul li.selected { + background-color: var(--secondary-bg-color); + font-weight: bold; +} + +body > footer nav > ul { flex-direction: column; + align-items: flex-start; + gap: 0; +} + +body > footer nav > ul > li { + padding: var(--secondary-gap) 0; +} + +fieldset > select, +span > select { + color: rgb(49 49 51); +} + +/** +* Main content container +*/ +#contents { min-height: 100vh; - margin: auto; } -.h-box { - padding-left: 1em; - padding-right: 1em; +.container { + margin-right: auto; + margin-left: auto; + max-width: 100%; } -.v-box { - padding-top: 1em; - padding-bottom: 1em; +@media (min-width: 36em) { + .container { + max-width: 31.875em; + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 48em) { + .container { + max-width: 43.75em; + } +} + +@media (min-width: 64em) { + .container { + max-width: 60em; + } +} + +@media (min-width: 80em) { + .container { + max-width: 75em; + } +} + +@media (min-width: 96em) { + .container { + max-width: 90em; + } +} + +body > header { + padding: var(--secondary-gap) 0; + opacity: 0.7; + transition: opacity 240ms ease-in-out; +} + +body > header:hover, +body > header:focus-within { + opacity: 1; +} + +time { + font-style: italic; + font-size: 0.95em; + text-wrap: nowrap; + cursor: help; +} + +/** +* Formatting for raw text blocks (comments, description) +*/ +.raw-text { + white-space: pre-wrap; } .deleted { - background-color: rgb(255, 0, 0, 0.5); + background-color: rgb(255 0 0 / 50%); } .underlined { - border-bottom: 1px solid; - margin-bottom: 20px; -} - -.title { - margin: 0.5em 0 1em 0; + border-bottom: 0.125rem solid; + margin-bottom: 1.25rem; } /* A flex container */ @@ -51,6 +395,7 @@ body { flex-flow: row wrap; justify-content: flex-start; } + .flex-right { display: flex; flex: 2 0 auto; @@ -58,84 +403,126 @@ body { justify-content: flex-end; } - /* * Channel page */ -.channel-profile > * { - font-size: 1.17em; - font-weight: bold; - vertical-align: middle; +.title { + display: flex; + justify-content: space-between; + gap: var(--secondary-gap); + margin: var(--secondary-gap) 0; +} + +@media (max-width: 30em) { + .title { + flex-direction: column; + } +} + +.channel-profile { + display: flex; + align-items: center; + gap: var(--secondary-gap); +} + +#channel-name { + margin: 0; +} + +.profile-pic { border-radius: 50%; } +.channel-profile > h3 { + font-size: 1.17em; + font-weight: bold; + vertical-align: middle; + text-wrap: nowrap; +} + +.channel-profile > h4 { + display: flex; + gap: var(--secondary-gap); +} + .channel-profile > img { - width: 48px; + width: 2rem; height: auto; + border-radius: 50%; } body a.channel-owner { - background-color: #008bec; - color: #fff; - border-radius: 9px; - padding: 1px 6px; + background-color: var(--accent-bg-color); + border-radius: var(--radius); + padding: 0 calc(var(--secondary-gap) * 2); +} + +body a.channel-owner:hover { + color: var(--fg-color); } .creator-heart-container { display: inline-block; - padding: 0px 7px 6px 0px; - margin: 0px -7px -4px 0px; + padding: 0 0.4375rem 0.375rem 0; + margin: 0 -0.4375rem -0.25rem 0; } .creator-heart { display: inline-block; position: relative; - width: 16px; - height: 16px; - border: 2px none; + width: 1rem; + height: 1rem; + border: 0.125rem none; } .creator-heart-background-hearted { - width: 16px; - height: 16px; - padding: 0px; + width: 1em; + height: 1em; + padding: 0; position: relative; } .creator-heart-small-hearted { position: absolute; - right: -7px; - bottom: -4px; + right: -0.4375rem; + bottom: -0.25rem; } .creator-heart-small-container { display: block; position: relative; - width: 13px; - height: 13px; - color: rgb(255, 0, 0); + width: 0.8125rem; + height: 0.8125rem; + color: rgb(255 0 0); +} + +.menu { + display: flex; + justify-content: space-between; + margin: var(--gap) 0; +} + +.menu li { + padding: 0; +} + +nav.filters { + margin: var(--gap) 0; } .feed-menu { display: flex; justify-content: center; flex-wrap: wrap; + gap: var(--secondary-gap); + margin: var(--gap) 0; } -.feed-menu-item { +.feed-menu li { text-align: center; -} - -@media screen and (max-width: 640px) { - .feed-menu-item { - flex: 0 0 40%; - } -} - -div { - overflow-wrap: break-word; - word-wrap: break-word; + background-color: var(--secondary-bg-color); + text-transform: uppercase; } .loading { @@ -143,46 +530,302 @@ div { animation: spin 2s linear infinite; } -.playlist-restricted { - height: 20em; - padding-right: 10px; +.subscription-menu { + display: flex; + justify-content: space-between; + align-items: center; + margin: var(--gap) 0; } +.subscription-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.subscribe { + background-color: var(--accent-bg-color); +} + +.unsubscribe { + background-color: var(--secondary-bg-color); +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-group { + display: flex; + justify-content: space-between; + align-items: center; +} + +.playlist-header { + display: flex; + justify-content: space-between; +} + +.playlist-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin: var(--gap) 0; +} + +.playlist-meta ul { + list-style: none; + display: flex; + gap: var(--secondary-gap); + margin: 0; + padding: 0; +} + +.playlist-restricted { + height: 20em; + padding-right: 0.625rem; +} + +/* +* Watch +*/ +.watch-meta { + opacity: 0.6; + transition: opacity 96ms ease-in-out; +} + +.watch-meta:hover, +.watch-meta:focus-within { + opacity: 1; +} + +.watch-meta nav ul { + flex-direction: column; + align-items: flex-start; +} + +.watch-meta nav li { + padding: 0; +} + +.watch { + display: grid; + grid-template-columns: 1fr 3fr 1fr; + grid-gap: var(--gap); + max-width: 100%; +} + +@media (max-width: 30em) { + .watch { + grid-template-columns: 1fr; + padding: 0 var(--secondary-gap); + } +} + +.watch:has(details[open]) { + grid-template-columns: 3fr 1fr; +} + +/* Initially hide the content of the details section */ +details[open] ~ .watch-meta { + display: block; +} + +/* Hide the grid item when the details box is closed */ +details:not([open]) ~ .watch-meta { + display: none; +} + +.watch-context { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.watch-context nav ul { + flex-direction: column; +} + +.video-header { + display: flex; + justify-content: space-between; +} + +.listen { + display: flex; + align-items: center; + font-size: 1.25em; +} + +.video-meta { + display: flex; + gap: var(--secondary-gap); + justify-content: space-between; +} + +@media (max-width: 30em) { + .video-meta { + flex-direction: column; + } +} + +.video-interaction-meta { + display: flex; + gap: var(--secondary-gap); + padding-inline: var(--secondary-gap); +} + +@media (max-width: 30em) { + .video-interaction-meta { + padding-inline: 0; + } +} + +.content-meta { + list-style: none; + margin: 0; + padding: 0; +} /* * Buttons */ -body a.pure-button { - color: rgba(0,0,0,.8); +button { + background-color: transparent; + border: none; +} + +button, +*[role="button"], +*[type="submit"], +summary { + line-height: 1; + font-size: inherit; + color: currentcolor; + border: unset; + border-radius: var(--radius); + padding: var(--secondary-gap) calc(var(--secondary-gap) * 2); + transition: outline-width 96ms ease-in-out; + outline-width: 0; + outline-style: solid; + outline-color: var(--secondary-bg-color); + outline-offset: calc(var(--border-width) / 1.5); + white-space: nowrap; +} + +/** prevent inner elements from being click targets */ +button > *, +*[role="button"] > *, +*[type="submit"] > * { + pointer-events: none; +} + +button:focus, +*[role="button"]:focus, +*[type="submit"]:focus, +*[type="number"]:focus, +summary:focus, +select:focus { + outline-width: var(--border-width); +} + +button:active, +*[role="button"]:active, +*[type="submit"]:active, +*[type="number"]:active, +summary:active, +select:active { + transform: translate(0.125rem 0.125rem); + outline-color: var(--accent-bg-color); +} + +button:hover, +*[role="button"]:hover, +*[type="submit"]:hover, +*[type="number"]:hover, +summary:hover, +select:hover { + outline-width: var(--border-width); +} + +a[role="button"] { + text-decoration: none; + color: currentcolor; } button.pure-button-primary, -body a.pure-button-primary, +a.pure-button-primary, .channel-owner:hover, .channel-owner:focus { - background-color: #a0a0a0; - color: rgba(35, 35, 35, 1); + background-color: var(--accent-bg-color); } -.pure-button-primary, +.primary, +.pure-button-primary { + font-weight: bold; + background-color: var(--accent-bg-color); +} + +.secondary, .pure-button-secondary { - border: 1px solid #a0a0a0; - border-radius: 3px; - margin: 0 .4em; + color: var(--secondary-color); + background-color: var(--secondary-bg-color); } -.pure-button-secondary.low-profile { - padding: 5px 10px; - margin: 0; +.pure-button-primary:hover, +.pure-button-primary:focus { + border-color: var(--accent-color); + background-color: var(--accent-bg-color); +} + +.secondary:hover, +.secondary:focus, +.pure-button-secondary:hover, +.pure-button-secondary:focus { + border-color: var(--secondary-color); + background-color: var(--secondary-bg-color); } -/* Has to be combined with flex-left/right */ .button-container { - flex-flow: wrap; - gap: 0.5em 0.75em; + display: flex; + gap: var(--secondary-gap); + align-items: center; } +.controls { + display: grid; + gap: var(--secondary-gap); +} + +.control-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--secondary-gap); + place-items: center flex-start; +} + +.control-message { + display: flex; + align-items: center; + gap: var(--secondary-gap); +} + +.action-controls { + display: grid; + padding: var(--secondary-gap); + padding-left: calc(50% + (var(--secondary-gap) / 2)); + gap: var(--gap); + place-items: center flex-start; + background-color: var(--secondary-bg-color); + border-radius: var(--radius); +} + +.control-group label { + justify-self: flex-end; +} /* * Video thumbnails @@ -191,229 +834,436 @@ body a.pure-button-primary, div.thumbnail { position: relative; width: 100%; - box-sizing: border-box; + background-color: var(--secondary-bg-color); } img.thumbnail { - display: block; /* See: https://stackoverflow.com/a/11635197 */ width: 100%; + max-width: 100%; + max-height: 100%; object-fit: cover; aspect-ratio: 16 / 9; } .thumbnail-placeholder { - min-height: 50px; - border: 2px dotted; + min-height: calc(var(--gap) * 2); + background-color: var(--secondary-bg-color); } div.watched-overlay { z-index: 50; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(255,255,255,.4); + inset: 0; + background-color: var(--watched-overlay-color); } div.watched-indicator { position: absolute; left: 0; bottom: 0; - height: 4px; + height: 0.25rem; width: 100%; - background-color: red; + background-color: var(--accent-bg-color); } -div.thumbnail > .top-left-overlay, -div.thumbnail > .bottom-right-overlay { - z-index: 100; - position: absolute; - padding: 0; - margin: 0; - font-size: 16px; +.top-left-overlay { + top: var(--secondary-gap); + left: var(--secondary-gap); } -.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: 3px; + bottom: var(--secondary-gap); + right: var(--secondary-gap); + z-index: 100; + position: absolute; + padding: 0 var(--secondary-gap); + border-radius: var(--radius); + color: white; + background-color: hsl(175deg 0 0% / 90%); + line-height: 1; } -.length, .top-left-overlay button { - color: #eee; - background-color: rgba(35, 35, 35, 0.85) !important; +div.thumbnail > .top-left-overlay { + z-index: 100; + position: absolute; } +.length, +.top-left-overlay button { + opacity: 0.9; +} + +/* +* Related videos +*/ + +.related-videos { + display: grid; + grid-gap: var(--gap); + grid-template-columns: 18rem; +} + +/* +* Videos +*/ + +.videos { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(20em, 1fr)); + grid-gap: var(--gap); +} + +.notifications { + margin-bottom: var(--gap); +} + +/* +* Video card +*/ + +.video-card { + display: flex; + gap: var(--secondary-gap); + flex-direction: column; +} + +.video-card h3, +.video-card h4 { + margin: 0; + font-weight: normal; + line-height: 1.1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.video-card p { + font-size: 0.9em; +} + +.video-meta-sub { + display: grid; + grid-template-columns: auto auto; + justify-content: space-between; + align-items: center; + opacity: 0.7; + transition: opacity 96ms ease-in-out; +} + +.video-meta-sub h3 { + margin: 0; +} + +.video-meta-sub:hover, +.video-meta-sub:focus-within { + opacity: 1; +} + +.video-card.hide { + opacity: 0.3; + pointer-events: none; +} /* * Navbar */ .navbar { - margin: 1em 0; - display: flex; /* this is also defined in framework, but in case of future changes */ + display: grid; + grid-template-columns: 1fr 2fr 1fr; + grid-template-areas: "index-link site-search user-nav"; align-items: center; + grid-gap: var(--secondary-gap); justify-content: space-between; } -.navbar > div { - flex: 1; +@media (max-width: 30em) { + .navbar { + grid-template-columns: 1fr 1fr; + grid-template-areas: + "index-link user-nav" + "site-search site-search"; + } } -.searchbar { - flex-grow: 2; /* take double the space of the other items */ +.navbar ul:nth-child(3) { + justify-self: flex-end; } -.navbar a { - padding: 0; /* this way it will stay aligned with content under */ -} - -.navbar .index-link { - font-weight: bold; - display: inline; -} - -.searchbar .pure-form { - display: flex; -} - -.searchbar .pure-form fieldset { +.navbar li { padding: 0; - flex: 1; + text-wrap: nowrap; } -.searchbar input[type="search"] { - width: 100%; - margin: 1px; - - border: 1px solid; - border-color: rgba(0,0,0,0); - border-bottom-color: #CCC; - border-radius: 0; - - box-shadow: none; - appearance: none; - -webkit-appearance: none; +#site-search { + display: flex; + justify-content: center; + grid-area: site-search; } -.searchbar input[type="search"]:focus { - margin: 0; - border: 2px solid; - border-color: rgba(0,0,0,0); - border-bottom-color: #FED; +#index-link { + grid-area: index-link; +} + +#user-nav { + grid-area: user-nav; + display: flex; + align-items: center; +} + +@media (max-width: 30em) { + #index-link { + margin-left: var(--secondary-gap); + } + + #user-nav { + margin-right: var(--secondary-gap); + } +} + +.login, +.logout { + all: unset; +} + +.login:hover, +.logout:hover, +.login:focus, +.logout:focus { + color: var(--accent-color); +} + +.login:hover, +.login:active, +.logout:hover, +.logout:active { + text-decoration-color: var(--accent-bg-color); +} + +.search-box { + display: flex; + align-items: center; + position: relative; +} + +.search-box:focus-within button { + transform: translateX(3em); +} + +.search-box button { + position: absolute; + left: 19em; + transition: transform 96ms ease-in-out; +} + +@media (max-width: 30em) { + .search-box button { + display: none; + } +} + +.search { + width: 20em; +} + +input[type="search"]:hover, +input[type="text"]:hover, +input[type="password"]:hover, +textarea:hover { + outline: var(--border-width) solid var(--secondary-bg-color); + outline-offset: var(--border-width); +} + +input[type="search"]:focus, +input[type="text"]:focus, +input[type="password"]:focus, +textarea:focus { + outline: var(--border-width) solid var(--accent-color); + outline-offset: var(--border-width); } /* https://stackoverflow.com/a/55170420 */ input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; - height: 14px; - width: 14px; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAn0lEQVR42u3UMQrDMBBEUZ9WfQqDmm22EaTyjRMHAlM5K+Y7lb0wnUZPIKHlnutOa+25Z4D++MRBX98MD1V/trSppLKHqj9TTBWKcoUqffbUcbBBEhTjBOV4ja4l4OIAZThEOV6jHO8ARXD+gPPvKMABinGOrnu6gTNUawrcQKNCAQ7QeTxORzle3+sDfjJpPCqhJh7GixZq4rHcc9l5A9qZ+WeBhgEuAAAAAElFTkSuQmCC); - background-size: 14px; + height: 1em; + width: 1em; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAn0lEQVR42u3UMQrDMBBEUZ9WfQqDmm22EaTyjRMHAlM5K+Y7lb0wnUZPIKHlnutOa+25Z4D++MRBX98MD1V/trSppLKHqj9TTBWKcoUqffbUcbBBEhTjBOV4ja4l4OIAZThEOV6jHO8ARXD+gPPvKMABinGOrnu6gTNUawrcQKNCAQ7QeTxORzle3+sDfjJpPCqhJh7GixZq4rHcc9l5A9qZ+WeBhgEuAAAAAElFTkSuQmCC"); + background-size: 1em; } -.searchbar #searchbutton { - border: none; - background: none; - margin-top: 0; +/* #searchbutton { */ +/* border: none; */ +/* background: none; */ +/* margin-top: 0; */ +/* } */ +/**/ +/* #searchbutton:hover { */ +/* color: var(--accent-color); */ +/* } */ +.search-action { + background-color: var(--secondary-bg-color); + padding: var(--secondary-gap); } -.searchbar #searchbutton:hover { - color: rgb(0, 182, 240); -} - -.user-field { - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; -} - -.user-field div { - width: auto; -} - -.user-field div:not(:last-child) { - margin-right: 1em; -} - - /* * Responsive rules */ -@media only screen and (max-aspect-ratio: 16/9) { - .player-dimensions.vjs-fluid { - padding-top: 46.86% !important; - } - - #player-container { - padding-bottom: 46.86% !important; - } -} - -@media screen and (max-width: 767px) { - .navbar { - flex-direction: column; - } - - .navbar > div { - display: flex; - justify-content: center; - margin-bottom: 25px; - } - - .navbar > .searchbar > form { - 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) { - .navbar > .searchbar > form { - width: 100%; - margin: 0 1em; - } -} - - +/* @media only screen and (max-aspect-ratio: 16/9) { */ +/* .player-dimensions.vjs-fluid { */ +/* padding-top: 46.86%; */ +/* } */ +/**/ +/* #player-container { */ +/* padding-bottom: 46.86%; */ +/* } */ +/* } */ +/**/ /* * Video "cards" (results/playlist/channel videos) */ -.video-card-row { margin: 15px 0; } +.video-card-row { + margin: 0.9375rem 0; +} -p.channel-name { margin: 0; } -p.video-data { margin: 0; font-weight: bold; font-size: 80%; } +.item-block h3 { + font-weight: normal; +} +.icon-buttons ul { + gap: 0; +} + +.icon-buttons li { + padding: 0; +} + +.icon-buttons li a { + padding: var(--secondary-gap); +} /* * Comments & community posts */ +.community { + margin: var(--gap) auto; +} + +.comments-header { + font-size: 0.9em; + margin: var(--gap) 0; +} + +.comments-header h3 { + font-weight: normal; +} + +.comments-header li { + display: flex; + padding: 0; + align-items: center; + gap: var(--secondary-gap); + text-wrap: nowrap; +} + +@media (max-width: 30em) { + .comments-header ul { + flex-direction: column; + } +} + .comments { - max-width: 800px; - margin: auto; + max-width: 50rem; + display: grid; + grid-gap: var(--gap); + justify-content: stretch; +} + +.replies { + padding-left: var(--secondary-gap); + border-left: 0.2em solid var(--secondary-bg-color); +} + +@media (max-width: 30em) { + .replies { + padding-left: var(--secondary-gap); + } +} + +.comment { + display: flex; + flex-direction: column; +} + +.comment h4 { + margin: 0; + font-size: 1em; + opacity: 0.6; + transition: opacity 96ms ease-in-out; +} + +.comment h4:hover { + opacity: 1; +} + +.comment-header { + display: grid; + grid-auto-flow: column; + align-items: center; + gap: var(--gap); + justify-content: space-between; + font-size: 0.9em; +} + +@media (max-width: 30em) { + .comment-header { + gap: var(--secondary-gap); + justify-content: stretch; + } + + .comment-header .comment-meta-sub { + justify-self: flex-end; + } +} + +ul.comment-meta-sub { + display: flex; + list-style: none; + gap: var(--secondary-gap); + padding: 0; + margin: 0; + opacity: 0.7; + transition: opacity 240ms ease-in-out; +} + +ul.comment-meta-sub:hover, +ul.comment-meta-sub:focus { + opacity: 1; +} + +.comment-meta-sub li { + text-wrap: nowrap; +} + +.replies button { + font-size: 0.95em; +} + +.comment-temporal { + display: flex; + gap: var(--secondary-gap); + align-items: center; + align-content: center; +} + +.comment-temporal time { + opacity: 0.7; + transition: opacity 96ms ease-in-out; +} + +.comment-temporal time:hover { + opacity: 1; } /* @@ -439,344 +1289,61 @@ 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 - */ - -footer { - margin-top: auto; - padding: 1.5em 0; - text-align: center; - max-height: 30vh; -} - -.light-theme footer { - color: #7c7c7c; -} - -.dark-theme footer { - color: #adadad; -} - -.light-theme footer a { - color: #7c7c7c !important; -} - -.dark-theme footer a { - color: #adadad !important; -} - -footer span { - margin: 4px 0; - display: block; -} - -/* keyframes */ - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -fieldset > select, -span > select { - color: rgba(49, 49, 51, 1); +.pagination { + margin: calc(var(--gap) / 2) 0; } .pure-control-group label { word-wrap: normal; } - -/* - * Light theme - */ - -.light-theme a:hover, -.light-theme a:active, -.light-theme summary:hover, -.light-theme a:focus, -.light-theme summary:focus { - color: #075A9E !important; -} - -.light-theme .pure-button-primary:hover, -.light-theme .pure-button-primary:focus, -.light-theme .pure-button-secondary:hover, -.light-theme .pure-button-secondary:focus { - color: #fff !important; - border-color: rgba(0, 182, 240, 0.75) !important; - background-color: rgba(0, 182, 240, 0.75) !important; -} - -.light-theme .pure-button-secondary:not(.low-profile) { - color: #335d7a; - background-color: #fff2; -} - -.light-theme a { - color: #335d7a; - text-decoration: none; -} - -/* All links that do not fit with the default color goes here */ -.light-theme a:not([data-id]) > .icon, -.light-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"], -.light-theme .playlist-restricted > ol > li > a { - color: #303030; -} - -.light-theme .pure-menu-heading { - color: #565d64; -} - -@media (prefers-color-scheme: light) { - .no-theme a:hover, - .no-theme a:active, - .no-theme summary:hover, - .no-theme a:focus, - .no-theme summary:focus { - color: #075A9E !important; - } - - .no-theme .pure-button-primary:hover, - .no-theme .pure-button-primary:focus, - .no-theme .pure-button-secondary:hover, - .no-theme .pure-button-secondary:focus { - color: #fff !important; - border-color: rgba(0, 182, 240, 0.75) !important; - background-color: rgba(0, 182, 240, 0.75) !important; - } - - .no-theme .pure-button-secondary:not(.low-profile) { - color: #335d7a; - background-color: #fff2; - } - - .no-theme a { - color: #335d7a; - text-decoration: none; - } - - /* All links that do not fit with the default color goes here */ - .no-theme a:not([data-id]) > .icon, - .no-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"], - .no-theme .playlist-restricted > ol > li > a { - color: #303030; - } - - .no-theme footer { - color: #7c7c7c; - } - - .no-theme footer a { - color: #7c7c7c !important; - } - - .light-theme .pure-menu-heading { - color: #565d64; - } -} - - -/* - * Dark theme - */ - -.dark-theme a:hover, -.dark-theme a:active, -.dark-theme summary:hover, -.dark-theme a:focus, -.dark-theme summary:focus { - color: rgb(0, 182, 240); -} - -.dark-theme .pure-button-primary:hover, -.dark-theme .pure-button-primary:focus, -.dark-theme .pure-button-secondary:hover, -.dark-theme .pure-button-secondary:focus { - color: #fff !important; - border-color: rgb(0, 182, 240) !important; - background-color: rgba(0, 182, 240, 1) !important; -} - -.dark-theme .pure-button-secondary { - background-color: #0002; - color: #ddd; -} - -.dark-theme a { - color: #adadad; - text-decoration: none; -} - -body.dark-theme { - background-color: rgba(35, 35, 35, 1); - color: #f0f0f0; -} - -.dark-theme .pure-form legend { - color: #f0f0f0; -} - -.dark-theme .pure-menu-heading { - color: #f0f0f0; -} - -.dark-theme input, -.dark-theme select, -.dark-theme textarea { - color: rgba(35, 35, 35, 1); -} - -.dark-theme .pure-form input[type="file"] { - color: #f0f0f0; -} - -.dark-theme .searchbar input { - background-color: inherit; - color: inherit; -} - -@media (prefers-color-scheme: dark) { - .no-theme a:hover, - .no-theme a:active, - .no-theme a:focus { - color: rgb(0, 182, 240); - } - - .no-theme .pure-button-primary:hover, - .no-theme .pure-button-primary:focus, - .no-theme .pure-button-secondary:hover, - .no-theme .pure-button-secondary:focus { - color: #fff !important; - border-color: rgb(0, 182, 240) !important; - background-color: rgba(0, 182, 240, 1) !important; - } - - .no-theme .pure-button-secondary { - background-color: #0002; - color: #ddd; - } - - .no-theme a { - color: #adadad; - text-decoration: none; - } - - body.no-theme { - background-color: rgba(35, 35, 35, 1); - color: #f0f0f0; - } - - .no-theme .pure-form legend { - color: #f0f0f0; - } - - .no-theme .pure-menu-heading { - color: #f0f0f0; - } - - .no-theme input, - .no-theme select, - .no-theme textarea { - color: rgba(35, 35, 35, 1); - } - - .no-theme .pure-form input[type="file"] { - color: #f0f0f0; - } - - .no-theme .searchbar input { - background-color: inherit; - color: inherit; - } - - .no-theme footer { - color: #adadad; - } - - .no-theme footer a { - color: #adadad !important; - } -} - - /* * 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 */ -hr { - margin: 10px 0 10px 0; -} - -/* Description Expansion Styling*/ -#descexpansionbutton, +/* Description Expansion Styling */ +#description-expansion, #music-desc-expansion { display: none; } -#descexpansionbutton ~ div { +#description-expansion ~ p, +#music-desc-expansion ~ p { overflow: hidden; } -#descexpansionbutton:not(:checked) ~ div { +#description-expansion:not(:checked) ~ p { max-height: 8.3em; } -#descexpansionbutton:checked ~ div { +#description-expansion:checked ~ p { overflow: unset; height: 100%; } -#descexpansionbutton ~ label { +#description-expansion ~ label { order: 1; - margin-top: 20px; + margin-top: 1.25rem; } -label[for="descexpansionbutton"]:hover, +label[for="description-expansion"]:hover, label[for="music-desc-expansion"]:hover { cursor: pointer; } /* Bidi (bidirectional text) support */ -h1, h2, h3, h4, h5, p, -#descriptionWrapper, +.raw-text, +#description, #description-box, #music-description-box { unicode-bidi: plaintext; text-align: start; } -#descriptionWrapper { - max-width: 600px; - white-space: pre-wrap; -} - #music-description-box { display: none; } @@ -797,22 +1364,56 @@ h1, h2, h3, h4, h5, p, /* Select all the music items except the first one */ .music-item + .music-item { - border-top: 1px solid #ffffff; + border-top: 1px solid white; } /* Center the "invidious" logo on the search page */ -#logo > h1 { text-align: center; } - -/* IE11 fixes */ -:-ms-input-placeholder { color: #888; } - -/* Wider settings name to less word wrap */ -.pure-form-aligned .pure-control-group label { width: 19em; } +#logo > h1 { + text-align: center; +} .channel-emoji { - margin: 0 2px; + margin: 0 0.125rem; } #download_widget { - width: 100%; + width: 100%; +} + +.watch-action-group { + margin: var(--gap) 0; + display: grid; + grid-gap: var(--secondary-gap); +} + +.subscriptions { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-gap: var(--gap); +} + +.subscriptions > div { + border: 0.15em solid var(--secondary-bg-color); + border-radius: var(--radius); + padding: 1em; +} + +.secondary-button { + border: none; + padding: 0.6em 1em; + background-color: var(--secondary-bg-color); + color: var(--secondary-color); +} + +fieldset.preferences, +fieldset.form { + display: grid; + grid-gap: var(--secondary-gap); + padding-block: var(--gap); + margin: var(--gap) 0; +} + +#views, +#likes { + text-wrap: nowrap; } diff --git a/assets/css/embed.css b/assets/css/embed.css index cbafcfea..74188b69 100644 --- a/assets/css/embed.css +++ b/assets/css/embed.css @@ -10,18 +10,18 @@ } .watch-on-invidious { - font-size: 1.3em !important; + font-size: 1.3em; font-weight: bold; white-space: nowrap; - margin: 0 1em 0 1em !important; + margin: 0 1em; order: 3; } +/**/ +/* .watch-on-invidious > a { */ +/* color: white; */ +/* } */ -.watch-on-invidious > a { - color: white; -} - -.watch-on-invidious > a:hover, -.watch-on-invidious > a:focus { - color: rgba(0, 182, 240, 1);; -} +/* .watch-on-invidious > a:hover, */ +/* .watch-on-invidious > a:focus { */ +/* color: ; */ +/* } */ diff --git a/assets/css/empty.css b/assets/css/empty.css index 6ad1515d..94ff4aab 100644 --- a/assets/css/empty.css +++ b/assets/css/empty.css @@ -1,16 +1,32 @@ + +/* Widget covers the whole page */ #search-widget { - text-align: center; - margin: 20vh 0 50px 0; + position: absolute; + height: 100%; + width: 100%; + display: grid; + justify-content: center; + grid-gap: var(--gap); + top: 0; + left: 0; + pointer-events: none; } -#logo > h1 { - font-size: 3.5em; - margin: 0; - padding: 0; +.search-homepage { + display: grid; + grid-template-rows: 1fr 1fr; } -@media screen and (max-width: 1500px) and (max-height: 1000px) { - #logo > h1 { - font-size: 10vmin; - } +#search-widget > h1 { + align-self: flex-end; + font-size: 3em; + text-transform: uppercase; + margin: 0; + padding: 0; + text-align: center; +} + +.searchbar { + /* reset pointer events for interactive components */ + pointer-events: initial; } diff --git a/assets/css/player.css b/assets/css/player.css index 9cb400ad..ff730a3d 100644 --- a/assets/css/player.css +++ b/assets/css/player.css @@ -1,92 +1,106 @@ +.video-js { + font-family: inherit; + font-size: inherit; +} + /* Youtube player style */ -.video-js.player-style-youtube .vjs-progress-control { - height: 0; -} +/* .video-js.player-style-youtube .vjs-progress-control { */ +/* height: 0; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, */ +/* .video-js.player-style-youtube .vjs-progress-control { */ +/* position: absolute; */ +/* right: 0; */ +/* left: 0; */ +/* width: 100%; */ +/* margin: 0; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-control-bar { */ +/* background: linear-gradient(rgb(0 0 0 / 10%), rgba(0 0 0 / 50%)); */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-slider { */ +/* background-color: rgb(255 255 255 / 20%); */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-load-progress > div { */ +/* background-color: rgb(255 255 255 / 20%); */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-play-progress { */ +/* background-color: var(--accent-bg-color); */ +/* } */ +/**/ +/* .video-js.player-style-youtube */ +/* .vjs-progress-control:hover */ +/* .vjs-progress-holder { */ +/* font-size: 1em; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-control-bar > .vjs-spacer { */ +/* flex: 1; */ +/* order: 2; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-play-progress .vjs-time-tooltip { */ +/* display: none; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-play-progress::before { */ +/* color: var(--accent-bg-color); */ +/* font-size: 0.85em; */ +/* display: none; */ +/* } */ +/**/ +/* .video-js.player-style-youtube */ +/* .vjs-progress-holder:hover */ +/* .vjs-play-progress::before { */ +/* display: unset; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-control-bar { */ +/* display: flex; */ +/* flex-direction: row; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-big-play-button { */ +/* /* */ +/* Styles copied from video-js.min.css, definition of */ +/* .vjs-big-play-centered .vjs-big-play-button */ +/* */ +*/ +/* top: 50%; */ +/* left: 50%; */ +/* margin-top: -0.8167em; */ +/* margin-left: -1.5em; */ +/* } */ -.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control { - position: absolute; - right: 0; - left: 0; - width: 100%; - margin: 0; -} +/* .video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu { */ +/* margin-bottom: 2em; */ +/* padding-top: 2em; */ +/* } */ +/**/ +/* .video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, */ +/* .video-js.player-style-youtube .vjs-progress-control { */ +/* height: 0.3em; */ +/* margin-bottom: 0.6em; */ +/* } */ +/**/ +/* ul.vjs-menu-content::-webkit-scrollbar { */ +/* display: none; */ +/* } */ +/**/ +/* .vjs-user-inactive { */ +/* cursor: none; */ +/* } */ -.video-js.player-style-youtube .vjs-control-bar { - background: linear-gradient(rgba(0,0,0,0.1), rgba(0, 0, 0,0.5)); -} - -.video-js.player-style-youtube .vjs-slider { - background-color: rgba(255,255,255,0.2); -} - -.video-js.player-style-youtube .vjs-load-progress > div { - background-color: rgba(255,255,255,0.5); -} - -.video-js.player-style-youtube .vjs-play-progress { - background-color: red; -} - -.video-js.player-style-youtube .vjs-progress-control:hover .vjs-progress-holder { - font-size: 15px; -} - -.video-js.player-style-youtube .vjs-control-bar > .vjs-spacer { - flex: 1; - order: 2; -} - -.video-js.player-style-youtube .vjs-play-progress .vjs-time-tooltip { - display: none; -} - -.video-js.player-style-youtube .vjs-play-progress::before { - color: red; - font-size: 0.85em; - display: none; -} - -.video-js.player-style-youtube .vjs-progress-holder:hover .vjs-play-progress::before { - display: unset; -} - -.video-js.player-style-youtube .vjs-control-bar { - display: flex; - flex-direction: row; -} - -.video-js.player-style-youtube .vjs-big-play-button { - /* - Styles copied from video-js.min.css, definition of - .vjs-big-play-centered .vjs-big-play-button - */ - top: 50%; - left: 50%; - margin-top: -0.81666em; - margin-left: -1.5em; -} - -.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu { - margin-bottom: 2em; - padding-top: 2em -} - -.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px; -margin-bottom: 10px;} - -ul.vjs-menu-content::-webkit-scrollbar { - display: none; -} - -.vjs-user-inactive { - cursor: none; -} - -.video-js .vjs-text-track-display > div > div > div { - background-color: rgba(0, 0, 0, 0.75) !important; - border-radius: 9px !important; - padding: 5px !important; -} +/* .video-js .vjs-text-track-display > div > div > div { */ +/* background-color: rgb(0 0 0 / 75%); */ +/* border-radius: 0.5em; */ +/* padding: 0.3em; */ +/* } */ .vjs-play-control, .vjs-volume-panel, @@ -123,143 +137,252 @@ ul.vjs-menu-content::-webkit-scrollbar { order: 7; } -.vjs-playback-rate > .vjs-menu { - width: 50px; +/* .vjs-playback-rate > .vjs-menu { */ +/* width: 3.2em; */ +/* } */ +/**/ + +/* Make the video relative, instead of absolute, so that + the parent container will size based on the video. Also, + note the max-height rule. Note the line-height 0 is to prevent + a small artifact on the bottom of the video. + https://stackoverflow.com/questions/46747320/limit-the-height-in-videojs-in-fluid-mode/47039499#47039499 + */ +.video-js.vjs-fluid, +.video-js.vjs-16-9, +.video-js.vjs-4-3, +video.video-js, +video.vjs-tech { + max-height: 85vh; + position: relative !important; + width: 100%; + height: auto; + max-width: 100% !important; + padding-top: 0 !important; + line-height: 0; } -.vjs-control-bar { +.video-js.vjs-16-9 { + /* Keep the video spaced to fit */ + aspect-ratio: 16 / 9; +} + +.video-js.vjs-4-3 { + /* Keep the video spaced to fit */ + aspect-ratio: 4 / 3; +} + +#player { + /* Default to 16/9 video spacing */ + aspect-ratio: 16 / 9; + max-width: 100%; +} + +.vjs-error .vjs-error-display:before, +.video-js .vjs-volume-tooltip, +.video-js .vjs-time-tooltip, +.vjs-menu .vjs-menu-content { + font-family: inherit; +} + +.vjs-menu-button-popup .vjs-menu .vjs-menu-content { + background-color: var(--secondary-bg-color-dark); +} + +.vjs-menu li.vjs-menu-item:focus, +.vjs-menu li.vjs-menu-item:hover, +.js-focus-visible .vjs-menu li.vjs-menu-item:hover { + background-color: var(--accent-bg-color-dark); +} + +.video-js .vjs-slider { + background-color: rgb(166 166 166 / 50%); +} + +.video-js .vjs-load-progress { + background-color: rgb(227 227 227 / 75%); +} + +@media (max-width: 480px) { + #player { + /* Default to 16/9 video spacing */ + aspect-ratio: unset; + } +} + +.vjs-fullscreen video { + max-height: 100vh !important; +} + +.video-js .vjs-control-bar { display: flex; flex-direction: row; scrollbar-width: none; + /* Fix the control bar due to us resetting the line-height on the video-js */ + line-height: 1; + background: linear-gradient( + to bottom, + transparent 0%, + var(--secondary-bg-color-dark) 50% + ); +} + +.vjs-control-bar button:hover, +.vjs-control-bar button:focus { + outline-width: 0; } .vjs-control-bar::-webkit-scrollbar { display: none; } -.video-js .vjs-icon-cog { - font-size: 18px; +.vjs-playback-rate .vjs-playback-rate-value { + font-size: 1em; + line-height: 3rem; } -.video-js .vjs-control-bar, -.vjs-menu-button-popup .vjs-menu .vjs-menu-content { - background-color: rgba(35, 35, 35, 0.75); +.vjs-button > .vjs-icon-placeholder::before { + font-size: 1.25em; + line-height: 3rem; } -.vjs-menu li.vjs-menu-item:focus, -.vjs-menu li.vjs-menu-item:hover { - background-color: rgba(255, 255, 255, 0.75); - color: rgba(49, 49, 51, 0.75); +.vjs-menu li { + font-size: 1em; } -.vjs-menu li.vjs-selected, -.vjs-menu li.vjs-selected:focus, -.vjs-menu li.vjs-selected:hover { - background-color: rgba(0, 182, 240, 0.75); +.video-js .vjs-control { + width: 3em; } -/* Progress Bar */ -.video-js .vjs-slider { - background-color: rgba(15, 15, 15, 0.5); +.video-js .vjs-time-control { + width: auto; } -.video-js .vjs-load-progress, -.video-js .vjs-load-progress div { - background: rgba(87, 87, 88, 1); +.vjs-poster { + background-size: cover; } +.video-js .vjs-share__short-link-wrapper { + color: var(--fg-color-dark); + background-color: var(--secondary-bg-color-dark); + height: 2em; + margin: 0 auto; + margin-bottom: var(--secondary-gap); + border-radius: var(--radius); +} + +.video-js .vjs-share__short-link { + padding: var(--secondary-gap); + background-color: var(--secondary-bg-color-dark); + color: var(--fg-color-dark); + font-family: var(--monospace); + min-width: 15em; +} + +.video-js .vjs-share__btn { + background-color: var(--secondary-bg-color-dark); + color: var(--fg-color-dark); + height: 2em; + width: 2.25em; + padding: var(--secondary-gap); +} + +.video-js .vjs-share__title { + font-size: 1.25em; + color: var(--fg-color-dark); +} + +.video-js .vjs-share__subtitle { + font-size: 1em; + color: var(--fg-color-dark); + margin: 0 auto var(--secondary-gap); +} + +.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button { + height: var(--gap); +} + +.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button::before { + content: ""; +} + +.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder::before { + line-height: 1.67; +} + +.video-js .vjs-share__middle { + padding: 0 var(--secondary-gap); +} + +.vjs-modal-dialog .vjs-modal-dialog-content { + font-size: 1em; + line-height: 1.1; +} + +/**/ +/* .video-js .vjs-icon-cog { */ +/* font-size: 1em; */ +/* } */ +/**/ + +/* .video-js .vjs-control-bar, */ +/* .vjs-menu-button-popup .vjs-menu .vjs-menu-content { */ +/* background-color: rgb(35 35 35 / 75%); */ +/* } */ +/**/ +/* .vjs-menu li.vjs-menu-item:focus, */ +/* .vjs-menu li.vjs-menu-item:hover { */ +/* background-color: rgb(255 255 255 / 75%); */ +/* color: rgb(49 49 51 / 75%); */ +/* } */ +/**/ +/* .vjs-menu li.vjs-selected, */ +/* .vjs-menu li.vjs-selected:focus, */ +/* .vjs-menu li.vjs-selected:hover { */ +/* background-color: rgb(0 182 240 / 75%); */ +/* } */ +/**/ +/* /* Progress Bar */ +*/ +/* .video-js .vjs-slider { */ +/* background-color: rgb(15 15 15 / 50%); */ +/* } */ +/**/ +/* .video-js .vjs-load-progress, */ +/* .video-js .vjs-load-progress div { */ +/* background: rgb(87 87 88); */ +/* } */ +/**/ .video-js .vjs-slider:hover, .video-js button:hover { - color: rgba(0, 182, 240, 1); + background-color: var(--accent-bg-color); } .video-js.player-style-invidious .vjs-play-progress { - background-color: rgba(0, 182, 240, 1); + background-color: var(--accent-bg-color); } -/* Overlay */ -.video-js .vjs-overlay { - background-color: rgba(35, 35, 35, 0.75) !important; -} -.video-js .vjs-overlay * { - color: rgba(255, 255, 255, 1) !important; - text-align: center; +.vjs-modal-dialog .vjs-modal-dialog-content { + padding: var(--gap); } -/* ProgressBar marker */ -.vjs-marker { - background-color: rgba(255, 255, 255, 1); - z-index: 0; -} - -/* Big "Play" Button */ .video-js .vjs-big-play-button { - background-color: rgba(35, 35, 35, 0.5); + font-size: var(--gap); + line-height: var(--gap); + height: var(--gap); + width: calc(var(--gap) * 2); + top: var(--secondary-gap); + left: var(--secondary-gap); + background-color: var(--secondary-bg-color); + border: none; + opacity: 0.7; + transition: opacity 240ms; } -.video-js:hover .vjs-big-play-button { - background-color: rgba(35, 35, 35, 0.75); +.video-js .vjs-big-play-button:hover { + opacity: 0.9; } -.video-js .vjs-current-time, -.video-js .vjs-time-divider, -.video-js .vjs-duration { - display: block; -} - -.video-js .vjs-time-divider { - min-width: 0px; - padding-left: 0px; - padding-right: 0px; -} - -.video-js .vjs-poster { - background-size: cover; - object-fit: cover; -} - -.player-dimensions.vjs-fluid { - padding-top: 82vh; -} - -video.video-js { - position: absolute; - height: 100%; -} - -#player-container { - position: relative; - padding-left: 0; - padding-right: 0; - margin-left: 1em; - margin-right: 1em; - padding-bottom: 82vh; - height: 0; -} - -.mobile-operations-bar { - display: flex; - position: absolute; - top: 0; - right: 1px !important; - left: initial !important; - width: initial !important; -} - -.mobile-operations-bar ul { - position: absolute !important; - bottom: unset !important; - top: 1.5em; -} - -@media screen and (max-width: 700px) { - .video-js .vjs-share { - justify-content: unset; - } -} - -@media screen and (max-width: 650px) { - .vjs-modal-dialog-content { - overflow-x: hidden; - } +.vjs-control-bar { + background-color: var(--secondary-bg-color-dark); } diff --git a/assets/css/pure-fix.css b/assets/css/pure-fix.css new file mode 100644 index 00000000..4aceaa96 --- /dev/null +++ b/assets/css/pure-fix.css @@ -0,0 +1,73 @@ +/** fixes for pure to support our colors */ +.pure-form input[type="color"], +.pure-form input[type="date"], +.pure-form input[type="datetime-local"], +.pure-form input[type="datetime"], +.pure-form input[type="email"], +.pure-form input[type="month"], +.pure-form input[type="number"], +.pure-form input[type="password"], +.pure-form input[type="search"], +.pure-form input[type="tel"], +.pure-form input[type="text"], +.pure-form input[type="time"], +.pure-form input[type="url"], +.pure-form input[type="week"], +.pure-form select, +.pure-form textarea { + font-size: inherit; + padding: var(--secondary-gap); + color: var(--fg-color); + background-color: var(--secondary-bg-color); + border: 1px solid var(--secondary-bg-color); + box-shadow: unset; + border-radius: var(--radius); +} + +.pure-menu-heading, +.pure-g, +.pure-g [class*="pure-u"] { + font-family: inherit; + letter-spacing: initial; +} + +.pure-form legend { + color: var(--fg-color); + border-bottom: 1px solid var(--accent-color); +} + +legend { + border: initial; + padding: initial; +} + +.pure-button { + font-family: inherit; + font-size: inherit; + padding: 0; + color: currentcolor; + border: none; + border: none transparent; + background-color: transparent; + text-decoration: none; + border-radius: inherit; +} + +.pure-button-hover, +.pure-button:focus, +.pure-button:hover { + background-image: none; +} + +/* Wider settings name to less word wrap */ +.pure-form-aligned .pure-control-group label { + width: 19em; +} + +.pure-menu-heading { + color: var(--accent-color); +} + +a:active, a:hover { + outline: inherit; +} diff --git a/assets/css/search.css b/assets/css/search.css index 7036fd28..5a6e7fb9 100644 --- a/assets/css/search.css +++ b/assets/css/search.css @@ -1,121 +1,111 @@ summary { - /* This should hide the marker */ - display: block; - - font-size: 1.17em; - font-weight: bold; - margin: 0 auto 10px auto; - cursor: pointer; + display: block; + font-size: 1.17em; + margin: 0 auto 0.625em; + cursor: pointer; } summary::-webkit-details-marker, -summary::marker { display: none; } - -summary:before { - border-radius: 5px; - content: "[ + ]"; - margin: -2px 10px 0 10px; - padding: 1px 0 3px 0; - text-align: center; - width: 40px; +summary::marker { + display: none; } -details[open] > summary:before { content: "[ − ]"; } - - -#filters-box { - padding: 10px 20px 20px 10px; - margin: 10px 15px; +summary::before { + content: "+ "; + text-align: center; } -#filters-flex { + +details[open] > summary::before { + content: "− "; +} + +.filters { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12em, 1fr)); + grid-gap: var(--secondary-gap); + margin: var(--gap) 0; +} + +.filters fieldset { + display: grid; + grid-gap: var(--secondary-gap); + align-content: baseline; +} + +.filters fieldset div { display: flex; - flex-wrap: wrap; - flex-direction: row; - align-items: flex-start; - align-content: flex-start; - justify-content: flex-start; + gap: var(--secondary-gap); } - -fieldset, legend { - display: contents !important; - border: none !important; - margin: 0 !important; - padding: 0 !important; -} - - -.filter-column { - display: inline-block; - display: inline-flex; - width: max-content; - min-width: max-content; - max-width: 16em; - margin: 15px; - flex-grow: 2; - flex-basis: auto; - flex-direction: column; -} -.filter-name, .filter-options { - display: block; - padding: 5px 10px; - margin: 0; - text-align: start; -} - -.filter-options div { margin: 6px 0; } -.filter-options div * { vertical-align: middle; } -.filter-options label { margin: 0 10px; } - - -#filters-apply { - text-align: right; /* IE11 only */ - text-align: end; /* Override for compatible browsers */ -} +/* #filters-box { */ +/* background-color: var(--secondary-bg-color); */ +/* } */ +/**/ +/* #filters-flex { */ +/* display: flex; */ +/* flex-flow: row wrap; */ +/* align-items: flex-start; */ +/* place-content: flex-start flex-start; */ +/* } */ +/**/ +/* .filter-column { */ +/* display: inline-block; */ +/* display: inline-flex; */ +/* width: max-content; */ +/* min-width: max-content; */ +/* max-width: 16em; */ +/* margin: 0.9375rem; */ +/* flex-grow: 2; */ +/* flex-basis: auto; */ +/* flex-direction: column; */ +/* } */ +/**/ +/* .filter-name, */ +/* .filter-options { */ +/* display: block; */ +/* padding: 0.3125rem 0.625rem; */ +/* margin: 0; */ +/* text-align: start; */ +/* } */ +/**/ +/* .filter-options div { */ +/* margin: 0.375rem 0; */ +/* } */ +/**/ +/* .filter-options div * { */ +/* vertical-align: middle; */ +/* } */ +/**/ +/* .filter-options label { */ +/* margin: 0 0.625rem; */ +/* } */ +/**/ +/* #filters-apply { */ +/* text-align: right; /* IE11 only */ */ +/* text-align: end; /* Override for compatible browsers */ */ +/* } */ /* Error message */ .no-results-error { - text-align: center; - line-height: 180%; - font-size: 110%; - padding: 15px 15px 125px 15px; + text-align: center; + font-size: 1.1em; + padding: 1em 1em 8em; } -/* Responsive rules */ - -@media only screen and (max-width: 800px) { - summary { font-size: 1.30em; } - #filters-box { - margin: 10px 0 0 0; - padding: 0; - } - #filters-apply { - text-align: center; - padding: 15px; - } -} - -/* Light theme */ - -.light-theme #filters-box { - background: #dfdfdf; -} - -@media (prefers-color-scheme: light) { - .no-theme #filters-box { - background: #dfdfdf; - } -} - -/* Dark theme */ - -.dark-theme #filters-box { - background: #373737; -} - -@media (prefers-color-scheme: dark) { - .no-theme #filters-box { - background: #373737; - } -} +/* @media only screen and (max-width: 50px) { */ +/* summary { */ +/* font-size: 1.3em; */ +/* } */ +/**/ +/* #filters-box { */ +/* margin: 0.6em 0 0; */ +/* padding: 0; */ +/* } */ +/**/ +/* #filters-apply { */ +/* text-align: center; */ +/* padding: 1em; */ +/* } */ +/* } */ +/**/ diff --git a/assets/css/theme-catppuccin-latte.css b/assets/css/theme-catppuccin-latte.css new file mode 100644 index 00000000..e541fca5 --- /dev/null +++ b/assets/css/theme-catppuccin-latte.css @@ -0,0 +1,16 @@ +:root { + --fg-color-dark: #f8f8f2; + --bg-color-dark: #eff1f5; + --accent-color-dark: #fff; + --accent-bg-color-dark: #181926; + --secondary-color-dark: #e3e3e3; + --secondary-bg-color-dark: #494d64; + + /* light theme colors */ + --fg-color-light: #4c4f69; + --bg-color-light: #eff1f5; + --accent-color-light: #bcc0cc; + --accent-bg-color-light: #7287fd; + --secondary-color-light: #5c5f77; + --secondary-bg-color-light: #e6e9ef; +} diff --git a/assets/css/theme-catppuccin-macchiato.css b/assets/css/theme-catppuccin-macchiato.css new file mode 100644 index 00000000..45d3e632 --- /dev/null +++ b/assets/css/theme-catppuccin-macchiato.css @@ -0,0 +1,16 @@ +:root { + --fg-color-dark: #f8f8f2; + --bg-color-dark: #24273a; + --accent-color-dark: #fff; + --accent-bg-color-dark: #181926; + --secondary-color-dark: #e3e3e3; + --secondary-bg-color-dark: #494d64; + + /* light theme colors */ + --fg-color-light: black; + --bg-color-light: #eee; + --accent-color-light: #3a3a3a; + --accent-bg-color-light: #008bec; + --secondary-color-light: #424242; + --secondary-bg-color-light: #d9d9d9; +} diff --git a/assets/css/theme-dracula.css b/assets/css/theme-dracula.css new file mode 100644 index 00000000..69331e02 --- /dev/null +++ b/assets/css/theme-dracula.css @@ -0,0 +1,16 @@ +:root { + --fg-color-dark: #f8f8f2; + --bg-color-dark: #282a36; + --accent-color-dark: #fff; + --accent-bg-color-dark: #44475a; + --secondary-color-dark: #e3e3e3; + --secondary-bg-color-dark: #21222C; + + /* light theme colors */ + --fg-color-light: black; + --bg-color-light: #eee; + --accent-color-light: #3a3a3a; + --accent-bg-color-light: #008bec; + --secondary-color-light: #424242; + --secondary-bg-color-light: #d9d9d9; +} diff --git a/assets/css/theme-tokyonight.css b/assets/css/theme-tokyonight.css new file mode 100644 index 00000000..654d24ce --- /dev/null +++ b/assets/css/theme-tokyonight.css @@ -0,0 +1,16 @@ +:root { + --fg-color-dark: #f8f8f2; + --bg-color-dark: #1a1b26; + --accent-color-dark: #fff; + --accent-bg-color-dark: #3e4f80; + --secondary-color-dark: #e3e3e3; + --secondary-bg-color-dark: #2e2e3e; + + /* light theme colors */ + --fg-color-light: black; + --bg-color-light: #eee; + --accent-color-light: #3a3a3a; + --accent-bg-color-light: #008bec; + --secondary-color-light: #424242; + --secondary-bg-color-light: #d9d9d9; +} diff --git a/assets/css/theme.css b/assets/css/theme.css new file mode 100644 index 00000000..57ab937b --- /dev/null +++ b/assets/css/theme.css @@ -0,0 +1,59 @@ +:root { + --fg-color-dark: #f0f0f0; + --bg-color-dark: #131313; + --accent-color-dark: #27a6ff; + --accent-bg-color-dark: #004a7e; + --secondary-color-dark: #e3e3e3; + --secondary-bg-color-dark: #313131; + --watched-overlay-color-dark: rgb(0 0 0 / 40%); + + /* light theme colors */ + --fg-color-light: black; + --bg-color-light: #eee; + --accent-color-light: #044c99; + --accent-bg-color-light: #3eaefd; + --secondary-color-light: #404040; + --secondary-bg-color-light: #dbdbdb; + --watched-overlay-color-light: rgb(255 255 255 / 40%); + + /** apply default colors to dark */ + --fg-color: var(--fg-color-dark); + --bg-color: var(--bg-color-dark); + --accent-color: var(--accent-color-dark); + --accent-bg-color: var(--accent-bg-color-dark); + --secondary-color: var(--secondary-color-dark); + --secondary-bg-color: var(--secondary-bg-color-dark); + --watched-overlay-color: var(--watched-overlay-color-dark); +} + +@media (prefers-color-scheme: light) { + :root { + --fg-color: var(--fg-color-light); + --bg-color: var(--bg-color-light); + --accent-color: var(--accent-color-light); + --accent-bg-color: var(--accent-bg-color-light); + --secondary-color: var(--secondary-color-light); + --secondary-bg-color: var(--secondary-bg-color-light); + --watched-overlay-color: var(--watched-overlay-color-dark); + } +} + +.light-theme { + --fg-color: var(--fg-color-light); + --bg-color: var(--bg-color-light); + --accent-color: var(--accent-color-light); + --accent-bg-color: var(--accent-bg-color-light); + --secondary-color: var(--secondary-color-light); + --secondary-bg-color: var(--secondary-bg-color-light); + --watched-overlay-color: var(--watched-overlay-color-light); +} + +.dark-theme { + --fg-color: var(--fg-color-dark); + --bg-color: var(--bg-color-dark); + --accent-color: var(--accent-color-dark); + --accent-bg-color: var(--accent-bg-color-dark); + --secondary-color: var(--secondary-color-dark); + --secondary-bg-color: var(--secondary-bg-color-dark); + --watched-overlay-color: var(--watched-overlay-color-dark); +} diff --git a/assets/js/comments.js b/assets/js/comments.js index 35ffa96e..57c99f52 100644 --- a/assets/js/comments.js +++ b/assets/js/comments.js @@ -1,6 +1,6 @@ var video_data = JSON.parse(document.getElementById('video_data').textContent); -var spinnerHTML = '

'; +var spinnerHTML = '
'; var spinnerHTMLwithHR = spinnerHTML + '
'; String.prototype.supplant = function (o) { @@ -11,14 +11,14 @@ String.prototype.supplant = function (o) { }; function toggle_comments(event) { - var target = event.target; - var body = target.parentNode.parentNode.parentNode.children[1]; - if (body.style.display === 'none') { - target.textContent = '[ − ]'; - body.style.display = ''; + const target = event.target; + const comments = document.querySelector(".comments"); + if (comments.style.display === 'none') { + target.textContent = '−'; + comments.style.display = ''; } else { - target.textContent = '[ + ]'; - body.style.display = 'none'; + target.textContent = '+'; + comments.style.display = 'none'; } } @@ -39,6 +39,7 @@ function hide_youtube_replies(event) { function show_youtube_replies(event) { var target = event.target; + console.log(target); var sub_text = target.getAttribute('data-inner-text'); var inner_text = target.getAttribute('data-sub-text'); @@ -75,23 +76,24 @@ function get_youtube_comments() { helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { on200: function (response) { var commentInnerHtml = ' \ -
\ -

\ - [ − ] \ +

\ - \ - ' + \ + \ +
  • ' if (video_data.support_reddit) { - commentInnerHtml += ' \ + commentInnerHtml += ' \ ' } - commentInnerHtml += ' \ -
  • \ -
    {contentHtml}
    \ -
    ' + commentInnerHtml += ' \ + \ + \ +
    {contentHtml}
    ' commentInnerHtml = commentInnerHtml.supplant({ contentHtml: response.contentHtml, redditComments: video_data.reddit_comments_text, @@ -104,9 +106,9 @@ function get_youtube_comments() { }) }); comments.innerHTML = commentInnerHtml; - comments.children[0].children[0].children[0].onclick = toggle_comments; + document.getElementById("toggle-comments").onclick = toggle_comments; if (video_data.support_reddit) { - comments.children[0].children[1].children[0].onclick = swap_comments; + comments.children[1].children[1].onclick = swap_comments; } }, onNon200: onNon200, // declared above @@ -122,7 +124,7 @@ function get_youtube_comments() { function get_youtube_replies(target, load_more, load_replies) { var continuation = target.getAttribute('data-continuation'); - var body = target.parentNode.parentNode; + var body = target.parentNode; var fallback = body.innerHTML; body.innerHTML = spinnerHTML; var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id @@ -140,26 +142,24 @@ function get_youtube_replies(target, load_more, load_replies) { helpers.xhr('GET', url, {}, { on200: function (response) { if (load_more) { - body = body.parentNode.parentNode; + body = body.parentNode; body.removeChild(body.lastElementChild); body.insertAdjacentHTML('beforeend', response.contentHtml); } else { body.removeChild(body.lastElementChild); - var p = document.createElement('p'); - var a = document.createElement('a'); - p.appendChild(a); + var div = document.createElement('div'); + var button = document.createElement('button'); + div.appendChild(button); - a.href = 'javascript:void(0)'; - a.onclick = hide_youtube_replies; - a.setAttribute('data-sub-text', video_data.hide_replies_text); - a.setAttribute('data-inner-text', video_data.show_replies_text); - a.textContent = video_data.hide_replies_text; + button.onclick = hide_youtube_replies; + button.setAttribute('data-sub-text', video_data.hide_replies_text); + button.setAttribute('data-inner-text', video_data.show_replies_text); + button.textContent = video_data.hide_replies_text; var div = document.createElement('div'); div.innerHTML = response.contentHtml; - body.appendChild(p); body.appendChild(div); } }, @@ -171,4 +171,4 @@ function get_youtube_replies(target, load_more, load_replies) { body.innerHTML = fallback; } }); -} \ No newline at end of file +} diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 539974fb..da58d7fd 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -58,13 +58,13 @@ el.onclick = function () { mark_unwatched(el); }; }); document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) { - el.onclick = function () { add_playlist_video(el); }; + el.onclick = function (e) { add_playlist_video(e); }; }); document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) { - el.onclick = function () { add_playlist_item(el); }; + el.onclick = function (e) { add_playlist_item(e); }; }); document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) { - el.onclick = function () { remove_playlist_item(el); }; + el.onclick = function (e) { remove_playlist_item(e); }; }); document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) { el.onclick = function () { revoke_token(el); }; diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 55b7a15c..2c8552c8 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -1,131 +1,197 @@ -'use strict'; -var notification_data = JSON.parse(document.getElementById('notification_data').textContent); +"use strict"; +var notification_data = JSON.parse( + document.getElementById("notification_data").textContent, +); /** Boolean meaning 'some tab have stream' */ -const STORAGE_KEY_STREAM = 'stream'; +const STORAGE_KEY_STREAM = "stream"; /** Number of notifications. May be increased or reset */ -const STORAGE_KEY_NOTIF_COUNT = 'notification_count'; +const STORAGE_KEY_NOTIF_COUNT = "notification_count"; var notifications, delivered; -var notifications_mock = { close: function () { } }; +var notifications_mock = { close: function () {} }; -function get_subscriptions() { - helpers.xhr('GET', '/api/v1/auth/subscriptions', { +async function get_subscriptions_call() { + return new Promise((resolve) => { + helpers.xhr( + "GET", + "/api/v1/auth/subscriptions", + { retries: 5, - entity_name: 'subscriptions' - }, { - on200: create_notification_stream - }); + entity_name: "subscriptions", + }, + { + on200: function (subscriptions) { + create_notification_stream(subscriptions); + resolve(subscriptions); + }, + }, + ); + }); } +// Start the retry mechanism +const get_subscriptions = exponential_backoff(get_subscriptions_call, 100, 1000); + function create_notification_stream(subscriptions) { - // sse.js can't be replaced to EventSource in place as it lack support of payload and headers - // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource - notifications = new SSE( - '/api/v1/auth/notifications', { - withCredentials: true, - payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','), - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }); - delivered = []; + // sse.js can't be replaced to EventSource in place as it lack support of payload and headers + // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource + notifications = new SSE("/api/v1/auth/notifications", { + withCredentials: true, + payload: + "topics=" + + subscriptions + .map(function (subscription) { + return subscription.authorId; + }) + .join(","), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }); + delivered = []; - var start_time = Math.round(new Date() / 1000); + var start_time = Math.round(new Date() / 1000); - notifications.onmessage = function (event) { - if (!event.id) return; + notifications.onmessage = function (event) { + if (!event.id) return; - var notification = JSON.parse(event.data); - console.info('Got notification:', notification); + var notification = JSON.parse(event.data); + console.info("Got notification:", notification); - // Ignore not actual and delivered notifications - if (start_time > notification.published || delivered.includes(notification.videoId)) return; + // Ignore not actual and delivered notifications + if ( + start_time > notification.published || + delivered.includes(notification.videoId) + ) + return; - delivered.push(notification.videoId); + delivered.push(notification.videoId); - let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0; - notification_count++; - helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); + let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0; + notification_count++; + helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); - update_ticker_count(); + update_ticker_count(); - // permission for notifications handled on settings page. JS handler is in handlers.js - if (window.Notification && Notification.permission === 'granted') { - var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text; - notification_text = notification_text.replace('`x`', notification.author); + // permission for notifications handled on settings page. JS handler is in handlers.js + if (window.Notification && Notification.permission === "granted") { + var notification_text = notification.liveNow + ? notification_data.live_now_text + : notification_data.upload_text; + notification_text = notification_text.replace("`x`", notification.author); - var system_notification = new Notification(notification_text, { - body: notification.title, - icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname, - img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname - }); + var system_notification = new Notification(notification_text, { + body: notification.title, + icon: "/ggpht" + new URL(notification.authorThumbnails[2].url).pathname, + img: "/ggpht" + new URL(notification.authorThumbnails[4].url).pathname, + }); - system_notification.onclick = function (e) { - open('/watch?v=' + notification.videoId, '_blank'); - }; - } - }; + system_notification.onclick = function (e) { + open("/watch?v=" + notification.videoId, "_blank"); + }; + } + }; - notifications.addEventListener('error', function (e) { - console.warn('Something went wrong with notifications, trying to reconnect...'); - notifications = notifications_mock; - setTimeout(get_subscriptions, 1000); - }); + notifications.addEventListener("error", function (e) { + console.warn( + "Something went wrong with notifications, trying to reconnect...", + ); + notifications = notifications_mock; - notifications.stream(); + }); + + notifications.stream(); } function update_ticker_count() { - var notification_ticker = document.getElementById('notification_ticker'); + var notification_ticker = document.getElementById("notification_ticker"); - const notification_count = helpers.storage.get(STORAGE_KEY_STREAM); - if (notification_count > 0) { - notification_ticker.innerHTML = - '' + notification_count + ' '; - } else { - notification_ticker.innerHTML = - ''; - } + const notification_count = helpers.storage.get(STORAGE_KEY_STREAM); + if (notification_count > 0) { + notification_ticker.innerHTML = + '' + + notification_count + + ' '; + } else { + notification_ticker.innerHTML = + ''; + } } function start_stream_if_needed() { - // random wait for other tabs set 'stream' flag - setTimeout(function () { - if (!helpers.storage.get(STORAGE_KEY_STREAM)) { - // if no one set 'stream', set it by yourself and start stream - helpers.storage.set(STORAGE_KEY_STREAM, true); - notifications = notifications_mock; - get_subscriptions(); - } - }, Math.random() * 1000 + 50); // [0.050 .. 1.050) second + // random wait for other tabs set 'stream' flag + setTimeout( + function () { + if (!helpers.storage.get(STORAGE_KEY_STREAM)) { + // if no one set 'stream', set it by yourself and start stream + helpers.storage.set(STORAGE_KEY_STREAM, true); + notifications = notifications_mock; + get_subscriptions(); + } + }, + Math.random() * 1000 + 50, + ); // [0.050 .. 1.050) second } +addEventListener("storage", function (e) { + if (e.key === STORAGE_KEY_NOTIF_COUNT) update_ticker_count(); -addEventListener('storage', function (e) { - if (e.key === STORAGE_KEY_NOTIF_COUNT) - update_ticker_count(); - - // if 'stream' key was removed - if (e.key === STORAGE_KEY_STREAM && !helpers.storage.get(STORAGE_KEY_STREAM)) { - if (notifications) { - // restore it if we have active stream - helpers.storage.set(STORAGE_KEY_STREAM, true); - } else { - start_stream_if_needed(); - } + // if 'stream' key was removed + if ( + e.key === STORAGE_KEY_STREAM && + !helpers.storage.get(STORAGE_KEY_STREAM) + ) { + if (notifications) { + // restore it if we have active stream + helpers.storage.set(STORAGE_KEY_STREAM, true); + } else { + start_stream_if_needed(); } + } }); -addEventListener('load', function () { - var notification_count_el = document.getElementById('notification_count'); - var notification_count = notification_count_el ? parseInt(notification_count_el.textContent) : 0; - helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); +addEventListener("load", function () { + var notification_count_el = document.getElementById("notification_count"); + var notification_count = notification_count_el + ? parseInt(notification_count_el.textContent) + : 0; + helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); - if (helpers.storage.get(STORAGE_KEY_STREAM)) - helpers.storage.remove(STORAGE_KEY_STREAM); - start_stream_if_needed(); + if (helpers.storage.get(STORAGE_KEY_STREAM)) + helpers.storage.remove(STORAGE_KEY_STREAM); + start_stream_if_needed(); }); -addEventListener('unload', function () { - // let chance to other tabs to be a streamer via firing 'storage' event - if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM); +addEventListener("unload", function () { + // let chance to other tabs to be a streamer via firing 'storage' event + if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM); }); + +function exponential_backoff( + fn, + maxRetries = 5, + initialDelay = 1000, + randomnessFactor = 0.5, +) { + let attempt = 0; + + return function tryFunction() { + fn() + .then((response) => { + attempt = 0; + }) + .catch((error) => { + if (attempt < maxRetries) { + attempt++; + let delay = initialDelay * Math.pow(2, attempt); // Exponential backoff + let randomMultiplier = 1 + Math.random() * randomnessFactor; + delay = delay * randomMultiplier; + console.log( + `Attempt ${attempt} failed. Retrying in ${(delay / 1000).toPrecision(2)} seconds...`, + ); + setTimeout(tryFunction, delay); // Retry after delay + } else { + console.log("Max retries reached. Operation failed:", error); + } + }); + } +} diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js index c92592ac..3f73622d 100644 --- a/assets/js/playlist_widget.js +++ b/assets/js/playlist_widget.js @@ -2,8 +2,9 @@ var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent); var payload = 'csrf_token=' + playlist_data.csrf_token; -function add_playlist_video(target) { - var select = target.parentNode.children[0].children[1]; +function add_playlist_video(event) { + const target = event.target; + var select = document.querySelector("#playlists"); var option = select.children[select.selectedIndex]; var url = '/playlist_ajax?action_add_video=1&redirect=false' + @@ -12,37 +13,43 @@ function add_playlist_video(target) { helpers.xhr('POST', url, {payload: payload}, { on200: function (response) { - option.textContent = '✓' + option.textContent; + option.textContent = '✓ ' + option.textContent; } }); } -function add_playlist_item(target) { - var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; - tile.style.display = 'none'; +function add_playlist_item(event) { + event.preventDefault(); + const target = event.target; + const video_id = target.getAttribute('data-id'); + var card = document.querySelector(`#video-card-${video_id}`); + card.classList.add("hide"); var url = '/playlist_ajax?action_add_video=1&redirect=false' + - '&video_id=' + target.getAttribute('data-id') + + '&video_id=' + video_id + '&playlist_id=' + target.getAttribute('data-plid'); helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { - tile.style.display = ''; + card.classList.remove("hide"); } }); } -function remove_playlist_item(target) { - var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; - tile.style.display = 'none'; +function remove_playlist_item(event) { + event.preventDefault(); + const target = event.target; + const video_index = target.getAttribute('data-index'); + const card = document.querySelector(`.video-card [data-index="${video_index}"]`) + card.classList.add("hide"); var url = '/playlist_ajax?action_remove_video=1&redirect=false' + - '&set_video_id=' + target.getAttribute('data-index') + + '&set_video_id=' + video_index + '&playlist_id=' + target.getAttribute('data-plid'); helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { - tile.style.display = ''; + card.classList.remove("hide"); } }); } diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js index 7665a00b..51ade385 100644 --- a/assets/js/subscribe_widget.js +++ b/assets/js/subscribe_widget.js @@ -3,7 +3,6 @@ var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textCo var payload = 'csrf_token=' + subscribe_data.csrf_token; var subscribe_button = document.getElementById('subscribe'); -subscribe_button.parentNode.action = 'javascript:void(0)'; if (subscribe_button.getAttribute('data-type') === 'subscribe') { subscribe_button.onclick = subscribe; @@ -11,10 +10,29 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') { subscribe_button.onclick = unsubscribe; } -function subscribe() { - var fallback = subscribe_button.innerHTML; - subscribe_button.onclick = unsubscribe; - subscribe_button.innerHTML = '' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + ''; +function toggleSubscribeButton() { + subscribe_button.classList.remove("primary"); + subscribe_button.classList.remove("secondary"); + subscribe_button.classList.remove("unsubscribe"); + subscribe_button.classList.remove("subscribe"); + + if (subscribe_button.getAttribute('data-type') === 'subscribe') { + subscribe_button.textContent = subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text; + subscribe_button.onclick = unsubscribe; + subscribe_button.classList.add("secondary"); + subscribe_button.classList.add("unsubscribe"); + } else { + subscribe_button.textContent = subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text; + subscribe_button.onclick = subscribe; + subscribe_button.classList.add("primary"); + subscribe_button.classList.add("subscribe"); + } +} + +function subscribe(e) { + e.preventDefault(); + var fallback = subscribe_button.textContent; + toggleSubscribeButton(); var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' + '&c=' + subscribe_data.ucid; @@ -22,15 +40,15 @@ function subscribe() { helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, { onNon200: function (xhr) { subscribe_button.onclick = subscribe; - subscribe_button.innerHTML = fallback; + subscribe_button.textContent = fallback; } }); } -function unsubscribe() { - var fallback = subscribe_button.innerHTML; - subscribe_button.onclick = subscribe; - subscribe_button.innerHTML = '' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + ''; +function unsubscribe(e) { + e.preventDefault(); + var fallback = subscribe_button.textContent; + toggleSubscribeButton(); var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + '&c=' + subscribe_data.ucid; @@ -38,7 +56,7 @@ function unsubscribe() { helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, { onNon200: function (xhr) { subscribe_button.onclick = unsubscribe; - subscribe_button.innerHTML = fallback; + subscribe_button.textContent = fallback; } }); } diff --git a/assets/js/theme.js b/assets/js/theme.js new file mode 100644 index 00000000..1c615d27 --- /dev/null +++ b/assets/js/theme.js @@ -0,0 +1,44 @@ +const themeSelector = document.querySelector("#theme-selector"); +themeSelector.addEventListener("change", (event) => { + const select = event.target; + const selected = select.options[select.selectedIndex].text; + applyTheme(selected); +}); + +const colorSchemeSelector = document.querySelector("#color-scheme"); +colorSchemeSelector.addEventListener("change", (event) => { + const select = event.target; + const selected = select.options[select.selectedIndex].text; + applyColorScheme(selected); +}); + +function applyTheme(theme) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/css/theme-${theme}.css`; + link.id = "theme-css"; + + const themeCss = document.querySelector("#theme-css"); + if (themeCss) { + themeCss.parentNode.removeChild(themeCss); + } + + const head = document.getElementsByTagName("head")[0]; + head.appendChild(link); +} + +function applyColorScheme(colorScheme) { + document.body.classList.remove("dark-theme"); + document.body.classList.remove("light-theme"); + + if (colorScheme === "dark" || colorScheme === "light") { + document.body.classList.add(`${colorScheme}-theme`); + } +} + +applyTheme(themeSelector.options[themeSelector.selectedIndex].text); +applyColorScheme("dark"); + +// +// +// diff --git a/assets/js/themes.js b/assets/js/themes.js index 84a9f6d9..545d2c18 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,13 +1,13 @@ 'use strict'; var toggle_theme = document.getElementById('toggle_theme'); -toggle_theme.href = 'javascript:void(0)'; const STORAGE_KEY_THEME = 'dark_mode'; const THEME_DARK = 'dark'; const THEME_LIGHT = 'light'; // TODO: theme state controlled by system -toggle_theme.addEventListener('click', function () { +toggle_theme.addEventListener('click', function (e) { + e.preventDefault(); const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK; setTheme(newTheme); diff --git a/assets/js/watch.js b/assets/js/watch.js index 26ad138f..bdfc63cf 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -69,6 +69,8 @@ function get_playlist(plid) { helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, { on200: function (response) { + if (response === null) return; + playlist.innerHTML = response.playlistHtml; if (!response.nextVideo) return; diff --git a/docker-compose.yml b/docker-compose.yml index 905dff50..404b9a87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,13 @@ -# Warning: This docker-compose file is made for development purposes. -# Using it will build an image from the locally cloned repository. -# -# If you want to use Invidious in production, see the docker-compose.yml file provided -# in the installation documentation: https://docs.invidious.io/installation/ - ---- - version: "3" services: invidious: - build: - context: . - dockerfile: docker/Dockerfile - image: rockerboo/invidious + image: quay.io/invidious/invidious:master + # image: quay.io/invidious/invidious:master-arm64 # ARM64/AArch64 devices restart: unless-stopped + # Remove "127.0.0.1:" if used from an external IP ports: - - "127.0.0.1:9999:3000" + - "127.0.0.1:3000:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: @@ -29,22 +20,63 @@ services: host: invidious-db port: 5432 check_tables: true + invidious_companion: + # URL used for the internal communication between invidious and invidious companion + # There is no need to change that except if Invidious companion does not run on the same docker compose file. + - private_url: "http://companion:8282" + # (public) URL used for the communication between your browser and invidious companion + # IF you are using a reverse proxy OR accessing invidious from an external IP then you NEED to change this value + # Please consult for more doc: https://github.com/unixfox/invidious/blob/invidious-companion/config/config.example.yml#L57-L88 + # And make sure to add the routes from the post install when using a reverse proxy! + public_url: "http://localhost:8282" + # IT is NOT recommended to use the same key as HMAC KEY. Generate a new key! + # Use the key generated in the 2nd step + invidious_companion_key: "foo0te5naijooTho" # external_port: # domain: # https_only: false # statistics_enabled: false - hmac_key: "CHANGE_ME!!" + # Use the key generated in the 2nd step + hmac_key: "foo0te5naijooTho" healthcheck: test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1 interval: 30s timeout: 5s retries: 2 + logging: + options: + max-size: "1G" + max-file: "4" + depends_on: + - invidious-db + + companion: + image: quay.io/invidious/invidious-companion:latest + environment: + # Use the key generated in the 2nd step + - SERVER_SECRET_KEY=foo0te5naijooTho + restart: unless-stopped + # Remove "127.0.0.1:" if used from an external IP + ports: + - "127.0.0.1:8282:8282" + logging: + options: + max-size: "1G" + max-file: "4" + cap_drop: + - ALL + read_only: true + # cache for youtube library + volumes: + - companioncache:/var/tmp/youtubei.js:rw + security_opt: + - no-new-privileges:true invidious-db: image: docker.io/library/postgres:14 restart: unless-stopped volumes: - - postgresdata:/var/lib/postgresql/data + - postgresdata-companion:/var/lib/postgresql/data - ./config/sql:/config/sql - ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh environment: @@ -55,4 +87,5 @@ services: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] volumes: - postgresdata: + postgresdata-companion: + companioncache: diff --git a/locales/en-US.json b/locales/en-US.json index c23f6bc3..d9dedd32 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -463,6 +463,7 @@ "next_steps_error_message": "After which you should try to: ", "next_steps_error_message_refresh": "Refresh", "next_steps_error_message_go_to_youtube": "Go to YouTube", + "invidious_footer_description": "A free and open source frontend for Youtube that that respects your privacy! Now you can watch videos (ad-free), subscribe to channels, create playlist and much more all without the prying eyes of Google!", "footer_donate_page": "Donate", "footer_documentation": "Documentation", "footer_source_code": "Source code", diff --git a/src/invidious.cr b/src/invidious.cr index b422dcbb..bbda5715 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -133,6 +133,9 @@ Kemal.config.extra_options do |parser| Invidious::Database::Migrator.new(PG_DB).migrate exit end + parser.on("--disable-static-cache", "Disable static file handler cache") do + CONFIG.disable_static_cache = true + end end Kemal::CLI.new ARGV diff --git a/src/invidious/config.cr b/src/invidious/config.cr index c4ca622f..573cc2a7 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -158,6 +158,9 @@ class Config # Playlist length limit property playlist_length_limit : Int32 = 500 + # Disable static file handle cache. Prefer only for development. --disable_static_cache also available on build. + property disable_static_cache : Bool = false + def disabled?(option) case disabled = CONFIG.disable_proxy when Bool diff --git a/src/invidious/frontend/channel_page.cr b/src/invidious/frontend/channel_page.cr index fe7d6d6e..3452211b 100644 --- a/src/invidious/frontend/channel_page.cr +++ b/src/invidious/frontend/channel_page.cr @@ -23,22 +23,15 @@ module Invidious::Frontend::ChannelPage tab_name = tab.to_s.downcase if channel.tabs.includes? tab_name - str << %(
    \n) + # Video tab doesn't have the last path component + url = tab.videos? ? base_url : "#{base_url}/#{tab_name}" + selected_class = tab == selected_tab ? "selected" : "" - if tab == selected_tab - str << "\t" - str << translate(locale, "channel_tab_#{tab_name}_label") - str << "\n" - else - # Video tab doesn't have the last path component - url = tab.videos? ? base_url : "#{base_url}/#{tab_name}" - - str << %(\t) - str << translate(locale, "channel_tab_#{tab_name}_label") - str << "\n" - end - - str << "
    " + str << %(
  • \n) + str << %(\t) + str << translate(locale, "channel_tab_#{tab_name}_label") + str << "\n" + str << "
  • " end end end diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index a0e1d783..29686b1c 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -13,14 +13,9 @@ module Invidious::Frontend::Comments ) replies_html = <<-END_HTML -
    -
    - +
    +
    END_HTML elsif comments["authorId"]? && !comments["singlePost"]? @@ -32,21 +27,20 @@ module Invidious::Frontend::Comments ) replies_html = <<-END_HTML -
    -
    - + END_HTML end - if !thin_mode - author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}" - else + if thin_mode author_thumbnail = "" + else + author_thumbnail_url = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}" + + author_thumbnail = <<-THUMBNAIL + + THUMBNAIL end author_name = HTML.escape(child["author"].as_s) @@ -65,18 +59,76 @@ module Invidious::Frontend::Comments str << %(width="16" height="16" />) end end + html << <<-END_HTML -
    -
    - +
    +
    +
    +
    + #{author_thumbnail} +

    + #{author_name} + #{sponsor_icon} +

    + +
    +
    -
    -

    - - #{author_name} - - #{sponsor_icon} -

    #{child["contentHtml"]}

    + END_HTML + + html << <<-END_HTML +
      + END_HTML + + + if child["likeCount"]? + html << <<-END_HTML +
    • + #{number_with_separator(child["likeCount"])} +
    • + END_HTML + end + + if child["creatorHeart"]? + if !thin_mode + creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}" + else + creator_thumbnail = "" + end + + html << <<-END_HTML +
    • + + + + + + + + +
    • + END_HTML + end + + if comments["videoId"]? + html << <<-END_HTML +
    • + YT +
    • + END_HTML + elsif comments["authorId"]? + html << <<-END_HTML +
    • + YT +
    • + END_HTML + end + + html << <<-END_HTML +
    +
    +
    +

    #{child["contentHtml"]}

    END_HTML if child["attachment"]? @@ -87,10 +139,8 @@ module Invidious::Frontend::Comments attachment = attachment["imageThumbnails"][1] html << <<-END_HTML -
    -
    - -
    +
    +
    END_HTML when "video" @@ -141,50 +191,8 @@ module Invidious::Frontend::Comments end end - html << <<-END_HTML -

    - #{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""} - | - END_HTML - - if comments["videoId"]? - html << <<-END_HTML - [YT] - | - END_HTML - elsif comments["authorId"]? - html << <<-END_HTML - [YT] - | - END_HTML - end html << <<-END_HTML - #{number_with_separator(child["likeCount"])} - END_HTML - - if child["creatorHeart"]? - if !thin_mode - creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}" - else - creator_thumbnail = "" - end - - html << <<-END_HTML -   - - - - - - - - - END_HTML - end - - html << <<-END_HTML -

    #{replies_html}
    @@ -193,16 +201,17 @@ module Invidious::Frontend::Comments if comments["continuation"]? html << <<-END_HTML -
    - +
    +
    END_HTML end + + # Closes comment box + html << <<-END_HTML +
    + END_HTML end end end diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr index 3f931f4e..2fb369a0 100644 --- a/src/invidious/frontend/pagination.cr +++ b/src/invidious/frontend/pagination.cr @@ -5,7 +5,7 @@ module Invidious::Frontend::Pagination private def previous_page(str : String::Builder, locale : String?, url : String) # Link - str << %() + str << %() if locale_is_rtl?(locale) # Inverted arrow ("previous" points to the right) @@ -24,12 +24,12 @@ module Invidious::Frontend::Pagination private def next_page(str : String::Builder, locale : String?, url : String) # Link - str << %() + str << %() if locale_is_rtl?(locale) # Inverted arrow ("next" points to the left) str << %() - str << "  " + str << " " str << translate(locale, "Next page") else # Regular arrow ("next" points to the right) @@ -43,55 +43,49 @@ module Invidious::Frontend::Pagination def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true) return String.build do |str| - str << %(
    \n) - str << %(\n) - str << %(
    \n\n) + str << %(\n\n) end end def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?) + puts ctoken return String.build do |str| - str << %(
    \n) - str << %(\n) - str << %(
    \n\n) end end end diff --git a/src/invidious/frontend/search_filters.cr b/src/invidious/frontend/search_filters.cr index 8ac0af2e..c2111b7b 100644 --- a/src/invidious/frontend/search_filters.cr +++ b/src/invidious/frontend/search_filters.cr @@ -5,15 +5,15 @@ module Invidious::Frontend::SearchFilters def generate(filters : Search::Filters, query : String, page : Int, locale : String) : String return String.build(8000) do |str| str << "
    \n" - str << "\t
    " + str << "\t
    " str << "\t\t" << translate(locale, "search_filters_title") << "\n" - str << "\t\t
    \n" + str << "\t\t\n" str << "\t\t\t\n" str << "\t\t\t\n" - str << "\t\t\t
    " + str << "\t\t\t
    " filter_wrapper(date) filter_wrapper(type) @@ -23,32 +23,32 @@ module Invidious::Frontend::SearchFilters str << "\t\t\t
    \n" - str << "\t\t\t
    " - str << "
    \n" str << "\t\t
    \n" str << "\t
    \n" - str << "
    \n" + # str << "
    \n" end end # Generate wrapper HTML (`
    `, filter name, etc...) around the # `` elements of a search filter macro filter_wrapper(name) - str << "\t\t\t\t
    \n" + str << "\t\t\t\t
    \n" - str << "\t\t\t\t\t
    " + str << "\t\t\t\t\t" str << translate(locale, "search_filters_{{name}}_label") - str << "
    \n" + str << "\n" - str << "\t\t\t\t\t
    \n" + str << "\t\t\t\t\t\n" make_{{name}}_filter_options(str, filters.{{name}}, locale) - str << "\t\t\t\t\t
    " + str << "\t\t\t\t\t" - str << "\t\t\t\t
    \n" + str << "\t\t\t\t\n" end # Generates the HTML for the list of radio buttons of the "date" search filter diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index c8cb7110..cb9ca76a 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -25,7 +25,7 @@ module Invidious::Frontend::WatchPage return String.build(4000) do |str| str << "\n" str << "\n" - str << "\t
    \n" + # str << "\t
    \n" str << "\t\t
    \n" + # str << "\t
    \n" - str << "\t\n" + # str << "\t
    " str << "\n" end diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr index 823ca85b..915ab206 100644 --- a/src/invidious/mixes.cr +++ b/src/invidious/mixes.cr @@ -98,11 +98,11 @@ def template_mix(mix)
    -

    #{recode_length_seconds(video["lengthSeconds"].as_i)}

    +
    #{recode_length_seconds(video["lengthSeconds"].as_i)}
    -

    #{video["title"]}

    +

    #{video["title"]}

    - #{video["author"]} + #{video["author"]}

    diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index a51e88b4..558e9053 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -450,6 +450,8 @@ end def extract_playlist_videos(initial_data : Hash(String, JSON::Any)) videos = [] of PlaylistVideo + puts initial_data.inspect + if initial_data["contents"]? tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"] tabs_renderer = tabs.as_a.select(&.["tabRenderer"]["selected"]?.try &.as_bool)[0]["tabRenderer"] @@ -480,6 +482,9 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any)) title = i["title"].try { |t| t["simpleText"]? || t["runs"]?.try &.[0]["text"]? }.try &.as_s || "" author = i["shortBylineText"]?.try &.["runs"][0]["text"].as_s || "" ucid = i["shortBylineText"]?.try &.["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s || "" + + # TODO: Possibly add author_thumbnail support + # author_thumbnail = i["playlistSidebarSecondaryInfoRenderer"]?.try &.["videoOwner"]["videoOwnerRenderer"]["thumbnail"]["thumbnails"][0]["url"].as_s || "" length_seconds = i["lengthSeconds"]?.try &.as_s.to_i live = false @@ -512,23 +517,29 @@ def template_playlist(playlist) #{playlist["title"]} -
    -
      +
      +
        END_HTML playlist["videos"].as_a.each do |video| html += <<-END_HTML -
      1. - +
      2. -

        #{video["title"]}

        -

        - #{video["author"]} -

        - +

        + + #{video["title"]} + +

        +

        + + #{video["author"]} + +

      3. END_HTML end @@ -536,7 +547,6 @@ def template_playlist(playlist) html += <<-END_HTML
      -
      END_HTML html diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 9c6843e9..a109be42 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -267,6 +267,8 @@ module Invidious::Routes::Playlists show_next: (items.size >= 20) ) + puts items + env.set "add_playlist_items", plid templated "add_playlist_items" end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 44970922..5cd73dd7 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -67,6 +67,10 @@ module Invidious::Routes::Search redirect_url = Invidious::Frontend::Misc.redirect_url(env) + puts items.size + puts (items.size >= 20) + puts query.page + # Pagination page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/search?#{query.to_http_params}", diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 6aea82ae..842a6c2e 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -3,25 +3,38 @@ <% end %> -
      -
      -
      -
      -
      - <%= translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %> - -
      - value="<%= HTML.escape(query.text) %>"<% end %> - placeholder="<%= translate(locale, "Search for videos") %>"> - -
      -
      -
      -
      -
      +
      +

      <%= translate(locale, "Editing playlist") %>

      +
      +

      <%= HTML.escape(playlist.title) %>

      + +
      +
      + <%= translate(locale, "Search for videos") %> + + + +
      +
      + - <%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index a84e44bc..a1e40ab7 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -46,9 +46,4 @@ <%= rendered "components/channel_info" %> -
      -
      -
      - - <%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index d2a305d3..bb8dfb5e 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -17,18 +17,12 @@ <%= rendered "components/channel_info" %> -
      -
      -
      - <% if error_message %> -
      -

      <%= error_message %>

      -
      +

      <%= error_message %>

      <% else %> -
      - <%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %> -
      +
      + <%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %> +
      <% end %> <% else %> - "> - <%= translate(locale, "Subscribe") %> | <%= sub_count_text %> + <%= translate(locale, "Subscribe") %> | <%= sub_count_text %> <% end %> diff --git a/src/invidious/views/components/video-context-buttons.ecr b/src/invidious/views/components/video-context-buttons.ecr index 22458a03..7599d955 100644 --- a/src/invidious/views/components/video-context-buttons.ecr +++ b/src/invidious/views/components/video-context-buttons.ecr @@ -1,21 +1,29 @@ - \ No newline at end of file diff --git a/src/invidious/views/create_playlist.ecr b/src/invidious/views/create_playlist.ecr index 807244e6..2e29fd5a 100644 --- a/src/invidious/views/create_playlist.ecr +++ b/src/invidious/views/create_playlist.ecr @@ -2,38 +2,31 @@ <%= translate(locale, "Create playlist") %> - Invidious <% end %> -
      -
      -
      -
      -
      -
      - <%= translate(locale, "Create playlist") %> + +
      + <%= translate(locale, "Create playlist") %> -
      - - "> -
      +
      + + "> +
      -
      - - -
      +
      + + +
      -
      - -
      - -
      - -
      -
      -
      -
      + + + +
      + +
      + diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 34157c67..c2e381b4 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -5,56 +5,62 @@ <% end %> -
      - + -
      -
      -

      -
      -
      - -
      -
      - - <%= HTML.escape(playlist.author) %> | - <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | - +
        +
      • + <%= HTML.escape(playlist.author) %> +
      • +
      • + <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> +
      • +
      • -
      -
      + + -
      - -
      +
      + Edit playlist +
      + + +
      + +
      + + +
      +
      + +
      +
      -
      -
      -
      - <%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index bda4e1f3..87e7bf7c 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -2,20 +2,18 @@ <%= translate(locale, "History") %> - Invidious <% end %> -
      -
      -

      <%= translate_count(locale, "generic_videos_count", user.watched.size, NumberFormatting::HtmlSpan) %>

      -
      - - +
      +

      <%= translate_count(locale, "generic_videos_count", user.watched.size, NumberFormatting::HtmlSpan) %>

      +
      -
      +
      <% watched.each do |item| %> -
      -
      -
      - - - +
      +
      + + + -
      -
      " method="post"> - "> - -
      -
      -
      -

      -
      -
      +
      +
      " method="post"> + "> + +
      +
      +
      +
      <% end %>
      diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr index 2a4b6edd..125ac3dd 100644 --- a/src/invidious/views/feeds/playlists.ecr +++ b/src/invidious/views/feeds/playlists.ecr @@ -4,38 +4,33 @@ <%= rendered "components/feed_menu" %> -
      -
      -

      <%= translate(locale, "user_created_playlists", %(#{items_created.size})) %>

      -
      - - +
      +

      <%= translate(locale, "user_created_playlists", %(#{items_created.size})) %>

      +
      -
      +
      <% items_created.each do |item| %> <%= rendered "components/item" %> <% end %>
      -
      -
      -

      <%= translate(locale, "user_saved_playlists", %(#{items_saved.size})) %>

      -
      -
      +

      <%= translate(locale, "user_saved_playlists", %(#{items_saved.size})) %>

      -
      +
      <% items_saved.each do |item| %> + <% puts item %> <%= rendered "components/item" %> <% end %>
      diff --git a/src/invidious/views/feeds/popular.ecr b/src/invidious/views/feeds/popular.ecr index 5fbe539c..0bb7439c 100644 --- a/src/invidious/views/feeds/popular.ecr +++ b/src/invidious/views/feeds/popular.ecr @@ -11,10 +11,10 @@ <%= rendered "components/feed_menu" %> -
      +
      <% popular_videos.each do |item| %> <%= rendered "components/item" %> <% end %> -
      + diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index c36bd00f..b7593f11 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -5,63 +5,59 @@ <%= rendered "components/feed_menu" %> - +
      +
      + <% if CONFIG.enable_user_notifications %> -<% if CONFIG.enable_user_notifications %> +
      + <%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %> +
      + <% else %> +
      + <% end %> -
      - <%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %> -
      + +
      -<% if !notifications.empty? %> -
      -
      -
      -<% end %> + <% if CONFIG.enable_user_notifications %> + <% if !notifications.empty? %> +
      + <% notifications.each do |item| %> + <%= rendered "components/item" %> + <% end %> +
      + <% end %> -
      -<% notifications.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
      + <% end %> -<% end %> - -
      -
      -
      - - - + + -
      -<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
      +
      + <% videos.each do |item| %> + <%= rendered "components/item" %> + <% end %> +
      +
      diff --git a/src/invidious/views/feeds/trending.ecr b/src/invidious/views/feeds/trending.ecr index 7dc416c6..02652731 100644 --- a/src/invidious/views/feeds/trending.ecr +++ b/src/invidious/views/feeds/trending.ecr @@ -11,39 +11,31 @@ <%= rendered "components/feed_menu" %> -
      -
      +
      -
      -
      - <% {"Default", "Music", "Gaming", "Movies"}.each do |option| %> -
      - <% if trending_type == option %> - <%= translate(locale, option) %> - <% else %> - - <%= translate(locale, option) %> - - <% end %> -
      - <% end %> -
      -
      -
      + + + -
      -
      -
      - -
      +
      <% trending.each do |item| %> <%= rendered "components/item" %> <% end %> -
      + diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index c27ddba6..7c3e254f 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -6,109 +6,141 @@ <% end %> -
      -

      <%= title %>

      +
      +

      <%= title %>

      - + + +
      -
      -
      - <% if playlist.is_a? InvidiousPlaylist %> - - <% if playlist.author == user.try &.email %> - <%= author %> | - <% else %> - <%= author %> | - <% end %> - <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | - <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | - <% case playlist.as(InvidiousPlaylist).privacy when %> - <% when PlaylistPrivacy::Public %> - <%= translate(locale, "Public") %> - <% when PlaylistPrivacy::Unlisted %> - <%= translate(locale, "Unlisted") %> - <% when PlaylistPrivacy::Private %> - <%= translate(locale, "Private") %> - <% end %> - - <% else %> - - <% if !author.empty? %> - <%= author %> | - <% elsif !playlist.subtitle.nil? %> - <% subtitle = playlist.subtitle || "" %> - <%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %> | - <% end %> - <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | - <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> - - <% end %> + - <% if !playlist.is_a? InvidiousPlaylist %> -
      - - <%= translate(locale, "View playlist on YouTube") %> - - | +
      +
        + <% if playlist.is_a? InvidiousPlaylist %> +
      • + <%= translate_count(locale, "generic_videos_count", playlist.video_count) %>
      • + +
      • + +
      • +
      • + <% case playlist.as(InvidiousPlaylist).privacy when %> + <% when PlaylistPrivacy::Public %> + <%= translate(locale, "Public") %> + <% when PlaylistPrivacy::Unlisted %> + <%= translate(locale, "Unlisted") %> + <% when PlaylistPrivacy::Private %> + <%= translate(locale, "Private") %> + <% end %> +
      • + <% else %> +
      • + <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> +
      • +
      • + +
      • + <% end %> - <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> - "> - <%= translate(locale, "Switch Invidious Instance") %> - - <% else %> - - <%= translate(locale, "Switch Invidious Instance") %> - - <% end %> -
      - <% end %> -
      + + +
      -
      -
      <%= playlist.description_html %>
      -
      - -
      -
      -
      +
      <%= playlist.description_html %>
      <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> <% end %> - + <%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index b1300214..7acee69f 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -5,15 +5,17 @@ <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> -
      - <%- if items.empty? -%> -
      +
      - <%= translate(locale, "search_message_no_results") %>

      - <%= translate(locale, "search_message_change_filters_or_query") %>

      - <%= translate(locale, "search_message_use_another_instance", redirect_url) %> + <%= translate(locale, "search_message_no_results") %> +
      +
      + <%= translate(locale, "search_message_change_filters_or_query") %> +
      +
      + <%= translate(locale, "search_message_use_another_instance", redirect_url) %>
      <%- else -%> diff --git a/src/invidious/views/search_homepage.ecr b/src/invidious/views/search_homepage.ecr index 2424a1cf..d269667a 100644 --- a/src/invidious/views/search_homepage.ecr +++ b/src/invidious/views/search_homepage.ecr @@ -6,15 +6,11 @@ <% end %> -<%= rendered "components/feed_menu" %> +
      + <%= rendered "components/feed_menu" %> -
      - -
      - -
      +
      +

      Invidious

      + <% autofocus = true %><%= rendered "components/search_box" %> +
      diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 9904b4fc..1b73cf9b 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -18,8 +18,11 @@ + + + @@ -27,135 +30,209 @@ -theme"> -
      -
      - +
      + +
      - +
      + + + + +
      + +
      +

      <%= translate(locale, "Legal")%>

      + +
      +
      +
      + + <% if env.get? "user" %> diff --git a/src/invidious/views/user/data_control.ecr b/src/invidious/views/user/data_control.ecr index 9ce42c99..bd66276d 100644 --- a/src/invidious/views/user/data_control.ecr +++ b/src/invidious/views/user/data_control.ecr @@ -2,17 +2,18 @@ <%= translate(locale, "Import and Export Data") %> - Invidious <% end %> -
      -
      -
      +

      Import/export

      + + +
      <%= translate(locale, "Import") %> -
      +
      + <%= translate(locale, "Export") %> -
      diff --git a/src/invidious/views/user/login.ecr b/src/invidious/views/user/login.ecr index 2b03d280..3baa87ed 100644 --- a/src/invidious/views/user/login.ecr +++ b/src/invidious/views/user/login.ecr @@ -2,76 +2,84 @@ <%= translate(locale, "Log in") %> - Invidious <% end %> -
      -
      -
      -
      <% case account_type when %> <% else # "invidious" %> - -
      + +
      + Log in <% if email %> +
      +
      <% else %> - - "> +
      + + "> +
      <% end %> <% if password %> - +
      + +
      <% else %> - - "> +
      + + "> +
      <% end %> +
      + <% if captcha %> <% case captcha_type when %> <% when "image" %> <% captcha = captcha.not_nil! %> - + <% captcha[:tokens].each_with_index do |token, i| %> <% end %> +
      +
      <% else # "text" %> <% captcha = captcha.not_nil! %> <% captcha[:tokens].each_with_index do |token, i| %> <% end %> +
      "> +
      <% end %> - <% case captcha_type when %> <% when "image" %> <% else # "text" %> <% end %> <% else %> +
      +
      <% end %> -
      <% end %> -
      -
      -
      -
      diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index cf8b5593..ce11113f 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -2,362 +2,382 @@ <%= translate(locale, "Preferences") %> - Invidious <% end %> -
      -
      -
      - <%= translate(locale, "preferences_category_player") %> +

      Preferences

      -
      - - checked<% end %>> -
      + +
      + <%= translate(locale, "preferences_category_player") %> -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>> +
      -
      - - -
      +
      + + checked<% end %>> +
      -
      - - -
      +
      + + +
      - <% if !CONFIG.disabled?("dash") %> -
      - - -
      - <% end %> +
      + + +
      -
      - - - <%= preferences.volume %> -
      + <% if !CONFIG.disabled?("dash") %> +
      + + +
      + <% end %> -
      - - <% preferences.comments.each_with_index do |comments, index| %> - - <% end %> -
      +
      + +
      + + <%= preferences.volume %> +
      +
      -
      - - <% preferences.captions.each_with_index do |caption, index| %> - - <% end %> -
      +
      + +
      + <% preferences.comments.each_with_index do |comments, index| %> + + <% end %> +
      +
      -
      - - checked<% end %>> -
      +
      + +
      + <% preferences.captions.each_with_index do |caption, index| %> + + <% end %> +
      +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      - <%= translate(locale, "preferences_category_visual") %> +
      + + checked<% end %>> +
      +
      +
      + <%= translate(locale, "preferences_category_visual") %> -
      - - -
      +
      + + +
      -
      - - -
      +
      + + +
      -
      - - -
      +
      + + +
      -
      - - -
      +
      + + +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      - <% if env.get?("user") %> - <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %> - <% else %> - <% feed_options = {"", "Popular", "Trending"} %> - <% end %> + <% if env.get?("user") %> + <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %> + <% else %> + <% feed_options = {"", "Popular", "Trending"} %> + <% end %> -
      - - -
      +
      + + +
      -
      - - <% (feed_options.size - 1).times do |index| %> - - <% end %> -
      - <% if env.get? "user" %> -
      - - checked<% end %>> -
      - <% end %> +
      + +
      + <% (feed_options.size - 1).times do |index| %> + + <% end %> +
      +
      + <% if env.get? "user" %> +
      + + checked<% end %>> +
      + <% end %> - <%= translate(locale, "preferences_category_misc") %> +
      +
      -
      - - checked<% end %>> -
      + <%= translate(locale, "preferences_category_misc") %> - <% if env.get? "user" %> - <%= translate(locale, "preferences_category_subscription") %> +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      -
      - - checked<% end %>> -
      + <% if env.get? "user" %> +
      + <%= translate(locale, "preferences_category_subscription") %> -
      - - -
      +
      + + checked<% end %>> +
      -
      - - -
      +
      + + checked<% end %>> +
      -
      - <% if preferences.unseen_only %> - - <% else %> - - <% end %> - checked<% end %>> -
      +
      + + +
      -
      - - checked<% end %>> -
      +
      + + +
      - <% if CONFIG.enable_user_notifications %> -
      - - checked<% end %>> -
      +
      + <% if preferences.unseen_only %> + + <% else %> + + <% end %> + checked<% end %>> +
      - <% # Web notifications are only supported over HTTPS %> - <% if Kemal.config.ssl || CONFIG.https_only %> - - <% end %> - <% end %> - <% end %> +
      + + checked<% end %>> +
      - <% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %> - <%= translate(locale, "preferences_category_admin") %> + <% if CONFIG.enable_user_notifications %> +
      + + checked<% end %>> +
      -
      - - -
      + <% # Web notifications are only supported over HTTPS %> + <% if Kemal.config.ssl || CONFIG.https_only %> + + <% end %> + <% end %> +
      + <% end %> -
      - - <% (feed_options.size - 1).times do |index| %> - - <% end %> -
      + <% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %> +
      + <%= translate(locale, "preferences_category_admin") %> -
      - - checked<% end %>> -
      +
      + + +
      + +
      + + <% (feed_options.size - 1).times do |index| %> + + <% end %> +
      + +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - checked<% end %>> -
      +
      + + checked<% end %>> +
      -
      - - -
      - <% end %> +
      + + +
      +
      + <% end %> - <% if env.get? "user" %> - <%= translate(locale, "preferences_category_data") %> + <% if env.get? "user" %> +
      + <%= translate(locale, "preferences_category_data") %> - + - + - + - + - + - + - + - - <% end %> + +
      + <% end %> -
      - -
      -
      -
      -
      + +
      + +
      + diff --git a/src/invidious/views/user/subscription_manager.ecr b/src/invidious/views/user/subscription_manager.ecr index c9801f09..90af60b6 100644 --- a/src/invidious/views/user/subscription_manager.ecr +++ b/src/invidious/views/user/subscription_manager.ecr @@ -2,51 +2,40 @@ <%= translate(locale, "Subscription manager") %> - Invidious <% end %> -
      - - - + +
      <% subscriptions.each do |channel| %> -
      -
      -
      -

      + -
      -
      -

      -
      " method="post"> - "> - "> -
      -

      -
      -

      - - <% if subscriptions[-1].author != channel.author %> -
      - <% end %> -
      +
      " method="post"> + "> + "> +
      +
      <% end %> +
      diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 45c58a16..05ed223b 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -34,11 +34,11 @@ we're going to need to do it here in order to allow for translations. --> @@ -70,305 +70,317 @@ we're going to need to do it here in order to allow for translations. %> -
      - <%= rendered "components/player" %> -
      +
      +
      +
      + <%= rendered "components/player" %> +
      -
      -

      - <%= title %> - <% if params.listen %> - " href="/watch?<%= env.params.query %>&listen=0"> - - - <% else %> - " href="/watch?<%= env.params.query %>&listen=1"> - - - <% end %> -

      +
      +

      + <%= title %> +

      + +
      - <% if !video.is_listed %> -

      - <%= translate(locale, "Unlisted") %> -

      - <% end %> +
      +
      + <% if video.reason %> +

      + <%= video.reason %> +

      + <% elsif video.premiere_timestamp.try &.> Time.utc %> +

      + +

      + <% elsif video.live_now %> +

      + +

      + <% end %> + -

      - <% if params.annotations %> - - <%= translate(locale, "Hide annotations") %> - - <% else %> - - <%=translate(locale, "Show annotations")%> - - <% end %> -

      + <% if user %> + <% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %> + <% if !playlists.empty? %> +
      + + - <% if user %> - <% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %> - <% if !playlists.empty? %> - -
      - - -
      + "> + + + +
      + + + <% end %> + <% end %> - "> - - - - - - - <% end %> - <% end %> + <%= Invidious::Frontend::WatchPage.download_widget(locale, video, video_assets) %> - <%= Invidious::Frontend::WatchPage.download_widget(locale, video, video_assets) %> + + -

      <%= number_with_separator(video.views) %>

      -

      <%= number_with_separator(video.likes) %>

      - -

      <%= translate(locale, "Genre: ") %> - <% if !video.genre_url %> - <%= video.genre %> - <% else %> - <%= video.genre %> - <% end %> -

      - <% if video.license %> - <% if video.license.empty? %> -

      <%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %>

      - <% else %> -

      <%= translate(locale, "License: ") %><%= video.license %>

      - <% end %> - <% end %> -

      <%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %>

      - - - - <% if video.allowed_regions.size != REGIONS.size %> -

      - <% if video.allowed_regions.size < REGIONS.size // 2 %> - <%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %> - <% else %> - <%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %> - <% end %> -

      - <% end %> -
      -
      +
      +
      +
      + -
      +
      + <% sub_count_text = video.sub_count_text %> + <%= rendered "components/subscribe_widget" %> +
      +
      -
      - +
      -
      -
      - <% sub_count_text = video.sub_count_text %> - <%= rendered "components/subscribe_widget" %> -
      -
      -
      +
      + <% if video.premiere_timestamp.try &.> Time.utc %> + + <% else %> + + <% end %> +
      +
      +
      <%= number_with_separator(video.views) %>
      +
      <%= number_with_separator(video.likes) %>
      +
      +
      -
      -

      - <% if video.premiere_timestamp.try &.> Time.utc %> - <%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %> - <% else %> - <%= translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %> - <% end %> -

      +
      + <% if video.description.size < 200 || params.extend_desc %> +

      <%= video.description_html %>

      + <% else %> + +

      <%= video.description_html %>

      + + <% end %> +
      +
      -
      - <% if video.description.size < 200 || params.extend_desc %> -
      <%= video.description_html %>
      - <% else %> - -
      <%= video.description_html %>
      - - <% end %> -
      + <% if !video.music.empty? %> + + -
      +
      + <% video.music.each do |music| %> +
      +
      <%= translate(locale, "Song: ") %><%= music.song %>
      +
      <%= translate(locale, "Artist: ") %><%= music.artist %>
      +
      <%= translate(locale, "Album: ") %><%= music.album %>
      +
      + <% end %> +
      + <% end %> - <% if !video.music.empty? %> - - + +
      -
      - <% video.music.each do |music| %> -
      -

      <%= translate(locale, "Song: ") %><%= music.song %>

      -

      <%= translate(locale, "Artist: ") %><%= music.artist %>

      -

      <%= translate(locale, "Album: ") %><%= music.album %>

      -
      - <% end %> -
      -
      + <% if params.related_videos || plid %> +
      -
      + <% if params.related_videos %> + + + <% end %> <% # if rv["id"]? %> + <% end %> <% # video.related_videos.each do |rv| %> +
      <% # + + From 09019fc379e75a8fdce52744261c6ec73f90fcde Mon Sep 17 00:00:00 2001 From: "Dave Lage (rockerBOO)" Date: Fri, 25 Nov 2022 02:28:51 -0500 Subject: [PATCH 03/10] update local ports for docker --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index afda8726..905dff50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ +--- + version: "3" services: @@ -11,9 +13,10 @@ services: build: context: . dockerfile: docker/Dockerfile + image: rockerboo/invidious restart: unless-stopped ports: - - "127.0.0.1:3000:3000" + - "127.0.0.1:9999:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: From 70b36ed1a17e252a9f7fc8116c6e9b189a2eeb27 Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Wed, 30 Apr 2025 20:41:51 -0400 Subject: [PATCH 04/10] Formatting --- assets/js/_helpers.js | 459 ++--- assets/js/comments.js | 270 +-- assets/js/community.js | 130 +- assets/js/embed.js | 121 +- assets/js/handlers.js | 357 ++-- assets/js/notifications.js | 11 +- assets/js/pagination.js | 144 +- assets/js/player.js | 1224 +++++++------ assets/js/playlist_widget.js | 108 +- assets/js/post.js | 4 +- ...silvermine-videojs-quality-selector.min.js | 1549 ++++++++++++++++- assets/js/sse.js | 119 +- assets/js/subscribe_widget.js | 106 +- assets/js/themes.js | 64 +- assets/js/videojs-youtube-annotations.min.js | 937 +++++++++- assets/js/watch.js | 292 ++-- assets/js/watched_indicator.js | 34 +- assets/js/watched_widget.js | 64 +- src/invidious/frontend/pagination.cr | 19 +- 19 files changed, 4429 insertions(+), 1583 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 8e18169e..30904e8e 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -1,254 +1,273 @@ -'use strict'; +"use strict"; // Contains only auxiliary methods // May be included and executed unlimited number of times without any consequences // Polyfills for IE11 -Array.prototype.find = Array.prototype.find || function (condition) { +Array.prototype.find = + Array.prototype.find || + function (condition) { return this.filter(condition)[0]; -}; + }; -Array.from = Array.from || function (source) { +Array.from = + Array.from || + function (source) { return Array.prototype.slice.call(source); -}; -NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) { + }; +NodeList.prototype.forEach = + NodeList.prototype.forEach || + function (callback) { Array.from(this).forEach(callback); -}; -String.prototype.includes = String.prototype.includes || function (searchString) { + }; +String.prototype.includes = + String.prototype.includes || + function (searchString) { return this.indexOf(searchString) >= 0; -}; -String.prototype.startsWith = String.prototype.startsWith || function (prefix) { + }; +String.prototype.startsWith = + String.prototype.startsWith || + function (prefix) { return this.substr(0, prefix.length) === prefix; -}; -Math.sign = Math.sign || function(x) { + }; +Math.sign = + Math.sign || + function (x) { x = +x; if (!x) return x; // 0 and NaN return x > 0 ? 1 : -1; -}; -if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) { - window.mockHTMLDetailsElement = true; - const style = 'details:not([open]) > :not(summary) {display: none}'; - document.head.appendChild(document.createElement('style')).textContent = style; + }; +if ( + !window.hasOwnProperty("HTMLDetailsElement") && + !window.hasOwnProperty("mockHTMLDetailsElement") +) { + window.mockHTMLDetailsElement = true; + const style = "details:not([open]) > :not(summary) {display: none}"; + document.head.appendChild(document.createElement("style")).textContent = + style; - addEventListener('click', function (e) { - if (e.target.nodeName !== 'SUMMARY') return; - const details = e.target.parentElement; - if (details.hasAttribute('open')) - details.removeAttribute('open'); - else - details.setAttribute('open', ''); - }); + addEventListener("click", function (e) { + if (e.target.nodeName !== "SUMMARY") return; + const details = e.target.parentElement; + if (details.hasAttribute("open")) details.removeAttribute("open"); + else details.setAttribute("open", ""); + }); } // Monstrous global variable for handy code // Includes: clamp, xhr, storage.{get,set,remove} window.helpers = window.helpers || { - /** - * https://en.wikipedia.org/wiki/Clamping_(graphics) - * @param {Number} num Source number - * @param {Number} min Low border - * @param {Number} max High border - * @returns {Number} Clamped value - */ - clamp: function (num, min, max) { - if (max < min) { - var t = max; max = min; min = t; // swap max and min + /** + * https://en.wikipedia.org/wiki/Clamping_(graphics) + * @param {Number} num Source number + * @param {Number} min Low border + * @param {Number} max High border + * @returns {Number} Clamped value + */ + clamp: function (num, min, max) { + if (max < min) { + var t = max; + max = min; + min = t; // swap max and min + } + + if (max < num) return max; + if (min > num) return min; + return num; + }, + + /** @private */ + _xhr: function (method, url, options, callbacks) { + const xhr = new XMLHttpRequest(); + xhr.open(method, url); + + // Default options + xhr.responseType = "json"; + xhr.timeout = 10000; + // Default options redefining + if (options.responseType) xhr.responseType = options.responseType; + if (options.timeout) xhr.timeout = options.timeout; + + if (method === "POST") + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963 + xhr.onloadend = function () { + if (xhr.status === 200) { + if (callbacks.on200) { + // fix for IE11. It doesn't convert response to JSON + if (xhr.responseType === "" && typeof xhr.response === "string") + callbacks.on200(JSON.parse(xhr.response)); + else callbacks.on200(xhr.response); } + } else { + // handled by onerror + if (xhr.status === 0) return; - if (max < num) - return max; - if (min > num) - return min; - return num; - }, + if (callbacks.onNon200) callbacks.onNon200(xhr); + } + }; - /** @private */ - _xhr: function (method, url, options, callbacks) { - const xhr = new XMLHttpRequest(); - xhr.open(method, url); + xhr.ontimeout = function () { + if (callbacks.onTimeout) callbacks.onTimeout(xhr); + }; - // Default options - xhr.responseType = 'json'; - xhr.timeout = 10000; - // Default options redefining - if (options.responseType) - xhr.responseType = options.responseType; - if (options.timeout) - xhr.timeout = options.timeout; + xhr.onerror = function () { + if (callbacks.onError) callbacks.onError(xhr); + }; - if (method === 'POST') - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + if (options.payload) xhr.send(options.payload); + else xhr.send(); + }, + /** @private */ + _xhrRetry: function (method, url, options, callbacks) { + if (options.retries <= 0) { + console.warn("Failed to pull", options.entity_name); + if (callbacks.onTotalFail) callbacks.onTotalFail(); + return; + } + helpers._xhr(method, url, options, callbacks); + }, + /** + * @callback callbackXhrOn200 + * @param {Object} response - xhr.response + */ + /** + * @callback callbackXhrError + * @param {XMLHttpRequest} xhr + */ + /** + * @param {'GET'|'POST'} method - 'GET' or 'POST' + * @param {String} url - URL to send request to + * @param {Object} options - other XHR options + * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests + * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json] + * @param {Number} [options.timeout=10000] + * @param {Number} [options.retries=1] + * @param {String} [options.entity_name='unknown'] - string to log + * @param {Number} [options.retry_timeout=1000] + * @param {Object} callbacks - functions to execute on events fired + * @param {callbackXhrOn200} [callbacks.on200] + * @param {callbackXhrError} [callbacks.onNon200] + * @param {callbackXhrError} [callbacks.onTimeout] + * @param {callbackXhrError} [callbacks.onError] + * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries + */ + xhr: function (method, url, options, callbacks) { + if (!options.retries || options.retries <= 1) { + helpers._xhr(method, url, options, callbacks); + return; + } - // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963 - xhr.onloadend = function () { - if (xhr.status === 200) { - if (callbacks.on200) { - // fix for IE11. It doesn't convert response to JSON - if (xhr.responseType === '' && typeof(xhr.response) === 'string') - callbacks.on200(JSON.parse(xhr.response)); - else - callbacks.on200(xhr.response); - } - } else { - // handled by onerror - if (xhr.status === 0) return; - - if (callbacks.onNon200) - callbacks.onNon200(xhr); - } - }; - - xhr.ontimeout = function () { - if (callbacks.onTimeout) - callbacks.onTimeout(xhr); - }; - - xhr.onerror = function () { - if (callbacks.onError) - callbacks.onError(xhr); - }; - - if (options.payload) - xhr.send(options.payload); - else - xhr.send(); - }, - /** @private */ - _xhrRetry: function(method, url, options, callbacks) { - if (options.retries <= 0) { - console.warn('Failed to pull', options.entity_name); - if (callbacks.onTotalFail) - callbacks.onTotalFail(); - return; - } - helpers._xhr(method, url, options, callbacks); - }, - /** - * @callback callbackXhrOn200 - * @param {Object} response - xhr.response - */ - /** - * @callback callbackXhrError - * @param {XMLHttpRequest} xhr - */ - /** - * @param {'GET'|'POST'} method - 'GET' or 'POST' - * @param {String} url - URL to send request to - * @param {Object} options - other XHR options - * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests - * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json] - * @param {Number} [options.timeout=10000] - * @param {Number} [options.retries=1] - * @param {String} [options.entity_name='unknown'] - string to log - * @param {Number} [options.retry_timeout=1000] - * @param {Object} callbacks - functions to execute on events fired - * @param {callbackXhrOn200} [callbacks.on200] - * @param {callbackXhrError} [callbacks.onNon200] - * @param {callbackXhrError} [callbacks.onTimeout] - * @param {callbackXhrError} [callbacks.onError] - * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries - */ - xhr: function(method, url, options, callbacks) { - if (!options.retries || options.retries <= 1) { - helpers._xhr(method, url, options, callbacks); - return; - } - - if (!options.entity_name) options.entity_name = 'unknown'; - if (!options.retry_timeout) options.retry_timeout = 1000; - const retries_total = options.retries; - let currentTry = 1; - - const retry = function () { - console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total); - setTimeout(function () { - options.retries--; - helpers._xhrRetry(method, url, options, callbacks); - }, options.retry_timeout); - }; - - // Pack retry() call into error handlers - callbacks._onError = callbacks.onError; - callbacks.onError = function (xhr) { - if (callbacks._onError) - callbacks._onError(xhr); - retry(); - }; - callbacks._onTimeout = callbacks.onTimeout; - callbacks.onTimeout = function (xhr) { - if (callbacks._onTimeout) - callbacks._onTimeout(xhr); - retry(); - }; + if (!options.entity_name) options.entity_name = "unknown"; + if (!options.retry_timeout) options.retry_timeout = 1000; + const retries_total = options.retries; + let currentTry = 1; + const retry = function () { + console.warn( + "Pulling " + + options.entity_name + + " failed... " + + currentTry++ + + "/" + + retries_total, + ); + setTimeout(function () { + options.retries--; helpers._xhrRetry(method, url, options, callbacks); - }, + }, options.retry_timeout); + }; - /** - * @typedef {Object} invidiousStorage - * @property {(key:String) => Object} get - * @property {(key:String, value:Object)} set - * @property {(key:String)} remove - */ + // Pack retry() call into error handlers + callbacks._onError = callbacks.onError; + callbacks.onError = function (xhr) { + if (callbacks._onError) callbacks._onError(xhr); + retry(); + }; + callbacks._onTimeout = callbacks.onTimeout; + callbacks.onTimeout = function (xhr) { + if (callbacks._onTimeout) callbacks._onTimeout(xhr); + retry(); + }; - /** - * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies - * @type {invidiousStorage} - */ - storage: (function () { - // access to localStorage throws exception in Tor Browser, so try is needed - let localStorageIsUsable = false; - try{localStorageIsUsable = !!localStorage.setItem;}catch(e){} + helpers._xhrRetry(method, url, options, callbacks); + }, - if (localStorageIsUsable) { - return { - get: function (key) { - let storageItem = localStorage.getItem(key) - if (!storageItem) return; - try { - return JSON.parse(decodeURIComponent(storageItem)); - } catch(e) { - // Erase non parsable value - helpers.storage.remove(key); - } - }, - set: function (key, value) { - let encoded_value = encodeURIComponent(JSON.stringify(value)) - localStorage.setItem(key, encoded_value); - }, - remove: function (key) { localStorage.removeItem(key); } - }; + /** + * @typedef {Object} invidiousStorage + * @property {(key:String) => Object} get + * @property {(key:String, value:Object)} set + * @property {(key:String)} remove + */ + + /** + * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies + * @type {invidiousStorage} + */ + storage: (function () { + // access to localStorage throws exception in Tor Browser, so try is needed + let localStorageIsUsable = false; + try { + localStorageIsUsable = !!localStorage.setItem; + } catch (e) {} + + if (localStorageIsUsable) { + return { + get: function (key) { + let storageItem = localStorage.getItem(key); + if (!storageItem) return; + try { + return JSON.parse(decodeURIComponent(storageItem)); + } catch (e) { + // Erase non parsable value + helpers.storage.remove(key); + } + }, + set: function (key, value) { + let encoded_value = encodeURIComponent(JSON.stringify(value)); + localStorage.setItem(key, encoded_value); + }, + remove: function (key) { + localStorage.removeItem(key); + }, + }; + } + + // TODO: fire 'storage' event for cookies + console.info( + "Storage: localStorage is disabled or unaccessible. Cookies used as fallback", + ); + return { + get: function (key) { + const cookiePrefix = key + "="; + function findCallback(cookie) { + return cookie.startsWith(cookiePrefix); } + const matchedCookie = document.cookie.split("; ").find(findCallback); + if (matchedCookie) { + const cookieBody = matchedCookie.replace(cookiePrefix, ""); + if (cookieBody.length === 0) return; + try { + return JSON.parse(decodeURIComponent(cookieBody)); + } catch (e) { + // Erase non parsable value + helpers.storage.remove(key); + } + } + }, + set: function (key, value) { + const cookie_data = encodeURIComponent(JSON.stringify(value)); - // TODO: fire 'storage' event for cookies - console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback'); - return { - get: function (key) { - const cookiePrefix = key + '='; - function findCallback(cookie) {return cookie.startsWith(cookiePrefix);} - const matchedCookie = document.cookie.split('; ').find(findCallback); - if (matchedCookie) { - const cookieBody = matchedCookie.replace(cookiePrefix, ''); - if (cookieBody.length === 0) return; - try { - return JSON.parse(decodeURIComponent(cookieBody)); - } catch(e) { - // Erase non parsable value - helpers.storage.remove(key); - } - } - }, - set: function (key, value) { - const cookie_data = encodeURIComponent(JSON.stringify(value)); + // Set expiration in 2 year + const date = new Date(); + date.setFullYear(date.getFullYear() + 2); - // Set expiration in 2 year - const date = new Date(); - date.setFullYear(date.getFullYear()+2); - - document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString(); - }, - remove: function (key) { - document.cookie = key + '=; Max-Age=0'; - } - }; - })() + document.cookie = + key + "=" + cookie_data + "; expires=" + date.toGMTString(); + }, + remove: function (key) { + document.cookie = key + "=; Max-Age=0"; + }, + }; + })(), }; diff --git a/assets/js/comments.js b/assets/js/comments.js index 57c99f52..7c68ae54 100644 --- a/assets/js/comments.js +++ b/assets/js/comments.js @@ -1,81 +1,91 @@ -var video_data = JSON.parse(document.getElementById('video_data').textContent); +var video_data = JSON.parse(document.getElementById("video_data").textContent); -var spinnerHTML = '
      '; -var spinnerHTMLwithHR = spinnerHTML + '
      '; +var spinnerHTML = + '
      '; +var spinnerHTMLwithHR = spinnerHTML + "
      "; String.prototype.supplant = function (o) { - return this.replace(/{([^{}]*)}/g, function (a, b) { - var r = o[b]; - return typeof r === 'string' || typeof r === 'number' ? r : a; - }); + return this.replace(/{([^{}]*)}/g, function (a, b) { + var r = o[b]; + return typeof r === "string" || typeof r === "number" ? r : a; + }); }; function toggle_comments(event) { - const target = event.target; - const comments = document.querySelector(".comments"); - if (comments.style.display === 'none') { - target.textContent = '−'; - comments.style.display = ''; - } else { - target.textContent = '+'; - comments.style.display = 'none'; - } + const target = event.target; + const comments = document.querySelector(".comments"); + if (comments.style.display === "none") { + target.textContent = "−"; + comments.style.display = ""; + } else { + target.textContent = "+"; + comments.style.display = "none"; + } } function hide_youtube_replies(event) { - var target = event.target; + var target = event.target; - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); + var sub_text = target.getAttribute("data-inner-text"); + var inner_text = target.getAttribute("data-sub-text"); - var body = target.parentNode.parentNode.children[1]; - body.style.display = 'none'; + var body = target.parentNode.parentNode.children[1]; + body.style.display = "none"; - target.textContent = sub_text; - target.onclick = show_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); + target.textContent = sub_text; + target.onclick = show_youtube_replies; + target.setAttribute("data-inner-text", inner_text); + target.setAttribute("data-sub-text", sub_text); } function show_youtube_replies(event) { - var target = event.target; - console.log(target); + var target = event.target; + console.log(target); - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); + var sub_text = target.getAttribute("data-inner-text"); + var inner_text = target.getAttribute("data-sub-text"); - var body = target.parentNode.parentNode.children[1]; - body.style.display = ''; + var body = target.parentNode.parentNode.children[1]; + body.style.display = ""; - target.textContent = sub_text; - target.onclick = hide_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); + target.textContent = sub_text; + target.onclick = hide_youtube_replies; + target.setAttribute("data-inner-text", inner_text); + target.setAttribute("data-sub-text", sub_text); } function get_youtube_comments() { - var comments = document.getElementById('comments'); + var comments = document.getElementById("comments"); - var fallback = comments.innerHTML; - comments.innerHTML = spinnerHTML; + var fallback = comments.innerHTML; + comments.innerHTML = spinnerHTML; - var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id - var url = baseUrl + - '?format=html' + - '&hl=' + video_data.preferences.locale + - '&thin_mode=' + video_data.preferences.thin_mode; + var baseUrl = video_data.base_url || "/api/v1/comments/" + video_data.id; + var url = + baseUrl + + "?format=html" + + "&hl=" + + video_data.preferences.locale + + "&thin_mode=" + + video_data.preferences.thin_mode; - if (video_data.ucid) { - url += '&ucid=' + video_data.ucid - } + if (video_data.ucid) { + url += "&ucid=" + video_data.ucid; + } - var onNon200 = function (xhr) { comments.innerHTML = fallback; }; - if (video_data.params.comments[1] === 'youtube') - onNon200 = function (xhr) {}; + var onNon200 = function (xhr) { + comments.innerHTML = fallback; + }; + if (video_data.params.comments[1] === "youtube") onNon200 = function (xhr) {}; - helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { - on200: function (response) { - var commentInnerHtml = ' \ + helpers.xhr( + "GET", + url, + { retries: 5, entity_name: "comments" }, + { + on200: function (response) { + var commentInnerHtml = + ' \ \ -
      {contentHtml}
      ' - commentInnerHtml = commentInnerHtml.supplant({ - contentHtml: response.contentHtml, - redditComments: video_data.reddit_comments_text, - commentsText: video_data.comments_text.supplant({ - // toLocaleString correctly splits number with local thousands separator. e.g.: - // '1,234,567.89' for user with English locale - // '1 234 567,89' for user with Russian locale - // '1.234.567,89' for user with Portuguese locale - commentCount: response.commentCount.toLocaleString() - }) - }); - comments.innerHTML = commentInnerHtml; - document.getElementById("toggle-comments").onclick = toggle_comments; - if (video_data.support_reddit) { - comments.children[1].children[1].onclick = swap_comments; - } - }, - onNon200: onNon200, // declared above - onError: function (xhr) { - comments.innerHTML = spinnerHTML; - }, - onTimeout: function (xhr) { - comments.innerHTML = spinnerHTML; +
      {contentHtml}
      '; + commentInnerHtml = commentInnerHtml.supplant({ + contentHtml: response.contentHtml, + redditComments: video_data.reddit_comments_text, + commentsText: video_data.comments_text.supplant({ + // toLocaleString correctly splits number with local thousands separator. e.g.: + // '1,234,567.89' for user with English locale + // '1 234 567,89' for user with Russian locale + // '1.234.567,89' for user with Portuguese locale + commentCount: response.commentCount.toLocaleString(), + }), + }); + comments.innerHTML = commentInnerHtml; + document.getElementById("toggle-comments").onclick = toggle_comments; + if (video_data.support_reddit) { + comments.children[1].children[1].onclick = swap_comments; } - }); + }, + onNon200: onNon200, // declared above + onError: function (xhr) { + comments.innerHTML = spinnerHTML; + }, + onTimeout: function (xhr) { + comments.innerHTML = spinnerHTML; + }, + }, + ); } function get_youtube_replies(target, load_more, load_replies) { - var continuation = target.getAttribute('data-continuation'); + var continuation = target.getAttribute("data-continuation"); - var body = target.parentNode; - var fallback = body.innerHTML; - body.innerHTML = spinnerHTML; - var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id - var url = baseUrl + - '?format=html' + - '&hl=' + video_data.preferences.locale + - '&thin_mode=' + video_data.preferences.thin_mode + - '&continuation=' + continuation; + var body = target.parentNode; + var fallback = body.innerHTML; + body.innerHTML = spinnerHTML; + var baseUrl = video_data.base_url || "/api/v1/comments/" + video_data.id; + var url = + baseUrl + + "?format=html" + + "&hl=" + + video_data.preferences.locale + + "&thin_mode=" + + video_data.preferences.thin_mode + + "&continuation=" + + continuation; - if (video_data.ucid) { - url += '&ucid=' + video_data.ucid - } - if (load_replies) url += '&action=action_get_comment_replies'; + if (video_data.ucid) { + url += "&ucid=" + video_data.ucid; + } + if (load_replies) url += "&action=action_get_comment_replies"; - helpers.xhr('GET', url, {}, { - on200: function (response) { - if (load_more) { - body = body.parentNode; - body.removeChild(body.lastElementChild); - body.insertAdjacentHTML('beforeend', response.contentHtml); - } else { - body.removeChild(body.lastElementChild); + helpers.xhr( + "GET", + url, + {}, + { + on200: function (response) { + if (load_more) { + body = body.parentNode; + body.removeChild(body.lastElementChild); + body.insertAdjacentHTML("beforeend", response.contentHtml); + } else { + body.removeChild(body.lastElementChild); - var div = document.createElement('div'); - var button = document.createElement('button'); - div.appendChild(button); + var div = document.createElement("div"); + var button = document.createElement("button"); + div.appendChild(button); - button.onclick = hide_youtube_replies; - button.setAttribute('data-sub-text', video_data.hide_replies_text); - button.setAttribute('data-inner-text', video_data.show_replies_text); - button.textContent = video_data.hide_replies_text; + button.onclick = hide_youtube_replies; + button.setAttribute("data-sub-text", video_data.hide_replies_text); + button.setAttribute("data-inner-text", video_data.show_replies_text); + button.textContent = video_data.hide_replies_text; - var div = document.createElement('div'); - div.innerHTML = response.contentHtml; + var div = document.createElement("div"); + div.innerHTML = response.contentHtml; - body.appendChild(div); - } - }, - onNon200: function (xhr) { - body.innerHTML = fallback; - }, - onTimeout: function (xhr) { - console.warn('Pulling comments failed'); - body.innerHTML = fallback; + body.appendChild(div); } - }); + }, + onNon200: function (xhr) { + body.innerHTML = fallback; + }, + onTimeout: function (xhr) { + console.warn("Pulling comments failed"); + body.innerHTML = fallback; + }, + }, + ); } diff --git a/assets/js/community.js b/assets/js/community.js index 32fe4ebc..ef8be78b 100644 --- a/assets/js/community.js +++ b/assets/js/community.js @@ -1,82 +1,94 @@ -'use strict'; -var community_data = JSON.parse(document.getElementById('community_data').textContent); +"use strict"; +var community_data = JSON.parse( + document.getElementById("community_data").textContent, +); function hide_youtube_replies(event) { - var target = event.target; + var target = event.target; - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); + var sub_text = target.getAttribute("data-inner-text"); + var inner_text = target.getAttribute("data-sub-text"); - var body = target.parentNode.parentNode.children[1]; - body.style.display = 'none'; + var body = target.parentNode.parentNode.children[1]; + body.style.display = "none"; - target.innerHTML = sub_text; - target.onclick = show_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); + target.innerHTML = sub_text; + target.onclick = show_youtube_replies; + target.setAttribute("data-inner-text", inner_text); + target.setAttribute("data-sub-text", sub_text); } function show_youtube_replies(event) { - var target = event.target; + var target = event.target; - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); + var sub_text = target.getAttribute("data-inner-text"); + var inner_text = target.getAttribute("data-sub-text"); - var body = target.parentNode.parentNode.children[1]; - body.style.display = ''; + var body = target.parentNode.parentNode.children[1]; + body.style.display = ""; - target.innerHTML = sub_text; - target.onclick = hide_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); + target.innerHTML = sub_text; + target.onclick = hide_youtube_replies; + target.setAttribute("data-inner-text", inner_text); + target.setAttribute("data-sub-text", sub_text); } function get_youtube_replies(target, load_more) { - var continuation = target.getAttribute('data-continuation'); + var continuation = target.getAttribute("data-continuation"); - var body = target.parentNode.parentNode; - var fallback = body.innerHTML; - body.innerHTML = - '

      '; + var body = target.parentNode.parentNode; + var fallback = body.innerHTML; + body.innerHTML = + '

      '; - var url = '/api/v1/channels/comments/' + community_data.ucid + - '?format=html' + - '&hl=' + community_data.preferences.locale + - '&thin_mode=' + community_data.preferences.thin_mode + - '&continuation=' + continuation; + var url = + "/api/v1/channels/comments/" + + community_data.ucid + + "?format=html" + + "&hl=" + + community_data.preferences.locale + + "&thin_mode=" + + community_data.preferences.thin_mode + + "&continuation=" + + continuation; - helpers.xhr('GET', url, {}, { - on200: function (response) { - if (load_more) { - body = body.parentNode.parentNode; - body.removeChild(body.lastElementChild); - body.innerHTML += response.contentHtml; - } else { - body.removeChild(body.lastElementChild); + helpers.xhr( + "GET", + url, + {}, + { + on200: function (response) { + if (load_more) { + body = body.parentNode.parentNode; + body.removeChild(body.lastElementChild); + body.innerHTML += response.contentHtml; + } else { + body.removeChild(body.lastElementChild); - var p = document.createElement('p'); - var a = document.createElement('a'); - p.appendChild(a); + var p = document.createElement("p"); + var a = document.createElement("a"); + p.appendChild(a); - a.href = 'javascript:void(0)'; - a.onclick = hide_youtube_replies; - a.setAttribute('data-sub-text', community_data.hide_replies_text); - a.setAttribute('data-inner-text', community_data.show_replies_text); - a.textContent = community_data.hide_replies_text; + a.href = "javascript:void(0)"; + a.onclick = hide_youtube_replies; + a.setAttribute("data-sub-text", community_data.hide_replies_text); + a.setAttribute("data-inner-text", community_data.show_replies_text); + a.textContent = community_data.hide_replies_text; - var div = document.createElement('div'); - div.innerHTML = response.contentHtml; + var div = document.createElement("div"); + div.innerHTML = response.contentHtml; - body.appendChild(p); - body.appendChild(div); - } - }, - onNon200: function (xhr) { - body.innerHTML = fallback; - }, - onTimeout: function (xhr) { - console.warn('Pulling comments failed'); - body.innerHTML = fallback; + body.appendChild(p); + body.appendChild(div); } - }); + }, + onNon200: function (xhr) { + body.innerHTML = fallback; + }, + onTimeout: function (xhr) { + console.warn("Pulling comments failed"); + body.innerHTML = fallback; + }, + }, + ); } diff --git a/assets/js/embed.js b/assets/js/embed.js index b11b5e5a..763f0ca8 100644 --- a/assets/js/embed.js +++ b/assets/js/embed.js @@ -1,64 +1,79 @@ -'use strict'; -var video_data = JSON.parse(document.getElementById('video_data').textContent); +"use strict"; +var video_data = JSON.parse(document.getElementById("video_data").textContent); function get_playlist(plid) { - var plid_url; - if (plid.startsWith('RD')) { - plid_url = '/api/v1/mixes/' + plid + - '?continuation=' + video_data.id + - '&format=html&hl=' + video_data.preferences.locale; - } else { - plid_url = '/api/v1/playlists/' + plid + - '?index=' + video_data.index + - '&continuation' + video_data.id + - '&format=html&hl=' + video_data.preferences.locale; - } + var plid_url; + if (plid.startsWith("RD")) { + plid_url = + "/api/v1/mixes/" + + plid + + "?continuation=" + + video_data.id + + "&format=html&hl=" + + video_data.preferences.locale; + } else { + plid_url = + "/api/v1/playlists/" + + plid + + "?index=" + + video_data.index + + "&continuation" + + video_data.id + + "&format=html&hl=" + + video_data.preferences.locale; + } - helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, { - on200: function (response) { - if (!response.nextVideo) - return; + helpers.xhr( + "GET", + plid_url, + { retries: 5, entity_name: "playlist" }, + { + on200: function (response) { + if (!response.nextVideo) return; - player.on('ended', function () { - var url = new URL('https://example.com/embed/' + response.nextVideo); + player.on("ended", function () { + var url = new URL("https://example.com/embed/" + response.nextVideo); - url.searchParams.set('list', plid); - if (!plid.startsWith('RD')) - url.searchParams.set('index', response.index); - if (video_data.params.autoplay || video_data.params.continue_autoplay) - url.searchParams.set('autoplay', '1'); - if (video_data.params.listen !== video_data.preferences.listen) - url.searchParams.set('listen', video_data.params.listen); - if (video_data.params.speed !== video_data.preferences.speed) - url.searchParams.set('speed', video_data.params.speed); - if (video_data.params.local !== video_data.preferences.local) - url.searchParams.set('local', video_data.params.local); + url.searchParams.set("list", plid); + if (!plid.startsWith("RD")) + url.searchParams.set("index", response.index); + if (video_data.params.autoplay || video_data.params.continue_autoplay) + url.searchParams.set("autoplay", "1"); + if (video_data.params.listen !== video_data.preferences.listen) + url.searchParams.set("listen", video_data.params.listen); + if (video_data.params.speed !== video_data.preferences.speed) + url.searchParams.set("speed", video_data.params.speed); + if (video_data.params.local !== video_data.preferences.local) + url.searchParams.set("local", video_data.params.local); - location.assign(url.pathname + url.search); - }); - } - }); + location.assign(url.pathname + url.search); + }); + }, + }, + ); } -addEventListener('load', function (e) { - if (video_data.plid) { - get_playlist(video_data.plid); - } else if (video_data.video_series) { - player.on('ended', function () { - var url = new URL('https://example.com/embed/' + video_data.video_series.shift()); +addEventListener("load", function (e) { + if (video_data.plid) { + get_playlist(video_data.plid); + } else if (video_data.video_series) { + player.on("ended", function () { + var url = new URL( + "https://example.com/embed/" + video_data.video_series.shift(), + ); - if (video_data.params.autoplay || video_data.params.continue_autoplay) - url.searchParams.set('autoplay', '1'); - if (video_data.params.listen !== video_data.preferences.listen) - url.searchParams.set('listen', video_data.params.listen); - if (video_data.params.speed !== video_data.preferences.speed) - url.searchParams.set('speed', video_data.params.speed); - if (video_data.params.local !== video_data.preferences.local) - url.searchParams.set('local', video_data.params.local); - if (video_data.video_series.length !== 0) - url.searchParams.set('playlist', video_data.video_series.join(',')); + if (video_data.params.autoplay || video_data.params.continue_autoplay) + url.searchParams.set("autoplay", "1"); + if (video_data.params.listen !== video_data.preferences.listen) + url.searchParams.set("listen", video_data.params.listen); + if (video_data.params.speed !== video_data.preferences.speed) + url.searchParams.set("speed", video_data.params.speed); + if (video_data.params.local !== video_data.preferences.local) + url.searchParams.set("local", video_data.params.local); + if (video_data.video_series.length !== 0) + url.searchParams.set("playlist", video_data.video_series.join(",")); - location.assign(url.pathname + url.search); - }); - } + location.assign(url.pathname + url.search); + }); + } }); diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 51de08f6..efd0e1cf 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -1,149 +1,220 @@ -'use strict'; +"use strict"; (function () { - var video_player = document.getElementById('player_html5_api'); - if (video_player) { - video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; - video_player.onmouseleave = function () { video_player['title'] = video_player['data-title']; video_player['data-title'] = ''; }; - video_player.oncontextmenu = function () { video_player['title'] = video_player['data-title']; }; + var video_player = document.getElementById("player_html5_api"); + if (video_player) { + video_player.onmouseenter = function () { + video_player["data-title"] = video_player["title"]; + video_player["title"] = ""; + }; + video_player.onmouseleave = function () { + video_player["title"] = video_player["data-title"]; + video_player["data-title"] = ""; + }; + video_player.oncontextmenu = function () { + video_player["title"] = video_player["data-title"]; + }; + } + + // For dynamically inserted elements + addEventListener("click", function (e) { + if (!e || !e.target) return; + + var t = e.target; + var handler_name = t.getAttribute("data-onclick"); + + switch (handler_name) { + case "jump_to_time": + e.preventDefault(); + var time = t.getAttribute("data-jump-time"); + player.currentTime(time); + break; + case "get_youtube_replies": + var load_more = t.getAttribute("data-load-more") !== null; + var load_replies = t.getAttribute("data-load-replies") !== null; + get_youtube_replies(t, load_more, load_replies); + break; + case "toggle_parent": + e.preventDefault(); + toggle_parent(t); + break; + default: + break; + } + }); + + document + .querySelectorAll('[data-mouse="switch_classes"]') + .forEach(function (el) { + var classes = el.getAttribute("data-switch-classes").split(","); + var classOnEnter = classes[0]; + var classOnLeave = classes[1]; + function toggle_classes(toAdd, toRemove) { + el.classList.add(toAdd); + el.classList.remove(toRemove); + } + el.onmouseenter = function () { + toggle_classes(classOnEnter, classOnLeave); + }; + el.onmouseleave = function () { + toggle_classes(classOnLeave, classOnEnter); + }; + }); + + document + .querySelectorAll('[data-onsubmit="return_false"]') + .forEach(function (el) { + el.onsubmit = function () { + return false; + }; + }); + + document + .querySelectorAll('[data-onclick="mark_watched"]') + .forEach(function (el) { + el.onclick = function () { + mark_watched(el); + }; + }); + document + .querySelectorAll('[data-onclick="mark_unwatched"]') + .forEach(function (el) { + el.onclick = function () { + mark_unwatched(el); + }; + }); + document + .querySelectorAll('[data-onclick="add_playlist_video"]') + .forEach(function (el) { + el.onclick = function (e) { + add_playlist_video(e); + }; + }); + document + .querySelectorAll('[data-onclick="add_playlist_item"]') + .forEach(function (el) { + el.onclick = function (e) { + add_playlist_item(e); + }; + }); + document + .querySelectorAll('[data-onclick="remove_playlist_item"]') + .forEach(function (el) { + el.onclick = function (e) { + remove_playlist_item(e); + }; + }); + document + .querySelectorAll('[data-onclick="revoke_token"]') + .forEach(function (el) { + el.onclick = function () { + revoke_token(el); + }; + }); + document + .querySelectorAll('[data-onclick="remove_subscription"]') + .forEach(function (el) { + el.onclick = function () { + remove_subscription(el); + }; + }); + document + .querySelectorAll('[data-onclick="notification_requestPermission"]') + .forEach(function (el) { + el.onclick = function () { + Notification.requestPermission(); + }; + }); + + document + .querySelectorAll('[data-onrange="update_volume_value"]') + .forEach(function (el) { + function update_volume_value() { + document.getElementById("volume-value").textContent = el.value; + } + el.oninput = update_volume_value; + el.onchange = update_volume_value; + }); + + function revoke_token(target) { + var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; + row.style.display = "none"; + var count = document.getElementById("count"); + count.textContent--; + + var url = + "/token_ajax?action=revoke_token&redirect=false" + + "&referer=" + + encodeURIComponent(location.href) + + "&session=" + + target.getAttribute("data-session"); + + var payload = + "csrf_token=" + + target.parentNode.querySelector('input[name="csrf_token"]').value; + + helpers.xhr( + "POST", + url, + { payload: payload }, + { + onNon200: function (xhr) { + count.textContent++; + row.style.display = ""; + }, + }, + ); + } + + function remove_subscription(target) { + var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; + row.style.display = "none"; + var count = document.getElementById("count"); + count.textContent--; + + var url = + "/subscription_ajax?action=remove_subscriptions&redirect=false" + + "&referer=" + + encodeURIComponent(location.href) + + "&c=" + + target.getAttribute("data-ucid"); + + var payload = + "csrf_token=" + + target.parentNode.querySelector('input[name="csrf_token"]').value; + + helpers.xhr( + "POST", + url, + { payload: payload }, + { + onNon200: function (xhr) { + count.textContent++; + row.style.display = ""; + }, + }, + ); + } + + // Handle keypresses + addEventListener("keydown", function (event) { + // Ignore modifier keys + if (event.ctrlKey || event.metaKey) return; + + // Ignore shortcuts if any text input is focused + let focused_tag = document.activeElement.tagName.toLowerCase(); + const allowed = /^(button|checkbox|file|radio|submit)$/; + + if (focused_tag === "textarea") return; + if (focused_tag === "input") { + let focused_type = document.activeElement.type.toLowerCase(); + if (!allowed.test(focused_type)) return; } - // For dynamically inserted elements - addEventListener('click', function (e) { - if (!e || !e.target) return; - - var t = e.target; - var handler_name = t.getAttribute('data-onclick'); - - switch (handler_name) { - case 'jump_to_time': - e.preventDefault(); - var time = t.getAttribute('data-jump-time'); - player.currentTime(time); - break; - case 'get_youtube_replies': - var load_more = t.getAttribute('data-load-more') !== null; - var load_replies = t.getAttribute('data-load-replies') !== null; - get_youtube_replies(t, load_more, load_replies); - break; - case 'toggle_parent': - e.preventDefault(); - toggle_parent(t); - break; - default: - break; - } - }); - - document.querySelectorAll('[data-mouse="switch_classes"]').forEach(function (el) { - var classes = el.getAttribute('data-switch-classes').split(','); - var classOnEnter = classes[0]; - var classOnLeave = classes[1]; - function toggle_classes(toAdd, toRemove) { - el.classList.add(toAdd); - el.classList.remove(toRemove); - } - el.onmouseenter = function () { toggle_classes(classOnEnter, classOnLeave); }; - el.onmouseleave = function () { toggle_classes(classOnLeave, classOnEnter); }; - }); - - document.querySelectorAll('[data-onsubmit="return_false"]').forEach(function (el) { - el.onsubmit = function () { return false; }; - }); - - document.querySelectorAll('[data-onclick="mark_watched"]').forEach(function (el) { - el.onclick = function () { mark_watched(el); }; - }); - document.querySelectorAll('[data-onclick="mark_unwatched"]').forEach(function (el) { - el.onclick = function () { mark_unwatched(el); }; - }); - document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) { - el.onclick = function (e) { add_playlist_video(e); }; - }); - document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) { - el.onclick = function (e) { add_playlist_item(e); }; - }); - document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) { - el.onclick = function (e) { remove_playlist_item(e); }; - }); - document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) { - el.onclick = function () { revoke_token(el); }; - }); - document.querySelectorAll('[data-onclick="remove_subscription"]').forEach(function (el) { - el.onclick = function () { remove_subscription(el); }; - }); - document.querySelectorAll('[data-onclick="notification_requestPermission"]').forEach(function (el) { - el.onclick = function () { Notification.requestPermission(); }; - }); - - document.querySelectorAll('[data-onrange="update_volume_value"]').forEach(function (el) { - function update_volume_value() { - document.getElementById('volume-value').textContent = el.value; - } - el.oninput = update_volume_value; - el.onchange = update_volume_value; - }); - - - function revoke_token(target) { - var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; - row.style.display = 'none'; - var count = document.getElementById('count'); - count.textContent--; - - var url = '/token_ajax?action=revoke_token&redirect=false' + - '&referer=' + encodeURIComponent(location.href) + - '&session=' + target.getAttribute('data-session'); - - var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value; - - helpers.xhr('POST', url, {payload: payload}, { - onNon200: function (xhr) { - count.textContent++; - row.style.display = ''; - } - }); + // Focus search bar on '/' + if (event.key === "/") { + document.getElementById("searchbox").focus(); + event.preventDefault(); } - - function remove_subscription(target) { - var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; - row.style.display = 'none'; - var count = document.getElementById('count'); - count.textContent--; - - var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' + - '&referer=' + encodeURIComponent(location.href) + - '&c=' + target.getAttribute('data-ucid'); - - var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value; - - helpers.xhr('POST', url, {payload: payload}, { - onNon200: function (xhr) { - count.textContent++; - row.style.display = ''; - } - }); - } - - // Handle keypresses - addEventListener('keydown', function (event) { - // Ignore modifier keys - if (event.ctrlKey || event.metaKey) return; - - // Ignore shortcuts if any text input is focused - let focused_tag = document.activeElement.tagName.toLowerCase(); - const allowed = /^(button|checkbox|file|radio|submit)$/; - - if (focused_tag === 'textarea') return; - if (focused_tag === 'input') { - let focused_type = document.activeElement.type.toLowerCase(); - if (!allowed.test(focused_type)) return; - } - - // Focus search bar on '/' - if (event.key === '/') { - document.getElementById('searchbox').focus(); - event.preventDefault(); - } - }); + }); })(); diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 2c8552c8..0732d28d 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -31,7 +31,11 @@ async function get_subscriptions_call() { } // Start the retry mechanism -const get_subscriptions = exponential_backoff(get_subscriptions_call, 100, 1000); +const get_subscriptions = exponential_backoff( + get_subscriptions_call, + 100, + 1000, +); function create_notification_stream(subscriptions) { // sse.js can't be replaced to EventSource in place as it lack support of payload and headers @@ -96,7 +100,6 @@ function create_notification_stream(subscriptions) { "Something went wrong with notifications, trying to reconnect...", ); notifications = notifications_mock; - }); notifications.stream(); @@ -177,7 +180,7 @@ function exponential_backoff( return function tryFunction() { fn() .then((response) => { - attempt = 0; + attempt = 0; }) .catch((error) => { if (attempt < maxRetries) { @@ -193,5 +196,5 @@ function exponential_backoff( console.log("Max retries reached. Operation failed:", error); } }); - } + }; } diff --git a/assets/js/pagination.js b/assets/js/pagination.js index 2e560a34..9d67b839 100644 --- a/assets/js/pagination.js +++ b/assets/js/pagination.js @@ -1,93 +1,103 @@ -'use strict'; +"use strict"; -const CURRENT_CONTINUATION = (new URL(document.location)).searchParams.get("continuation"); +const CURRENT_CONTINUATION = new URL(document.location).searchParams.get( + "continuation", +); const CONT_CACHE_KEY = `continuation_cache_${encodeURIComponent(window.location.pathname)}`; -function get_data(){ - return JSON.parse(sessionStorage.getItem(CONT_CACHE_KEY)) || []; +function get_data() { + return JSON.parse(sessionStorage.getItem(CONT_CACHE_KEY)) || []; } -function save_data(){ - const prev_data = get_data(); - prev_data.push(CURRENT_CONTINUATION); +function save_data() { + const prev_data = get_data(); + prev_data.push(CURRENT_CONTINUATION); - sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data)); + sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data)); } -function button_press(){ - let prev_data = get_data(); - if (!prev_data.length) return null; +function button_press() { + let prev_data = get_data(); + if (!prev_data.length) return null; - // Sanity check. Nowhere should the current continuation token exist in the cache - // but it can happen when using the browser's back feature. As such we'd need to travel - // back to the point where the current continuation token first appears in order to - // account for the rewind. - const conflict_at = prev_data.indexOf(CURRENT_CONTINUATION); - if (conflict_at != -1) { - prev_data.length = conflict_at; - } + // Sanity check. Nowhere should the current continuation token exist in the cache + // but it can happen when using the browser's back feature. As such we'd need to travel + // back to the point where the current continuation token first appears in order to + // account for the rewind. + const conflict_at = prev_data.indexOf(CURRENT_CONTINUATION); + if (conflict_at != -1) { + prev_data.length = conflict_at; + } - const prev_ctoken = prev_data.pop(); - - // On the first page, the stored continuation token is null. - if (prev_ctoken === null) { - sessionStorage.removeItem(CONT_CACHE_KEY); - let url = set_continuation(); - window.location.href = url; - - return; - } - - sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data)); - let url = set_continuation(prev_ctoken); + const prev_ctoken = prev_data.pop(); + // On the first page, the stored continuation token is null. + if (prev_ctoken === null) { + sessionStorage.removeItem(CONT_CACHE_KEY); + let url = set_continuation(); window.location.href = url; -}; + + return; + } + + sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data)); + let url = set_continuation(prev_ctoken); + + window.location.href = url; +} // Method to set the current page's continuation token // Removes the continuation parameter when a continuation token is not given -function set_continuation(prev_ctoken = null){ - let url = window.location.href.split('?')[0]; - let params = window.location.href.split('?')[1]; - let url_params = new URLSearchParams(params); +function set_continuation(prev_ctoken = null) { + let url = window.location.href.split("?")[0]; + let params = window.location.href.split("?")[1]; + let url_params = new URLSearchParams(params); - if (prev_ctoken) { - url_params.set("continuation", prev_ctoken); - } else { - url_params.delete('continuation'); - }; + if (prev_ctoken) { + url_params.set("continuation", prev_ctoken); + } else { + url_params.delete("continuation"); + } - if(Array.from(url_params).length > 0){ - return `${url}?${url_params.toString()}`; - } else { - return url; - } + if (Array.from(url_params).length > 0) { + return `${url}?${url_params.toString()}`; + } else { + return url; + } } -addEventListener('DOMContentLoaded', function(){ - const pagination_data = JSON.parse(document.getElementById('pagination-data').textContent); - const next_page_containers = document.getElementsByClassName("page-next-container"); +addEventListener("DOMContentLoaded", function () { + const pagination_data = JSON.parse( + document.getElementById("pagination-data").textContent, + ); + const next_page_containers = document.getElementsByClassName( + "page-next-container", + ); - for (let container of next_page_containers){ - const next_page_button = container.getElementsByClassName("pure-button") + for (let container of next_page_containers) { + const next_page_button = container.getElementsByClassName("pure-button"); - // exists? - if (next_page_button.length > 0){ - next_page_button[0].addEventListener("click", save_data); - } + // exists? + if (next_page_button.length > 0) { + next_page_button[0].addEventListener("click", save_data); } + } - // Only add previous page buttons when not on the first page - if (CURRENT_CONTINUATION) { - const prev_page_containers = document.getElementsByClassName("page-prev-container") + // Only add previous page buttons when not on the first page + if (CURRENT_CONTINUATION) { + const prev_page_containers = document.getElementsByClassName( + "page-prev-container", + ); - for (let container of prev_page_containers) { - if (pagination_data.is_rtl) { - container.innerHTML = `` - } else { - container.innerHTML = `` - } - container.getElementsByClassName("pure-button")[0].addEventListener("click", button_press); - } + for (let container of prev_page_containers) { + if (pagination_data.is_rtl) { + container.innerHTML = ``; + } else { + container.innerHTML = ``; + } + container + .getElementsByClassName("pure-button")[0] + .addEventListener("click", button_press); } + } }); diff --git a/assets/js/player.js b/assets/js/player.js index f32c9b56..ed74ed85 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -1,98 +1,99 @@ -'use strict'; -var player_data = JSON.parse(document.getElementById('player_data').textContent); -var video_data = JSON.parse(document.getElementById('video_data').textContent); +"use strict"; +var player_data = JSON.parse( + document.getElementById("player_data").textContent, +); +var video_data = JSON.parse(document.getElementById("video_data").textContent); var options = { - liveui: true, - playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], - controlBar: { - children: [ - 'playToggle', - 'volumePanel', - 'currentTimeDisplay', - 'timeDivider', - 'durationDisplay', - 'progressControl', - 'remainingTimeDisplay', - 'Spacer', - 'captionsButton', - 'audioTrackButton', - 'qualitySelector', - 'playbackRateMenuButton', - 'fullscreenToggle' - ] + liveui: true, + playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], + controlBar: { + children: [ + "playToggle", + "volumePanel", + "currentTimeDisplay", + "timeDivider", + "durationDisplay", + "progressControl", + "remainingTimeDisplay", + "Spacer", + "captionsButton", + "audioTrackButton", + "qualitySelector", + "playbackRateMenuButton", + "fullscreenToggle", + ], + }, + html5: { + preloadTextTracks: false, + vhs: { + overrideNative: true, }, - html5: { - preloadTextTracks: false, - vhs: { - overrideNative: true - } - } + }, }; if (player_data.aspect_ratio) { - options.aspectRatio = player_data.aspect_ratio; + options.aspectRatio = player_data.aspect_ratio; } var embed_url = new URL(location); -embed_url.searchParams.delete('v'); -var short_url = location.origin + '/' + video_data.id + embed_url.search; -embed_url = location.origin + '/embed/' + video_data.id + embed_url.search; +embed_url.searchParams.delete("v"); +var short_url = location.origin + "/" + video_data.id + embed_url.search; +embed_url = location.origin + "/embed/" + video_data.id + embed_url.search; -var save_player_pos_key = 'save_player_pos'; +var save_player_pos_key = "save_player_pos"; -videojs.Vhs.xhr.beforeRequest = function(options) { - // set local if requested not videoplayback - if (!options.uri.includes('videoplayback')) { - if (!options.uri.includes('local=true')) - options.uri += '?local=true'; - } - return options; +videojs.Vhs.xhr.beforeRequest = function (options) { + // set local if requested not videoplayback + if (!options.uri.includes("videoplayback")) { + if (!options.uri.includes("local=true")) options.uri += "?local=true"; + } + return options; }; -var player = videojs('player', options); +var player = videojs("player", options); -player.on('error', function () { - if (video_data.params.quality === 'dash') return; +player.on("error", function () { + if (video_data.params.quality === "dash") return; - var localNotDisabled = ( - !player.currentSrc().includes('local=true') && !video_data.local_disabled - ); - var reloadMakesSense = ( - player.error().code === MediaError.MEDIA_ERR_NETWORK || - player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED + var localNotDisabled = + !player.currentSrc().includes("local=true") && !video_data.local_disabled; + var reloadMakesSense = + player.error().code === MediaError.MEDIA_ERR_NETWORK || + player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED; + + if (localNotDisabled) { + // add local=true to all current sources + player.src( + player.currentSources().map(function (source) { + source.src += "&local=true"; + return source; + }), ); + } else if (reloadMakesSense) { + setTimeout(function () { + console.warn("An error occurred in the player, reloading..."); - if (localNotDisabled) { - // add local=true to all current sources - player.src(player.currentSources().map(function (source) { - source.src += '&local=true'; - return source; - })); - } else if (reloadMakesSense) { - setTimeout(function () { - console.warn('An error occurred in the player, reloading...'); + // After load() all parameters are reset. Save them + var currentTime = player.currentTime(); + var playbackRate = player.playbackRate(); + var paused = player.paused(); - // After load() all parameters are reset. Save them - var currentTime = player.currentTime(); - var playbackRate = player.playbackRate(); - var paused = player.paused(); + player.load(); - player.load(); + if (currentTime > 0.5) currentTime -= 0.5; - if (currentTime > 0.5) currentTime -= 0.5; - - player.currentTime(currentTime); - player.playbackRate(playbackRate); - if (!paused) player.play(); - }, 5000); - } + player.currentTime(currentTime); + player.playbackRate(playbackRate); + if (!paused) player.play(); + }, 5000); + } }); -if (video_data.params.quality === 'dash') { - player.reloadSourceOnError({ - errorInterval: 10 - }); +if (video_data.params.quality === "dash") { + player.reloadSourceOnError({ + errorInterval: 10, + }); } /** @@ -103,14 +104,12 @@ if (video_data.params.quality === 'dash') { * @returns {URL} urlWithTimeArg */ function addCurrentTimeToURL(url, base) { - var urlUsed = new URL(url, base); - urlUsed.searchParams.delete('start'); - var currentTime = Math.ceil(player.currentTime()); - if (currentTime > 0) - urlUsed.searchParams.set('t', currentTime); - else if (urlUsed.searchParams.has('t')) - urlUsed.searchParams.delete('t'); - return urlUsed; + var urlUsed = new URL(url, base); + urlUsed.searchParams.delete("start"); + var currentTime = Math.ceil(player.currentTime()); + if (currentTime > 0) urlUsed.searchParams.set("t", currentTime); + else if (urlUsed.searchParams.has("t")) urlUsed.searchParams.delete("t"); + return urlUsed; } /** @@ -125,148 +124,182 @@ var timeupdate_last_ts = 5; /** * Callback that updates the timestamp on all external links */ -player.on('timeupdate', function () { - // Only update once every second - let current_ts = Math.floor(player.currentTime()); - if (current_ts > timeupdate_last_ts) timeupdate_last_ts = current_ts; - else return; +player.on("timeupdate", function () { + // Only update once every second + let current_ts = Math.floor(player.currentTime()); + if (current_ts > timeupdate_last_ts) timeupdate_last_ts = current_ts; + else return; - // YouTube links + // YouTube links - let elem_yt_watch = document.getElementById('link-yt-watch'); - if (elem_yt_watch) { - let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url'); - elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch); - } - - let elem_yt_embed = document.getElementById('link-yt-embed'); - if (elem_yt_embed) { - let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); - elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed); - } + let elem_yt_watch = document.getElementById("link-yt-watch"); + if (elem_yt_watch) { + let base_url_yt_watch = elem_yt_watch.getAttribute("data-base-url"); + elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch); + } - // Invidious links + let elem_yt_embed = document.getElementById("link-yt-embed"); + if (elem_yt_embed) { + let base_url_yt_embed = elem_yt_embed.getAttribute("data-base-url"); + elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed); + } - let domain = window.location.origin; + // Invidious links - let elem_iv_embed = document.getElementById('link-iv-embed'); - if (elem_iv_embed) { - let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); - elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain); - } - - let elem_iv_other = document.getElementById('link-iv-other'); - if (elem_iv_other) { - let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); - elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); - } + let domain = window.location.origin; + + let elem_iv_embed = document.getElementById("link-iv-embed"); + if (elem_iv_embed) { + let base_url_iv_embed = elem_iv_embed.getAttribute("data-base-url"); + elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain); + } + + let elem_iv_other = document.getElementById("link-iv-other"); + if (elem_iv_other) { + let base_url_iv_other = elem_iv_other.getAttribute("data-base-url"); + elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); + } }); - var shareOptions = { - socials: ['fbFeed', 'tw', 'reddit', 'email'], + socials: ["fbFeed", "tw", "reddit", "email"], - get url() { - return addCurrentTimeToURL(short_url); - }, - title: player_data.title, - description: player_data.description, - image: player_data.thumbnail, - get embedCode() { - // Single quotes inside here required. HTML inserted as is into value attribute of input - return ""; - } + get url() { + return addCurrentTimeToURL(short_url); + }, + title: player_data.title, + description: player_data.description, + image: player_data.thumbnail, + get embedCode() { + // Single quotes inside here required. HTML inserted as is into value attribute of input + return ( + "" + ); + }, }; -if (location.pathname.startsWith('/embed/')) { - var overlay_content = '

      ' + player_data.title + '

      '; - player.overlay({ - overlays: [ - { start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'}, - { start: 'pause', content: overlay_content, end: 'playing', align: 'top'} - ] - }); +if (location.pathname.startsWith("/embed/")) { + var overlay_content = + '

      ' + + player_data.title + + "

      "; + player.overlay({ + overlays: [ + { + start: "loadstart", + content: overlay_content, + end: "playing", + align: "top", + }, + { + start: "pause", + content: overlay_content, + end: "playing", + align: "top", + }, + ], + }); } // Detect mobile users and initialize mobileUi for better UX // Detection code taken from https://stackoverflow.com/a/20293441 function isMobile() { - try{ document.createEvent('TouchEvent'); return true; } - catch(e){ return false; } + try { + document.createEvent("TouchEvent"); + return true; + } catch (e) { + return false; + } } if (isMobile()) { - player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); + player.mobileUi({ + touchControls: { seekSeconds: 5 * player.playbackRate() }, + }); - var buttons = ['playToggle', 'volumePanel', 'captionsButton']; + var buttons = ["playToggle", "volumePanel", "captionsButton"]; - if (!video_data.params.listen && video_data.params.quality === 'dash') buttons.push('audioTrackButton'); - if (video_data.params.listen || video_data.params.quality !== 'dash') buttons.push('qualitySelector'); + if (!video_data.params.listen && video_data.params.quality === "dash") + buttons.push("audioTrackButton"); + if (video_data.params.listen || video_data.params.quality !== "dash") + buttons.push("qualitySelector"); - // Create new control bar object for operation buttons - const ControlBar = videojs.getComponent('controlBar'); - let operations_bar = new ControlBar(player, { - children: [], - playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] - }); - buttons.slice(1).forEach(function (child) {operations_bar.addChild(child);}); + // Create new control bar object for operation buttons + const ControlBar = videojs.getComponent("controlBar"); + let operations_bar = new ControlBar(player, { + children: [], + playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], + }); + buttons.slice(1).forEach(function (child) { + operations_bar.addChild(child); + }); - // Remove operation buttons from primary control bar - var primary_control_bar = player.getChild('controlBar'); - buttons.forEach(function (child) {primary_control_bar.removeChild(child);}); + // Remove operation buttons from primary control bar + var primary_control_bar = player.getChild("controlBar"); + buttons.forEach(function (child) { + primary_control_bar.removeChild(child); + }); - var operations_bar_element = operations_bar.el(); - operations_bar_element.classList.add('mobile-operations-bar'); - player.addChild(operations_bar); + var operations_bar_element = operations_bar.el(); + operations_bar_element.classList.add("mobile-operations-bar"); + player.addChild(operations_bar); - // Playback menu doesn't work when it's initialized outside of the primary control bar - var playback_element = document.getElementsByClassName('vjs-playback-rate')[0]; - operations_bar_element.append(playback_element); + // Playback menu doesn't work when it's initialized outside of the primary control bar + var playback_element = + document.getElementsByClassName("vjs-playback-rate")[0]; + operations_bar_element.append(playback_element); - // The share and http source selector element can't be fetched till the players ready. - player.one('playing', function () { - var share_element = document.getElementsByClassName('vjs-share-control')[0]; - operations_bar_element.append(share_element); + // The share and http source selector element can't be fetched till the players ready. + player.one("playing", function () { + var share_element = document.getElementsByClassName("vjs-share-control")[0]; + operations_bar_element.append(share_element); - if (!video_data.params.listen && video_data.params.quality === 'dash') { - var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0]; - operations_bar_element.append(http_source_selector); - } - }); + if (!video_data.params.listen && video_data.params.quality === "dash") { + var http_source_selector = document.getElementsByClassName( + "vjs-http-source-selector vjs-menu-button", + )[0]; + operations_bar_element.append(http_source_selector); + } + }); } // Enable VR video support if (!video_data.params.listen && video_data.vr && video_data.params.vr_mode) { - player.crossOrigin('anonymous'); - switch (video_data.projection_type) { - case 'EQUIRECTANGULAR': - player.vr({projection: 'equirectangular'}); - default: // Should only be 'MESH' but we'll use this as a fallback. - player.vr({projection: 'EAC'}); - } + player.crossOrigin("anonymous"); + switch (video_data.projection_type) { + case "EQUIRECTANGULAR": + player.vr({ projection: "equirectangular" }); + default: // Should only be 'MESH' but we'll use this as a fallback. + player.vr({ projection: "EAC" }); + } } // Add markers if (video_data.params.video_start > 0 || video_data.params.video_end > 0) { - var markers = [{ time: video_data.params.video_start, text: 'Start' }]; + var markers = [{ time: video_data.params.video_start, text: "Start" }]; - if (video_data.params.video_end < 0) { - markers.push({ time: video_data.length_seconds - 0.5, text: 'End' }); - } else { - markers.push({ time: video_data.params.video_end, text: 'End' }); - } + if (video_data.params.video_end < 0) { + markers.push({ time: video_data.length_seconds - 0.5, text: "End" }); + } else { + markers.push({ time: video_data.params.video_end, text: "End" }); + } - player.markers({ - onMarkerReached: function (marker) { - if (marker.text === 'End') - player.loop() ? player.markers.prev('Start') : player.pause(); - }, - markers: markers - }); + player.markers({ + onMarkerReached: function (marker) { + if (marker.text === "End") + player.loop() ? player.markers.prev("Start") : player.pause(); + }, + markers: markers, + }); - player.currentTime(video_data.params.video_start); + player.currentTime(video_data.params.video_start); } player.volume(video_data.params.volume / 100); @@ -279,11 +312,12 @@ player.playbackRate(video_data.params.speed); * @returns {String|null} cookieValue */ function getCookieValue(name) { - var cookiePrefix = name + '='; - var matchedCookie = document.cookie.split(';').find(function (item) {return item.includes(cookiePrefix);}); - if (matchedCookie) - return matchedCookie.replace(cookiePrefix, ''); - return null; + var cookiePrefix = name + "="; + var matchedCookie = document.cookie.split(";").find(function (item) { + return item.includes(cookiePrefix); + }); + if (matchedCookie) return matchedCookie.replace(cookiePrefix, ""); + return null; } /** @@ -293,502 +327,596 @@ function getCookieValue(name) { * @param {number} newSpeed New speed defined (null if unchanged) */ function updateCookie(newVolume, newSpeed) { - var volumeValue = newVolume !== null ? newVolume : video_data.params.volume; - var speedValue = newSpeed !== null ? newSpeed : video_data.params.speed; + var volumeValue = newVolume !== null ? newVolume : video_data.params.volume; + var speedValue = newSpeed !== null ? newSpeed : video_data.params.speed; - var cookieValue = getCookieValue('PREFS'); - var cookieData; + var cookieValue = getCookieValue("PREFS"); + var cookieData; - if (cookieValue !== null) { - var cookieJson = JSON.parse(decodeURIComponent(cookieValue)); - cookieJson.volume = volumeValue; - cookieJson.speed = speedValue; - cookieData = encodeURIComponent(JSON.stringify(cookieJson)); - } else { - cookieData = encodeURIComponent(JSON.stringify({ 'volume': volumeValue, 'speed': speedValue })); - } + if (cookieValue !== null) { + var cookieJson = JSON.parse(decodeURIComponent(cookieValue)); + cookieJson.volume = volumeValue; + cookieJson.speed = speedValue; + cookieData = encodeURIComponent(JSON.stringify(cookieJson)); + } else { + cookieData = encodeURIComponent( + JSON.stringify({ volume: volumeValue, speed: speedValue }), + ); + } - // Set expiration in 2 year - var date = new Date(); - date.setFullYear(date.getFullYear() + 2); + // Set expiration in 2 year + var date = new Date(); + date.setFullYear(date.getFullYear() + 2); - var ipRegex = /^((\d+\.){3}\d+|[\dA-Fa-f]*:[\d:A-Fa-f]*:[\d:A-Fa-f]+)$/; - var domainUsed = location.hostname; + var ipRegex = /^((\d+\.){3}\d+|[\dA-Fa-f]*:[\d:A-Fa-f]*:[\d:A-Fa-f]+)$/; + var domainUsed = location.hostname; - // Fix for a bug in FF where the leading dot in the FQDN is not ignored - if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost') - domainUsed = '.' + location.hostname; + // Fix for a bug in FF where the leading dot in the FQDN is not ignored + if ( + domainUsed.charAt(0) !== "." && + !ipRegex.test(domainUsed) && + domainUsed !== "localhost" + ) + domainUsed = "." + location.hostname; - var secure = location.protocol.startsWith("https") ? " Secure;" : ""; + var secure = location.protocol.startsWith("https") ? " Secure;" : ""; - document.cookie = 'PREFS=' + cookieData + '; SameSite=Lax; path=/; domain=' + - domainUsed + '; expires=' + date.toGMTString() + ';' + secure; + document.cookie = + "PREFS=" + + cookieData + + "; SameSite=Lax; path=/; domain=" + + domainUsed + + "; expires=" + + date.toGMTString() + + ";" + + secure; - video_data.params.volume = volumeValue; - video_data.params.speed = speedValue; + video_data.params.volume = volumeValue; + video_data.params.speed = speedValue; } -player.on('ratechange', function () { - updateCookie(null, player.playbackRate()); - if (isMobile()) { - player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); - } +player.on("ratechange", function () { + updateCookie(null, player.playbackRate()); + if (isMobile()) { + player.mobileUi({ + touchControls: { seekSeconds: 5 * player.playbackRate() }, + }); + } }); -player.on('volumechange', function () { - updateCookie(Math.ceil(player.volume() * 100), null); +player.on("volumechange", function () { + updateCookie(Math.ceil(player.volume() * 100), null); }); -player.on('waiting', function () { - if (player.playbackRate() > 1 && player.liveTracker.isLive() && player.liveTracker.atLiveEdge()) { - console.info('Player has caught up to source, resetting playbackRate'); - player.playbackRate(1); - } +player.on("waiting", function () { + if ( + player.playbackRate() > 1 && + player.liveTracker.isLive() && + player.liveTracker.atLiveEdge() + ) { + console.info("Player has caught up to source, resetting playbackRate"); + player.playbackRate(1); + } }); -if (video_data.premiere_timestamp && Math.round(new Date() / 1000) < video_data.premiere_timestamp) { - player.getChild('bigPlayButton').hide(); +if ( + video_data.premiere_timestamp && + Math.round(new Date() / 1000) < video_data.premiere_timestamp +) { + player.getChild("bigPlayButton").hide(); } if (video_data.params.save_player_pos) { - const url = new URL(location); - const hasTimeParam = url.searchParams.has('t'); - const rememberedTime = get_video_time(); - let lastUpdated = 0; + const url = new URL(location); + const hasTimeParam = url.searchParams.has("t"); + const rememberedTime = get_video_time(); + let lastUpdated = 0; - if(!hasTimeParam) { - if (rememberedTime >= video_data.length_seconds - 20) - set_seconds_after_start(0); - else - set_seconds_after_start(rememberedTime); + if (!hasTimeParam) { + if (rememberedTime >= video_data.length_seconds - 20) + set_seconds_after_start(0); + else set_seconds_after_start(rememberedTime); + } + + player.on("timeupdate", function () { + const raw = player.currentTime(); + const time = Math.floor(raw); + + if (lastUpdated !== time && raw <= video_data.length_seconds - 15) { + save_video_time(time); + lastUpdated = time; } - - player.on('timeupdate', function () { - const raw = player.currentTime(); - const time = Math.floor(raw); - - if(lastUpdated !== time && raw <= video_data.length_seconds - 15) { - save_video_time(time); - lastUpdated = time; - } - }); -} -else remove_all_video_times(); + }); +} else remove_all_video_times(); if (video_data.params.autoplay) { - var bpb = player.getChild('bigPlayButton'); - bpb.hide(); + var bpb = player.getChild("bigPlayButton"); + bpb.hide(); - player.ready(function () { - new Promise(function (resolve, reject) { - setTimeout(function () {resolve(1);}, 1); - }).then(function (result) { - var promise = player.play(); + player.ready(function () { + new Promise(function (resolve, reject) { + setTimeout(function () { + resolve(1); + }, 1); + }).then(function (result) { + var promise = player.play(); - if (promise !== undefined) { - promise.then(function () { - }).catch(function (error) { - bpb.show(); - }); - } - }); + if (promise !== undefined) { + promise + .then(function () {}) + .catch(function (error) { + bpb.show(); + }); + } }); + }); } -if (!video_data.params.listen && video_data.params.quality === 'dash') { - player.httpSourceSelector(); +if (!video_data.params.listen && video_data.params.quality === "dash") { + player.httpSourceSelector(); - if (video_data.params.quality_dash !== 'auto') { - player.ready(function () { - player.on('loadedmetadata', function () { - const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;}); - let targetQualityLevel; - switch (video_data.params.quality_dash) { - case 'best': - targetQualityLevel = qualityLevels.length - 1; - break; - case 'worst': - targetQualityLevel = 0; - break; - default: - const targetHeight = parseInt(video_data.params.quality_dash); - for (let i = 0; i < qualityLevels.length; i++) { - if (qualityLevels[i].height <= targetHeight) - targetQualityLevel = i; - else - break; - } - } - qualityLevels.forEach(function (level, index) { - level.enabled = (index === targetQualityLevel); - }); - }); + if (video_data.params.quality_dash !== "auto") { + player.ready(function () { + player.on("loadedmetadata", function () { + const qualityLevels = Array.from(player.qualityLevels()).sort( + function (a, b) { + return a.height - b.height; + }, + ); + let targetQualityLevel; + switch (video_data.params.quality_dash) { + case "best": + targetQualityLevel = qualityLevels.length - 1; + break; + case "worst": + targetQualityLevel = 0; + break; + default: + const targetHeight = parseInt(video_data.params.quality_dash); + for (let i = 0; i < qualityLevels.length; i++) { + if (qualityLevels[i].height <= targetHeight) + targetQualityLevel = i; + else break; + } + } + qualityLevels.forEach(function (level, index) { + level.enabled = index === targetQualityLevel; }); - } + }); + }); + } } player.vttThumbnails({ - src: '/api/v1/storyboards/' + video_data.id + '?height=90', - showTimestamp: true + src: "/api/v1/storyboards/" + video_data.id + "?height=90", + showTimestamp: true, }); // Enable annotations if (!video_data.params.listen && video_data.params.annotations) { - addEventListener('load', function (e) { - addEventListener('__ar_annotation_click', function (e) { - const url = e.detail.url, - target = e.detail.target, - seconds = e.detail.seconds; - var path = new URL(url); + addEventListener("load", function (e) { + addEventListener("__ar_annotation_click", function (e) { + const url = e.detail.url, + target = e.detail.target, + seconds = e.detail.seconds; + var path = new URL(url); - if (path.href.startsWith('https://www.youtube.com/watch?') && seconds) { - path.search += '&t=' + seconds; - } + if (path.href.startsWith("https://www.youtube.com/watch?") && seconds) { + path.search += "&t=" + seconds; + } - path = path.pathname + path.search; - - if (target === 'current') { - location.href = path; - } else if (target === 'new') { - open(path, '_blank'); - } - }); - - helpers.xhr('GET', '/api/v1/annotations/' + video_data.id, { - responseType: 'text', - timeout: 60000 - }, { - on200: function (response) { - var video_container = document.getElementById('player'); - videojs.registerPlugin('youtubeAnnotationsPlugin', youtubeAnnotationsPlugin); - if (player.paused()) { - player.one('play', function (event) { - player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); - }); - } else { - player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); - } - } - }); + path = path.pathname + path.search; + if (target === "current") { + location.href = path; + } else if (target === "new") { + open(path, "_blank"); + } }); + + helpers.xhr( + "GET", + "/api/v1/annotations/" + video_data.id, + { + responseType: "text", + timeout: 60000, + }, + { + on200: function (response) { + var video_container = document.getElementById("player"); + videojs.registerPlugin( + "youtubeAnnotationsPlugin", + youtubeAnnotationsPlugin, + ); + if (player.paused()) { + player.one("play", function (event) { + player.youtubeAnnotationsPlugin({ + annotationXml: response, + videoContainer: video_container, + }); + }); + } else { + player.youtubeAnnotationsPlugin({ + annotationXml: response, + videoContainer: video_container, + }); + } + }, + }, + ); + }); } function change_volume(delta) { - const curVolume = player.volume(); - let newVolume = curVolume + delta; - newVolume = helpers.clamp(newVolume, 0, 1); - player.volume(newVolume); + const curVolume = player.volume(); + let newVolume = curVolume + delta; + newVolume = helpers.clamp(newVolume, 0, 1); + player.volume(newVolume); } function toggle_muted() { - player.muted(!player.muted()); + player.muted(!player.muted()); } function skip_seconds(delta) { - const duration = player.duration(); - const curTime = player.currentTime(); - let newTime = curTime + delta; - newTime = helpers.clamp(newTime, 0, duration); - player.currentTime(newTime); + const duration = player.duration(); + const curTime = player.currentTime(); + let newTime = curTime + delta; + newTime = helpers.clamp(newTime, 0, duration); + player.currentTime(newTime); } function set_seconds_after_start(delta) { - const start = video_data.params.video_start; - player.currentTime(start + delta); + const start = video_data.params.video_start; + player.currentTime(start + delta); } function save_video_time(seconds) { - const all_video_times = get_all_video_times(); - all_video_times[video_data.id] = seconds; - helpers.storage.set(save_player_pos_key, all_video_times); + const all_video_times = get_all_video_times(); + all_video_times[video_data.id] = seconds; + helpers.storage.set(save_player_pos_key, all_video_times); } function get_video_time() { - return get_all_video_times()[video_data.id] || 0; + return get_all_video_times()[video_data.id] || 0; } function get_all_video_times() { - return helpers.storage.get(save_player_pos_key) || {}; + return helpers.storage.get(save_player_pos_key) || {}; } function remove_all_video_times() { - helpers.storage.remove(save_player_pos_key); + helpers.storage.remove(save_player_pos_key); } function set_time_percent(percent) { - const duration = player.duration(); - const newTime = duration * (percent / 100); - player.currentTime(newTime); + const duration = player.duration(); + const newTime = duration * (percent / 100); + player.currentTime(newTime); } -function play() { player.play(); } -function pause() { player.pause(); } -function stop() { player.pause(); player.currentTime(0); } -function toggle_play() { player.paused() ? play() : pause(); } +function play() { + player.play(); +} +function pause() { + player.pause(); +} +function stop() { + player.pause(); + player.currentTime(0); +} +function toggle_play() { + player.paused() ? play() : pause(); +} const toggle_captions = (function () { - let toggledTrack = null; + let toggledTrack = null; - function bindChange(onOrOff) { - player.textTracks()[onOrOff]('change', function (e) { - toggledTrack = null; - }); + function bindChange(onOrOff) { + player.textTracks()[onOrOff]("change", function (e) { + toggledTrack = null; + }); + } + + // Wrapper function to ignore our own emitted events and only listen + // to events emitted by Video.js on click on the captions menu items. + function setMode(track, mode) { + bindChange("off"); + track.mode = mode; + setTimeout(function () { + bindChange("on"); + }, 0); + } + + bindChange("on"); + return function () { + if (toggledTrack !== null) { + if (toggledTrack.mode !== "showing") { + setMode(toggledTrack, "showing"); + } else { + setMode(toggledTrack, "disabled"); + } + toggledTrack = null; + return; } - // Wrapper function to ignore our own emitted events and only listen - // to events emitted by Video.js on click on the captions menu items. - function setMode(track, mode) { - bindChange('off'); - track.mode = mode; - setTimeout(function () { - bindChange('on'); - }, 0); + // Used as a fallback if no captions are currently active. + // TODO: Make this more intelligent by e.g. relying on browser language. + let fallbackCaptionsTrack = null; + + const tracks = player.textTracks(); + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + if (track.kind !== "captions") continue; + + if (fallbackCaptionsTrack === null) { + fallbackCaptionsTrack = track; + } + if (track.mode === "showing") { + setMode(track, "disabled"); + toggledTrack = track; + return; + } } - bindChange('on'); - return function () { - if (toggledTrack !== null) { - if (toggledTrack.mode !== 'showing') { - setMode(toggledTrack, 'showing'); - } else { - setMode(toggledTrack, 'disabled'); - } - toggledTrack = null; - return; - } - - // Used as a fallback if no captions are currently active. - // TODO: Make this more intelligent by e.g. relying on browser language. - let fallbackCaptionsTrack = null; - - const tracks = player.textTracks(); - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i]; - if (track.kind !== 'captions') continue; - - if (fallbackCaptionsTrack === null) { - fallbackCaptionsTrack = track; - } - if (track.mode === 'showing') { - setMode(track, 'disabled'); - toggledTrack = track; - return; - } - } - - // Fallback if no captions are currently active. - if (fallbackCaptionsTrack !== null) { - setMode(fallbackCaptionsTrack, 'showing'); - toggledTrack = fallbackCaptionsTrack; - } - }; + // Fallback if no captions are currently active. + if (fallbackCaptionsTrack !== null) { + setMode(fallbackCaptionsTrack, "showing"); + toggledTrack = fallbackCaptionsTrack; + } + }; })(); function toggle_fullscreen() { - player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); + player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); } function increase_playback_rate(steps) { - const maxIndex = options.playbackRates.length - 1; - const curIndex = options.playbackRates.indexOf(player.playbackRate()); - let newIndex = curIndex + steps; - newIndex = helpers.clamp(newIndex, 0, maxIndex); - player.playbackRate(options.playbackRates[newIndex]); + const maxIndex = options.playbackRates.length - 1; + const curIndex = options.playbackRates.indexOf(player.playbackRate()); + let newIndex = curIndex + steps; + newIndex = helpers.clamp(newIndex, 0, maxIndex); + player.playbackRate(options.playbackRates[newIndex]); } -addEventListener('keydown', function (e) { - if (e.target.tagName.toLowerCase() === 'input') { - // Ignore input when focus is on certain elements, e.g. form fields. - return; +addEventListener( + "keydown", + function (e) { + if (e.target.tagName.toLowerCase() === "input") { + // Ignore input when focus is on certain elements, e.g. form fields. + return; } // See https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L310-L313 - const isPlayerFocused = false - || e.target === document.querySelector('.video-js') - || e.target === document.querySelector('.vjs-tech') - || e.target === document.querySelector('.iframeblocker') - || e.target === document.querySelector('.vjs-control-bar') - ; + const isPlayerFocused = + false || + e.target === document.querySelector(".video-js") || + e.target === document.querySelector(".vjs-tech") || + e.target === document.querySelector(".iframeblocker") || + e.target === document.querySelector(".vjs-control-bar"); let action = null; const code = e.keyCode; const decoratedKey = - e.key - + (e.altKey ? '+alt' : '') - + (e.ctrlKey ? '+ctrl' : '') - + (e.metaKey ? '+meta' : '') - ; + e.key + + (e.altKey ? "+alt" : "") + + (e.ctrlKey ? "+ctrl" : "") + + (e.metaKey ? "+meta" : ""); switch (decoratedKey) { - case ' ': - case 'k': - case 'MediaPlayPause': - action = toggle_play; - break; + case " ": + case "k": + case "MediaPlayPause": + action = toggle_play; + break; - case 'MediaPlay': action = play; break; - case 'MediaPause': action = pause; break; - case 'MediaStop': action = stop; break; + case "MediaPlay": + action = play; + break; + case "MediaPause": + action = pause; + break; + case "MediaStop": + action = stop; + break; - case 'ArrowUp': - if (isPlayerFocused) action = change_volume.bind(this, 0.1); - break; - case 'ArrowDown': - if (isPlayerFocused) action = change_volume.bind(this, -0.1); - break; + case "ArrowUp": + if (isPlayerFocused) action = change_volume.bind(this, 0.1); + break; + case "ArrowDown": + if (isPlayerFocused) action = change_volume.bind(this, -0.1); + break; - case 'm': - action = toggle_muted; - break; + case "m": + action = toggle_muted; + break; - case 'ArrowRight': - case 'MediaFastForward': - action = skip_seconds.bind(this, 5 * player.playbackRate()); - break; - case 'ArrowLeft': - case 'MediaTrackPrevious': - action = skip_seconds.bind(this, -5 * player.playbackRate()); - break; - case 'l': - action = skip_seconds.bind(this, 10 * player.playbackRate()); - break; - case 'j': - action = skip_seconds.bind(this, -10 * player.playbackRate()); - break; + case "ArrowRight": + case "MediaFastForward": + action = skip_seconds.bind(this, 5 * player.playbackRate()); + break; + case "ArrowLeft": + case "MediaTrackPrevious": + action = skip_seconds.bind(this, -5 * player.playbackRate()); + break; + case "l": + action = skip_seconds.bind(this, 10 * player.playbackRate()); + break; + case "j": + action = skip_seconds.bind(this, -10 * player.playbackRate()); + break; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - // Ignore numpad numbers - if (code > 57) break; + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + // Ignore numpad numbers + if (code > 57) break; - const percent = (code - 48) * 10; - action = set_time_percent.bind(this, percent); - break; + const percent = (code - 48) * 10; + action = set_time_percent.bind(this, percent); + break; - case 'c': action = toggle_captions; break; - case 'f': action = toggle_fullscreen; break; + case "c": + action = toggle_captions; + break; + case "f": + action = toggle_fullscreen; + break; - case 'N': - case 'MediaTrackNext': - action = next_video; - break; - case 'P': - case 'MediaTrackPrevious': - // TODO: Add support to play back previous video. - break; + case "N": + case "MediaTrackNext": + action = next_video; + break; + case "P": + case "MediaTrackPrevious": + // TODO: Add support to play back previous video. + break; - // TODO: More precise step. Now FPS is taken equal to 29.97 - // Common FPS: https://forum.videohelp.com/threads/81868#post323588 - // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/ - case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break; - case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break; + // TODO: More precise step. Now FPS is taken equal to 29.97 + // Common FPS: https://forum.videohelp.com/threads/81868#post323588 + // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/ + case ",": + action = function () { + pause(); + skip_seconds(-1 / 29.97); + }; + break; + case ".": + action = function () { + pause(); + skip_seconds(1 / 29.97); + }; + break; - case '>': action = increase_playback_rate.bind(this, 1); break; - case '<': action = increase_playback_rate.bind(this, -1); break; + case ">": + action = increase_playback_rate.bind(this, 1); + break; + case "<": + action = increase_playback_rate.bind(this, -1); + break; - default: - console.info('Unhandled key down event: %s:', decoratedKey, e); - break; + default: + console.info("Unhandled key down event: %s:", decoratedKey, e); + break; } if (action) { - e.preventDefault(); - action(); + e.preventDefault(); + action(); } -}, false); + }, + false, +); // Add support for controlling the player volume by scrolling over it. Adapted from // https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328 (function () { - const pEl = document.getElementById('player'); + const pEl = document.getElementById("player"); - var volumeHover = false; - var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel'); - if (volumeSelector !== null) { - volumeSelector.onmouseover = function () { volumeHover = true; }; - volumeSelector.onmouseout = function () { volumeHover = false; }; - } + var volumeHover = false; + var volumeSelector = + pEl.querySelector(".vjs-volume-menu-button") || + pEl.querySelector(".vjs-volume-panel"); + if (volumeSelector !== null) { + volumeSelector.onmouseover = function () { + volumeHover = true; + }; + volumeSelector.onmouseout = function () { + volumeHover = false; + }; + } - function mouseScroll(event) { - // When controls are disabled, hotkeys will be disabled as well - if (!player.controls() || !volumeHover) return; + function mouseScroll(event) { + // When controls are disabled, hotkeys will be disabled as well + if (!player.controls() || !volumeHover) return; - event.preventDefault(); - var wheelMove = event.wheelDelta || -event.detail; - var volumeSign = Math.sign(wheelMove); + event.preventDefault(); + var wheelMove = event.wheelDelta || -event.detail; + var volumeSign = Math.sign(wheelMove); - change_volume(volumeSign * 0.05); // decrease/increase by 5% - } + change_volume(volumeSign * 0.05); // decrease/increase by 5% + } - player.on('mousewheel', mouseScroll); - player.on('DOMMouseScroll', mouseScroll); -}()); + player.on("mousewheel", mouseScroll); + player.on("DOMMouseScroll", mouseScroll); +})(); // Since videojs-share can sometimes be blocked, we defer it until last if (player.share) player.share(shareOptions); // show the preferred caption by default if (player_data.preferred_caption_found) { - player.ready(function () { - if (!video_data.params.listen && video_data.params.quality === 'dash') { - // play.textTracks()[0] on DASH mode is showing some debug messages - player.textTracks()[1].mode = 'showing'; - } else { - player.textTracks()[0].mode = 'showing'; - } - }); + player.ready(function () { + if (!video_data.params.listen && video_data.params.quality === "dash") { + // play.textTracks()[0] on DASH mode is showing some debug messages + player.textTracks()[1].mode = "showing"; + } else { + player.textTracks()[0].mode = "showing"; + } + }); } // Safari audio double duration fix -if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { - player.on('loadedmetadata', function () { - player.on('timeupdate', function () { - if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) { - player.currentTime(player.duration() - 1); - } - }); +if (navigator.vendor === "Apple Computer, Inc." && video_data.params.listen) { + player.on("loadedmetadata", function () { + player.on("timeupdate", function () { + if ( + player.remainingTime() < player.duration() / 2 && + player.remainingTime() >= 2 + ) { + player.currentTime(player.duration() - 1); + } }); + }); } // Safari screen timeout on looped video playback fix -if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) { - player.loop(false); - player.ready(function () { - player.on('ended', function () { - player.currentTime(0); - player.play(); - }); +if ( + navigator.vendor === "Apple Computer, Inc." && + !video_data.params.listen && + video_data.params.video_loop +) { + player.loop(false); + player.ready(function () { + player.on("ended", function () { + player.currentTime(0); + player.play(); }); + }); } // Watch on Invidious link -if (location.pathname.startsWith('/embed/')) { - const Button = videojs.getComponent('Button'); - let watch_on_invidious_button = new Button(player); +if (location.pathname.startsWith("/embed/")) { + const Button = videojs.getComponent("Button"); + let watch_on_invidious_button = new Button(player); - // Create hyperlink for current instance - var redirect_element = document.createElement('a'); - redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v=')); - redirect_element.appendChild(document.createTextNode('Invidious')); + // Create hyperlink for current instance + var redirect_element = document.createElement("a"); + redirect_element.setAttribute( + "href", + location.pathname.replace("/embed/", "/watch?v="), + ); + redirect_element.appendChild(document.createTextNode("Invidious")); - watch_on_invidious_button.el().appendChild(redirect_element); - watch_on_invidious_button.addClass('watch-on-invidious'); + watch_on_invidious_button.el().appendChild(redirect_element); + watch_on_invidious_button.addClass("watch-on-invidious"); - var cb = player.getChild('ControlBar'); - cb.addChild(watch_on_invidious_button); + var cb = player.getChild("ControlBar"); + cb.addChild(watch_on_invidious_button); } -addEventListener('DOMContentLoaded', function () { - // Save time during redirection on another instance - const changeInstanceLink = document.querySelector('#watch-on-another-invidious-instance > a'); - if (changeInstanceLink) changeInstanceLink.addEventListener('click', function () { - changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); +addEventListener("DOMContentLoaded", function () { + // Save time during redirection on another instance + const changeInstanceLink = document.querySelector( + "#watch-on-another-invidious-instance > a", + ); + if (changeInstanceLink) + changeInstanceLink.addEventListener("click", function () { + changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); }); }); diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js index d40ae985..7001061b 100644 --- a/assets/js/playlist_widget.js +++ b/assets/js/playlist_widget.js @@ -1,55 +1,83 @@ -'use strict'; -var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent); -var payload = 'csrf_token=' + playlist_data.csrf_token; +"use strict"; +var playlist_data = JSON.parse( + document.getElementById("playlist_data").textContent, +); +var payload = "csrf_token=" + playlist_data.csrf_token; function add_playlist_video(event) { - const target = event.target; - var select = document.querySelector("#playlists"); - var option = select.children[select.selectedIndex]; + const target = event.target; + var select = document.querySelector("#playlists"); + var option = select.children[select.selectedIndex]; - var url = '/playlist_ajax?action=add_video&redirect=false' + - '&video_id=' + target.getAttribute('data-id') + - '&playlist_id=' + option.getAttribute('data-plid'); + var url = + "/playlist_ajax?action=add_video&redirect=false" + + "&video_id=" + + target.getAttribute("data-id") + + "&playlist_id=" + + option.getAttribute("data-plid"); - helpers.xhr('POST', url, {payload: payload}, { - on200: function (response) { - option.textContent = '✓ ' + option.textContent; - } - }); + helpers.xhr( + "POST", + url, + { payload: payload }, + { + on200: function (response) { + option.textContent = "✓ " + option.textContent; + }, + }, + ); } function add_playlist_item(event) { - event.preventDefault(); - const target = event.target; - const video_id = target.getAttribute('data-id'); - var card = document.querySelector(`#video-card-${video_id}`); - card.classList.add("hide"); + event.preventDefault(); + const target = event.target; + const video_id = target.getAttribute("data-id"); + var card = document.querySelector(`#video-card-${video_id}`); + card.classList.add("hide"); - var url = '/playlist_ajax?action=add_video&redirect=false' + - '&video_id=' + target.getAttribute('data-id') + - '&playlist_id=' + target.getAttribute('data-plid'); + var url = + "/playlist_ajax?action=add_video&redirect=false" + + "&video_id=" + + target.getAttribute("data-id") + + "&playlist_id=" + + target.getAttribute("data-plid"); - helpers.xhr('POST', url, {payload: payload}, { - onNon200: function (xhr) { - card.classList.remove("hide"); - } - }); + helpers.xhr( + "POST", + url, + { payload: payload }, + { + onNon200: function (xhr) { + card.classList.remove("hide"); + }, + }, + ); } function remove_playlist_item(event) { - event.preventDefault(); - const target = event.target; - const video_index = target.getAttribute('data-index'); - const card = document.querySelector(`.video-card [data-index="${video_index}"]`) - card.classList.add("hide"); + event.preventDefault(); + const target = event.target; + const video_index = target.getAttribute("data-index"); + const card = document.querySelector( + `.video-card [data-index="${video_index}"]`, + ); + card.classList.add("hide"); - var url = '/playlist_ajax?action=remove_video&redirect=false' + - '&set_video_id=' + target.getAttribute('data-index') + - '&playlist_id=' + target.getAttribute('data-plid'); + var url = + "/playlist_ajax?action=remove_video&redirect=false" + + "&set_video_id=" + + target.getAttribute("data-index") + + "&playlist_id=" + + target.getAttribute("data-plid"); - helpers.xhr('POST', url, {payload: payload}, { - onNon200: function (xhr) { - card.classList.remove("hide"); - } - }); + helpers.xhr( + "POST", + url, + { payload: payload }, + { + onNon200: function (xhr) { + card.classList.remove("hide"); + }, + }, + ); } diff --git a/assets/js/post.js b/assets/js/post.js index fcbc9155..ba09acbb 100644 --- a/assets/js/post.js +++ b/assets/js/post.js @@ -1,3 +1,3 @@ -addEventListener('load', function (e) { - get_youtube_comments(); +addEventListener("load", function (e) { + get_youtube_comments(); }); diff --git a/assets/js/silvermine-videojs-quality-selector.min.js b/assets/js/silvermine-videojs-quality-selector.min.js index 1877047d..b80bc733 100644 --- a/assets/js/silvermine-videojs-quality-selector.min.js +++ b/assets/js/silvermine-videojs-quality-selector.min.js @@ -1,4 +1,1549 @@ /*! @silvermine/videojs-quality-selector 2022-04-13 v1.1.2-43-gaa06e72-dirty */ -!function u(o,c,a){function l(e,n){if(!c[e]){if(!o[e]){var t="function"==typeof require&&require;if(!n&&t)return t(e,!0);if(s)return s(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var i=c[e]={exports:{}};o[e][0].call(i.exports,function(n){return l(o[e][1][n]||n)},i,i.exports,u,o,c,a)}return c[e].exports}for(var s="function"==typeof require&&require,n=0;n":">",'"':""","'":"'","`":"`"},W=h.invert(P);h.escape=D(P),h.unescape=D(W),h.result=function(n,e,t){h.isArray(e)||(e=[e]);var r=e.length;if(!r)return h.isFunction(t)?t.call(n):t;for(var i=0;i/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};function Y(n){return"\\"+K[n]}var z=/(.)^/,K={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},G=/\\|'|\r|\n|\u2028|\u2029/g;h.template=function(u,n,e){!n&&e&&(n=e),n=h.defaults({},n,h.templateSettings);var t,r=RegExp([(n.escape||z).source,(n.interpolate||z).source,(n.evaluate||z).source].join("|")+"|$","g"),o=0,c="__p+='";u.replace(r,function(n,e,t,r,i){return c+=u.slice(o,i).replace(G,Y),o=i+n.length,e?c+="'+\n((__t=("+e+"))==null?'':_.escape(__t))+\n'":t?c+="'+\n((__t=("+t+"))==null?'':__t)+\n'":r&&(c+="';\n"+r+"\n__p+='"),n}),c+="';\n",n.variable||(c="with(obj||{}){\n"+c+"}\n"),c="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+c+"return __p;\n";try{t=new Function(n.variable||"obj","_",c)}catch(n){throw n.source=c,n}function i(n){return t.call(this,n,h)}var a=n.variable||"obj";return i.source="function("+a+"){\n"+c+"}",i},h.chain=function(n){var e=h(n);return e._chain=!0,e};function H(n,e){return n._chain?h(e).chain():e}h.mixin=function(t){return h.each(h.functions(t),function(n){var e=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return i.apply(n,arguments),H(this,e.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(e){var t=r[e];h.prototype[e]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==e&&"splice"!==e||0!==n.length||delete n[0],H(this,n)}}),h.each(["concat","join","slice"],function(n){var e=r[n];h.prototype[n]=function(){return H(this,e.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}()}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events");e.exports=function(n){var r=n.getComponent("MenuItem");return n.extend(r,{constructor:function(n,e){var t=e.source;if(!i.isObject(t))throw new Error('was not provided a "source" object, but rather: '+typeof t);e=i.extend({selectable:!0,label:t.label},e),r.call(this,n,e),this.source=t},handleClick:function(n){r.prototype.handleClick.call(this,n),this.player().trigger(u.QUALITY_REQUESTED,this.source)}})}},{"../events":5,underscore:2}],4:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events"),o=n("./QualityOption"),c="vjs-quality-changing";e.exports=function(n){var e,r=n.getComponent("MenuButton"),t=o(n);return e=n.extend(r,{constructor:function(t,n){r.call(this,t,n),t.on(u.QUALITY_REQUESTED,function(n,e){this.setSelectedSource(e),t.addClass(c),t.one("loadeddata",function(){t.removeClass(c)})}.bind(this)),t.on(u.PLAYER_SOURCES_CHANGED,function(){this.update()}.bind(this)),t.on(u.QUALITY_SELECTED,function(n,e){this.setSelectedSource(e)}.bind(this)),t.one("ready",function(){this.selectedSrc=t.src(),this.update()}.bind(this)),this.controlText("Open quality selector menu")},setSelectedSource:function(n){var e=n?n.src:void 0;this.selectedSrc!==e&&(this.selectedSrc=e,i.each(this.items,function(n){n.selected(n.source.src===e)}))},createItems:function(){var e=this.player(),n=e.currentSources();return n=n.filter(function(n){return null==n.hidequalityoption}),i.map(n,function(n){return new t(e,{source:n,selected:n.src===this.selectedSrc})}.bind(this))},buildWrapperCSSClass:function(){return"vjs-quality-selector "+r.prototype.buildWrapperCSSClass.call(this)}}),n.registerComponent("QualitySelector",e),e}},{"../events":5,"./QualityOption":3,underscore:2}],5:[function(n,e,t){"use strict";e.exports={QUALITY_REQUESTED:"qualityRequested",QUALITY_SELECTED:"qualitySelected",PLAYER_SOURCES_CHANGED:"playerSourcesChanged"}},{}],6:[function(n,e,t){"use strict";var c=n("underscore"),r=n("./events"),i=n("./components/QualitySelector"),u=n("./middleware/SourceInterceptor"),a=n("./util/SafeSeek");e.exports=function(n){n=n||window.videojs,i(n),u(n),n.hook("setup",function(o){o.on(r.QUALITY_REQUESTED,function(n,e){var t=o.currentSources(),r=o.currentTime(),i=o.playbackRate(),u=o.paused();c.each(t,function(n){n.selected=!1}),c.findWhere(t,{src:e.src}).selected=!0,o._qualitySelectorSafeSeek&&o._qualitySelectorSafeSeek.onQualitySelectionChange(),o.src(t),o.ready(function(){o._qualitySelectorSafeSeek&&!o._qualitySelectorSafeSeek.hasFinished()||(o._qualitySelectorSafeSeek=new a(o,r),o.playbackRate(i)),u||o.play()})})})},e.exports.EVENTS=r},{"./components/QualitySelector":4,"./events":5,"./middleware/SourceInterceptor":7,"./util/SafeSeek":9,underscore:2}],7:[function(n,e,t){"use strict";var u=n("underscore"),o=n("../events");e.exports=function(n){n.use("*",function(i){return{setSource:function(n,e){var t,r=i.currentSources();i._qualitySelectorSafeSeek&&i._qualitySelectorSafeSeek.onPlayerSourcesChange(),u.isEqual(r,i._qualitySelectorPreviousSources)||(i.trigger(o.PLAYER_SOURCES_CHANGED,r),i._qualitySelectorPreviousSources=r),t=u.find(r,function(n){return!0===n.selected||"true"===n.selected||"selected"===n.selected})||n,i.trigger(o.QUALITY_SELECTED,t),e(null,t)}}})}},{"../events":5,underscore:2}],8:[function(n,e,t){"use strict";n("./index")()},{"./index":6}],9:[function(n,e,t){"use strict";var r=n("class.extend");e.exports=r.extend({init:function(n,e){this._player=n,this._seekToTime=e,this._hasFinished=!1,this._keepThisInstanceWhenPlayerSourcesChange=!1,this._seekWhenSafe()},_seekWhenSafe:function(){this._player.readyState()<3?(this._seekFn=this._seek.bind(this),this._player.one("canplay",this._seekFn)):this._seek()},onPlayerSourcesChange:function(){this._keepThisInstanceWhenPlayerSourcesChange?this._keepThisInstanceWhenPlayerSourcesChange=!1:this.cancel()},onQualitySelectionChange:function(){this.hasFinished()||(this._keepThisInstanceWhenPlayerSourcesChange=!0)},_seek:function(){this._player.currentTime(this._seekToTime),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0},hasFinished:function(){return this._hasFinished},cancel:function(){this._player.off("canplay",this._seekFn),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0}})},{"class.extend":1}]},{},[8]); -//# sourceMappingURL=silvermine-videojs-quality-selector.min.js.map \ No newline at end of file +!(function u(o, c, a) { + function l(e, n) { + if (!c[e]) { + if (!o[e]) { + var t = "function" == typeof require && require; + if (!n && t) return t(e, !0); + if (s) return s(e, !0); + var r = new Error("Cannot find module '" + e + "'"); + throw ((r.code = "MODULE_NOT_FOUND"), r); + } + var i = (c[e] = { exports: {} }); + o[e][0].call( + i.exports, + function (n) { + return l(o[e][1][n] || n); + }, + i, + i.exports, + u, + o, + c, + a, + ); + } + return c[e].exports; + } + for ( + var s = "function" == typeof require && require, n = 0; + n < a.length; + n++ + ) + l(a[n]); + return l; +})( + { + 1: [ + function (n, e, t) { + !(function () { + var u = !1, + o = /xyz/.test(function () { + xyz; + }) + ? /\b_super\b/ + : /.*/; + (this.Class = function () {}), + (Class.extend = function (n) { + var i = this.prototype; + u = !0; + var e = new this(); + for (var t in ((u = !1), n)) + e[t] = + "function" == typeof n[t] && + "function" == typeof i[t] && + o.test(n[t]) + ? (function (t, r) { + return function () { + var n = this._super; + this._super = i[t]; + var e = r.apply(this, arguments); + return (this._super = n), e; + }; + })(t, n[t]) + : n[t]; + function r() { + !u && this.init && this.init.apply(this, arguments); + } + return ( + (((r.prototype = e).constructor = r).extend = arguments.callee), + r + ); + }), + (e.exports = Class); + })(); + }, + {}, + ], + 2: [ + function (n, J, $) { + (function (V) { + !(function () { + function t() {} + var n = + ("object" == typeof self && self.self === self && self) || + ("object" == typeof V && V.global === V && V) || + this || + {}, + e = n._, + r = Array.prototype, + o = Object.prototype, + f = "undefined" != typeof Symbol ? Symbol.prototype : null, + i = r.push, + a = r.slice, + p = o.toString, + u = o.hasOwnProperty, + c = Array.isArray, + l = Object.keys, + s = Object.create, + h = function (n) { + return n instanceof h + ? n + : this instanceof h + ? void (this._wrapped = n) + : new h(n); + }; + void 0 === $ || $.nodeType + ? (n._ = h) + : (void 0 !== J && + !J.nodeType && + J.exports && + ($ = J.exports = h), + ($._ = h)), + (h.VERSION = "1.9.1"); + function d(i, u, n) { + if (void 0 === u) return i; + switch (null == n ? 3 : n) { + case 1: + return function (n) { + return i.call(u, n); + }; + case 3: + return function (n, e, t) { + return i.call(u, n, e, t); + }; + case 4: + return function (n, e, t, r) { + return i.call(u, n, e, t, r); + }; + } + return function () { + return i.apply(u, arguments); + }; + } + function y(n, e, t) { + return h.iteratee !== v + ? h.iteratee(n, e) + : null == n + ? h.identity + : h.isFunction(n) + ? d(n, e, t) + : h.isObject(n) && !h.isArray(n) + ? h.matcher(n) + : h.property(n); + } + var v; + h.iteratee = v = function (n, e) { + return y(n, e, 1 / 0); + }; + function g(i, u) { + return ( + (u = null == u ? i.length - 1 : +u), + function () { + for ( + var n = Math.max(arguments.length - u, 0), + e = Array(n), + t = 0; + t < n; + t++ + ) + e[t] = arguments[t + u]; + switch (u) { + case 0: + return i.call(this, e); + case 1: + return i.call(this, arguments[0], e); + case 2: + return i.call(this, arguments[0], arguments[1], e); + } + var r = Array(u + 1); + for (t = 0; t < u; t++) r[t] = arguments[t]; + return (r[u] = e), i.apply(this, r); + } + ); + } + function S(n) { + if (!h.isObject(n)) return {}; + if (s) return s(n); + t.prototype = n; + var e = new t(); + return (t.prototype = null), e; + } + function m(e) { + return function (n) { + return null == n ? void 0 : n[e]; + }; + } + function _(n, e) { + return null != n && u.call(n, e); + } + function b(n, e) { + for (var t = e.length, r = 0; r < t; r++) { + if (null == n) return; + n = n[e[r]]; + } + return t ? n : void 0; + } + function x(n) { + var e = j(n); + return "number" == typeof e && 0 <= e && e <= k; + } + var k = Math.pow(2, 53) - 1, + j = m("length"); + (h.each = h.forEach = + function (n, e, t) { + var r, i; + if (((e = d(e, t)), x(n))) + for (r = 0, i = n.length; r < i; r++) e(n[r], r, n); + else { + var u = h.keys(n); + for (r = 0, i = u.length; r < i; r++) e(n[u[r]], u[r], n); + } + return n; + }), + (h.map = h.collect = + function (n, e, t) { + e = y(e, t); + for ( + var r = !x(n) && h.keys(n), + i = (r || n).length, + u = Array(i), + o = 0; + o < i; + o++ + ) { + var c = r ? r[o] : o; + u[o] = e(n[c], c, n); + } + return u; + }); + function E(a) { + return function (n, e, t, r) { + var i = 3 <= arguments.length; + return (function (n, e, t, r) { + var i = !x(n) && h.keys(n), + u = (i || n).length, + o = 0 < a ? 0 : u - 1; + for ( + r || ((t = n[i ? i[o] : o]), (o += a)); + 0 <= o && o < u; + o += a + ) { + var c = i ? i[o] : o; + t = e(t, n[c], c, n); + } + return t; + })(n, d(e, r, 4), t, i); + }; + } + (h.reduce = h.foldl = h.inject = E(1)), + (h.reduceRight = h.foldr = E(-1)), + (h.find = h.detect = + function (n, e, t) { + var r = (x(n) ? h.findIndex : h.findKey)(n, e, t); + if (void 0 !== r && -1 !== r) return n[r]; + }), + (h.filter = h.select = + function (n, r, e) { + var i = []; + return ( + (r = y(r, e)), + h.each(n, function (n, e, t) { + r(n, e, t) && i.push(n); + }), + i + ); + }), + (h.reject = function (n, e, t) { + return h.filter(n, h.negate(y(e)), t); + }), + (h.every = h.all = + function (n, e, t) { + e = y(e, t); + for ( + var r = !x(n) && h.keys(n), i = (r || n).length, u = 0; + u < i; + u++ + ) { + var o = r ? r[u] : u; + if (!e(n[o], o, n)) return !1; + } + return !0; + }), + (h.some = h.any = + function (n, e, t) { + e = y(e, t); + for ( + var r = !x(n) && h.keys(n), i = (r || n).length, u = 0; + u < i; + u++ + ) { + var o = r ? r[u] : u; + if (e(n[o], o, n)) return !0; + } + return !1; + }), + (h.contains = + h.includes = + h.include = + function (n, e, t, r) { + return ( + x(n) || (n = h.values(n)), + ("number" == typeof t && !r) || (t = 0), + 0 <= h.indexOf(n, e, t) + ); + }), + (h.invoke = g(function (n, t, r) { + var i, u; + return ( + h.isFunction(t) + ? (u = t) + : h.isArray(t) && + ((i = t.slice(0, -1)), (t = t[t.length - 1])), + h.map(n, function (n) { + var e = u; + if (!e) { + if ((i && i.length && (n = b(n, i)), null == n)) return; + e = n[t]; + } + return null == e ? e : e.apply(n, r); + }) + ); + })), + (h.pluck = function (n, e) { + return h.map(n, h.property(e)); + }), + (h.where = function (n, e) { + return h.filter(n, h.matcher(e)); + }), + (h.findWhere = function (n, e) { + return h.find(n, h.matcher(e)); + }), + (h.max = function (n, r, e) { + var t, + i, + u = -1 / 0, + o = -1 / 0; + if ( + null == r || + ("number" == typeof r && "object" != typeof n[0] && null != n) + ) + for ( + var c = 0, a = (n = x(n) ? n : h.values(n)).length; + c < a; + c++ + ) + null != (t = n[c]) && u < t && (u = t); + else + (r = y(r, e)), + h.each(n, function (n, e, t) { + (i = r(n, e, t)), + (o < i || (i === -1 / 0 && u === -1 / 0)) && + ((u = n), (o = i)); + }); + return u; + }), + (h.min = function (n, r, e) { + var t, + i, + u = 1 / 0, + o = 1 / 0; + if ( + null == r || + ("number" == typeof r && "object" != typeof n[0] && null != n) + ) + for ( + var c = 0, a = (n = x(n) ? n : h.values(n)).length; + c < a; + c++ + ) + null != (t = n[c]) && t < u && (u = t); + else + (r = y(r, e)), + h.each(n, function (n, e, t) { + ((i = r(n, e, t)) < o || (i === 1 / 0 && u === 1 / 0)) && + ((u = n), (o = i)); + }); + return u; + }), + (h.shuffle = function (n) { + return h.sample(n, 1 / 0); + }), + (h.sample = function (n, e, t) { + if (null == e || t) + return x(n) || (n = h.values(n)), n[h.random(n.length - 1)]; + var r = x(n) ? h.clone(n) : h.values(n), + i = j(r); + e = Math.max(Math.min(e, i), 0); + for (var u = i - 1, o = 0; o < e; o++) { + var c = h.random(o, u), + a = r[o]; + (r[o] = r[c]), (r[c] = a); + } + return r.slice(0, e); + }), + (h.sortBy = function (n, r, e) { + var i = 0; + return ( + (r = y(r, e)), + h.pluck( + h + .map(n, function (n, e, t) { + return { value: n, index: i++, criteria: r(n, e, t) }; + }) + .sort(function (n, e) { + var t = n.criteria, + r = e.criteria; + if (t !== r) { + if (r < t || void 0 === t) return 1; + if (t < r || void 0 === r) return -1; + } + return n.index - e.index; + }), + "value", + ) + ); + }); + function w(o, e) { + return function (r, i, n) { + var u = e ? [[], []] : {}; + return ( + (i = y(i, n)), + h.each(r, function (n, e) { + var t = i(n, e, r); + o(u, n, t); + }), + u + ); + }; + } + (h.groupBy = w(function (n, e, t) { + _(n, t) ? n[t].push(e) : (n[t] = [e]); + })), + (h.indexBy = w(function (n, e, t) { + n[t] = e; + })), + (h.countBy = w(function (n, e, t) { + _(n, t) ? n[t]++ : (n[t] = 1); + })); + var A = + /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + (h.toArray = function (n) { + return n + ? h.isArray(n) + ? a.call(n) + : h.isString(n) + ? n.match(A) + : x(n) + ? h.map(n, h.identity) + : h.values(n) + : []; + }), + (h.size = function (n) { + return null == n ? 0 : x(n) ? n.length : h.keys(n).length; + }), + (h.partition = w(function (n, e, t) { + n[t ? 0 : 1].push(e); + }, !0)), + (h.first = + h.head = + h.take = + function (n, e, t) { + return null == n || n.length < 1 + ? null == e + ? void 0 + : [] + : null == e || t + ? n[0] + : h.initial(n, n.length - e); + }), + (h.initial = function (n, e, t) { + return a.call( + n, + 0, + Math.max(0, n.length - (null == e || t ? 1 : e)), + ); + }), + (h.last = function (n, e, t) { + return null == n || n.length < 1 + ? null == e + ? void 0 + : [] + : null == e || t + ? n[n.length - 1] + : h.rest(n, Math.max(0, n.length - e)); + }), + (h.rest = + h.tail = + h.drop = + function (n, e, t) { + return a.call(n, null == e || t ? 1 : e); + }), + (h.compact = function (n) { + return h.filter(n, Boolean); + }); + var T = function (n, e, t, r) { + for (var i = (r = r || []).length, u = 0, o = j(n); u < o; u++) { + var c = n[u]; + if (x(c) && (h.isArray(c) || h.isArguments(c))) + if (e) for (var a = 0, l = c.length; a < l; ) r[i++] = c[a++]; + else T(c, e, t, r), (i = r.length); + else t || (r[i++] = c); + } + return r; + }; + (h.flatten = function (n, e) { + return T(n, e, !1); + }), + (h.without = g(function (n, e) { + return h.difference(n, e); + })), + (h.uniq = h.unique = + function (n, e, t, r) { + h.isBoolean(e) || ((r = t), (t = e), (e = !1)), + null != t && (t = y(t, r)); + for (var i = [], u = [], o = 0, c = j(n); o < c; o++) { + var a = n[o], + l = t ? t(a, o, n) : a; + e && !t + ? ((o && u === l) || i.push(a), (u = l)) + : t + ? h.contains(u, l) || (u.push(l), i.push(a)) + : h.contains(i, a) || i.push(a); + } + return i; + }), + (h.union = g(function (n) { + return h.uniq(T(n, !0, !0)); + })), + (h.intersection = function (n) { + for ( + var e = [], t = arguments.length, r = 0, i = j(n); + r < i; + r++ + ) { + var u = n[r]; + if (!h.contains(e, u)) { + var o; + for (o = 1; o < t && h.contains(arguments[o], u); o++); + o === t && e.push(u); + } + } + return e; + }), + (h.difference = g(function (n, e) { + return ( + (e = T(e, !0, !0)), + h.filter(n, function (n) { + return !h.contains(e, n); + }) + ); + })), + (h.unzip = function (n) { + for ( + var e = (n && h.max(n, j).length) || 0, t = Array(e), r = 0; + r < e; + r++ + ) + t[r] = h.pluck(n, r); + return t; + }), + (h.zip = g(h.unzip)), + (h.object = function (n, e) { + for (var t = {}, r = 0, i = j(n); r < i; r++) + e ? (t[n[r]] = e[r]) : (t[n[r][0]] = n[r][1]); + return t; + }); + function O(u) { + return function (n, e, t) { + e = y(e, t); + for ( + var r = j(n), i = 0 < u ? 0 : r - 1; + 0 <= i && i < r; + i += u + ) + if (e(n[i], i, n)) return i; + return -1; + }; + } + (h.findIndex = O(1)), + (h.findLastIndex = O(-1)), + (h.sortedIndex = function (n, e, t, r) { + for (var i = (t = y(t, r, 1))(e), u = 0, o = j(n); u < o; ) { + var c = Math.floor((u + o) / 2); + t(n[c]) < i ? (u = c + 1) : (o = c); + } + return u; + }); + function C(u, o, c) { + return function (n, e, t) { + var r = 0, + i = j(n); + if ("number" == typeof t) + 0 < u + ? (r = 0 <= t ? t : Math.max(t + i, r)) + : (i = 0 <= t ? Math.min(t + 1, i) : t + i + 1); + else if (c && t && i) return n[(t = c(n, e))] === e ? t : -1; + if (e != e) + return 0 <= (t = o(a.call(n, r, i), h.isNaN)) ? t + r : -1; + for (t = 0 < u ? r : i - 1; 0 <= t && t < i; t += u) + if (n[t] === e) return t; + return -1; + }; + } + (h.indexOf = C(1, h.findIndex, h.sortedIndex)), + (h.lastIndexOf = C(-1, h.findLastIndex)), + (h.range = function (n, e, t) { + null == e && ((e = n || 0), (n = 0)), + (t = t || (e < n ? -1 : 1)); + for ( + var r = Math.max(Math.ceil((e - n) / t), 0), + i = Array(r), + u = 0; + u < r; + u++, n += t + ) + i[u] = n; + return i; + }), + (h.chunk = function (n, e) { + if (null == e || e < 1) return []; + for (var t = [], r = 0, i = n.length; r < i; ) + t.push(a.call(n, r, (r += e))); + return t; + }); + function I(n, e, t, r, i) { + if (!(r instanceof e)) return n.apply(t, i); + var u = S(n.prototype), + o = n.apply(u, i); + return h.isObject(o) ? o : u; + } + (h.bind = g(function (e, t, r) { + if (!h.isFunction(e)) + throw new TypeError("Bind must be called on a function"); + var i = g(function (n) { + return I(e, i, t, this, r.concat(n)); + }); + return i; + })), + (h.partial = g(function (i, u) { + var o = h.partial.placeholder, + c = function () { + for ( + var n = 0, e = u.length, t = Array(e), r = 0; + r < e; + r++ + ) + t[r] = u[r] === o ? arguments[n++] : u[r]; + for (; n < arguments.length; ) t.push(arguments[n++]); + return I(i, c, this, this, t); + }; + return c; + })), + ((h.partial.placeholder = h).bindAll = g(function (n, e) { + var t = (e = T(e, !1, !1)).length; + if (t < 1) + throw new Error("bindAll must be passed function names"); + for (; t--; ) { + var r = e[t]; + n[r] = h.bind(n[r], n); + } + })), + (h.memoize = function (r, i) { + var u = function (n) { + var e = u.cache, + t = "" + (i ? i.apply(this, arguments) : n); + return _(e, t) || (e[t] = r.apply(this, arguments)), e[t]; + }; + return (u.cache = {}), u; + }), + (h.delay = g(function (n, e, t) { + return setTimeout(function () { + return n.apply(null, t); + }, e); + })), + (h.defer = h.partial(h.delay, h, 1)), + (h.throttle = function (t, r, i) { + var u, + o, + c, + a, + l = 0; + i = i || {}; + function s() { + (l = !1 === i.leading ? 0 : h.now()), + (u = null), + (a = t.apply(o, c)), + u || (o = c = null); + } + function n() { + var n = h.now(); + l || !1 !== i.leading || (l = n); + var e = r - (n - l); + return ( + (o = this), + (c = arguments), + e <= 0 || r < e + ? (u && (clearTimeout(u), (u = null)), + (l = n), + (a = t.apply(o, c)), + u || (o = c = null)) + : u || !1 === i.trailing || (u = setTimeout(s, e)), + a + ); + } + return ( + (n.cancel = function () { + clearTimeout(u), (l = 0), (u = o = c = null); + }), + n + ); + }), + (h.debounce = function (t, r, i) { + function u(n, e) { + (o = null), e && (c = t.apply(n, e)); + } + var o, + c, + n = g(function (n) { + if ((o && clearTimeout(o), i)) { + var e = !o; + (o = setTimeout(u, r)), e && (c = t.apply(this, n)); + } else o = h.delay(u, r, this, n); + return c; + }); + return ( + (n.cancel = function () { + clearTimeout(o), (o = null); + }), + n + ); + }), + (h.wrap = function (n, e) { + return h.partial(e, n); + }), + (h.negate = function (n) { + return function () { + return !n.apply(this, arguments); + }; + }), + (h.compose = function () { + var t = arguments, + r = t.length - 1; + return function () { + for (var n = r, e = t[r].apply(this, arguments); n--; ) + e = t[n].call(this, e); + return e; + }; + }), + (h.after = function (n, e) { + return function () { + if (--n < 1) return e.apply(this, arguments); + }; + }), + (h.before = function (n, e) { + var t; + return function () { + return ( + 0 < --n && (t = e.apply(this, arguments)), + n <= 1 && (e = null), + t + ); + }; + }), + (h.once = h.partial(h.before, 2)), + (h.restArguments = g); + function F(n, e) { + var t = M.length, + r = n.constructor, + i = (h.isFunction(r) && r.prototype) || o, + u = "constructor"; + for (_(n, u) && !h.contains(e, u) && e.push(u); t--; ) + (u = M[t]) in n && + n[u] !== i[u] && + !h.contains(e, u) && + e.push(u); + } + var q = !{ toString: null }.propertyIsEnumerable("toString"), + M = [ + "valueOf", + "isPrototypeOf", + "toString", + "propertyIsEnumerable", + "hasOwnProperty", + "toLocaleString", + ]; + (h.keys = function (n) { + if (!h.isObject(n)) return []; + if (l) return l(n); + var e = []; + for (var t in n) _(n, t) && e.push(t); + return q && F(n, e), e; + }), + (h.allKeys = function (n) { + if (!h.isObject(n)) return []; + var e = []; + for (var t in n) e.push(t); + return q && F(n, e), e; + }), + (h.values = function (n) { + for ( + var e = h.keys(n), t = e.length, r = Array(t), i = 0; + i < t; + i++ + ) + r[i] = n[e[i]]; + return r; + }), + (h.mapObject = function (n, e, t) { + e = y(e, t); + for ( + var r = h.keys(n), i = r.length, u = {}, o = 0; + o < i; + o++ + ) { + var c = r[o]; + u[c] = e(n[c], c, n); + } + return u; + }), + (h.pairs = function (n) { + for ( + var e = h.keys(n), t = e.length, r = Array(t), i = 0; + i < t; + i++ + ) + r[i] = [e[i], n[e[i]]]; + return r; + }), + (h.invert = function (n) { + for (var e = {}, t = h.keys(n), r = 0, i = t.length; r < i; r++) + e[n[t[r]]] = t[r]; + return e; + }), + (h.functions = h.methods = + function (n) { + var e = []; + for (var t in n) h.isFunction(n[t]) && e.push(t); + return e.sort(); + }); + function N(a, l) { + return function (n) { + var e = arguments.length; + if ((l && (n = Object(n)), e < 2 || null == n)) return n; + for (var t = 1; t < e; t++) + for ( + var r = arguments[t], i = a(r), u = i.length, o = 0; + o < u; + o++ + ) { + var c = i[o]; + (l && void 0 !== n[c]) || (n[c] = r[c]); + } + return n; + }; + } + (h.extend = N(h.allKeys)), + (h.extendOwn = h.assign = N(h.keys)), + (h.findKey = function (n, e, t) { + e = y(e, t); + for (var r, i = h.keys(n), u = 0, o = i.length; u < o; u++) + if (e(n[(r = i[u])], r, n)) return r; + }); + function R(n, e, t) { + return e in t; + } + var Q, L; + (h.pick = g(function (n, e) { + var t = {}, + r = e[0]; + if (null == n) return t; + h.isFunction(r) + ? (1 < e.length && (r = d(r, e[1])), (e = h.allKeys(n))) + : ((r = R), (e = T(e, !1, !1)), (n = Object(n))); + for (var i = 0, u = e.length; i < u; i++) { + var o = e[i], + c = n[o]; + r(c, o, n) && (t[o] = c); + } + return t; + })), + (h.omit = g(function (n, t) { + var e, + r = t[0]; + return ( + h.isFunction(r) + ? ((r = h.negate(r)), 1 < t.length && (e = t[1])) + : ((t = h.map(T(t, !1, !1), String)), + (r = function (n, e) { + return !h.contains(t, e); + })), + h.pick(n, r, e) + ); + })), + (h.defaults = N(h.allKeys, !0)), + (h.create = function (n, e) { + var t = S(n); + return e && h.extendOwn(t, e), t; + }), + (h.clone = function (n) { + return h.isObject(n) + ? h.isArray(n) + ? n.slice() + : h.extend({}, n) + : n; + }), + (h.tap = function (n, e) { + return e(n), n; + }), + (h.isMatch = function (n, e) { + var t = h.keys(e), + r = t.length; + if (null == n) return !r; + for (var i = Object(n), u = 0; u < r; u++) { + var o = t[u]; + if (e[o] !== i[o] || !(o in i)) return !1; + } + return !0; + }), + (Q = function (n, e, t, r) { + if (n === e) return 0 !== n || 1 / n == 1 / e; + if (null == n || null == e) return !1; + if (n != n) return e != e; + var i = typeof n; + return ( + ("function" == i || "object" == i || "object" == typeof e) && + L(n, e, t, r) + ); + }), + (L = function (n, e, t, r) { + n instanceof h && (n = n._wrapped), + e instanceof h && (e = e._wrapped); + var i = p.call(n); + if (i !== p.call(e)) return !1; + switch (i) { + case "[object RegExp]": + case "[object String]": + return "" + n == "" + e; + case "[object Number]": + return +n != +n + ? +e != +e + : 0 == +n + ? 1 / +n == 1 / e + : +n == +e; + case "[object Date]": + case "[object Boolean]": + return +n == +e; + case "[object Symbol]": + return f.valueOf.call(n) === f.valueOf.call(e); + } + var u = "[object Array]" === i; + if (!u) { + if ("object" != typeof n || "object" != typeof e) return !1; + var o = n.constructor, + c = e.constructor; + if ( + o !== c && + !( + h.isFunction(o) && + o instanceof o && + h.isFunction(c) && + c instanceof c + ) && + "constructor" in n && + "constructor" in e + ) + return !1; + } + r = r || []; + for (var a = (t = t || []).length; a--; ) + if (t[a] === n) return r[a] === e; + if ((t.push(n), r.push(e), u)) { + if ((a = n.length) !== e.length) return !1; + for (; a--; ) if (!Q(n[a], e[a], t, r)) return !1; + } else { + var l, + s = h.keys(n); + if (((a = s.length), h.keys(e).length !== a)) return !1; + for (; a--; ) + if (((l = s[a]), !_(e, l) || !Q(n[l], e[l], t, r))) + return !1; + } + return t.pop(), r.pop(), !0; + }), + (h.isEqual = function (n, e) { + return Q(n, e); + }), + (h.isEmpty = function (n) { + return ( + null == n || + (x(n) && (h.isArray(n) || h.isString(n) || h.isArguments(n)) + ? 0 === n.length + : 0 === h.keys(n).length) + ); + }), + (h.isElement = function (n) { + return !(!n || 1 !== n.nodeType); + }), + (h.isArray = + c || + function (n) { + return "[object Array]" === p.call(n); + }), + (h.isObject = function (n) { + var e = typeof n; + return "function" == e || ("object" == e && !!n); + }), + h.each( + [ + "Arguments", + "Function", + "String", + "Number", + "Date", + "RegExp", + "Error", + "Symbol", + "Map", + "WeakMap", + "Set", + "WeakSet", + ], + function (e) { + h["is" + e] = function (n) { + return p.call(n) === "[object " + e + "]"; + }; + }, + ), + h.isArguments(arguments) || + (h.isArguments = function (n) { + return _(n, "callee"); + }); + var U = n.document && n.document.childNodes; + "function" != typeof /./ && + "object" != typeof Int8Array && + "function" != typeof U && + (h.isFunction = function (n) { + return "function" == typeof n || !1; + }), + (h.isFinite = function (n) { + return !h.isSymbol(n) && isFinite(n) && !isNaN(parseFloat(n)); + }), + (h.isNaN = function (n) { + return h.isNumber(n) && isNaN(n); + }), + (h.isBoolean = function (n) { + return !0 === n || !1 === n || "[object Boolean]" === p.call(n); + }), + (h.isNull = function (n) { + return null === n; + }), + (h.isUndefined = function (n) { + return void 0 === n; + }), + (h.has = function (n, e) { + if (!h.isArray(e)) return _(n, e); + for (var t = e.length, r = 0; r < t; r++) { + var i = e[r]; + if (null == n || !u.call(n, i)) return !1; + n = n[i]; + } + return !!t; + }), + (h.noConflict = function () { + return (n._ = e), this; + }), + (h.identity = function (n) { + return n; + }), + (h.constant = function (n) { + return function () { + return n; + }; + }), + (h.noop = function () {}), + (h.property = function (e) { + return h.isArray(e) + ? function (n) { + return b(n, e); + } + : m(e); + }), + (h.propertyOf = function (e) { + return null == e + ? function () {} + : function (n) { + return h.isArray(n) ? b(e, n) : e[n]; + }; + }), + (h.matcher = h.matches = + function (e) { + return ( + (e = h.extendOwn({}, e)), + function (n) { + return h.isMatch(n, e); + } + ); + }), + (h.times = function (n, e, t) { + var r = Array(Math.max(0, n)); + e = d(e, t, 1); + for (var i = 0; i < n; i++) r[i] = e(i); + return r; + }), + (h.random = function (n, e) { + return ( + null == e && ((e = n), (n = 0)), + n + Math.floor(Math.random() * (e - n + 1)) + ); + }), + (h.now = + Date.now || + function () { + return new Date().getTime(); + }); + function D(e) { + function t(n) { + return e[n]; + } + var n = "(?:" + h.keys(e).join("|") + ")", + r = RegExp(n), + i = RegExp(n, "g"); + return function (n) { + return ( + (n = null == n ? "" : "" + n), r.test(n) ? n.replace(i, t) : n + ); + }; + } + var P = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`", + }, + W = h.invert(P); + (h.escape = D(P)), + (h.unescape = D(W)), + (h.result = function (n, e, t) { + h.isArray(e) || (e = [e]); + var r = e.length; + if (!r) return h.isFunction(t) ? t.call(n) : t; + for (var i = 0; i < r; i++) { + var u = null == n ? void 0 : n[e[i]]; + void 0 === u && ((u = t), (i = r)), + (n = h.isFunction(u) ? u.call(n) : u); + } + return n; + }); + var B = 0; + (h.uniqueId = function (n) { + var e = ++B + ""; + return n ? n + e : e; + }), + (h.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g, + }); + function Y(n) { + return "\\" + K[n]; + } + var z = /(.)^/, + K = { + "'": "'", + "\\": "\\", + "\r": "r", + "\n": "n", + "\u2028": "u2028", + "\u2029": "u2029", + }, + G = /\\|'|\r|\n|\u2028|\u2029/g; + (h.template = function (u, n, e) { + !n && e && (n = e), (n = h.defaults({}, n, h.templateSettings)); + var t, + r = RegExp( + [ + (n.escape || z).source, + (n.interpolate || z).source, + (n.evaluate || z).source, + ].join("|") + "|$", + "g", + ), + o = 0, + c = "__p+='"; + u.replace(r, function (n, e, t, r, i) { + return ( + (c += u.slice(o, i).replace(G, Y)), + (o = i + n.length), + e + ? (c += + "'+\n((__t=(" + e + "))==null?'':_.escape(__t))+\n'") + : t + ? (c += "'+\n((__t=(" + t + "))==null?'':__t)+\n'") + : r && (c += "';\n" + r + "\n__p+='"), + n + ); + }), + (c += "';\n"), + n.variable || (c = "with(obj||{}){\n" + c + "}\n"), + (c = + "var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n" + + c + + "return __p;\n"); + try { + t = new Function(n.variable || "obj", "_", c); + } catch (n) { + throw ((n.source = c), n); + } + function i(n) { + return t.call(this, n, h); + } + var a = n.variable || "obj"; + return (i.source = "function(" + a + "){\n" + c + "}"), i; + }), + (h.chain = function (n) { + var e = h(n); + return (e._chain = !0), e; + }); + function H(n, e) { + return n._chain ? h(e).chain() : e; + } + (h.mixin = function (t) { + return ( + h.each(h.functions(t), function (n) { + var e = (h[n] = t[n]); + h.prototype[n] = function () { + var n = [this._wrapped]; + return i.apply(n, arguments), H(this, e.apply(h, n)); + }; + }), + h + ); + }), + h.mixin(h), + h.each( + [ + "pop", + "push", + "reverse", + "shift", + "sort", + "splice", + "unshift", + ], + function (e) { + var t = r[e]; + h.prototype[e] = function () { + var n = this._wrapped; + return ( + t.apply(n, arguments), + ("shift" !== e && "splice" !== e) || + 0 !== n.length || + delete n[0], + H(this, n) + ); + }; + }, + ), + h.each(["concat", "join", "slice"], function (n) { + var e = r[n]; + h.prototype[n] = function () { + return H(this, e.apply(this._wrapped, arguments)); + }; + }), + (h.prototype.value = function () { + return this._wrapped; + }), + (h.prototype.valueOf = h.prototype.toJSON = h.prototype.value), + (h.prototype.toString = function () { + return String(this._wrapped); + }), + "function" == typeof define && + define.amd && + define("underscore", [], function () { + return h; + }); + })(); + }).call( + this, + "undefined" != typeof global + ? global + : "undefined" != typeof self + ? self + : "undefined" != typeof window + ? window + : {}, + ); + }, + {}, + ], + 3: [ + function (n, e, t) { + "use strict"; + var i = n("underscore"), + u = n("../events"); + e.exports = function (n) { + var r = n.getComponent("MenuItem"); + return n.extend(r, { + constructor: function (n, e) { + var t = e.source; + if (!i.isObject(t)) + throw new Error( + 'was not provided a "source" object, but rather: ' + typeof t, + ); + (e = i.extend({ selectable: !0, label: t.label }, e)), + r.call(this, n, e), + (this.source = t); + }, + handleClick: function (n) { + r.prototype.handleClick.call(this, n), + this.player().trigger(u.QUALITY_REQUESTED, this.source); + }, + }); + }; + }, + { "../events": 5, underscore: 2 }, + ], + 4: [ + function (n, e, t) { + "use strict"; + var i = n("underscore"), + u = n("../events"), + o = n("./QualityOption"), + c = "vjs-quality-changing"; + e.exports = function (n) { + var e, + r = n.getComponent("MenuButton"), + t = o(n); + return ( + (e = n.extend(r, { + constructor: function (t, n) { + r.call(this, t, n), + t.on( + u.QUALITY_REQUESTED, + function (n, e) { + this.setSelectedSource(e), + t.addClass(c), + t.one("loadeddata", function () { + t.removeClass(c); + }); + }.bind(this), + ), + t.on( + u.PLAYER_SOURCES_CHANGED, + function () { + this.update(); + }.bind(this), + ), + t.on( + u.QUALITY_SELECTED, + function (n, e) { + this.setSelectedSource(e); + }.bind(this), + ), + t.one( + "ready", + function () { + (this.selectedSrc = t.src()), this.update(); + }.bind(this), + ), + this.controlText("Open quality selector menu"); + }, + setSelectedSource: function (n) { + var e = n ? n.src : void 0; + this.selectedSrc !== e && + ((this.selectedSrc = e), + i.each(this.items, function (n) { + n.selected(n.source.src === e); + })); + }, + createItems: function () { + var e = this.player(), + n = e.currentSources(); + return ( + (n = n.filter(function (n) { + return null == n.hidequalityoption; + })), + i.map( + n, + function (n) { + return new t(e, { + source: n, + selected: n.src === this.selectedSrc, + }); + }.bind(this), + ) + ); + }, + buildWrapperCSSClass: function () { + return ( + "vjs-quality-selector " + + r.prototype.buildWrapperCSSClass.call(this) + ); + }, + })), + n.registerComponent("QualitySelector", e), + e + ); + }; + }, + { "../events": 5, "./QualityOption": 3, underscore: 2 }, + ], + 5: [ + function (n, e, t) { + "use strict"; + e.exports = { + QUALITY_REQUESTED: "qualityRequested", + QUALITY_SELECTED: "qualitySelected", + PLAYER_SOURCES_CHANGED: "playerSourcesChanged", + }; + }, + {}, + ], + 6: [ + function (n, e, t) { + "use strict"; + var c = n("underscore"), + r = n("./events"), + i = n("./components/QualitySelector"), + u = n("./middleware/SourceInterceptor"), + a = n("./util/SafeSeek"); + (e.exports = function (n) { + (n = n || window.videojs), + i(n), + u(n), + n.hook("setup", function (o) { + o.on(r.QUALITY_REQUESTED, function (n, e) { + var t = o.currentSources(), + r = o.currentTime(), + i = o.playbackRate(), + u = o.paused(); + c.each(t, function (n) { + n.selected = !1; + }), + (c.findWhere(t, { src: e.src }).selected = !0), + o._qualitySelectorSafeSeek && + o._qualitySelectorSafeSeek.onQualitySelectionChange(), + o.src(t), + o.ready(function () { + (o._qualitySelectorSafeSeek && + !o._qualitySelectorSafeSeek.hasFinished()) || + ((o._qualitySelectorSafeSeek = new a(o, r)), + o.playbackRate(i)), + u || o.play(); + }); + }); + }); + }), + (e.exports.EVENTS = r); + }, + { + "./components/QualitySelector": 4, + "./events": 5, + "./middleware/SourceInterceptor": 7, + "./util/SafeSeek": 9, + underscore: 2, + }, + ], + 7: [ + function (n, e, t) { + "use strict"; + var u = n("underscore"), + o = n("../events"); + e.exports = function (n) { + n.use("*", function (i) { + return { + setSource: function (n, e) { + var t, + r = i.currentSources(); + i._qualitySelectorSafeSeek && + i._qualitySelectorSafeSeek.onPlayerSourcesChange(), + u.isEqual(r, i._qualitySelectorPreviousSources) || + (i.trigger(o.PLAYER_SOURCES_CHANGED, r), + (i._qualitySelectorPreviousSources = r)), + (t = + u.find(r, function (n) { + return ( + !0 === n.selected || + "true" === n.selected || + "selected" === n.selected + ); + }) || n), + i.trigger(o.QUALITY_SELECTED, t), + e(null, t); + }, + }; + }); + }; + }, + { "../events": 5, underscore: 2 }, + ], + 8: [ + function (n, e, t) { + "use strict"; + n("./index")(); + }, + { "./index": 6 }, + ], + 9: [ + function (n, e, t) { + "use strict"; + var r = n("class.extend"); + e.exports = r.extend({ + init: function (n, e) { + (this._player = n), + (this._seekToTime = e), + (this._hasFinished = !1), + (this._keepThisInstanceWhenPlayerSourcesChange = !1), + this._seekWhenSafe(); + }, + _seekWhenSafe: function () { + this._player.readyState() < 3 + ? ((this._seekFn = this._seek.bind(this)), + this._player.one("canplay", this._seekFn)) + : this._seek(); + }, + onPlayerSourcesChange: function () { + this._keepThisInstanceWhenPlayerSourcesChange + ? (this._keepThisInstanceWhenPlayerSourcesChange = !1) + : this.cancel(); + }, + onQualitySelectionChange: function () { + this.hasFinished() || + (this._keepThisInstanceWhenPlayerSourcesChange = !0); + }, + _seek: function () { + this._player.currentTime(this._seekToTime), + (this._keepThisInstanceWhenPlayerSourcesChange = !1), + (this._hasFinished = !0); + }, + hasFinished: function () { + return this._hasFinished; + }, + cancel: function () { + this._player.off("canplay", this._seekFn), + (this._keepThisInstanceWhenPlayerSourcesChange = !1), + (this._hasFinished = !0); + }, + }); + }, + { "class.extend": 1 }, + ], + }, + {}, + [8], +); +//# sourceMappingURL=silvermine-videojs-quality-selector.min.js.map diff --git a/assets/js/sse.js b/assets/js/sse.js index 4f7320b3..4b29f460 100644 --- a/assets/js/sse.js +++ b/assets/js/sse.js @@ -17,18 +17,18 @@ var SSE = function (url, options) { options = options || {}; this.headers = options.headers || {}; - this.payload = options.payload !== undefined ? options.payload : ''; - this.method = options.method || (this.payload && 'POST' || 'GET'); + this.payload = options.payload !== undefined ? options.payload : ""; + this.method = options.method || (this.payload && "POST") || "GET"; - this.FIELD_SEPARATOR = ':'; + this.FIELD_SEPARATOR = ":"; this.listeners = {}; this.xhr = null; this.readyState = this.INITIALIZING; this.progress = 0; - this.chunk = ''; + this.chunk = ""; - this.addEventListener = function(type, listener) { + this.addEventListener = function (type, listener) { if (this.listeners[type] === undefined) { this.listeners[type] = []; } @@ -38,13 +38,13 @@ var SSE = function (url, options) { } }; - this.removeEventListener = function(type, listener) { + this.removeEventListener = function (type, listener) { if (this.listeners[type] === undefined) { return; } var filtered = []; - this.listeners[type].forEach(function(element) { + this.listeners[type].forEach(function (element) { if (element !== listener) { filtered.push(element); } @@ -56,14 +56,14 @@ var SSE = function (url, options) { } }; - this.dispatchEvent = function(e) { + this.dispatchEvent = function (e) { if (!e) { return true; } e.source = this; - var onHandler = 'on' + e.type; + var onHandler = "on" + e.type; if (this.hasOwnProperty(onHandler)) { this[onHandler].call(this, e); if (e.defaultPrevented) { @@ -72,7 +72,7 @@ var SSE = function (url, options) { } if (this.listeners[e.type]) { - return this.listeners[e.type].every(function(callback) { + return this.listeners[e.type].every(function (callback) { callback(e); return !e.defaultPrevented; }); @@ -82,78 +82,82 @@ var SSE = function (url, options) { }; this._setReadyState = function (state) { - var event = new CustomEvent('readystatechange'); + var event = new CustomEvent("readystatechange"); event.readyState = state; this.readyState = state; this.dispatchEvent(event); }; - this._onStreamFailure = function(e) { - this.dispatchEvent(new CustomEvent('error')); + this._onStreamFailure = function (e) { + this.dispatchEvent(new CustomEvent("error")); this.close(); - } + }; - this._onStreamProgress = function(e) { + this._onStreamProgress = function (e) { if (this.xhr.status !== 200 && this.readyState !== this.CLOSED) { this._onStreamFailure(e); return; } if (this.readyState == this.CONNECTING) { - this.dispatchEvent(new CustomEvent('open')); + this.dispatchEvent(new CustomEvent("open")); this._setReadyState(this.OPEN); } var data = this.xhr.responseText.substring(this.progress); this.progress += data.length; - data.split(/(\r\n|\r|\n){2}/g).forEach(function(part) { - if (part.trim().length === 0) { - this.dispatchEvent(this._parseEventChunk(this.chunk.trim())); - this.chunk = ''; - } else { - this.chunk += part; - } - }.bind(this)); + data.split(/(\r\n|\r|\n){2}/g).forEach( + function (part) { + if (part.trim().length === 0) { + this.dispatchEvent(this._parseEventChunk(this.chunk.trim())); + this.chunk = ""; + } else { + this.chunk += part; + } + }.bind(this), + ); }; - this._onStreamLoaded = function(e) { + this._onStreamLoaded = function (e) { this._onStreamProgress(e); // Parse the last chunk. this.dispatchEvent(this._parseEventChunk(this.chunk)); - this.chunk = ''; + this.chunk = ""; }; /** * Parse a received SSE event chunk into a constructed event object. */ - this._parseEventChunk = function(chunk) { + this._parseEventChunk = function (chunk) { if (!chunk || chunk.length === 0) { return null; } - var e = {'id': null, 'retry': null, 'data': '', 'event': 'message'}; - chunk.split(/\n|\r\n|\r/).forEach(function(line) { - line = line.trimRight(); - var index = line.indexOf(this.FIELD_SEPARATOR); - if (index <= 0) { - // Line was either empty, or started with a separator and is a comment. - // Either way, ignore. - return; - } + var e = { id: null, retry: null, data: "", event: "message" }; + chunk.split(/\n|\r\n|\r/).forEach( + function (line) { + line = line.trimRight(); + var index = line.indexOf(this.FIELD_SEPARATOR); + if (index <= 0) { + // Line was either empty, or started with a separator and is a comment. + // Either way, ignore. + return; + } - var field = line.substring(0, index); - if (!(field in e)) { - return; - } + var field = line.substring(0, index); + if (!(field in e)) { + return; + } - var value = line.substring(index + 1).trimLeft(); - if (field === 'data') { - e[field] += value; - } else { - e[field] = value; - } - }.bind(this)); + var value = line.substring(index + 1).trimLeft(); + if (field === "data") { + e[field] += value; + } else { + e[field] = value; + } + }.bind(this), + ); var event = new CustomEvent(e.event); event.data = e.data; @@ -161,21 +165,24 @@ var SSE = function (url, options) { return event; }; - this._checkStreamClosed = function() { + this._checkStreamClosed = function () { if (this.xhr.readyState === XMLHttpRequest.DONE) { this._setReadyState(this.CLOSED); } }; - this.stream = function() { + this.stream = function () { this._setReadyState(this.CONNECTING); this.xhr = new XMLHttpRequest(); - this.xhr.addEventListener('progress', this._onStreamProgress.bind(this)); - this.xhr.addEventListener('load', this._onStreamLoaded.bind(this)); - this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this)); - this.xhr.addEventListener('error', this._onStreamFailure.bind(this)); - this.xhr.addEventListener('abort', this._onStreamFailure.bind(this)); + this.xhr.addEventListener("progress", this._onStreamProgress.bind(this)); + this.xhr.addEventListener("load", this._onStreamLoaded.bind(this)); + this.xhr.addEventListener( + "readystatechange", + this._checkStreamClosed.bind(this), + ); + this.xhr.addEventListener("error", this._onStreamFailure.bind(this)); + this.xhr.addEventListener("abort", this._onStreamFailure.bind(this)); this.xhr.open(this.method, this.url); for (var header in this.headers) { this.xhr.setRequestHeader(header, this.headers[header]); @@ -183,7 +190,7 @@ var SSE = function (url, options) { this.xhr.send(this.payload); }; - this.close = function() { + this.close = function () { if (this.readyState === this.CLOSED) { return; } @@ -195,6 +202,6 @@ var SSE = function (url, options) { }; // Export our SSE module for npm.js -if (typeof exports !== 'undefined') { +if (typeof exports !== "undefined") { exports.SSE = SSE; } diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js index d0a2c975..be934187 100644 --- a/assets/js/subscribe_widget.js +++ b/assets/js/subscribe_widget.js @@ -1,62 +1,80 @@ -'use strict'; -var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent); -var payload = 'csrf_token=' + subscribe_data.csrf_token; +"use strict"; +var subscribe_data = JSON.parse( + document.getElementById("subscribe_data").textContent, +); +var payload = "csrf_token=" + subscribe_data.csrf_token; -var subscribe_button = document.getElementById('subscribe'); +var subscribe_button = document.getElementById("subscribe"); -if (subscribe_button.getAttribute('data-type') === 'subscribe') { - subscribe_button.onclick = subscribe; +if (subscribe_button.getAttribute("data-type") === "subscribe") { + subscribe_button.onclick = subscribe; } else { - subscribe_button.onclick = unsubscribe; + subscribe_button.onclick = unsubscribe; } function toggleSubscribeButton() { - subscribe_button.classList.remove("primary"); - subscribe_button.classList.remove("secondary"); - subscribe_button.classList.remove("unsubscribe"); - subscribe_button.classList.remove("subscribe"); + subscribe_button.classList.remove("primary"); + subscribe_button.classList.remove("secondary"); + subscribe_button.classList.remove("unsubscribe"); + subscribe_button.classList.remove("subscribe"); - if (subscribe_button.getAttribute('data-type') === 'subscribe') { - subscribe_button.textContent = subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text; - subscribe_button.onclick = unsubscribe; - subscribe_button.classList.add("secondary"); - subscribe_button.classList.add("unsubscribe"); - } else { - subscribe_button.textContent = subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text; - subscribe_button.onclick = subscribe; - subscribe_button.classList.add("primary"); - subscribe_button.classList.add("subscribe"); - } + if (subscribe_button.getAttribute("data-type") === "subscribe") { + subscribe_button.textContent = + subscribe_data.unsubscribe_text + " | " + subscribe_data.sub_count_text; + subscribe_button.onclick = unsubscribe; + subscribe_button.classList.add("secondary"); + subscribe_button.classList.add("unsubscribe"); + } else { + subscribe_button.textContent = + subscribe_data.subscribe_text + " | " + subscribe_data.sub_count_text; + subscribe_button.onclick = subscribe; + subscribe_button.classList.add("primary"); + subscribe_button.classList.add("subscribe"); + } } function subscribe(e) { - e.preventDefault(); - var fallback = subscribe_button.textContent; - toggleSubscribeButton(); + e.preventDefault(); + var fallback = subscribe_button.textContent; + toggleSubscribeButton(); - var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' + - '&c=' + subscribe_data.ucid; + var url = + "/subscription_ajax?action=create_subscription_to_channel&redirect=false" + + "&c=" + + subscribe_data.ucid; - helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, { - onNon200: function (xhr) { - subscribe_button.onclick = subscribe; - subscribe_button.textContent = fallback; - } - }); + helpers.xhr( + "POST", + url, + { payload: payload, retries: 5, entity_name: "subscribe request" }, + { + onNon200: function (xhr) { + subscribe_button.onclick = subscribe; + subscribe_button.textContent = fallback; + }, + }, + ); } function unsubscribe(e) { - e.preventDefault(); - var fallback = subscribe_button.textContent; - toggleSubscribeButton(); + e.preventDefault(); + var fallback = subscribe_button.textContent; + toggleSubscribeButton(); - var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' + - '&c=' + subscribe_data.ucid; + var url = + "/subscription_ajax?action=remove_subscriptions&redirect=false" + + "&c=" + + subscribe_data.ucid; - helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, { - onNon200: function (xhr) { - subscribe_button.onclick = unsubscribe; - subscribe_button.textContent = fallback; - } - }); + helpers.xhr( + "POST", + url, + { payload: payload, retries: 5, entity_name: "unsubscribe request" }, + { + onNon200: function (xhr) { + subscribe_button.onclick = unsubscribe; + subscribe_button.textContent = fallback; + }, + }, + ); } diff --git a/assets/js/themes.js b/assets/js/themes.js index 545d2c18..50dc2dfd 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,46 +1,46 @@ -'use strict'; -var toggle_theme = document.getElementById('toggle_theme'); +"use strict"; +var toggle_theme = document.getElementById("toggle_theme"); -const STORAGE_KEY_THEME = 'dark_mode'; -const THEME_DARK = 'dark'; -const THEME_LIGHT = 'light'; +const STORAGE_KEY_THEME = "dark_mode"; +const THEME_DARK = "dark"; +const THEME_LIGHT = "light"; // TODO: theme state controlled by system -toggle_theme.addEventListener('click', function (e) { - e.preventDefault(); - const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; - const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK; - setTheme(newTheme); - helpers.storage.set(STORAGE_KEY_THEME, newTheme); - helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); +toggle_theme.addEventListener("click", function (e) { + e.preventDefault(); + const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; + const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK; + setTheme(newTheme); + helpers.storage.set(STORAGE_KEY_THEME, newTheme); + helpers.xhr("GET", "/toggle_theme?redirect=false", {}, {}); }); /** @param {THEME_DARK|THEME_LIGHT} theme */ function setTheme(theme) { - // By default body element has .no-theme class that uses OS theme via CSS @media rules - // It rewrites using hard className below - if (theme === THEME_DARK) { - toggle_theme.children[0].className = 'icon ion-ios-sunny'; - document.body.className = 'dark-theme'; - } else if (theme === THEME_LIGHT) { - toggle_theme.children[0].className = 'icon ion-ios-moon'; - document.body.className = 'light-theme'; - } else { - document.body.className = 'no-theme'; - } + // By default body element has .no-theme class that uses OS theme via CSS @media rules + // It rewrites using hard className below + if (theme === THEME_DARK) { + toggle_theme.children[0].className = "icon ion-ios-sunny"; + document.body.className = "dark-theme"; + } else if (theme === THEME_LIGHT) { + toggle_theme.children[0].className = "icon ion-ios-moon"; + document.body.className = "light-theme"; + } else { + document.body.className = "no-theme"; + } } // Handles theme change event caused by other tab -addEventListener('storage', function (e) { - if (e.key === STORAGE_KEY_THEME) - setTheme(helpers.storage.get(STORAGE_KEY_THEME)); +addEventListener("storage", function (e) { + if (e.key === STORAGE_KEY_THEME) + setTheme(helpers.storage.get(STORAGE_KEY_THEME)); }); // Set theme from preferences on page load -addEventListener('DOMContentLoaded', function () { - const prefTheme = document.getElementById('dark_mode_pref').textContent; - if (prefTheme) { - setTheme(prefTheme); - helpers.storage.set(STORAGE_KEY_THEME, prefTheme); - } +addEventListener("DOMContentLoaded", function () { + const prefTheme = document.getElementById("dark_mode_pref").textContent; + if (prefTheme) { + setTheme(prefTheme); + helpers.storage.set(STORAGE_KEY_THEME, prefTheme); + } }); diff --git a/assets/js/videojs-youtube-annotations.min.js b/assets/js/videojs-youtube-annotations.min.js index c93e14e8..1943744d 100644 --- a/assets/js/videojs-youtube-annotations.min.js +++ b/assets/js/videojs-youtube-annotations.min.js @@ -1 +1,936 @@ -class AnnotationParser{static get defaultAppearanceAttributes(){return{bgColor:16777215,bgOpacity:.8,fgColor:0,textSize:3.15}}static get attributeMap(){return{type:"tp",style:"s",x:"x",y:"y",width:"w",height:"h",sx:"sx",sy:"sy",timeStart:"ts",timeEnd:"te",text:"t",actionType:"at",actionUrl:"au",actionUrlTarget:"aut",actionSeconds:"as",bgOpacity:"bgo",bgColor:"bgc",fgColor:"fgc",textSize:"txsz"}}deserializeAnnotation(serializedAnnotation){const map=this.constructor.attributeMap;const attributes=serializedAnnotation.split(",");const annotation={};for(const attribute of attributes){const[key,value]=attribute.split("=");const mappedKey=this.getKeyByValue(map,key);let finalValue="";if(["text","actionType","actionUrl","actionUrlTarget","type","style"].indexOf(mappedKey)>-1){finalValue=decodeURIComponent(value)}else{finalValue=parseFloat(value,10)}annotation[mappedKey]=finalValue}return annotation}serializeAnnotation(annotation){const map=this.constructor.attributeMap;let serialized="";for(const key in annotation){const mappedKey=map[key];if(["text","actionType","actionUrl","actionUrlTarget"].indexOf(key)>-1&&mappedKey&&annotation.hasOwnProperty(key)){let text=encodeURIComponent(annotation[key]);serialized+=`${mappedKey}=${text},`}else if(["text","actionType","actionUrl","actionUrlTarget"].indexOf("key")===-1&&mappedKey&&annotation.hasOwnProperty(key)){serialized+=`${mappedKey}=${annotation[key]},`}}return serialized.substring(0,serialized.length-1)}deserializeAnnotationList(serializedAnnotationString){const serializedAnnotations=serializedAnnotationString.split(";");serializedAnnotations.length=serializedAnnotations.length-1;const annotations=[];for(const annotation of serializedAnnotations){annotations.push(this.deserializeAnnotation(annotation))}return annotations}serializeAnnotationList(annotations){let serialized="";for(const annotation of annotations){serialized+=this.serializeAnnotation(annotation)+";"}return serialized}xmlToDom(xml){const parser=new DOMParser;const dom=parser.parseFromString(xml,"application/xml");return dom}getAnnotationsFromXml(xml){const dom=this.xmlToDom(xml);return dom.getElementsByTagName("annotation")}parseYoutubeAnnotationList(annotationElements){const annotations=[];for(const el of annotationElements){const parsedAnnotation=this.parseYoutubeAnnotation(el);if(parsedAnnotation)annotations.push(parsedAnnotation)}return annotations}parseYoutubeAnnotation(annotationElement){const base=annotationElement;const attributes=this.getAttributesFromBase(base);if(!attributes.type||attributes.type==="pause")return null;const text=this.getTextFromBase(base);const action=this.getActionFromBase(base);const backgroundShape=this.getBackgroundShapeFromBase(base);if(!backgroundShape)return null;const timeStart=backgroundShape.timeRange.start;const timeEnd=backgroundShape.timeRange.end;if(isNaN(timeStart)||isNaN(timeEnd)||timeStart===null||timeEnd===null){return null}const appearance=this.getAppearanceFromBase(base);let annotation={type:attributes.type,x:backgroundShape.x,y:backgroundShape.y,width:backgroundShape.width,height:backgroundShape.height,timeStart:timeStart,timeEnd:timeEnd};if(attributes.style)annotation.style=attributes.style;if(text)annotation.text=text;if(action)annotation=Object.assign(action,annotation);if(appearance)annotation=Object.assign(appearance,annotation);if(backgroundShape.hasOwnProperty("sx"))annotation.sx=backgroundShape.sx;if(backgroundShape.hasOwnProperty("sy"))annotation.sy=backgroundShape.sy;return annotation}getBackgroundShapeFromBase(base){const movingRegion=base.getElementsByTagName("movingRegion")[0];if(!movingRegion)return null;const regionType=movingRegion.getAttribute("type");const regions=movingRegion.getElementsByTagName(`${regionType}Region`);const timeRange=this.extractRegionTime(regions);const shape={type:regionType,x:parseFloat(regions[0].getAttribute("x"),10),y:parseFloat(regions[0].getAttribute("y"),10),width:parseFloat(regions[0].getAttribute("w"),10),height:parseFloat(regions[0].getAttribute("h"),10),timeRange:timeRange};const sx=regions[0].getAttribute("sx");const sy=regions[0].getAttribute("sy");if(sx)shape.sx=parseFloat(sx,10);if(sy)shape.sy=parseFloat(sy,10);return shape}getAttributesFromBase(base){const attributes={};attributes.type=base.getAttribute("type");attributes.style=base.getAttribute("style");return attributes}getTextFromBase(base){const textElement=base.getElementsByTagName("TEXT")[0];if(textElement)return textElement.textContent}getActionFromBase(base){const actionElement=base.getElementsByTagName("action")[0];if(!actionElement)return null;const typeAttr=actionElement.getAttribute("type");const urlElement=actionElement.getElementsByTagName("url")[0];if(!urlElement)return null;const actionUrlTarget=urlElement.getAttribute("target");const href=urlElement.getAttribute("value");if(href.startsWith("https://www.youtube.com/")){const url=new URL(href);const srcVid=url.searchParams.get("src_vid");const toVid=url.searchParams.get("v");return this.linkOrTimestamp(url,srcVid,toVid,actionUrlTarget)}}linkOrTimestamp(url,srcVid,toVid,actionUrlTarget){if(srcVid&&toVid&&srcVid===toVid){let seconds=0;const hash=url.hash;if(hash&&hash.startsWith("#t=")){const timeString=url.hash.split("#t=")[1];seconds=this.timeStringToSeconds(timeString)}return{actionType:"time",actionSeconds:seconds}}else{return{actionType:"url",actionUrl:url.href,actionUrlTarget:actionUrlTarget}}}getAppearanceFromBase(base){const appearanceElement=base.getElementsByTagName("appearance")[0];const styles=this.constructor.defaultAppearanceAttributes;if(appearanceElement){const bgOpacity=appearanceElement.getAttribute("bgAlpha");const bgColor=appearanceElement.getAttribute("bgColor");const fgColor=appearanceElement.getAttribute("fgColor");const textSize=appearanceElement.getAttribute("textSize");if(bgOpacity)styles.bgOpacity=parseFloat(bgOpacity,10);if(bgColor)styles.bgColor=parseInt(bgColor,10);if(fgColor)styles.fgColor=parseInt(fgColor,10);if(textSize)styles.textSize=parseFloat(textSize,10)}return styles}extractRegionTime(regions){let timeStart=regions[0].getAttribute("t");timeStart=this.hmsToSeconds(timeStart);let timeEnd=regions[regions.length-1].getAttribute("t");timeEnd=this.hmsToSeconds(timeEnd);return{start:timeStart,end:timeEnd}}hmsToSeconds(hms){let p=hms.split(":");let s=0;let m=1;while(p.length>0){s+=m*parseFloat(p.pop(),10);m*=60}return s}timeStringToSeconds(time){let seconds=0;const h=time.split("h");const m=(h[1]||time).split("m");const s=(m[1]||time).split("s");if(h[0]&&h.length===2)seconds+=parseInt(h[0],10)*60*60;if(m[0]&&m.length===2)seconds+=parseInt(m[0],10)*60;if(s[0]&&s.length===2)seconds+=parseInt(s[0],10);return seconds}getKeyByValue(obj,value){for(const key in obj){if(obj.hasOwnProperty(key)){if(obj[key]===value){return key}}}}}class AnnotationRenderer{constructor(annotations,container,playerOptions,updateInterval=1e3){if(!annotations)throw new Error("Annotation objects must be provided");if(!container)throw new Error("An element to contain the annotations must be provided");if(playerOptions&&playerOptions.getVideoTime&&playerOptions.seekTo){this.playerOptions=playerOptions}else{console.info("AnnotationRenderer is running without a player. The update method will need to be called manually.")}this.annotations=annotations;this.container=container;this.annotationsContainer=document.createElement("div");this.annotationsContainer.classList.add("__cxt-ar-annotations-container__");this.annotationsContainer.setAttribute("data-layer","4");this.annotationsContainer.addEventListener("click",e=>{this.annotationClickHandler(e)});this.container.prepend(this.annotationsContainer);this.createAnnotationElements();this.updateAllAnnotationSizes();window.addEventListener("DOMContentLoaded",e=>{this.updateAllAnnotationSizes()});this.updateInterval=updateInterval;this.updateIntervalId=null}changeAnnotationData(annotations){this.stop();this.removeAnnotationElements();this.annotations=annotations;this.createAnnotationElements();this.start()}createAnnotationElements(){for(const annotation of this.annotations){const el=document.createElement("div");el.classList.add("__cxt-ar-annotation__");annotation.__element=el;el.__annotation=annotation;const closeButton=this.createCloseElement();closeButton.addEventListener("click",e=>{el.setAttribute("hidden","");el.setAttribute("data-ar-closed","");if(el.__annotation.__speechBubble){const speechBubble=el.__annotation.__speechBubble;speechBubble.style.display="none"}});el.append(closeButton);if(annotation.text){const textNode=document.createElement("span");textNode.textContent=annotation.text;el.append(textNode);el.setAttribute("data-ar-has-text","")}if(annotation.style==="speech"){const containerDimensions=this.container.getBoundingClientRect();const speechX=this.percentToPixels(containerDimensions.width,annotation.x);const speechY=this.percentToPixels(containerDimensions.height,annotation.y);const speechWidth=this.percentToPixels(containerDimensions.width,annotation.width);const speechHeight=this.percentToPixels(containerDimensions.height,annotation.height);const speechPointX=this.percentToPixels(containerDimensions.width,annotation.sx);const speechPointY=this.percentToPixels(containerDimensions.height,annotation.sy);const bubbleColor=this.getFinalAnnotationColor(annotation,false);const bubble=this.createSvgSpeechBubble(speechX,speechY,speechWidth,speechHeight,speechPointX,speechPointY,bubbleColor,annotation.__element);bubble.style.display="none";bubble.style.overflow="visible";el.style.pointerEvents="none";bubble.__annotationEl=el;annotation.__speechBubble=bubble;const path=bubble.getElementsByTagName("path")[0];path.addEventListener("mouseover",()=>{closeButton.style.display="block";closeButton.style.cursor="pointer";path.setAttribute("fill",this.getFinalAnnotationColor(annotation,true))});path.addEventListener("mouseout",e=>{if(!e.relatedTarget.classList.contains("__cxt-ar-annotation-close__")){closeButton.style.display="none";closeButton.style.cursor="default";path.setAttribute("fill",this.getFinalAnnotationColor(annotation,false))}});closeButton.addEventListener("mouseleave",()=>{closeButton.style.display="none";path.style.cursor="default";closeButton.style.cursor="default";path.setAttribute("fill",this.getFinalAnnotationColor(annotation,false))});el.prepend(bubble)}else if(annotation.type==="highlight"){el.style.backgroundColor="";el.style.border=`2.5px solid ${this.getFinalAnnotationColor(annotation,false)}`;if(annotation.actionType==="url")el.style.cursor="pointer"}else if(annotation.style!=="title"){el.style.backgroundColor=this.getFinalAnnotationColor(annotation);el.addEventListener("mouseenter",()=>{el.style.backgroundColor=this.getFinalAnnotationColor(annotation,true)});el.addEventListener("mouseleave",()=>{el.style.backgroundColor=this.getFinalAnnotationColor(annotation,false)});if(annotation.actionType==="url")el.style.cursor="pointer"}el.style.color=`#${this.decimalToHex(annotation.fgColor)}`;el.setAttribute("data-ar-type",annotation.type);el.setAttribute("hidden","");this.annotationsContainer.append(el)}}createCloseElement(){const svg=document.createElementNS("http://www.w3.org/2000/svg","svg");svg.setAttribute("viewBox","0 0 100 100");svg.classList.add("__cxt-ar-annotation-close__");const path=document.createElementNS(svg.namespaceURI,"path");path.setAttribute("d","M25 25 L 75 75 M 75 25 L 25 75");path.setAttribute("stroke","#bbb");path.setAttribute("stroke-width",10);path.setAttribute("x",5);path.setAttribute("y",5);const circle=document.createElementNS(svg.namespaceURI,"circle");circle.setAttribute("cx",50);circle.setAttribute("cy",50);circle.setAttribute("r",50);svg.append(circle,path);return svg}createSvgSpeechBubble(x,y,width,height,pointX,pointY,color="white",element,svg){const horizontalBaseStartMultiplier=.17379070765180116;const horizontalBaseEndMultiplier=.14896346370154384;const verticalBaseStartMultiplier=.12;const verticalBaseEndMultiplier=.3;let path;if(!svg){svg=document.createElementNS("http://www.w3.org/2000/svg","svg");svg.classList.add("__cxt-ar-annotation-speech-bubble__");path=document.createElementNS("http://www.w3.org/2000/svg","path");path.setAttribute("fill",color);svg.append(path)}else{path=svg.children[0]}svg.style.position="absolute";svg.setAttribute("width","100%");svg.setAttribute("height","100%");svg.style.left="0";svg.style.top="0";let positionStart;let baseStartX=0;let baseStartY=0;let baseEndX=0;let baseEndY=0;let pointFinalX=pointX;let pointFinalY=pointY;let commentRectPath;const pospad=20;let textWidth=0;let textHeight=0;let textX=0;let textY=0;let textElement;let closeElement;if(element){textElement=element.getElementsByTagName("span")[0];closeElement=element.getElementsByClassName("__cxt-ar-annotation-close__")[0]}if(pointX>x+width-width/2&&pointY>y+height){positionStart="br";baseStartX=width-width*horizontalBaseStartMultiplier*2;baseEndX=baseStartX+width*horizontalBaseEndMultiplier;baseStartY=height;baseEndY=height;pointFinalX=pointX-x;pointFinalY=pointY-y;element.style.height=pointY-y;commentRectPath=`L${width} ${height} L${width} 0 L0 0 L0 ${baseStartY} L${baseStartX} ${baseStartY}`;if(textElement){textWidth=width;textHeight=height;textX=0;textY=0}}else if(pointXy+height){positionStart="bl";baseStartX=width*horizontalBaseStartMultiplier;baseEndX=baseStartX+width*horizontalBaseEndMultiplier;baseStartY=height;baseEndY=height;pointFinalX=pointX-x;pointFinalY=pointY-y;element.style.height=`${pointY-y}px`;commentRectPath=`L${width} ${height} L${width} 0 L0 0 L0 ${baseStartY} L${baseStartX} ${baseStartY}`;if(textElement){textWidth=width;textHeight=height;textX=0;textY=0}}else if(pointX>x+width-width/2&&pointYx+width&&pointY>y-pospad&&pointYy&&pointY=start&&videoTimeend)){el.setAttribute("hidden","");if(annotation.style==="speech"&&annotation.__speechBubble){annotation.__speechBubble.style.display="none"}}}}start(){if(!this.playerOptions)throw new Error("playerOptions must be provided to use the start method");const videoTime=this.playerOptions.getVideoTime();if(!this.updateIntervalId){this.update(videoTime);this.updateIntervalId=setInterval(()=>{const videoTime=this.playerOptions.getVideoTime();this.update(videoTime);window.dispatchEvent(new CustomEvent("__ar_renderer_start"))},this.updateInterval)}}stop(){if(!this.playerOptions)throw new Error("playerOptions must be provided to use the stop method");const videoTime=this.playerOptions.getVideoTime();if(this.updateIntervalId){this.update(videoTime);clearInterval(this.updateIntervalId);this.updateIntervalId=null;window.dispatchEvent(new CustomEvent("__ar_renderer_stop"))}}updateAnnotationTextSize(annotation,containerHeight){if(annotation.textSize){const textSize=annotation.textSize/100*containerHeight;annotation.__element.style.fontSize=`${textSize}px`}}updateTextSize(){const containerHeight=this.container.getBoundingClientRect().height;for(const annotation of this.annotations){this.updateAnnotationTextSize(annotation,containerHeight)}}updateCloseSize(containerHeight){if(!containerHeight)containerHeight=this.container.getBoundingClientRect().height;const multiplier=.0423;this.annotationsContainer.style.setProperty("--annotation-close-size",`${containerHeight*multiplier}px`)}updateAnnotationDimensions(annotations,videoWidth,videoHeight){const playerWidth=this.container.getBoundingClientRect().width;const playerHeight=this.container.getBoundingClientRect().height;const widthDivider=playerWidth/videoWidth;const heightDivider=playerHeight/videoHeight;let scaledVideoWidth=playerWidth;let scaledVideoHeight=playerHeight;if(widthDivider%1!==0||heightDivider%1!==0){if(widthDivider>heightDivider){scaledVideoWidth=playerHeight/videoHeight*videoWidth;scaledVideoHeight=playerHeight}else if(heightDivider>widthDivider){scaledVideoWidth=playerWidth;scaledVideoHeight=playerWidth/videoWidth*videoHeight}}const verticalBlackBarWidth=(playerWidth-scaledVideoWidth)/2;const horizontalBlackBarHeight=(playerHeight-scaledVideoHeight)/2;const widthOffsetPercent=verticalBlackBarWidth/playerWidth*100;const heightOffsetPercent=horizontalBlackBarHeight/playerHeight*100;const widthMultiplier=scaledVideoWidth/playerWidth;const heightMultiplier=scaledVideoHeight/playerHeight;for(const annotation of annotations){const el=annotation.__element;let ax=widthOffsetPercent+annotation.x*widthMultiplier;let ay=heightOffsetPercent+annotation.y*heightMultiplier;let aw=annotation.width*widthMultiplier;let ah=annotation.height*heightMultiplier;el.style.left=`${ax}%`;el.style.top=`${ay}%`;el.style.width=`${aw}%`;el.style.height=`${ah}%`;let horizontalPadding=scaledVideoWidth*.008;let verticalPadding=scaledVideoHeight*.008;if(annotation.style==="speech"&&annotation.text){const pel=annotation.__element.getElementsByTagName("span")[0];horizontalPadding*=2;verticalPadding*=2;pel.style.paddingLeft=horizontalPadding+"px";pel.style.paddingRight=horizontalPadding+"px";pel.style.paddingBottom=verticalPadding+"px";pel.style.paddingTop=verticalPadding+"px"}else if(annotation.style!=="speech"){el.style.paddingLeft=horizontalPadding+"px";el.style.paddingRight=horizontalPadding+"px";el.style.paddingBottom=verticalPadding+"px";el.style.paddingTop=verticalPadding+"px"}if(annotation.__speechBubble){const asx=this.percentToPixels(playerWidth,ax);const asy=this.percentToPixels(playerHeight,ay);const asw=this.percentToPixels(playerWidth,aw);const ash=this.percentToPixels(playerHeight,ah);let sx=widthOffsetPercent+annotation.sx*widthMultiplier;let sy=heightOffsetPercent+annotation.sy*heightMultiplier;sx=this.percentToPixels(playerWidth,sx);sy=this.percentToPixels(playerHeight,sy);this.createSvgSpeechBubble(asx,asy,asw,ash,sx,sy,null,annotation.__element,annotation.__speechBubble)}this.updateAnnotationTextSize(annotation,scaledVideoHeight);this.updateCloseSize(scaledVideoHeight)}}updateAllAnnotationSizes(){if(this.playerOptions&&this.playerOptions.getOriginalVideoWidth&&this.playerOptions.getOriginalVideoHeight){const videoWidth=this.playerOptions.getOriginalVideoWidth();const videoHeight=this.playerOptions.getOriginalVideoHeight();this.updateAnnotationDimensions(this.annotations,videoWidth,videoHeight)}else{const playerWidth=this.container.getBoundingClientRect().width;const playerHeight=this.container.getBoundingClientRect().height;this.updateAnnotationDimensions(this.annotations,playerWidth,playerHeight)}}hideAll(){for(const annotation of this.annotations){annotation.__element.setAttribute("hidden","")}}annotationClickHandler(e){let annotationElement=e.target;if(!annotationElement.matches(".__cxt-ar-annotation__")&&!annotationElement.closest(".__cxt-ar-annotation-close__")){annotationElement=annotationElement.closest(".__cxt-ar-annotation__");if(!annotationElement)return null}let annotationData=annotationElement.__annotation;if(!annotationElement||!annotationData)return;if(annotationData.actionType==="time"){const seconds=annotationData.actionSeconds;if(this.playerOptions){this.playerOptions.seekTo(seconds);const videoTime=this.playerOptions.getVideoTime();this.update(videoTime)}window.dispatchEvent(new CustomEvent("__ar_seek_to",{detail:{seconds:seconds}}))}else if(annotationData.actionType==="url"){const data={url:annotationData.actionUrl,target:annotationData.actionUrlTarget||"current"};const timeHash=this.extractTimeHash(new URL(data.url));if(timeHash&&timeHash.hasOwnProperty("seconds")){data.seconds=timeHash.seconds}window.dispatchEvent(new CustomEvent("__ar_annotation_click",{detail:data}))}}setUpdateInterval(ms){this.updateInterval=ms;this.stop();this.start()}decimalToHex(dec){let hex=dec.toString(16);hex="000000".substr(0,6-hex.length)+hex;return hex}extractTimeHash(url){if(!url)throw new Error("A URL must be provided");const hash=url.hash;if(hash&&hash.startsWith("#t=")){const timeString=url.hash.split("#t=")[1];const seconds=this.timeStringToSeconds(timeString);return{seconds:seconds}}else{return false}}timeStringToSeconds(time){let seconds=0;const h=time.split("h");const m=(h[1]||time).split("m");const s=(m[1]||time).split("s");if(h[0]&&h.length===2)seconds+=parseInt(h[0],10)*60*60;if(m[0]&&m.length===2)seconds+=parseInt(m[0],10)*60;if(s[0]&&s.length===2)seconds+=parseInt(s[0],10);return seconds}percentToPixels(a,b){return a*b/100}}function youtubeAnnotationsPlugin(options){if(!options.annotationXml)throw new Error("Annotation data must be provided");if(!options.videoContainer)throw new Error("A video container to overlay the data on must be provided");const player=this;const xml=options.annotationXml;const parser=new AnnotationParser;const annotationElements=parser.getAnnotationsFromXml(xml);const annotations=parser.parseYoutubeAnnotationList(annotationElements);const videoContainer=options.videoContainer;const playerOptions={getVideoTime(){return player.currentTime()},seekTo(seconds){player.currentTime(seconds)},getOriginalVideoWidth(){return player.videoWidth()},getOriginalVideoHeight(){return player.videoHeight()}};raiseControls();const renderer=new AnnotationRenderer(annotations,videoContainer,playerOptions,options.updateInterval);setupEventListeners(player,renderer);renderer.start()}function setupEventListeners(player,renderer){if(!player)throw new Error("A video player must be provided");player.on("playerresize",e=>{renderer.updateAllAnnotationSizes(renderer.annotations)});player.one("loadedmetadata",e=>{renderer.updateAllAnnotationSizes(renderer.annotations)});player.on("pause",e=>{renderer.stop()});player.on("play",e=>{renderer.start()});player.on("seeking",e=>{renderer.update()});player.on("seeked",e=>{renderer.update()})}function raiseControls(){const styles=document.createElement("style");styles.textContent=`\n\t.vjs-control-bar {\n\t\tz-index: 21;\n\t}\n\t`;document.body.append(styles)} +class AnnotationParser { + static get defaultAppearanceAttributes() { + return { bgColor: 16777215, bgOpacity: 0.8, fgColor: 0, textSize: 3.15 }; + } + static get attributeMap() { + return { + type: "tp", + style: "s", + x: "x", + y: "y", + width: "w", + height: "h", + sx: "sx", + sy: "sy", + timeStart: "ts", + timeEnd: "te", + text: "t", + actionType: "at", + actionUrl: "au", + actionUrlTarget: "aut", + actionSeconds: "as", + bgOpacity: "bgo", + bgColor: "bgc", + fgColor: "fgc", + textSize: "txsz", + }; + } + deserializeAnnotation(serializedAnnotation) { + const map = this.constructor.attributeMap; + const attributes = serializedAnnotation.split(","); + const annotation = {}; + for (const attribute of attributes) { + const [key, value] = attribute.split("="); + const mappedKey = this.getKeyByValue(map, key); + let finalValue = ""; + if ( + [ + "text", + "actionType", + "actionUrl", + "actionUrlTarget", + "type", + "style", + ].indexOf(mappedKey) > -1 + ) { + finalValue = decodeURIComponent(value); + } else { + finalValue = parseFloat(value, 10); + } + annotation[mappedKey] = finalValue; + } + return annotation; + } + serializeAnnotation(annotation) { + const map = this.constructor.attributeMap; + let serialized = ""; + for (const key in annotation) { + const mappedKey = map[key]; + if ( + ["text", "actionType", "actionUrl", "actionUrlTarget"].indexOf(key) > + -1 && + mappedKey && + annotation.hasOwnProperty(key) + ) { + let text = encodeURIComponent(annotation[key]); + serialized += `${mappedKey}=${text},`; + } else if ( + ["text", "actionType", "actionUrl", "actionUrlTarget"].indexOf( + "key", + ) === -1 && + mappedKey && + annotation.hasOwnProperty(key) + ) { + serialized += `${mappedKey}=${annotation[key]},`; + } + } + return serialized.substring(0, serialized.length - 1); + } + deserializeAnnotationList(serializedAnnotationString) { + const serializedAnnotations = serializedAnnotationString.split(";"); + serializedAnnotations.length = serializedAnnotations.length - 1; + const annotations = []; + for (const annotation of serializedAnnotations) { + annotations.push(this.deserializeAnnotation(annotation)); + } + return annotations; + } + serializeAnnotationList(annotations) { + let serialized = ""; + for (const annotation of annotations) { + serialized += this.serializeAnnotation(annotation) + ";"; + } + return serialized; + } + xmlToDom(xml) { + const parser = new DOMParser(); + const dom = parser.parseFromString(xml, "application/xml"); + return dom; + } + getAnnotationsFromXml(xml) { + const dom = this.xmlToDom(xml); + return dom.getElementsByTagName("annotation"); + } + parseYoutubeAnnotationList(annotationElements) { + const annotations = []; + for (const el of annotationElements) { + const parsedAnnotation = this.parseYoutubeAnnotation(el); + if (parsedAnnotation) annotations.push(parsedAnnotation); + } + return annotations; + } + parseYoutubeAnnotation(annotationElement) { + const base = annotationElement; + const attributes = this.getAttributesFromBase(base); + if (!attributes.type || attributes.type === "pause") return null; + const text = this.getTextFromBase(base); + const action = this.getActionFromBase(base); + const backgroundShape = this.getBackgroundShapeFromBase(base); + if (!backgroundShape) return null; + const timeStart = backgroundShape.timeRange.start; + const timeEnd = backgroundShape.timeRange.end; + if ( + isNaN(timeStart) || + isNaN(timeEnd) || + timeStart === null || + timeEnd === null + ) { + return null; + } + const appearance = this.getAppearanceFromBase(base); + let annotation = { + type: attributes.type, + x: backgroundShape.x, + y: backgroundShape.y, + width: backgroundShape.width, + height: backgroundShape.height, + timeStart: timeStart, + timeEnd: timeEnd, + }; + if (attributes.style) annotation.style = attributes.style; + if (text) annotation.text = text; + if (action) annotation = Object.assign(action, annotation); + if (appearance) annotation = Object.assign(appearance, annotation); + if (backgroundShape.hasOwnProperty("sx")) + annotation.sx = backgroundShape.sx; + if (backgroundShape.hasOwnProperty("sy")) + annotation.sy = backgroundShape.sy; + return annotation; + } + getBackgroundShapeFromBase(base) { + const movingRegion = base.getElementsByTagName("movingRegion")[0]; + if (!movingRegion) return null; + const regionType = movingRegion.getAttribute("type"); + const regions = movingRegion.getElementsByTagName(`${regionType}Region`); + const timeRange = this.extractRegionTime(regions); + const shape = { + type: regionType, + x: parseFloat(regions[0].getAttribute("x"), 10), + y: parseFloat(regions[0].getAttribute("y"), 10), + width: parseFloat(regions[0].getAttribute("w"), 10), + height: parseFloat(regions[0].getAttribute("h"), 10), + timeRange: timeRange, + }; + const sx = regions[0].getAttribute("sx"); + const sy = regions[0].getAttribute("sy"); + if (sx) shape.sx = parseFloat(sx, 10); + if (sy) shape.sy = parseFloat(sy, 10); + return shape; + } + getAttributesFromBase(base) { + const attributes = {}; + attributes.type = base.getAttribute("type"); + attributes.style = base.getAttribute("style"); + return attributes; + } + getTextFromBase(base) { + const textElement = base.getElementsByTagName("TEXT")[0]; + if (textElement) return textElement.textContent; + } + getActionFromBase(base) { + const actionElement = base.getElementsByTagName("action")[0]; + if (!actionElement) return null; + const typeAttr = actionElement.getAttribute("type"); + const urlElement = actionElement.getElementsByTagName("url")[0]; + if (!urlElement) return null; + const actionUrlTarget = urlElement.getAttribute("target"); + const href = urlElement.getAttribute("value"); + if (href.startsWith("https://www.youtube.com/")) { + const url = new URL(href); + const srcVid = url.searchParams.get("src_vid"); + const toVid = url.searchParams.get("v"); + return this.linkOrTimestamp(url, srcVid, toVid, actionUrlTarget); + } + } + linkOrTimestamp(url, srcVid, toVid, actionUrlTarget) { + if (srcVid && toVid && srcVid === toVid) { + let seconds = 0; + const hash = url.hash; + if (hash && hash.startsWith("#t=")) { + const timeString = url.hash.split("#t=")[1]; + seconds = this.timeStringToSeconds(timeString); + } + return { actionType: "time", actionSeconds: seconds }; + } else { + return { + actionType: "url", + actionUrl: url.href, + actionUrlTarget: actionUrlTarget, + }; + } + } + getAppearanceFromBase(base) { + const appearanceElement = base.getElementsByTagName("appearance")[0]; + const styles = this.constructor.defaultAppearanceAttributes; + if (appearanceElement) { + const bgOpacity = appearanceElement.getAttribute("bgAlpha"); + const bgColor = appearanceElement.getAttribute("bgColor"); + const fgColor = appearanceElement.getAttribute("fgColor"); + const textSize = appearanceElement.getAttribute("textSize"); + if (bgOpacity) styles.bgOpacity = parseFloat(bgOpacity, 10); + if (bgColor) styles.bgColor = parseInt(bgColor, 10); + if (fgColor) styles.fgColor = parseInt(fgColor, 10); + if (textSize) styles.textSize = parseFloat(textSize, 10); + } + return styles; + } + extractRegionTime(regions) { + let timeStart = regions[0].getAttribute("t"); + timeStart = this.hmsToSeconds(timeStart); + let timeEnd = regions[regions.length - 1].getAttribute("t"); + timeEnd = this.hmsToSeconds(timeEnd); + return { start: timeStart, end: timeEnd }; + } + hmsToSeconds(hms) { + let p = hms.split(":"); + let s = 0; + let m = 1; + while (p.length > 0) { + s += m * parseFloat(p.pop(), 10); + m *= 60; + } + return s; + } + timeStringToSeconds(time) { + let seconds = 0; + const h = time.split("h"); + const m = (h[1] || time).split("m"); + const s = (m[1] || time).split("s"); + if (h[0] && h.length === 2) seconds += parseInt(h[0], 10) * 60 * 60; + if (m[0] && m.length === 2) seconds += parseInt(m[0], 10) * 60; + if (s[0] && s.length === 2) seconds += parseInt(s[0], 10); + return seconds; + } + getKeyByValue(obj, value) { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (obj[key] === value) { + return key; + } + } + } + } +} +class AnnotationRenderer { + constructor(annotations, container, playerOptions, updateInterval = 1e3) { + if (!annotations) throw new Error("Annotation objects must be provided"); + if (!container) + throw new Error("An element to contain the annotations must be provided"); + if (playerOptions && playerOptions.getVideoTime && playerOptions.seekTo) { + this.playerOptions = playerOptions; + } else { + console.info( + "AnnotationRenderer is running without a player. The update method will need to be called manually.", + ); + } + this.annotations = annotations; + this.container = container; + this.annotationsContainer = document.createElement("div"); + this.annotationsContainer.classList.add("__cxt-ar-annotations-container__"); + this.annotationsContainer.setAttribute("data-layer", "4"); + this.annotationsContainer.addEventListener("click", (e) => { + this.annotationClickHandler(e); + }); + this.container.prepend(this.annotationsContainer); + this.createAnnotationElements(); + this.updateAllAnnotationSizes(); + window.addEventListener("DOMContentLoaded", (e) => { + this.updateAllAnnotationSizes(); + }); + this.updateInterval = updateInterval; + this.updateIntervalId = null; + } + changeAnnotationData(annotations) { + this.stop(); + this.removeAnnotationElements(); + this.annotations = annotations; + this.createAnnotationElements(); + this.start(); + } + createAnnotationElements() { + for (const annotation of this.annotations) { + const el = document.createElement("div"); + el.classList.add("__cxt-ar-annotation__"); + annotation.__element = el; + el.__annotation = annotation; + const closeButton = this.createCloseElement(); + closeButton.addEventListener("click", (e) => { + el.setAttribute("hidden", ""); + el.setAttribute("data-ar-closed", ""); + if (el.__annotation.__speechBubble) { + const speechBubble = el.__annotation.__speechBubble; + speechBubble.style.display = "none"; + } + }); + el.append(closeButton); + if (annotation.text) { + const textNode = document.createElement("span"); + textNode.textContent = annotation.text; + el.append(textNode); + el.setAttribute("data-ar-has-text", ""); + } + if (annotation.style === "speech") { + const containerDimensions = this.container.getBoundingClientRect(); + const speechX = this.percentToPixels( + containerDimensions.width, + annotation.x, + ); + const speechY = this.percentToPixels( + containerDimensions.height, + annotation.y, + ); + const speechWidth = this.percentToPixels( + containerDimensions.width, + annotation.width, + ); + const speechHeight = this.percentToPixels( + containerDimensions.height, + annotation.height, + ); + const speechPointX = this.percentToPixels( + containerDimensions.width, + annotation.sx, + ); + const speechPointY = this.percentToPixels( + containerDimensions.height, + annotation.sy, + ); + const bubbleColor = this.getFinalAnnotationColor(annotation, false); + const bubble = this.createSvgSpeechBubble( + speechX, + speechY, + speechWidth, + speechHeight, + speechPointX, + speechPointY, + bubbleColor, + annotation.__element, + ); + bubble.style.display = "none"; + bubble.style.overflow = "visible"; + el.style.pointerEvents = "none"; + bubble.__annotationEl = el; + annotation.__speechBubble = bubble; + const path = bubble.getElementsByTagName("path")[0]; + path.addEventListener("mouseover", () => { + closeButton.style.display = "block"; + closeButton.style.cursor = "pointer"; + path.setAttribute( + "fill", + this.getFinalAnnotationColor(annotation, true), + ); + }); + path.addEventListener("mouseout", (e) => { + if ( + !e.relatedTarget.classList.contains("__cxt-ar-annotation-close__") + ) { + closeButton.style.display = "none"; + closeButton.style.cursor = "default"; + path.setAttribute( + "fill", + this.getFinalAnnotationColor(annotation, false), + ); + } + }); + closeButton.addEventListener("mouseleave", () => { + closeButton.style.display = "none"; + path.style.cursor = "default"; + closeButton.style.cursor = "default"; + path.setAttribute( + "fill", + this.getFinalAnnotationColor(annotation, false), + ); + }); + el.prepend(bubble); + } else if (annotation.type === "highlight") { + el.style.backgroundColor = ""; + el.style.border = `2.5px solid ${this.getFinalAnnotationColor(annotation, false)}`; + if (annotation.actionType === "url") el.style.cursor = "pointer"; + } else if (annotation.style !== "title") { + el.style.backgroundColor = this.getFinalAnnotationColor(annotation); + el.addEventListener("mouseenter", () => { + el.style.backgroundColor = this.getFinalAnnotationColor( + annotation, + true, + ); + }); + el.addEventListener("mouseleave", () => { + el.style.backgroundColor = this.getFinalAnnotationColor( + annotation, + false, + ); + }); + if (annotation.actionType === "url") el.style.cursor = "pointer"; + } + el.style.color = `#${this.decimalToHex(annotation.fgColor)}`; + el.setAttribute("data-ar-type", annotation.type); + el.setAttribute("hidden", ""); + this.annotationsContainer.append(el); + } + } + createCloseElement() { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 100 100"); + svg.classList.add("__cxt-ar-annotation-close__"); + const path = document.createElementNS(svg.namespaceURI, "path"); + path.setAttribute("d", "M25 25 L 75 75 M 75 25 L 25 75"); + path.setAttribute("stroke", "#bbb"); + path.setAttribute("stroke-width", 10); + path.setAttribute("x", 5); + path.setAttribute("y", 5); + const circle = document.createElementNS(svg.namespaceURI, "circle"); + circle.setAttribute("cx", 50); + circle.setAttribute("cy", 50); + circle.setAttribute("r", 50); + svg.append(circle, path); + return svg; + } + createSvgSpeechBubble( + x, + y, + width, + height, + pointX, + pointY, + color = "white", + element, + svg, + ) { + const horizontalBaseStartMultiplier = 0.17379070765180116; + const horizontalBaseEndMultiplier = 0.14896346370154384; + const verticalBaseStartMultiplier = 0.12; + const verticalBaseEndMultiplier = 0.3; + let path; + if (!svg) { + svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.classList.add("__cxt-ar-annotation-speech-bubble__"); + path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("fill", color); + svg.append(path); + } else { + path = svg.children[0]; + } + svg.style.position = "absolute"; + svg.setAttribute("width", "100%"); + svg.setAttribute("height", "100%"); + svg.style.left = "0"; + svg.style.top = "0"; + let positionStart; + let baseStartX = 0; + let baseStartY = 0; + let baseEndX = 0; + let baseEndY = 0; + let pointFinalX = pointX; + let pointFinalY = pointY; + let commentRectPath; + const pospad = 20; + let textWidth = 0; + let textHeight = 0; + let textX = 0; + let textY = 0; + let textElement; + let closeElement; + if (element) { + textElement = element.getElementsByTagName("span")[0]; + closeElement = element.getElementsByClassName( + "__cxt-ar-annotation-close__", + )[0]; + } + if (pointX > x + width - width / 2 && pointY > y + height) { + positionStart = "br"; + baseStartX = width - width * horizontalBaseStartMultiplier * 2; + baseEndX = baseStartX + width * horizontalBaseEndMultiplier; + baseStartY = height; + baseEndY = height; + pointFinalX = pointX - x; + pointFinalY = pointY - y; + element.style.height = pointY - y; + commentRectPath = `L${width} ${height} L${width} 0 L0 0 L0 ${baseStartY} L${baseStartX} ${baseStartY}`; + if (textElement) { + textWidth = width; + textHeight = height; + textX = 0; + textY = 0; + } + } else if (pointX < x + width - width / 2 && pointY > y + height) { + positionStart = "bl"; + baseStartX = width * horizontalBaseStartMultiplier; + baseEndX = baseStartX + width * horizontalBaseEndMultiplier; + baseStartY = height; + baseEndY = height; + pointFinalX = pointX - x; + pointFinalY = pointY - y; + element.style.height = `${pointY - y}px`; + commentRectPath = `L${width} ${height} L${width} 0 L0 0 L0 ${baseStartY} L${baseStartX} ${baseStartY}`; + if (textElement) { + textWidth = width; + textHeight = height; + textX = 0; + textY = 0; + } + } else if (pointX > x + width - width / 2 && pointY < y - pospad) { + positionStart = "tr"; + baseStartX = width - width * horizontalBaseStartMultiplier * 2; + baseEndX = baseStartX + width * horizontalBaseEndMultiplier; + const yOffset = y - pointY; + baseStartY = yOffset; + baseEndY = yOffset; + element.style.top = y - yOffset + "px"; + element.style.height = height + yOffset + "px"; + pointFinalX = pointX - x; + pointFinalY = 0; + commentRectPath = `L${width} ${yOffset} L${width} ${height + yOffset} L0 ${height + yOffset} L0 ${yOffset} L${baseStartX} ${baseStartY}`; + if (textElement) { + textWidth = width; + textHeight = height; + textX = 0; + textY = yOffset; + } + } else if (pointX < x + width - width / 2 && pointY < y) { + positionStart = "tl"; + baseStartX = width * horizontalBaseStartMultiplier; + baseEndX = baseStartX + width * horizontalBaseEndMultiplier; + const yOffset = y - pointY; + baseStartY = yOffset; + baseEndY = yOffset; + element.style.top = y - yOffset + "px"; + element.style.height = height + yOffset + "px"; + pointFinalX = pointX - x; + pointFinalY = 0; + commentRectPath = `L${width} ${yOffset} L${width} ${height + yOffset} L0 ${height + yOffset} L0 ${yOffset} L${baseStartX} ${baseStartY}`; + if (textElement) { + textWidth = width; + textHeight = height; + textX = 0; + textY = yOffset; + } + } else if ( + pointX > x + width && + pointY > y - pospad && + pointY < y + height - pospad + ) { + positionStart = "r"; + const xOffset = pointX - (x + width); + baseStartX = width; + baseEndX = width; + element.style.width = width + xOffset + "px"; + baseStartY = height * verticalBaseStartMultiplier; + baseEndY = baseStartY + height * verticalBaseEndMultiplier; + pointFinalX = width + xOffset; + pointFinalY = pointY - y; + commentRectPath = `L${baseStartX} ${height} L0 ${height} L0 0 L${baseStartX} 0 L${baseStartX} ${baseStartY}`; + if (textElement) { + textWidth = width; + textHeight = height; + textX = 0; + textY = 0; + } + } else if (pointX < x && pointY > y && pointY < y + height) { + positionStart = "l"; + const xOffset = x - pointX; + baseStartX = xOffset; + baseEndX = xOffset; + element.style.left = x - xOffset + "px"; + element.style.width = width + xOffset + "px"; + baseStartY = height * verticalBaseStartMultiplier; + baseEndY = baseStartY + height * verticalBaseEndMultiplier; + pointFinalX = 0; + pointFinalY = pointY - y; + commentRectPath = `L${baseStartX} ${height} L${width + baseStartX} ${height} L${width + baseStartX} 0 L${baseStartX} 0 L${baseStartX} ${baseStartY}`; + if (textElement) { + textWidth = width; + textHeight = height; + textX = xOffset; + textY = 0; + } + } else { + return svg; + } + if (textElement) { + textElement.style.left = textX + "px"; + textElement.style.top = textY + "px"; + textElement.style.width = textWidth + "px"; + textElement.style.height = textHeight + "px"; + } + if (closeElement) { + const closeSize = parseFloat( + this.annotationsContainer.style.getPropertyValue( + "--annotation-close-size", + ), + 10, + ); + if (closeSize) { + closeElement.style.left = textX + textWidth + closeSize / -1.8 + "px"; + closeElement.style.top = textY + closeSize / -1.8 + "px"; + } + } + const pathData = `M${baseStartX} ${baseStartY} L${pointFinalX} ${pointFinalY} L${baseEndX} ${baseEndY} ${commentRectPath}`; + path.setAttribute("d", pathData); + return svg; + } + getFinalAnnotationColor(annotation, hover = false) { + const alphaHex = hover + ? (230).toString(16) + : Math.floor(annotation.bgOpacity * 255).toString(16); + if (!isNaN(annotation.bgColor)) { + const bgColorHex = this.decimalToHex(annotation.bgColor); + const backgroundColor = `#${bgColorHex}${alphaHex}`; + return backgroundColor; + } + } + removeAnnotationElements() { + for (const annotation of this.annotations) { + annotation.__element.remove(); + } + } + update(videoTime) { + for (const annotation of this.annotations) { + const el = annotation.__element; + if (el.hasAttribute("data-ar-closed")) continue; + const start = annotation.timeStart; + const end = annotation.timeEnd; + if (el.hasAttribute("hidden") && videoTime >= start && videoTime < end) { + el.removeAttribute("hidden"); + if (annotation.style === "speech" && annotation.__speechBubble) { + annotation.__speechBubble.style.display = "block"; + } + } else if ( + !el.hasAttribute("hidden") && + (videoTime < start || videoTime > end) + ) { + el.setAttribute("hidden", ""); + if (annotation.style === "speech" && annotation.__speechBubble) { + annotation.__speechBubble.style.display = "none"; + } + } + } + } + start() { + if (!this.playerOptions) + throw new Error("playerOptions must be provided to use the start method"); + const videoTime = this.playerOptions.getVideoTime(); + if (!this.updateIntervalId) { + this.update(videoTime); + this.updateIntervalId = setInterval(() => { + const videoTime = this.playerOptions.getVideoTime(); + this.update(videoTime); + window.dispatchEvent(new CustomEvent("__ar_renderer_start")); + }, this.updateInterval); + } + } + stop() { + if (!this.playerOptions) + throw new Error("playerOptions must be provided to use the stop method"); + const videoTime = this.playerOptions.getVideoTime(); + if (this.updateIntervalId) { + this.update(videoTime); + clearInterval(this.updateIntervalId); + this.updateIntervalId = null; + window.dispatchEvent(new CustomEvent("__ar_renderer_stop")); + } + } + updateAnnotationTextSize(annotation, containerHeight) { + if (annotation.textSize) { + const textSize = (annotation.textSize / 100) * containerHeight; + annotation.__element.style.fontSize = `${textSize}px`; + } + } + updateTextSize() { + const containerHeight = this.container.getBoundingClientRect().height; + for (const annotation of this.annotations) { + this.updateAnnotationTextSize(annotation, containerHeight); + } + } + updateCloseSize(containerHeight) { + if (!containerHeight) + containerHeight = this.container.getBoundingClientRect().height; + const multiplier = 0.0423; + this.annotationsContainer.style.setProperty( + "--annotation-close-size", + `${containerHeight * multiplier}px`, + ); + } + updateAnnotationDimensions(annotations, videoWidth, videoHeight) { + const playerWidth = this.container.getBoundingClientRect().width; + const playerHeight = this.container.getBoundingClientRect().height; + const widthDivider = playerWidth / videoWidth; + const heightDivider = playerHeight / videoHeight; + let scaledVideoWidth = playerWidth; + let scaledVideoHeight = playerHeight; + if (widthDivider % 1 !== 0 || heightDivider % 1 !== 0) { + if (widthDivider > heightDivider) { + scaledVideoWidth = (playerHeight / videoHeight) * videoWidth; + scaledVideoHeight = playerHeight; + } else if (heightDivider > widthDivider) { + scaledVideoWidth = playerWidth; + scaledVideoHeight = (playerWidth / videoWidth) * videoHeight; + } + } + const verticalBlackBarWidth = (playerWidth - scaledVideoWidth) / 2; + const horizontalBlackBarHeight = (playerHeight - scaledVideoHeight) / 2; + const widthOffsetPercent = (verticalBlackBarWidth / playerWidth) * 100; + const heightOffsetPercent = (horizontalBlackBarHeight / playerHeight) * 100; + const widthMultiplier = scaledVideoWidth / playerWidth; + const heightMultiplier = scaledVideoHeight / playerHeight; + for (const annotation of annotations) { + const el = annotation.__element; + let ax = widthOffsetPercent + annotation.x * widthMultiplier; + let ay = heightOffsetPercent + annotation.y * heightMultiplier; + let aw = annotation.width * widthMultiplier; + let ah = annotation.height * heightMultiplier; + el.style.left = `${ax}%`; + el.style.top = `${ay}%`; + el.style.width = `${aw}%`; + el.style.height = `${ah}%`; + let horizontalPadding = scaledVideoWidth * 0.008; + let verticalPadding = scaledVideoHeight * 0.008; + if (annotation.style === "speech" && annotation.text) { + const pel = annotation.__element.getElementsByTagName("span")[0]; + horizontalPadding *= 2; + verticalPadding *= 2; + pel.style.paddingLeft = horizontalPadding + "px"; + pel.style.paddingRight = horizontalPadding + "px"; + pel.style.paddingBottom = verticalPadding + "px"; + pel.style.paddingTop = verticalPadding + "px"; + } else if (annotation.style !== "speech") { + el.style.paddingLeft = horizontalPadding + "px"; + el.style.paddingRight = horizontalPadding + "px"; + el.style.paddingBottom = verticalPadding + "px"; + el.style.paddingTop = verticalPadding + "px"; + } + if (annotation.__speechBubble) { + const asx = this.percentToPixels(playerWidth, ax); + const asy = this.percentToPixels(playerHeight, ay); + const asw = this.percentToPixels(playerWidth, aw); + const ash = this.percentToPixels(playerHeight, ah); + let sx = widthOffsetPercent + annotation.sx * widthMultiplier; + let sy = heightOffsetPercent + annotation.sy * heightMultiplier; + sx = this.percentToPixels(playerWidth, sx); + sy = this.percentToPixels(playerHeight, sy); + this.createSvgSpeechBubble( + asx, + asy, + asw, + ash, + sx, + sy, + null, + annotation.__element, + annotation.__speechBubble, + ); + } + this.updateAnnotationTextSize(annotation, scaledVideoHeight); + this.updateCloseSize(scaledVideoHeight); + } + } + updateAllAnnotationSizes() { + if ( + this.playerOptions && + this.playerOptions.getOriginalVideoWidth && + this.playerOptions.getOriginalVideoHeight + ) { + const videoWidth = this.playerOptions.getOriginalVideoWidth(); + const videoHeight = this.playerOptions.getOriginalVideoHeight(); + this.updateAnnotationDimensions( + this.annotations, + videoWidth, + videoHeight, + ); + } else { + const playerWidth = this.container.getBoundingClientRect().width; + const playerHeight = this.container.getBoundingClientRect().height; + this.updateAnnotationDimensions( + this.annotations, + playerWidth, + playerHeight, + ); + } + } + hideAll() { + for (const annotation of this.annotations) { + annotation.__element.setAttribute("hidden", ""); + } + } + annotationClickHandler(e) { + let annotationElement = e.target; + if ( + !annotationElement.matches(".__cxt-ar-annotation__") && + !annotationElement.closest(".__cxt-ar-annotation-close__") + ) { + annotationElement = annotationElement.closest(".__cxt-ar-annotation__"); + if (!annotationElement) return null; + } + let annotationData = annotationElement.__annotation; + if (!annotationElement || !annotationData) return; + if (annotationData.actionType === "time") { + const seconds = annotationData.actionSeconds; + if (this.playerOptions) { + this.playerOptions.seekTo(seconds); + const videoTime = this.playerOptions.getVideoTime(); + this.update(videoTime); + } + window.dispatchEvent( + new CustomEvent("__ar_seek_to", { detail: { seconds: seconds } }), + ); + } else if (annotationData.actionType === "url") { + const data = { + url: annotationData.actionUrl, + target: annotationData.actionUrlTarget || "current", + }; + const timeHash = this.extractTimeHash(new URL(data.url)); + if (timeHash && timeHash.hasOwnProperty("seconds")) { + data.seconds = timeHash.seconds; + } + window.dispatchEvent( + new CustomEvent("__ar_annotation_click", { detail: data }), + ); + } + } + setUpdateInterval(ms) { + this.updateInterval = ms; + this.stop(); + this.start(); + } + decimalToHex(dec) { + let hex = dec.toString(16); + hex = "000000".substr(0, 6 - hex.length) + hex; + return hex; + } + extractTimeHash(url) { + if (!url) throw new Error("A URL must be provided"); + const hash = url.hash; + if (hash && hash.startsWith("#t=")) { + const timeString = url.hash.split("#t=")[1]; + const seconds = this.timeStringToSeconds(timeString); + return { seconds: seconds }; + } else { + return false; + } + } + timeStringToSeconds(time) { + let seconds = 0; + const h = time.split("h"); + const m = (h[1] || time).split("m"); + const s = (m[1] || time).split("s"); + if (h[0] && h.length === 2) seconds += parseInt(h[0], 10) * 60 * 60; + if (m[0] && m.length === 2) seconds += parseInt(m[0], 10) * 60; + if (s[0] && s.length === 2) seconds += parseInt(s[0], 10); + return seconds; + } + percentToPixels(a, b) { + return (a * b) / 100; + } +} +function youtubeAnnotationsPlugin(options) { + if (!options.annotationXml) + throw new Error("Annotation data must be provided"); + if (!options.videoContainer) + throw new Error( + "A video container to overlay the data on must be provided", + ); + const player = this; + const xml = options.annotationXml; + const parser = new AnnotationParser(); + const annotationElements = parser.getAnnotationsFromXml(xml); + const annotations = parser.parseYoutubeAnnotationList(annotationElements); + const videoContainer = options.videoContainer; + const playerOptions = { + getVideoTime() { + return player.currentTime(); + }, + seekTo(seconds) { + player.currentTime(seconds); + }, + getOriginalVideoWidth() { + return player.videoWidth(); + }, + getOriginalVideoHeight() { + return player.videoHeight(); + }, + }; + raiseControls(); + const renderer = new AnnotationRenderer( + annotations, + videoContainer, + playerOptions, + options.updateInterval, + ); + setupEventListeners(player, renderer); + renderer.start(); +} +function setupEventListeners(player, renderer) { + if (!player) throw new Error("A video player must be provided"); + player.on("playerresize", (e) => { + renderer.updateAllAnnotationSizes(renderer.annotations); + }); + player.one("loadedmetadata", (e) => { + renderer.updateAllAnnotationSizes(renderer.annotations); + }); + player.on("pause", (e) => { + renderer.stop(); + }); + player.on("play", (e) => { + renderer.start(); + }); + player.on("seeking", (e) => { + renderer.update(); + }); + player.on("seeked", (e) => { + renderer.update(); + }); +} +function raiseControls() { + const styles = document.createElement("style"); + styles.textContent = `\n\t.vjs-control-bar {\n\t\tz-index: 21;\n\t}\n\t`; + document.body.append(styles); +} diff --git a/assets/js/watch.js b/assets/js/watch.js index 75ec99a3..d8fc61f3 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -1,135 +1,159 @@ -'use strict'; +"use strict"; function toggle_parent(target) { - var body = target.parentNode.parentNode.children[1]; - if (body.style.display === 'none') { - target.textContent = '[ − ]'; - body.style.display = ''; - } else { - target.textContent = '[ + ]'; - body.style.display = 'none'; - } + var body = target.parentNode.parentNode.children[1]; + if (body.style.display === "none") { + target.textContent = "[ − ]"; + body.style.display = ""; + } else { + target.textContent = "[ + ]"; + body.style.display = "none"; + } } function swap_comments(event) { - var source = event.target.getAttribute('data-comments'); + var source = event.target.getAttribute("data-comments"); - if (source === 'youtube') { - get_youtube_comments(); - } else if (source === 'reddit') { - get_reddit_comments(); - } + if (source === "youtube") { + get_youtube_comments(); + } else if (source === "reddit") { + get_reddit_comments(); + } } -var continue_button = document.getElementById('continue'); +var continue_button = document.getElementById("continue"); if (continue_button) { - continue_button.onclick = continue_autoplay; + continue_button.onclick = continue_autoplay; } function next_video() { - var url = new URL('https://example.com/watch?v=' + video_data.next_video); + var url = new URL("https://example.com/watch?v=" + video_data.next_video); - if (video_data.params.autoplay || video_data.params.continue_autoplay) - url.searchParams.set('autoplay', '1'); - if (video_data.params.listen !== video_data.preferences.listen) - url.searchParams.set('listen', video_data.params.listen); - if (video_data.params.speed !== video_data.preferences.speed) - url.searchParams.set('speed', video_data.params.speed); - if (video_data.params.local !== video_data.preferences.local) - url.searchParams.set('local', video_data.params.local); - url.searchParams.set('continue', '1'); + if (video_data.params.autoplay || video_data.params.continue_autoplay) + url.searchParams.set("autoplay", "1"); + if (video_data.params.listen !== video_data.preferences.listen) + url.searchParams.set("listen", video_data.params.listen); + if (video_data.params.speed !== video_data.preferences.speed) + url.searchParams.set("speed", video_data.params.speed); + if (video_data.params.local !== video_data.preferences.local) + url.searchParams.set("local", video_data.params.local); + url.searchParams.set("continue", "1"); - location.assign(url.pathname + url.search); + location.assign(url.pathname + url.search); } function continue_autoplay(event) { - if (event.target.checked) { - player.on('ended', next_video); - } else { - player.off('ended'); - } + if (event.target.checked) { + player.on("ended", next_video); + } else { + player.off("ended"); + } } function get_playlist(plid) { - var playlist = document.getElementById('playlist'); + var playlist = document.getElementById("playlist"); - playlist.innerHTML = spinnerHTMLwithHR; + playlist.innerHTML = spinnerHTMLwithHR; - var plid_url; - if (plid.startsWith('RD')) { - plid_url = '/api/v1/mixes/' + plid + - '?continuation=' + video_data.id + - '&format=html&hl=' + video_data.preferences.locale; - } else { - plid_url = '/api/v1/playlists/' + plid + - '?index=' + video_data.index + - '&continuation=' + video_data.id + - '&format=html&hl=' + video_data.preferences.locale; - } + var plid_url; + if (plid.startsWith("RD")) { + plid_url = + "/api/v1/mixes/" + + plid + + "?continuation=" + + video_data.id + + "&format=html&hl=" + + video_data.preferences.locale; + } else { + plid_url = + "/api/v1/playlists/" + + plid + + "?index=" + + video_data.index + + "&continuation=" + + video_data.id + + "&format=html&hl=" + + video_data.preferences.locale; + } - if (video_data.params.listen) { - plid_url += '&listen=1' - } + if (video_data.params.listen) { + plid_url += "&listen=1"; + } - helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, { - on200: function (response) { - if (response === null) return; + helpers.xhr( + "GET", + plid_url, + { retries: 5, entity_name: "playlist" }, + { + on200: function (response) { + if (response === null) return; - playlist.innerHTML = response.playlistHtml; + playlist.innerHTML = response.playlistHtml; - if (!response.nextVideo) return; + if (!response.nextVideo) return; - var nextVideo = document.getElementById(response.nextVideo); - nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop; + var nextVideo = document.getElementById(response.nextVideo); + nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop; - player.on('ended', function () { - var url = new URL('https://example.com/watch?v=' + response.nextVideo); + player.on("ended", function () { + var url = new URL( + "https://example.com/watch?v=" + response.nextVideo, + ); - url.searchParams.set('list', plid); - if (!plid.startsWith('RD')) - url.searchParams.set('index', response.index); - if (video_data.params.autoplay || video_data.params.continue_autoplay) - url.searchParams.set('autoplay', '1'); - if (video_data.params.listen !== video_data.preferences.listen) - url.searchParams.set('listen', video_data.params.listen); - if (video_data.params.speed !== video_data.preferences.speed) - url.searchParams.set('speed', video_data.params.speed); - if (video_data.params.local !== video_data.preferences.local) - url.searchParams.set('local', video_data.params.local); + url.searchParams.set("list", plid); + if (!plid.startsWith("RD")) + url.searchParams.set("index", response.index); + if (video_data.params.autoplay || video_data.params.continue_autoplay) + url.searchParams.set("autoplay", "1"); + if (video_data.params.listen !== video_data.preferences.listen) + url.searchParams.set("listen", video_data.params.listen); + if (video_data.params.speed !== video_data.preferences.speed) + url.searchParams.set("speed", video_data.params.speed); + if (video_data.params.local !== video_data.preferences.local) + url.searchParams.set("local", video_data.params.local); - location.assign(url.pathname + url.search); - }); - }, - onNon200: function (xhr) { - playlist.innerHTML = ''; - document.getElementById('continue').style.display = ''; - }, - onError: function (xhr) { - playlist.innerHTML = spinnerHTMLwithHR; - }, - onTimeout: function (xhr) { - playlist.innerHTML = spinnerHTMLwithHR; - } - }); + location.assign(url.pathname + url.search); + }); + }, + onNon200: function (xhr) { + playlist.innerHTML = ""; + document.getElementById("continue").style.display = ""; + }, + onError: function (xhr) { + playlist.innerHTML = spinnerHTMLwithHR; + }, + onTimeout: function (xhr) { + playlist.innerHTML = spinnerHTMLwithHR; + }, + }, + ); } function get_reddit_comments() { - var comments = document.getElementById('comments'); + var comments = document.getElementById("comments"); - var fallback = comments.innerHTML; - comments.innerHTML = spinnerHTML; + var fallback = comments.innerHTML; + comments.innerHTML = spinnerHTML; - var url = '/api/v1/comments/' + video_data.id + - '?source=reddit&format=html' + - '&hl=' + video_data.preferences.locale; + var url = + "/api/v1/comments/" + + video_data.id + + "?source=reddit&format=html" + + "&hl=" + + video_data.preferences.locale; - var onNon200 = function (xhr) { comments.innerHTML = fallback; }; - if (video_data.params.comments[1] === 'youtube') - onNon200 = function (xhr) {}; + var onNon200 = function (xhr) { + comments.innerHTML = fallback; + }; + if (video_data.params.comments[1] === "youtube") onNon200 = function (xhr) {}; - helpers.xhr('GET', url, {retries: 5, entity_name: ''}, { - on200: function (response) { - comments.innerHTML = ' \ + helpers.xhr( + "GET", + url, + { retries: 5, entity_name: "" }, + { + on200: function (response) { + comments.innerHTML = ' \
      \

      \ [ − ] \ @@ -148,52 +172,52 @@ function get_reddit_comments() {

      \
      {contentHtml}
      \
      '.supplant({ - title: response.title, - youtubeCommentsText: video_data.youtube_comments_text, - redditPermalinkText: video_data.reddit_permalink_text, - permalink: response.permalink, - contentHtml: response.contentHtml - }); + title: response.title, + youtubeCommentsText: video_data.youtube_comments_text, + redditPermalinkText: video_data.reddit_permalink_text, + permalink: response.permalink, + contentHtml: response.contentHtml, + }); - comments.children[0].children[0].children[0].onclick = toggle_comments; - comments.children[0].children[1].children[0].onclick = swap_comments; - }, - onNon200: onNon200, // declared above - }); + comments.children[0].children[0].children[0].onclick = toggle_comments; + comments.children[0].children[1].children[0].onclick = swap_comments; + }, + onNon200: onNon200, // declared above + }, + ); } if (video_data.play_next) { - player.on('ended', function () { - var url = new URL('https://example.com/watch?v=' + video_data.next_video); + player.on("ended", function () { + var url = new URL("https://example.com/watch?v=" + video_data.next_video); - if (video_data.params.autoplay || video_data.params.continue_autoplay) - url.searchParams.set('autoplay', '1'); - if (video_data.params.listen !== video_data.preferences.listen) - url.searchParams.set('listen', video_data.params.listen); - if (video_data.params.speed !== video_data.preferences.speed) - url.searchParams.set('speed', video_data.params.speed); - if (video_data.params.local !== video_data.preferences.local) - url.searchParams.set('local', video_data.params.local); - url.searchParams.set('continue', '1'); + if (video_data.params.autoplay || video_data.params.continue_autoplay) + url.searchParams.set("autoplay", "1"); + if (video_data.params.listen !== video_data.preferences.listen) + url.searchParams.set("listen", video_data.params.listen); + if (video_data.params.speed !== video_data.preferences.speed) + url.searchParams.set("speed", video_data.params.speed); + if (video_data.params.local !== video_data.preferences.local) + url.searchParams.set("local", video_data.params.local); + url.searchParams.set("continue", "1"); - location.assign(url.pathname + url.search); - }); + location.assign(url.pathname + url.search); + }); } -addEventListener('load', function (e) { - if (video_data.plid) - get_playlist(video_data.plid); +addEventListener("load", function (e) { + if (video_data.plid) get_playlist(video_data.plid); - if (video_data.params.comments[0] === 'youtube') { - get_youtube_comments(); - } else if (video_data.params.comments[0] === 'reddit') { - get_reddit_comments(); - } else if (video_data.params.comments[1] === 'youtube') { - get_youtube_comments(); - } else if (video_data.params.comments[1] === 'reddit') { - get_reddit_comments(); - } else { - var comments = document.getElementById('comments'); - comments.innerHTML = ''; - } + if (video_data.params.comments[0] === "youtube") { + get_youtube_comments(); + } else if (video_data.params.comments[0] === "reddit") { + get_reddit_comments(); + } else if (video_data.params.comments[1] === "youtube") { + get_youtube_comments(); + } else if (video_data.params.comments[1] === "reddit") { + get_reddit_comments(); + } else { + var comments = document.getElementById("comments"); + comments.innerHTML = ""; + } }); diff --git a/assets/js/watched_indicator.js b/assets/js/watched_indicator.js index e971cd80..d598e35e 100644 --- a/assets/js/watched_indicator.js +++ b/assets/js/watched_indicator.js @@ -1,24 +1,24 @@ -'use strict'; -var save_player_pos_key = 'save_player_pos'; +"use strict"; +var save_player_pos_key = "save_player_pos"; function get_all_video_times() { - return helpers.storage.get(save_player_pos_key) || {}; + return helpers.storage.get(save_player_pos_key) || {}; } -document.querySelectorAll('.watched-indicator').forEach(function (indicator) { - var watched_part = get_all_video_times()[indicator.dataset.id]; - var total = parseInt(indicator.dataset.length, 10); - if (watched_part === undefined) { - watched_part = total; - } - var percentage = Math.round((watched_part / total) * 100); +document.querySelectorAll(".watched-indicator").forEach(function (indicator) { + var watched_part = get_all_video_times()[indicator.dataset.id]; + var total = parseInt(indicator.dataset.length, 10); + if (watched_part === undefined) { + watched_part = total; + } + var percentage = Math.round((watched_part / total) * 100); - if (percentage < 5) { - percentage = 5; - } - if (percentage > 90) { - percentage = 100; - } + if (percentage < 5) { + percentage = 5; + } + if (percentage > 90) { + percentage = 100; + } - indicator.style.width = percentage + '%'; + indicator.style.width = percentage + "%"; }); diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 06af62cc..4ce4ba07 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -1,34 +1,50 @@ -'use strict'; -var watched_data = JSON.parse(document.getElementById('watched_data').textContent); -var payload = 'csrf_token=' + watched_data.csrf_token; +"use strict"; +var watched_data = JSON.parse( + document.getElementById("watched_data").textContent, +); +var payload = "csrf_token=" + watched_data.csrf_token; function mark_watched(target) { - var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; - tile.style.display = 'none'; + var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; + tile.style.display = "none"; - var url = '/watch_ajax?action=mark_watched&redirect=false' + - '&id=' + target.getAttribute('data-id'); + var url = + "/watch_ajax?action=mark_watched&redirect=false" + + "&id=" + + target.getAttribute("data-id"); - helpers.xhr('POST', url, {payload: payload}, { - onNon200: function (xhr) { - tile.style.display = ''; - } - }); + helpers.xhr( + "POST", + url, + { payload: payload }, + { + onNon200: function (xhr) { + tile.style.display = ""; + }, + }, + ); } function mark_unwatched(target) { - var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; - tile.style.display = 'none'; - var count = document.getElementById('count'); - count.textContent--; + var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; + tile.style.display = "none"; + var count = document.getElementById("count"); + count.textContent--; - var url = '/watch_ajax?action=mark_unwatched&redirect=false' + - '&id=' + target.getAttribute('data-id'); + var url = + "/watch_ajax?action=mark_unwatched&redirect=false" + + "&id=" + + target.getAttribute("data-id"); - helpers.xhr('POST', url, {payload: payload}, { - onNon200: function (xhr) { - count.textContent++; - tile.style.display = ''; - } - }); + helpers.xhr( + "POST", + url, + { payload: payload }, + { + onNon200: function (xhr) { + count.textContent++; + tile.style.display = ""; + }, + }, + ); } diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr index e75d38a5..cf78c4d0 100644 --- a/src/invidious/frontend/pagination.cr +++ b/src/invidious/frontend/pagination.cr @@ -89,13 +89,6 @@ module Invidious::Frontend::Pagination end def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params) - return String.build do |str| - if !ctoken.nil? - str << %(\n) end end end From 9e3a89cef3c6e3cdfd50b9eaa9901edaeb985b1e Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Wed, 30 Apr 2025 20:43:35 -0400 Subject: [PATCH 05/10] Fix docker compose file --- docker-compose.yml | 58 ++++++---------------------------------------- 1 file changed, 7 insertions(+), 51 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index cb4b8efb..afda8726 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,18 +4,16 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ ---- - version: "3" services: invidious: - image: quay.io/invidious/invidious:master - # image: quay.io/invidious/invidious:master-arm64 # ARM64/AArch64 devices + build: + context: . + dockerfile: docker/Dockerfile restart: unless-stopped - # Remove "127.0.0.1:" if used from an external IP ports: - - "127.0.0.1:9999:3000" + - "127.0.0.1:3000:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: @@ -28,63 +26,22 @@ services: host: invidious-db port: 5432 check_tables: true - invidious_companion: - # URL used for the internal communication between invidious and invidious companion - # There is no need to change that except if Invidious companion does not run on the same docker compose file. - - private_url: "http://companion:8282" - # (public) URL used for the communication between your browser and invidious companion - # IF you are using a reverse proxy OR accessing invidious from an external IP then you NEED to change this value - # Please consult for more doc: https://github.com/unixfox/invidious/blob/invidious-companion/config/config.example.yml#L57-L88 - # And make sure to add the routes from the post install when using a reverse proxy! - public_url: "http://localhost:8282" - # IT is NOT recommended to use the same key as HMAC KEY. Generate a new key! - # Use the key generated in the 2nd step - invidious_companion_key: "foo0te5naijooTho" # external_port: # domain: # https_only: false # statistics_enabled: false - # Use the key generated in the 2nd step - hmac_key: "foo0te5naijooTho" + hmac_key: "CHANGE_ME!!" healthcheck: test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1 interval: 30s timeout: 5s retries: 2 - logging: - options: - max-size: "1G" - max-file: "4" - depends_on: - - invidious-db - - companion: - image: quay.io/invidious/invidious-companion:latest - environment: - # Use the key generated in the 2nd step - - SERVER_SECRET_KEY=foo0te5naijooTho - restart: unless-stopped - # Remove "127.0.0.1:" if used from an external IP - ports: - - "127.0.0.1:8282:8282" - logging: - options: - max-size: "1G" - max-file: "4" - cap_drop: - - ALL - read_only: true - # cache for youtube library - volumes: - - companioncache:/var/tmp/youtubei.js:rw - security_opt: - - no-new-privileges:true invidious-db: image: docker.io/library/postgres:14 restart: unless-stopped volumes: - - postgresdata-companion:/var/lib/postgresql/data + - postgresdata:/var/lib/postgresql/data - ./config/sql:/config/sql - ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh environment: @@ -95,5 +52,4 @@ services: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] volumes: - postgresdata-companion: - companioncache: + postgresdata: From 15a397daee743992516fb541603bed1e26578279 Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Wed, 30 Apr 2025 20:44:55 -0400 Subject: [PATCH 06/10] Fix conflict bug --- src/invidious/views/feeds/history.ecr | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index e0c474b2..8552554d 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -33,17 +33,6 @@ -<<<<<<< HEAD -
      -
      " method="post"> - "> - -
      -
      -
      -
      -=======
      " method="post"> "> @@ -52,10 +41,8 @@
      -

      ->>>>>>> master <% end %>
      From 64a8561db8dfd887029a85327a66e6695eebebd9 Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Wed, 30 Apr 2025 23:50:59 -0400 Subject: [PATCH 07/10] Formatting --- src/invidious/frontend/comments_youtube.cr | 2 -- src/invidious/frontend/pagination.cr | 1 - src/invidious/routes/search.cr | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index 29686b1c..f9f50f04 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -80,7 +80,6 @@ module Invidious::Frontend::Comments
        END_HTML - if child["likeCount"]? html << <<-END_HTML
      • @@ -191,7 +190,6 @@ module Invidious::Frontend::Comments end end - html << <<-END_HTML #{replies_html}
      diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr index cf78c4d0..22c25a35 100644 --- a/src/invidious/frontend/pagination.cr +++ b/src/invidious/frontend/pagination.cr @@ -74,7 +74,6 @@ module Invidious::Frontend::Pagination str << %(\n) end - if show_next str << %(
    1. ) params_next = URI::Params{"page" => (current_page + 1).to_s} diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 5cd73dd7..c4f6391b 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -68,7 +68,7 @@ module Invidious::Routes::Search redirect_url = Invidious::Frontend::Misc.redirect_url(env) puts items.size - puts (items.size >= 20) + puts(items.size >= 20) puts query.page # Pagination From df6cebb4d013e6a346d14f1576bc5854e774979b Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Thu, 1 May 2025 01:22:08 -0400 Subject: [PATCH 08/10] Fix history page --- src/invidious/views/feeds/history.ecr | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 8552554d..ab9ebc1e 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -26,23 +26,23 @@
      - <% watched.each do |item| %> -
      -
      - - - + <% watched.each do |item| %> +
      +
      + + + -
      -
      " method="post"> - "> - -
      -
      -
      -
      +
      +
      " method="post"> + "> + +
      +
      +
      +
      <% end %>
      From 906d3f7c3f150271acb6caebf00670d34dc72f7c Mon Sep 17 00:00:00 2001 From: "Dave Lage (rockerBOO)" Date: Fri, 25 Nov 2022 02:28:51 -0500 Subject: [PATCH 09/10] update local ports for docker --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index afda8726..905dff50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ +--- + version: "3" services: @@ -11,9 +13,10 @@ services: build: context: . dockerfile: docker/Dockerfile + image: rockerboo/invidious restart: unless-stopped ports: - - "127.0.0.1:3000:3000" + - "127.0.0.1:9999:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: From b333b78ffe9e1360aa57c151cbc507095f9716a8 Mon Sep 17 00:00:00 2001 From: "Dave Lage (rockerBOO)" Date: Fri, 25 Nov 2022 02:28:51 -0500 Subject: [PATCH 10/10] update local ports for docker --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index afda8726..905dff50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ +--- + version: "3" services: @@ -11,9 +13,10 @@ services: build: context: . dockerfile: docker/Dockerfile + image: rockerboo/invidious restart: unless-stopped ports: - - "127.0.0.1:3000:3000" + - "127.0.0.1:9999:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: