diff --git a/DESIGN.html b/DESIGN.html new file mode 100644 index 000000000..15e9a47ba --- /dev/null +++ b/DESIGN.html @@ -0,0 +1,182 @@ + + + + + + Invidious UI refinement + + + +
+

Invidious UI refinement

+

Approved dark-theme reference for shared navigation, channel identity, contained video cards, and compact authentication. The source of truth for behaviour is DESIGN.md.

+ +

Dark theme tokens

+
+
Canvas#0b0c0e
+
Surface#15161a
+
Control#111216
+
Primary text#f4f4f5
+
Muted text#a9abb1
+
Boundary#303239
+
Accent / focus#83b5fc
+
Accent hover#b2d0ff
+
+
+

Light-theme pairings

+
+
Canvas#f7f8fa
+
Surface#ffffff
+
Control#fbfcfd
+
Primary text#17181b
+
Muted text#656a73
+
Boundary#d9dce2
+
Accent#255fbe
+
Accent hover#174a9f
+
+
+
+
8 px
12 px
16 px
24 px
+
+

Spacing: --redesign-space-1 8 px, --redesign-space-2 12 px, --redesign-space-3 16 px, --redesign-space-4 24 px. Card surfaces use an 8 px radius and the shared 1 px / 2 px edge shadow.

+ +

Wide search results

+
+
INVIDIOUSPreferences   Log in
+
+
+ +

Theo - t3.gg✓

Software development, AI, and TypeScript videos. Select the channel to browse its videos.

+

Theo Von

Search results

+
+
28:36

Stop Pretending You Understand Your Codebase

Theo - t3.gg

163K views

3 days ago Quick actions

+
49:22

You’re using AI agents wrong

Theo - t3.gg

163K views

3 days ago Quick actions

+
1:16:52

Fable Vs Astra Debate Is Over

Theo - t3.gg

163K views

3 days ago Quick actions

+
+
+
+ +

Account disclosure

+
♧
N

nr

Preferences

Light mode

Dark mode

Log out

+

Local initial avatar or generic person icon when the nickname is hidden. Native disclosure works without scripts. Actual notification and menu actions use the existing icon family.

+

Quiet login, CAPTCHA, and focus

+
+

Log in

+ + +
0:08:24
+ + +
+

Focus is a 3 px blue outline with a 3 px offset. Controls retain native semantics in the app; this page is visual evidence only.

+ +

Narrow composition

+
+
INVIDIOUS
+

Theo - t3.gg✓

Search results

28:36

Stop Pretending You Understand Your Codebase

Theo - t3.gg

163K views

3 days ago Quick actions

+
+

Search relationships and navigation

+

Channel matches appear together in a scrollable top row; the continuous media grid follows below. A channel never labels unrelated videos. Channel pages use horizontal content tabs and a compact sort row; account controls reserve space and search uses an icon disclosure below 600px.

+
Theo - t3.ggTheo VonTheo VRFS

Search results
Stop Pretending You Understand Your Codebase
Bishop Robert Barron
+

Watch composition and player behavior

+
+
+
▶

You're using AI agents wrong

▶12:35 / 49:211×▭⛶
Theo - t3.gg ✓
163K viewsAudio mode▶
Share

Invidious URL · YouTube URL

↓ Download

Existing formats and native download form.

More

Genre, license and remaining metadata.

+

Fable Vs Astra Debate Is Over

+
+
+

Theater mode is the default and expands the same video above the related rail. Below 900 px, related videos follow the main content. The visible seek track has its own row at every width, with theater beside fullscreen. Clip start/end markers remain functional but are visually hidden so the blue progress rail is the only position signal.

+

Player actions use existing Video.js icons and accessible names; time and speed remain text. With focus inside the player or on Play, J/L seek ten seconds, K and Space tap toggle playback, and Space or mouse holds temporarily play at 2× before restoring the prior rate and paused state. Enhancements remain optional.

+
+ + diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..16e0bd077 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,195 @@ +# Invidious UI refinement + +## Purpose + +The primary user is someone who wants to search and browse YouTube privately, +then sign in only when saved preferences or subscriptions need an account. The +interface is used in wide desktop browsing sessions and narrow touch sessions. +It remains server-rendered and usable with JavaScript disabled. + +The approved direction is the refined proposal: B's distinct channel treatment +and contained video cards, paired with a compact account form. Search results +should scan as media destinations, while a channel remains visibly different +from a video result. Existing search filters, saved home/feed preferences, +localization, thin mode, theme choice, authentication, CAPTCHA, and all +existing links/forms retain their behavior. + +## Tokens + +The implementation exposes the following values in `assets/css/redesign.css`. +They are semantic roles, not a new user preference. `.light-theme`, +`.dark-theme`, and the system-controlled `.no-theme` each assign the same +roles. Client-side theme changes must retain the structural `redesign-ui` body +class while replacing only the theme class. + +| Role | Light | Dark | Use | +| --- | --- | --- | --- | +| `--redesign-canvas` | `#f7f8fa` | `#0b0c0e` | Page background | +| `--redesign-surface` | `#ffffff` | `#15161a` | Media cards | +| `--redesign-control` | `#fbfcfd` | `#111216` | Search and text inputs | +| `--redesign-text` | `#17181b` | `#f4f4f5` | Primary text and titles | +| `--redesign-muted` | `#656a73` | `#a9abb1` | Metadata and quiet navigation | +| `--redesign-line` | `#d9dce2` | `#303239` | Stable boundaries | +| `--redesign-accent` | `#255fbe` | `#83b5fc` | Links and primary submit action | +| `--redesign-accent-hover` | `#174a9f` | `#b2d0ff` | Link and submit hover/focus state | +| `--redesign-on-accent` | `#ffffff` | `#121315` | Submit-action foreground | +| `--redesign-focus` | `#0b6ece` | `#83b5fc` | 3 px keyboard focus outline | +| `--redesign-radius` | `8px` | `8px` | Results and form surfaces | +| `--redesign-control-radius` | `6px` | `6px` | Inputs and compact controls | +| `--redesign-shadow` | `0 1px 2px rgb(15 23 42 / 0.08)` | `0 1px 2px rgb(0 0 0 / 0.38)` | Card edge separation only | + +The typeface remains the project’s existing system stack. Results use 16 px, +650-weight titles at a 1.32 line-height; channel names use 22 px at 1.3; +metadata uses 13–14 px and stays readable in both themes. The spacing scale is +`--redesign-space-1` = 8 px, `--redesign-space-2` = 12 px, +`--redesign-space-3` = 16 px, and `--redesign-space-4` = 24 px. + +## Layout and components + +- The navigation bar has a 64 px minimum height, a border separating it from + content, and a single clear search field. Search stays in the header row; below 600px a native icon disclosure opens the field. +- Search channel matches occupy their own horizontally scrollable row above + the continuous media grid. Each match keeps its avatar, linked identity, + metadata and description. Channels never act as headings for unrelated + video results; selecting a channel follows its existing channel link. +- Videos and playlists are contained cards with a 16:9 thumbnail, then title, + author and metadata. They use two columns from 768 px and three from 1100 px; + under 768 px each card is full width. The thumbnail’s duration and existing + action controls remain in their established positions. +- The login form is at most 352 px wide, sits directly on the canvas without an outer card, has persistent labels, native inputs, and + one full-width submit action. Existing server validation and CAPTCHA + rendering remain in the same form flow. The heading-to-field gap is 24 px; the page fills the viewport and its footer follows the flexible main area near the bottom. + +There are only two raised surface types: media cards and the expanded +filter panel. Each has a one-pixel boundary plus the same restrained shadow; +the page, navigation, and footer remain on the base plane. + +The watch-layout layer consumes the same theme through `--watch-border`, +`--watch-panel`, `--watch-muted`, and `--watch-focus` aliases. It owns the +watch composition and player controls; the shared stylesheet only supplies the +common color roles. + +## Access and adaptation + +All existing links, buttons, checkbox/radio controls, labels, forms, and +server-side errors retain their native semantics. A skip link targets the main +content. Keyboard focus uses the `--redesign-focus` outline with a 3 px offset; +focus is never indicated by color alone. Controls remain at least 36–44 px in +their compact contexts, and the account submit/input controls are 44 px high. + +Long translated titles and channel names wrap; no title or metadata is clamped. +The dark, light, and system theme roles are explicit. Reduced-motion users do +not receive new animated effects. Thin mode retains its placeholders and does +not request thumbnails. + +## Evidence and open questions + +`DESIGN.html` renders the token values, wide/narrow card compositions, focus, +the channel distinction, and login/CAPTCHA state using the same rules. The +application build and browser checks are recorded by the coordinating agent. +There are no unresolved visual decisions in this approved scope. + +## Watch page and optional player enhancements + +The accepted A watch composition places the player beside a compact related +rail, with the title, channel, compact action row, description and comments +beneath the player. The native theater checkbox expands the same video across +the grid; it preserves playback and also works without scripts. Below 900 px, +related videos follow the main content. All stream/source and download links +retain their existing behavior. + +Player actions retain Video.js's familiar icons and accessible names. Time and +speed remain text. All players put a visible 4px seek track on its own row above icon controls; the duplicate remaining-time display is hidden to conserve space. +Only while focus is inside the player, J/L seek exactly ten seconds; K and a Space tap toggle playback. Holding Space +or the left mouse button on the video for 350 ms temporarily plays at exactly +2×, then restores the prior speed and paused state. Editable fields, menus, +controls and modified shortcuts are excluded. Holds never save a temporary +speed preference. Blur/cancel restores state. The post-hold mouseup and click +are consumed because Video.js toggles playback on mouseup. + +## Runtime verification criterion + +The actual application must retain the design after scripts initialize and +when themes change. Verify rendered desktop and narrow login/search/watch +pages, real media playback, hold release, theater continuity, and the native +no-JavaScript flow. Static screenshots and mocked gesture tests alone cannot +prove these behaviors. This is project guidance proposed by the user's runtime +correction; it does not modify shared Fleet guidance. + +## Channel and feed navigation refinement + +Channel content tabs and sort links form a horizontal toolbar; utility links +remain separate and quiet. Subscribe and RSS retain their existing actions. +The signed-in header reserves intrinsic width for account controls, uses native +logout button semantics. Search stays in the header and becomes an icon below 600px. +Subscription feeds have a clear title, management/history/RSS actions and an +empty state linking to subscription management. Saved feed choices remain +unchanged. On narrow screens controls wrap rather than collide. + +The search results heading and top pagination share one row, with equal vertical +centers and outer edges aligned to the media cards. Pagination wraps within its +available space on narrow screens; card and channel geometry remain unchanged. + +## Account disclosure and card metadata + +Signed-in navigation shows the existing notification bell and a circular local +avatar. An initial is displayed only when show_nick is enabled; otherwise a +generic person icon preserves that privacy preference. A native details/summary +disclosure exposes the name, Preferences and CSRF-protected POST +logout. It works without JavaScript; optional script adds Escape, outside-click +and focus-out dismissal. No remote profile image or avatar service is used. + +Video cards place title first, channel and views on the second row, date and +existing quick actions on the third. Existing verified badges use bright #3ea6ff in dark mode and #0878d1 in light mode, a .2em inline gap and -.08em baseline adjustment; no verification statuses are added or inferred. + +Header avatars are 32px within 40px hit areas; the bell is approximately 22px. +Filter disclosure has horizontal padding, and card action links center their +glyphs alongside the date text. Theme choices set the requested mode explicitly +on both client and server; repeated selection never reverses the mode. + +Theater mode sits beside fullscreen in the enhanced player and has an explicit +text label in the no-JavaScript fallback. Storyboard URLs preserve root-relative +paths; previews show one timestamp and clamp to player edges. The timeline has +a neutral track, lighter buffered range and bright blue progress. + +Watch actions share a compact row: view/like counts on the left; Share, Download and More on the right. More holds audio-only mode, source/embed/instance links, annotations and metadata. Native disclosures retain download +formats, playlist forms and technical metadata, wrapping on narrow screens. +Subscribe is red (#d92332) with white text; subscribed state stays neutral. Channel verification marks stay bright blue; only comment verification is white. All keyboard handlers require focus +inside the player; search, page actions and outside clicks release shortcuts. + +Player follow-up: the large play control is centered. Play-button focus accepts +shortcuts; external inputs remain excluded. Fullscreen clears fluid aspect-ratio +padding, captions reserve 68px above visible controls, and caption menus use +readable padded rows. Share is only below the video, with Invidious/YouTube URL +choices and optional clipboard buttons; native links work without JavaScript. +Action menus dismiss on outside click or Escape, and only one sibling stays +open. Header bottom padding is 4px with no divider. Related titles use primary +text, with muted channel/views and consistent thumbnail geometry. + +Theater is the default watch layout, including without JavaScript; the player +button returns to the split layout. Light and Dark icon controls remain visible beside the avatar; Preferences stays inside its menu. + +Final refinement: Audio mode and Watch on YouTube are quick actions alongside +Share, Download and More. Instance/embed/annotation links are hidden; More holds +remaining metadata. Menus have bounded scrollable height. Comment avatars are +36px with consistent author/body/metadata/reply spacing. Cards align metadata +rows and avoid inherited paragraph margins. Player corners are 12px, with a +12px gap below navigation. Light mode strengthens muted text and surface borders. + +Embedded-fullscreen recovery: when the browser leaves native fullscreen pending +for 700ms, reuse Video.js full-window mode; F/button and Escape exit preserve +the same player and video. Regular browsers continue using native fullscreen. + +The watch title tops out at 25px to retain hierarchy without competing with the +player. Watch on YouTube is a 36px icon action with a localized accessible name +and native tooltip. Start/end clip markers continue controlling playback but are +visually hidden because the blue progress rail already communicates position. +The unused Video.js share plugin is excluded; sharing remains in the watch action +row through native links and the optional copy enhancement. + +The watch title tops out at 25px to retain hierarchy without competing with the +player. Watch on YouTube is a 36px icon action with a localized accessible name +and native tooltip. Start/end clip markers continue controlling playback but are +visually hidden because the blue progress rail already communicates position. +The unused Video.js share plugin is excluded; sharing remains in the watch action +row through native links and the optional copy enhancement. diff --git a/assets/css/player.css b/assets/css/player.css index d95549ac7..828372c83 100644 --- a/assets/css/player.css +++ b/assets/css/player.css @@ -116,10 +116,6 @@ ul.vjs-menu-content::-webkit-scrollbar { order: 5; } -.vjs-share-control { - order: 6; -} - .vjs-fullscreen-control { order: 7; } @@ -253,14 +249,139 @@ video.video-js { 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; } } + +/* Familiar icon controls, with readable time/speed and native accessible labels. */ +.video-js .vjs-control-bar { + background: #101114; + min-height: 3.6em; +} +.video-js .vjs-control:focus-visible, +.video-js .vjs-menu-item:focus-visible { + outline: 2px solid #9ac2fc; + outline-offset: -2px; +} +.video-js .vjs-play-progress, +.video-js.player-style-invidious .vjs-play-progress { + background: #9ac2fc; +} +.video-js .vjs-big-play-button { + background: rgba(8, 9, 11, .9); + border: 0; + border-radius: 8px; +} +.video-js .vjs-poster { + background-size: contain; + object-fit: contain; +} +.video-js .speed-hold-indicator { + display: none; + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 6px 14px; + border-radius: 5px; + background: rgba(8, 9, 11, .9); + color: #fff; + font: 600 16px/1.4 system-ui, sans-serif; + pointer-events: none; +} +.video-js.vjs-speed-hold .speed-hold-indicator { display: block; } + +/* One visible timeline above a consistent icon row in both player styles. */ +.video-js .vjs-control-bar, +.video-js.player-style-youtube .vjs-control-bar { + height: 5.8em; min-height: 5.8em; padding: 0 8px; + flex-wrap: wrap; align-items: center; background: rgba(12, 13, 16, .96); +} +.video-js .vjs-progress-control, +.video-js.player-style-youtube .vjs-progress-control { + position: relative; inset: auto; flex: 1 0 100%; order: -1; + width: 100%; height: 1.8em; min-height: 1.8em; margin: 0; font-size: 10px; +} +.video-js .vjs-progress-control .vjs-progress-holder, +.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder { + position: relative; inset: auto; width: auto; margin: 0 4px; height: 4px; + font-size: 10px; background: #62646a; border-radius: 2px; +} +.video-js .vjs-load-progress, .video-js .vjs-load-progress div { background: #a2a4aa; } +.video-js .vjs-play-progress, .video-js.player-style-youtube .vjs-play-progress { background: #3ea6ff; } +.video-js .vjs-play-progress::before, .video-js.player-style-youtube .vjs-play-progress::before { + content: ''; display: block; position: absolute; right: -5px; top: -3px; + width: 10px; height: 10px; background: #fff; border-radius: 50%; +} +.video-js .vjs-marker { display: none !important; } +.video-js .vjs-play-progress .vjs-time-tooltip { display: none; } +.video-js .vjs-vtt-thumbnail-display { + bottom: calc(100% + 24px); left: 0; border-radius: 4px; + background-color: #17181c; transition: none; +} +.video-js .vjs-vtt-thumbnail-display::after { + content: attr(data-time); position: absolute; top: 100%; left: 0; right: 0; + padding: 4px; color: white; background: #17181c; text-align: center; + font: 12px/16px system-ui, sans-serif; border-radius: 0 0 4px 4px; +} +.video-js .vjs-control-bar > .vjs-control:not(.vjs-progress-control) { height: 4em; } +.video-js .vjs-control-bar > .vjs-button, .video-js .vjs-control-bar > .vjs-menu-button { width: 3.6em; flex-shrink: 0; } +.video-js .vjs-button > .vjs-icon-placeholder::before { line-height: 2.22; } +.video-js .vjs-time-control { line-height: 4em; padding-inline: .35em; } +.video-js .vjs-playback-rate-value { font-size: 1.3em; line-height: 3.08; } +.video-js .vjs-remaining-time { display: none; } +.video-js .vjs-control-bar > .vjs-spacer { display: block; flex: 1; order: 1; } +.video-js .vjs-theater-control { order: 6; } +.video-js .vjs-theater-control .vjs-icon-placeholder { + display: block; width: 18px; height: 12px; border: 2px solid currentColor; + border-radius: 2px; margin: auto; box-sizing: border-box; +} +.video-js .vjs-theater-control[aria-pressed="true"] .vjs-icon-placeholder { border-bottom-width: 4px; } +@media screen and (max-width: 600px) { + .video-js .vjs-control-bar > .vjs-button, .video-js .vjs-control-bar > .vjs-menu-button { width: 2.8em; } + .video-js .vjs-control-bar { padding-inline: 4px; } + .video-js .vjs-volume-panel { width: 2.8em; } + .video-js .vjs-volume-panel .vjs-volume-control { display: none; } +} + +@media screen and (max-width: 600px) { + .video-js .vjs-time-control { font-size: 10px; min-width: 0; } +} + +.video-js .vjs-big-play-button, .video-js.player-style-youtube .vjs-big-play-button { + top: 50%; left: 50%; margin: 0; transform: translate(-50%, -50%); + width: 2.4em; height: 1.6em; line-height: 1.6em; +} + +/* Captions reserve space for the visible two-row control bar. */ +.video-js.vjs-user-active .vjs-text-track-display, +.video-js.vjs-paused .vjs-text-track-display { bottom: 68px; } +.video-js .vjs-text-track-display > div > div > div { + padding: 2px 5px !important; border-radius: 3px !important; line-height: 1.3 !important; +} +.video-js .vjs-menu-button-popup .vjs-menu, +.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu { + width: min(240px, 72vw); left: auto; right: 0; bottom: 4em; margin: 0; padding: 0; +} +.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-content { + box-sizing: border-box; padding: 6px; max-height: min(240px, 35vh); + background: #17191e; color: #f4f4f5; border: 1px solid #3d414b; + border-radius: 6px; box-shadow: 0 6px 18px #0005; +} +.video-js .vjs-menu li { font-size: 13px; line-height: 1.4; padding: 8px 10px; text-align: left; text-transform: none; border-radius: 3px; } +.video-js .vjs-menu li.vjs-menu-item:hover, .video-js .vjs-menu li.vjs-menu-item:focus { background: #2b303a; color: #fff; text-shadow: none; } +.video-js .vjs-menu li.vjs-selected { background: #263a54; color: #a8d1ff; } +.video-js .vjs-text-track-settings { color: #f4f4f5; background: #17191e; } +.video-js .vjs-text-track-settings .vjs-modal-dialog-content { padding: 20px; overflow: auto; } +.video-js .vjs-text-track-settings fieldset { margin-bottom: 12px; } +.video-js .vjs-text-track-settings select { color: white; background: #22262e; border: 1px solid #535965; border-radius: 4px; padding: 5px; } +.video-js .vjs-text-track-settings button { border-radius: 4px; padding: 6px 12px; } +.watch-page .video-js.vjs-fullscreen, .watch-page .player-dimensions.vjs-fluid.vjs-fullscreen { + padding-top: 0 !important; height: 100% !important; width: 100% !important; max-height: none; +} + +@media screen and (max-width: 600px) { + .video-js .vjs-captions-button .vjs-menu { width: min(220px, 52vw); } +} diff --git a/assets/css/redesign.css b/assets/css/redesign.css new file mode 100644 index 000000000..96081418c --- /dev/null +++ b/assets/css/redesign.css @@ -0,0 +1,912 @@ +/* + * Approved UI refinement + * + * The variables below describe the shared media-browser treatment. They are + * deliberately scoped to `.redesign-ui` so the established theme preference + * continues to control the appearance rather than being replaced by a new + * theme setting. + */ + +body.redesign-ui { + --redesign-canvas: #f7f8fa; + --redesign-surface: #ffffff; + --redesign-control: #fbfcfd; + --redesign-text: #17181b; + --redesign-muted: #656a73; + --redesign-line: #d9dce2; + --redesign-accent: #255fbe; + --redesign-accent-hover: #174a9f; + --redesign-on-accent: #ffffff; + --redesign-focus: #0b6ece; + --redesign-radius: 8px; + --redesign-control-radius: 6px; + --redesign-space-1: 0.5rem; + --redesign-space-2: 0.75rem; + --redesign-space-3: 1rem; + --redesign-space-4: 1.5rem; + --redesign-shadow: 0 1px 2px rgb(15 23 42 / 0.08); + + background: var(--redesign-canvas); + color: var(--redesign-text); +} + +.redesign-ui.dark-theme { + --redesign-canvas: #0b0c0e; + --redesign-surface: #15161a; + --redesign-control: #111216; + --redesign-text: #f4f4f5; + --redesign-muted: #a9abb1; + --redesign-line: #303239; + --redesign-accent: #83b5fc; + --redesign-accent-hover: #b2d0ff; + --redesign-on-accent: #121315; + --redesign-focus: #83b5fc; + --redesign-shadow: 0 1px 2px rgb(0 0 0 / 0.38); +} + +@media (prefers-color-scheme: dark) { + .redesign-ui.no-theme { + --redesign-canvas: #0b0c0e; + --redesign-surface: #15161a; + --redesign-control: #111216; + --redesign-text: #f4f4f5; + --redesign-muted: #a9abb1; + --redesign-line: #303239; + --redesign-accent: #83b5fc; + --redesign-accent-hover: #b2d0ff; + --redesign-on-accent: #121315; + --redesign-focus: #83b5fc; + --redesign-shadow: 0 1px 2px rgb(0 0 0 / 0.38); + } +} + +.redesign-ui a, +.redesign-ui .simulated_a { + color: var(--redesign-accent); +} + +.redesign-ui a:hover, +.redesign-ui a:active, +.redesign-ui .simulated_a:hover, +.redesign-ui .simulated_a:active { + color: var(--redesign-accent-hover) !important; +} + +.redesign-ui :focus-visible { + outline: 3px solid var(--redesign-focus); + outline-offset: 3px; +} + +.redesign-skip-link { + background: var(--redesign-surface); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-control-radius); + color: var(--redesign-accent); + left: var(--redesign-space-3); + padding: var(--redesign-space-2) var(--redesign-space-3); + position: fixed; + top: var(--redesign-space-3); + transform: translateY(-200%); + z-index: 1000; +} + +.redesign-skip-link:focus { + transform: translateY(0); +} + +.redesign-main { + min-width: 0; +} + +/* `watch-layout.css` owns the watch composition. These aliases let that + * layout consume the shared theme roles without introducing a competing + * player palette. */ +.redesign-ui .watch-page { + --watch-border: var(--redesign-line); + --watch-panel: var(--redesign-surface); + --watch-muted: var(--redesign-muted); + --watch-focus: var(--redesign-focus); +} + +.redesign-ui .redesign-navbar { + align-items: center; + border-bottom: 1px solid var(--redesign-line); + margin: 0; + min-height: 4rem; + padding-bottom: var(--redesign-space-2); + padding-top: var(--redesign-space-2); +} + +.redesign-ui .redesign-navbar .index-link { + color: var(--redesign-text); + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.redesign-ui .redesign-navbar .searchbar .pure-form { + align-items: center; + background: var(--redesign-control); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-control-radius); + min-height: 2.625rem; + padding: 0 0.25rem 0 0.625rem; +} + +.redesign-ui .redesign-navbar .searchbar input[type="search"] { + background: transparent; + border: 0; + border-radius: 0; + color: var(--redesign-text); + font: inherit; + margin: 0; + min-height: 2.375rem; + padding: 0.5rem 0; +} + +.redesign-ui .redesign-navbar .searchbar input[type="search"]:focus { + border: 0; + margin: 0; +} + +.redesign-ui .redesign-navbar #searchbutton { + border-radius: var(--redesign-control-radius); + color: var(--redesign-muted); + min-height: 2.25rem; + min-width: 2.25rem; +} + +.redesign-ui .redesign-navbar #searchbutton:hover, +.redesign-ui .redesign-navbar #searchbutton:focus-visible { + color: var(--redesign-accent) !important; +} + +.redesign-ui .redesign-navbar .user-field { + color: var(--redesign-muted); + gap: var(--redesign-space-3); +} + +.redesign-ui .redesign-navbar .user-field div:not(:last-child) { + margin-right: 0; +} + +.redesign-ui .redesign-navbar .user-field a, +.redesign-ui .redesign-navbar .user-field input[type="submit"] { + color: var(--redesign-muted); + font-size: 0.9375rem; +} + +.redesign-ui .redesign-banner h3 { + border-bottom: 1px solid var(--redesign-line); + font-size: 1rem; + margin: var(--redesign-space-3) 0 0; + padding-bottom: var(--redesign-space-3); +} + +/* Search filters retain their form and native controls while using the shared + * page rhythm and theme roles. */ +.redesign-search-page { + padding: var(--redesign-space-3) 0 var(--redesign-space-4); +} + +.redesign-search-page > hr { + border: 0; + border-top: 1px solid var(--redesign-line); + margin: 0 var(--redesign-space-3) var(--redesign-space-4); +} + +.redesign-search-page #filters-collapse summary { + color: var(--redesign-text); + font-size: 1rem; + font-weight: 600; + margin: 0 var(--redesign-space-3) var(--redesign-space-2); + padding: var(--redesign-space-2) 0; +} + +.redesign-search-page #filters-collapse summary::before { + background: var(--redesign-surface); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-control-radius); + color: var(--redesign-muted); + font-weight: 400; +} + +.redesign-search-page #filters-box { + background: var(--redesign-surface); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-radius); + box-shadow: var(--redesign-shadow); + margin: var(--redesign-space-2) var(--redesign-space-3) var(--redesign-space-4); + padding: var(--redesign-space-2) var(--redesign-space-3) var(--redesign-space-3); +} + +.redesign-search-page .filter-name { + border-bottom-color: var(--redesign-line); + color: var(--redesign-text); +} + +.redesign-search-page .filter-options, +.redesign-search-page .filter-options label { + color: var(--redesign-muted); +} + +.redesign-search-page .no-results-error { + background: var(--redesign-surface); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-radius); + box-shadow: var(--redesign-shadow); + margin: var(--redesign-space-4) auto; + max-width: 42rem; + padding-bottom: 4rem; +} + +/* Result cards */ +.redesign-item { + box-sizing: border-box; + padding: var(--redesign-space-2); +} + +.redesign-item-card { + background: var(--redesign-surface); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-radius); + box-shadow: var(--redesign-shadow); + box-sizing: border-box; + color: var(--redesign-text); +} + +.redesign-video-result .redesign-item-card, +.redesign-collection-result .redesign-item-card { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.redesign-video-result .thumbnail, +.redesign-collection-result .thumbnail { + background: var(--redesign-control); + border-radius: 0; + flex: 0 0 auto; +} + +.redesign-video-result .thumbnail-placeholder, +.redesign-collection-result .thumbnail-placeholder { + background: var(--redesign-control); + min-height: 8rem; +} + +.redesign-ui .redesign-video-result .length, +.redesign-ui .redesign-collection-result .length { + background: rgb(8 8 8 / 0.9) !important; + color: #ffffff; + font-size: 0.8125rem; + font-weight: 600; + padding: 0.125rem 0.3125rem; +} + +.redesign-video-result .video-card-row, +.redesign-collection-result .video-card-row { + margin: 0; + padding-left: var(--redesign-space-3); + padding-right: var(--redesign-space-3); +} + +.redesign-video-result .redesign-item-title, +.redesign-collection-result .redesign-item-title { + padding-top: var(--redesign-space-3); +} + +.redesign-item-title p { + color: var(--redesign-text); + font-size: 1rem; + font-weight: 650; + line-height: 1.32; + margin: 0; +} + +.redesign-item-title a { + color: var(--redesign-text); + text-decoration: none; +} + +.redesign-item-title a:hover, +.redesign-item-title a:focus-visible { + color: var(--redesign-accent) !important; + text-decoration: underline; +} + +.redesign-video-result .redesign-item-author, +.redesign-collection-result .redesign-item-author { + margin-top: var(--redesign-space-1); +} + +.redesign-video-result .channel-name, +.redesign-collection-result .channel-name { + color: var(--redesign-muted); + font-size: 0.875rem; +} + +.redesign-video-result .redesign-item-meta, +.redesign-collection-result .redesign-item-meta { + color: var(--redesign-muted); + margin-top: var(--redesign-space-1); + padding-bottom: var(--redesign-space-3); +} + +.redesign-video-result .video-data, +.redesign-collection-result .video-data { + color: var(--redesign-muted); + font-size: 0.8125rem; + font-weight: 400; +} + +.redesign-video-result .icon-buttons, +.redesign-collection-result .icon-buttons { + display: flex; + gap: var(--redesign-space-2); +} + +.redesign-video-result .icon-buttons a, +.redesign-collection-result .icon-buttons a { + color: var(--redesign-muted); + min-height: 2rem; + min-width: 2rem; + text-align: center; +} + +/* A channel carries a different relationship from a video. It spans the + * results grid and uses its own compact identity block instead of another + * media card. */ +.redesign-channel-result { + width: 100%; +} + +.redesign-channel-result .redesign-item-card { + padding: var(--redesign-space-4); +} + +.redesign-channel-result__identity { + align-items: center; + display: flex; + gap: 1.25rem; +} + +.redesign-channel-result__avatar-link { + display: block; + flex: 0 0 auto; +} + +.redesign-channel-result__avatar { + background: var(--redesign-control); + border-radius: 50%; + display: block; + height: 4.5rem; + object-fit: cover; + width: 4.5rem; +} + +.redesign-channel-result__body { + min-width: 0; +} + +.redesign-channel-result__name { + color: var(--redesign-text); + font-size: 1.375rem; + font-weight: 650; + line-height: 1.3; + margin: 0; +} + +.redesign-channel-result__metadata { + color: var(--redesign-muted); + display: flex; + flex-wrap: wrap; + font-size: 0.875rem; + gap: 0.25rem 0.5rem; + margin-top: 0.25rem; +} + +.redesign-channel-result__metadata > span:not(:first-child)::before { + content: "·"; + margin-right: 0.5rem; +} + +.redesign-channel-result__description { + color: var(--redesign-muted); + font-size: 0.9375rem; + line-height: 1.45; + margin-top: var(--redesign-space-2); + max-width: 46rem; +} + +.redesign-channel-result__description > :first-child { + margin-top: 0; +} + +.redesign-channel-result__description > :last-child { + margin-bottom: 0; +} + +.redesign-special-result .redesign-item-card { + padding: var(--redesign-space-3); +} + +/* Categories have no rendered card content in the existing item template. */ +.redesign-category-result { + display: none; +} + +/* Compact authentication form. Server-side validation and CAPTCHA fields use + * the same native form controls as the ordinary sign-in path. */ +.redesign-ui #contents:has(.redesign-login) { + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100svh; +} + +.redesign-main:has(.redesign-login) { + display: grid; + align-items: center; + flex: 1; +} + +.redesign-login { + justify-content: center; + padding: 3rem 1.5rem; +} + +.redesign-login__content { + padding: 0; +} + +.redesign-login-form { + box-sizing: border-box; + margin: 0 auto; + max-width: 22rem; + padding: 0; +} + +.redesign-login-form h1 { + color: var(--redesign-text); + font-size: 2rem; + letter-spacing: -0.025em; + line-height: 1.25; + margin: 0 0 1.5rem; +} + +.redesign-login-form fieldset { padding: 0; } +.redesign-login-form fieldset > label:first-child { margin-top: 0; } + +.redesign-login-form label { + color: var(--redesign-text); + font-size: 0.875rem; + font-weight: 500; + margin: 1rem 0 0.375rem; +} + +.redesign-login-form input[type="text"], +.redesign-login-form input[type="password"] { + background: var(--redesign-control); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-control-radius); + box-shadow: none; + color: var(--redesign-text); + min-height: 2.75rem; + padding: 0.625rem 0.75rem; +} + +.redesign-login-form input[type="text"]:focus, +.redesign-login-form input[type="password"]:focus { + border-color: var(--redesign-focus); + box-shadow: 0 0 0 1px var(--redesign-focus); +} + +.redesign-login-form__captcha { + background: var(--redesign-control); + border: 1px solid var(--redesign-line); + border-radius: var(--redesign-control-radius); + display: block; + margin: 1.25rem 0 0.5rem; + max-width: 100%; + width: 50%; +} + +.redesign-ui .redesign-login-form .pure-button-primary { + background: var(--redesign-accent); + border: 1px solid var(--redesign-accent); + border-radius: var(--redesign-control-radius); + color: var(--redesign-on-accent); + display: block; + font-weight: 650; + margin: 1.5rem 0 0; + min-height: 2.75rem; + padding: 0.625rem 0.75rem; + width: 100%; +} + +.redesign-ui .redesign-login-form .pure-button-primary:hover, +.redesign-ui .redesign-login-form .pure-button-primary:focus-visible { + background: var(--redesign-accent-hover) !important; + border-color: var(--redesign-accent-hover) !important; + color: var(--redesign-on-accent) !important; +} + +.redesign-footer { + border-top: 1px solid var(--redesign-line); + color: var(--redesign-muted) !important; + font-size: 0.8125rem; + margin-top: 2rem; +} + +.redesign-footer a { + color: var(--redesign-muted) !important; +} + +@media screen and (min-width: 768px) { + .redesign-video-result, + .redesign-collection-result { + width: 50%; + } +} + +@media screen and (min-width: 1100px) { + .redesign-video-result, + .redesign-collection-result { + width: 33.3333%; + } +} + +@media screen and (max-width: 767px) { + .redesign-ui .redesign-navbar { + align-items: stretch; + gap: var(--redesign-space-2); + padding-bottom: var(--redesign-space-3); + padding-top: var(--redesign-space-3); + } + + .redesign-ui .redesign-navbar > div { + margin-bottom: 0; + } + + .redesign-ui .redesign-navbar > .searchbar { + order: 3; + } + + .redesign-ui .redesign-navbar > .searchbar > form { + width: 100%; + } + + .redesign-ui .redesign-navbar .user-field { + font-size: 1rem; + justify-content: flex-end; + } + + .redesign-item { + padding: var(--redesign-space-2) var(--redesign-space-3); + } + + .redesign-channel-result .redesign-item-card { + padding: var(--redesign-space-3); + } + + .redesign-channel-result__identity { + align-items: flex-start; + gap: var(--redesign-space-3); + } + + .redesign-channel-result__avatar { + height: 3.5rem; + width: 3.5rem; + } + + .redesign-channel-result__name { + font-size: 1.1875rem; + } + + .redesign-channel-result__metadata > span::before { + display: none; + } + + .redesign-channel-result__metadata > span { + flex-basis: 100%; + } + + .redesign-login { + padding: 2rem var(--redesign-space-3); + } + + .redesign-login-form { + padding: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .redesign-ui *, + .redesign-ui *::before, + .redesign-ui *::after { + scroll-behavior: auto !important; + } +} + +/* Search matches are separate destinations, never headings for unrelated videos. */ +.redesign-channel-matches, +.redesign-media-matches { margin: 0 0 1.5rem; } +.redesign-channel-matches h2, +.redesign-media-matches h2 { font-size: 1.125rem; margin: 1rem 1rem .5rem; } +.redesign-channel-rail { + display: flex; + overflow-x: auto; + gap: .75rem; + padding: .5rem 1rem 1rem; +} +.redesign-channel-rail .redesign-channel-result { + flex: 0 0 19rem; + padding: 0; + width: 19rem; +} +.redesign-channel-rail .redesign-item-card { padding: 1rem; height: 100%; } +.redesign-channel-rail .redesign-channel-result__identity { gap: .75rem; align-items: flex-start; } +.redesign-channel-rail .redesign-channel-result__avatar { width: 3rem; height: 3rem; } +.redesign-channel-rail .redesign-channel-result__name { font-size: 1rem; } +.redesign-channel-rail .redesign-channel-result__metadata { font-size: .75rem; } +.redesign-channel-rail .redesign-channel-result__metadata > span { flex-basis: 100%; } +.redesign-channel-rail .redesign-channel-result__metadata > span::before { display: none; } +.redesign-channel-rail .redesign-channel-result__description { font-size: .75rem; line-height: 1.4; margin-top: .5rem; } +.redesign-media-matches:has(> h2) { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; +} +.redesign-media-matches > h2 { + grid-area: 1 / 1; + margin: 0 0 .5rem .75rem; +} +.redesign-media-matches > div { grid-column: 1 / -1; } +.redesign-media-matches:has(> h2) > .h-box:first-of-type { + grid-area: 1 / 2; + padding-inline: 1rem .75rem; + margin-bottom: .5rem; +} +.redesign-ui .redesign-media-matches:has(> h2) > .h-box:first-of-type .page-nav-container { + justify-content: flex-end; + flex-wrap: wrap; + gap: .5rem; + margin: 0; + padding: 0; +} +.redesign-media-matches:has(> h2) > .h-box:first-of-type .page-prev-container:empty { display: none; } +.redesign-media-matches:has(> h2) > .h-box:first-of-type .pure-button { margin: 0; } +@media screen and (max-width: 767px) { + .redesign-media-matches > h2 { margin-inline-start: 1rem; } + .redesign-media-matches:has(> h2) > .h-box:first-of-type { padding-inline-end: 1rem; } +} +.redesign-ui .page-nav-container { padding: 0 1rem; margin: .5rem 0 1rem; } +.redesign-ui .redesign-video-result .redesign-item-card, +.redesign-ui .redesign-collection-result .redesign-item-card { padding-inline: 0; } + +/* The header must reserve space for the signed-in actions before sizing search. */ +.redesign-ui .redesign-navbar { display: grid; grid-template-columns: minmax(7rem,1fr) minmax(14rem,30rem) auto; gap: 1.25rem; } +.redesign-ui .redesign-navbar > div { width: auto; margin: 0; min-width: 0; } +.redesign-ui .redesign-navbar .searchbar { flex: none; } +.redesign-ui .redesign-navbar .searchbar > form { width: 100%; box-sizing: border-box; } +.redesign-ui .redesign-navbar .user-field { gap: .25rem; flex: none; flex-wrap: wrap; } +.redesign-ui .redesign-navbar .user-field > div { width: auto; flex: none; } +.redesign-ui .redesign-navbar .user-field a, +.redesign-ui .redesign-navbar .redesign-signout { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + min-height: 2.5rem; + padding: .5rem; + text-transform: none; + line-height: 1.25; +} +.redesign-ui .redesign-navbar .user-field .icon { font-size: 1.2rem; } +.redesign-ui .redesign-signout { background: transparent; border: 0; color: var(--redesign-muted); font: inherit; cursor: pointer; } +.redesign-ui .redesign-navbar #user_name { display: block; max-width: 7rem; overflow: hidden; text-overflow: ellipsis; padding: 0 .5rem; font-size: .875rem; } + +.redesign-channel-page-header { padding-block: 1.5rem 1rem; } +.redesign-channel-page-header .title { margin: 0; gap: 1rem; } +.redesign-channel-page-header .title > .flex-left { flex: 1; min-width: 0; width: auto; } +.redesign-channel-page-header .title > .flex-right { flex: none; width: auto; } +.redesign-channel-page-header .channel-profile { display: flex; align-items: center; gap: .875rem; } +.redesign-channel-page-header .channel-profile img { width: 4rem; height: 4rem; } +.redesign-channel-page-header .channel-name { font-size: 1.5rem; line-height: 1.25; } +.redesign-channel-page-header .button-container { gap: .5rem; } +.redesign-channel-page-header #descriptionWrapper { max-width: 72ch; font-size: .9375rem; line-height: 1.5; color: var(--redesign-muted); } +.redesign-channel-links { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; font-size: .8125rem; } +.redesign-channel-links a { color: var(--redesign-muted); } +.redesign-channel-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .5rem 1.5rem; border-bottom: 1px solid var(--redesign-line); margin-bottom: .75rem; } +.redesign-channel-tabs { display: flex; flex-wrap: wrap; gap: 1rem; } +.redesign-channel-tabs > div { width: auto; } +.redesign-channel-tabs a, +.redesign-channel-tabs b { display: block; padding: .875rem 0; font-size: .9375rem; } +.redesign-channel-tabs b { border-bottom: 2px solid var(--redesign-text); } +.redesign-channel-tabs a { color: var(--redesign-muted); } +.redesign-channel-sort { display: flex; gap: 1rem; font-size: .8125rem; padding: .75rem 0; } +.redesign-channel-sort a { color: var(--redesign-muted); } +.redesign-channel-page-header .pure-button { min-height: 2.5rem; padding: .6rem .9rem; border: 1px solid var(--redesign-line); border-radius: 6px; background: var(--redesign-surface); color: var(--redesign-text); font-size: .875rem; } + +.redesign-ui #contents:has(.redesign-subscriptions) { display:flex; flex-direction:column; min-height:100svh; } +.redesign-main:has(.redesign-subscriptions) { flex: 1; } +.redesign-ui .feed-menu { display:flex; flex-wrap:wrap; justify-content:flex-start; gap:1.5rem; padding:.5rem 1rem 0; margin:0; border-bottom:1px solid var(--redesign-line); } +.redesign-ui .feed-menu-item { color:var(--redesign-muted); padding:.875rem 0; font-size:.9375rem; text-transform:none; } +.redesign-ui .feed-menu-item[aria-current="page"] { color:var(--redesign-text); border-bottom:2px solid var(--redesign-text); } +.redesign-feed-heading { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem; padding-block:1.5rem .75rem; } +.redesign-feed-heading h1 { font-size:1.5rem; margin:0; } +.redesign-feed-actions { display:flex; align-items:center; flex-wrap:wrap; gap:1rem; font-size:.875rem; } +.redesign-feed-actions a { color:var(--redesign-muted); } +.redesign-feed-empty { margin:2rem 1rem; padding:2rem 0; border-block:1px solid var(--redesign-line); } +.redesign-feed-empty p { font-size:1.125rem; margin:0 0 .75rem; } +.redesign-feed-status { font-size:.8125rem; color:var(--redesign-muted); } +.redesign-subscriptions > .h-box:has(hr) { display:none; } + +@media screen and (max-width: 1000px) { + .redesign-ui .redesign-navbar { grid-template-columns: 1fr auto; gap: .5rem 1rem; } + .redesign-ui .redesign-navbar .searchbar { grid-column:1 / -1; grid-row:2; order:initial; } + .redesign-ui .redesign-navbar .user-field { grid-column:2; grid-row:1; } + .redesign-ui .redesign-navbar .index-link { text-align:start; padding-left:0; } +} +@media screen and (max-width: 520px) { + .redesign-ui .redesign-navbar { gap:.5rem; } + .redesign-ui .redesign-navbar .user-field { gap:0; } + .redesign-ui .redesign-navbar .user-field a, + .redesign-ui .redesign-navbar .redesign-signout { min-width:2rem; padding:.375rem; font-size:.8125rem; } + .redesign-ui .redesign-navbar #user_name { max-width:3.5rem; padding:0 .25rem; } + .redesign-channel-page-header .title { flex-wrap:wrap; } + .redesign-channel-page-header .title > div { width:100%; justify-content:flex-start; } + .redesign-channel-page-header .title > .flex-left { flex-basis:100%; } + .redesign-channel-page-header .channel-name { font-size:1.25rem; } + .redesign-channel-page-header .channel-profile img { width:3rem; height:3rem; } +} + +/* Native account disclosure: a local avatar, with no remote profile request. */ +.redesign-ui .redesign-navbar .user-field:has(.redesign-account-menu) { gap: .75rem; flex-wrap: nowrap; } +.redesign-account-menu { position: relative; } +.redesign-account-trigger { + display: flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + list-style: none; + cursor: pointer; + border-radius: 50%; +} +.redesign-account-trigger::-webkit-details-marker { display: none; } +.redesign-account-trigger::before { content: none; } +.redesign-account-avatar { + display: grid; + place-items: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + background: var(--redesign-control); + border: 1px solid var(--redesign-line); + color: var(--redesign-text); + font-size: 1rem; + font-weight: 650; +} +.redesign-account-menu[open] .redesign-account-avatar { border-color: var(--redesign-focus); } +.redesign-ui .redesign-account-panel { + position: absolute; + inset-inline-end: 0; + top: calc(100% + .5rem); + z-index: 100; + width: min(15rem, calc(100vw - 2rem)); + box-sizing: border-box; + padding: .375rem; + background: var(--redesign-surface); + border: 1px solid var(--redesign-line); + border-radius: .5rem; + box-shadow: 0 8px 24px rgb(0 0 0 / .2); +} +.redesign-ui .redesign-navbar .redesign-account-panel > a, +.redesign-ui .redesign-navbar .redesign-account-panel .redesign-signout { + display: flex; + justify-content: flex-start; + gap: .75rem; + width: 100%; + min-height: 2.75rem; + padding: .625rem .75rem; + font-size: .875rem; + color: var(--redesign-text); + border-radius: .25rem; +} +.redesign-ui .redesign-account-panel > a:hover, +.redesign-ui .redesign-account-panel .redesign-signout:hover { background: var(--redesign-control); } +.redesign-account-identity { padding: .75rem; border-bottom: 1px solid var(--redesign-line); margin-bottom: .375rem; } +.redesign-ui .redesign-navbar .redesign-account-panel #user_name { max-width: none; padding: 0; overflow-wrap: anywhere; white-space: normal; color: var(--redesign-text); } +.redesign-account-panel form { border-top: 1px solid var(--redesign-line); margin-top: .375rem; padding-top: .375rem; } +.redesign-ui .redesign-navbar #notification_ticker { position: relative; } +.redesign-ui .redesign-navbar #notification_ticker > .icon { font-size: 1.65rem; } +#notification_ticker #notification_count { + position: absolute; + inset-inline-end: -.25rem; + top: -.15rem; + min-width: 1.15rem; + padding: .125rem .25rem; + border-radius: 1rem; + background: #bd1641; + border: 2px solid var(--redesign-canvas); + color: #fff; + font-size: .6875rem; + font-weight: 700; + line-height: 1.2; + text-align: center; +} + +.redesign-ui .ion-md-checkmark-circle { + color: #3ea6ff; + margin-inline-start: .2em; + vertical-align: -.08em; + line-height: 1; +} +.redesign-ui.light-theme .ion-md-checkmark-circle { color: #0878d1; } +@media (prefers-color-scheme: light) { + .redesign-ui.no-theme .ion-md-checkmark-circle { color: #0878d1; } +} +.redesign-video-result .redesign-item-author, +.redesign-video-result .redesign-item-meta { gap: .75rem; } +.redesign-video-result .redesign-item-author > .flex-left, +.redesign-video-result .redesign-item-meta > .flex-left { min-width: 0; } +.redesign-video-result .redesign-item-author > .flex-right, +.redesign-video-result .redesign-item-meta > .flex-right { flex: 0 0 auto; } + +/* Compact glyphs retain comfortable hit areas. */ +.redesign-account-avatar { width: 2rem; height: 2rem; font-size: .875rem; } +.redesign-account-trigger { width: 2.5rem; height: 2.5rem; } +.redesign-ui .redesign-navbar #notification_ticker > .icon { font-size: 1.35rem; } +.redesign-ui .redesign-navbar .redesign-account-panel .icon { font-size: 1rem; } +.redesign-account-panel [data-theme][aria-current="true"] { background: var(--redesign-control); } +.redesign-account-panel [data-theme][aria-current="true"]::after { content: '✓'; margin-inline-start: auto; color: var(--redesign-accent); } +.redesign-search-page #filters-collapse summary { + display: inline-flex; + align-items: center; + padding: .625rem .875rem; + border: 1px solid var(--redesign-line); + border-radius: .375rem; + min-height: 2.5rem; + box-sizing: border-box; +} +.redesign-search-page #filters-collapse summary::before { margin: 0 .625rem 0 0; border: 0; padding: 0; background: transparent; } +.redesign-video-result .redesign-item-meta { align-items: center; } +.redesign-video-result .redesign-item-meta .icon-buttons { align-items: center; gap: .5rem; } +.redesign-video-result .redesign-item-meta .icon-buttons a { display: inline-flex; align-items: center; justify-content: center; margin: 0; line-height: 1; font-size: 1rem; } + +/* Search stays in the header; only compact viewports disclose the field. */ +.redesign-search-toggle, .redesign-search-trigger { display: none; } +.redesign-ui .redesign-navbar { position: relative; grid-template-columns: minmax(7rem,1fr) minmax(12rem,30rem) auto; } +.redesign-ui .redesign-navbar .searchbar { grid-column: 2; grid-row: 1; } +.redesign-ui .redesign-navbar .user-field { grid-column: 3; grid-row: 1; } +.redesign-ui .redesign-account-avatar { box-sizing: border-box; } +@media screen and (max-width: 600px) { + .redesign-ui .redesign-navbar { grid-template-columns: minmax(0,1fr) 40px auto; gap: 4px; } + .redesign-ui .redesign-search-toggle { display: block; position: absolute; width: 1px; height: 1px; clip-path: inset(50%); overflow: hidden; } + .redesign-search-trigger { display: flex; grid-column: 2; grid-row: 1; align-items: center; justify-content: center; width: 40px; height: 40px; font-size: 22px; cursor: pointer; border-radius: 5px; } + .redesign-search-toggle:focus-visible + .redesign-search-trigger { outline: 3px solid var(--redesign-focus); } + .redesign-ui .redesign-navbar .searchbar { display: none; position: absolute; top: 100%; inset-inline: 0; z-index: 8; padding: 8px; background: var(--redesign-canvas); border-bottom: 1px solid var(--redesign-line); } + .redesign-ui .redesign-search-toggle:checked ~ .searchbar { display: block; } + .redesign-search-toggle:checked + .redesign-search-trigger { background: var(--redesign-surface); } +} + +.redesign-ui .redesign-navbar { box-sizing: border-box; min-height: 60px; padding-block: 8px 4px; align-content: center; border-bottom: 0; } + +.redesign-ui .redesign-navbar .user-field:has(.redesign-account-menu) { gap: 4px; } + +/* Stable metadata rows without inherited paragraph spacing or empty card tails. */ +.redesign-ui .redesign-video-result .redesign-item-card { display: grid; grid-template-rows: auto 1fr auto auto; } +.redesign-ui .redesign-video-result .video-card-row p { margin: 0; } +.redesign-ui .redesign-video-result .redesign-item-title { padding-top: 12px; } +.redesign-ui .redesign-video-result .redesign-item-author { margin-top: 10px; align-items: center; } +.redesign-ui .redesign-video-result .redesign-item-meta { margin-top: 6px; padding-bottom: 12px; min-height: 28px; align-items: center; } +.redesign-ui .redesign-video-result .redesign-item-meta .icon-buttons a { width: 28px; height: 28px; } +.redesign-ui .redesign-video-result .redesign-item-author .video-data { white-space: nowrap; } + +.redesign-ui .redesign-navbar .user-field .redesign-theme-choice { display: inline-flex; align-items: center; justify-content: center; min-width: 32px; height: 36px; box-sizing: border-box; padding: 6px; color: var(--redesign-muted); border: 1px solid transparent; border-radius: 5px; } +.redesign-ui .redesign-navbar .redesign-theme-choice .icon { font-size: 18px; } +.redesign-ui .redesign-navbar .redesign-theme-choice[aria-current="true"] { color: var(--redesign-text); border-color: var(--redesign-line); background: var(--redesign-surface); } diff --git a/assets/css/watch-layout.css b/assets/css/watch-layout.css new file mode 100644 index 000000000..050fafdf8 --- /dev/null +++ b/assets/css/watch-layout.css @@ -0,0 +1,488 @@ +.watch-page { + --watch-border: #3a3c42; + --watch-panel: #191a1e; + --watch-muted: #b4b6bc; + --watch-focus: #79b7ff; + box-sizing: border-box; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(16rem, 20rem); + column-gap: clamp(1.25rem, 2.5vw, 2rem); + width: 100%; + padding-inline: 1rem; +} + +.watch-page-wide { + grid-template-columns: minmax(0, 1fr); +} + +.watch-visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.watch-page .watch-theater-toggle { + grid-column: 1 / -1; + grid-row: 1; + justify-self: end; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + min-width: 2.75rem; + min-height: 2.5rem; + margin-block-end: 0.65rem; + border: 1px solid var(--watch-border); + border-radius: 0.35rem; + color: inherit; + cursor: pointer; +} + +.watch-page .watch-theater-toggle:hover { + border-color: var(--watch-muted); +} + +.watch-page #theater-mode-toggle:focus-visible + .watch-theater-toggle { + outline: 2px solid var(--watch-focus); + outline-offset: 3px; +} + +.watch-theater-icon { + position: relative; + display: block; + box-sizing: border-box; + width: 1.35rem; + height: 0.9rem; + border: 2px solid currentColor; + border-radius: 0.12rem; +} + +.watch-theater-icon::after { + position: absolute; + inset-inline: 0.16rem; + bottom: 0.12rem; + height: 0.16rem; + background: currentColor; + content: ""; +} + +#theater-mode-toggle:checked + .watch-theater-toggle .watch-theater-icon::after { + inset: 0.14rem; + height: auto; + border: 1px solid currentColor; + background: transparent; +} + +.watch-page > #player-container { + grid-column: 1; + grid-row: 2; + width: 100%; + margin-inline: 0; + padding-bottom: 56.25% !important; +} + +.watch-page .player-dimensions.vjs-fluid { + padding-top: 56.25% !important; +} + +.watch-page .watch-title { + grid-column: 1; + grid-row: 3; + padding-inline: 0; +} + +.watch-title h1 { + margin-block: 1rem 0.5rem; + font-size: clamp(1.35rem, 2vw, 1.5625rem); + line-height: 1.28; + letter-spacing: -0.015em; +} + +.watch-title h3 { + margin-block: 0.35rem; + font-size: 1rem; +} + +.watch-title #link-iv-listen { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + min-height: 2.5rem; + margin-inline-start: 0.25rem; + vertical-align: middle; +} + +.watch-page .watch-content, +.watch-page .watch-primary-content { + display: contents; +} + +.watch-content > .watch-channel { + grid-column: 1; + grid-row: 4; + min-width: 0; + margin-block: 0.25rem 0.75rem; + padding: 0; +} + +.watch-content > .watch-channel .channel-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.watch-content > .watch-channel .channel-profile > img { + width: 2.75rem; + height: 2.75rem; + flex: none; + object-fit: cover; +} + +.watch-content > .watch-channel .button-container { + min-width: max-content; +} + +.watch-utilities { + grid-column: 1; + grid-row: 5; + min-width: 0; + margin-block-end: 1rem; +} + +.watch-utility-disclosure { + border-block: 1px solid var(--watch-border); +} + +.watch-utility-disclosure > summary { + box-sizing: border-box; + width: max-content; + min-height: 2.75rem; + padding: 0.7rem 0; + font-weight: 600; + cursor: pointer; +} + +.watch-utility-disclosure > summary:focus-visible { + outline: 2px solid var(--watch-focus); + outline-offset: 3px; +} + +.watch-utility-content { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.8rem 1.25rem; + padding: 0.5rem 0 1rem; +} + +.watch-utility-content > p, +.watch-utility-content > span, +.watch-utility-content > form { + margin: 0; +} + +.watch-utility-content > form, +.watch-utility-content > #allowed_regions { + grid-column: 1 / -1; +} + +.watch-utility-content .pure-button { + min-height: 2.5rem; +} + +.watch-primary-content > .h-box { + grid-column: 1; + grid-row: 6; + min-width: 0; + padding: 0; +} + +.watch-primary-content #published-date { + color: var(--watch-muted); +} + +.watch-primary-content #description-box { + box-sizing: border-box; + margin-block: 1rem; + padding: 1rem; + border-radius: 0.4rem; + background: var(--watch-panel); +} + +.watch-primary-content #descriptionWrapper { + max-width: 70ch; +} + +.watch-primary-content #comments { + max-width: none; + margin: 0; +} + +.watch-related { + grid-column: 2; + grid-row: 2 / span 5; + min-width: 0; +} + +.watch-related > .h-box { + padding: 0; +} + +.watch-related .pure-u-1 { + display: grid; + grid-template-columns: 7rem minmax(0, 1fr); + grid-template-rows: min-content min-content; + gap: 0.15rem 0.7rem; + width: 100%; + margin-block-end: 1rem; +} + +.watch-related .pure-u-1 > .thumbnail { + grid-column: 1; + grid-row: 1 / span 2; +} + +.watch-related .video-card-row { + grid-column: 2; + grid-row: 1; + min-width: 0; + margin: 0; +} + +.watch-related .video-card-row p { + display: -webkit-box; + margin: 0; + overflow: hidden; + font-size: 0.9rem; + font-weight: 600; + line-height: 1.3; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.watch-related .pure-u-1 > h5 { + grid-column: 2; + grid-row: 2; + min-width: 0; + margin: 0; + color: var(--watch-muted); + font-size: 0.75rem; + font-weight: 400; + line-height: 1.35; +} + +.watch-related .pure-u-1 > h5 > div { + width: 100%; + text-align: start !important; +} + +.watch-related .pure-u-1 > h5 b { + font-weight: 400; +} + +#theater-mode-toggle:checked ~ #player-container { + grid-column: 1 / -1; +} + +#theater-mode-toggle:checked ~ .watch-content .watch-related { + grid-column: 2; + grid-row: 3 / span 4; +} + +.light-theme .watch-page { + --watch-border: #d2d5da; + --watch-panel: #f0f1f3; + --watch-muted: #5c6068; + --watch-focus: #075a9e; +} + +@media (prefers-color-scheme: light) { + .no-theme .watch-page { + --watch-border: #d2d5da; + --watch-panel: #f0f1f3; + --watch-muted: #5c6068; + --watch-focus: #075a9e; + } +} + +@media screen and (max-width: 900px) { + .watch-page, + .watch-page-wide { + grid-template-columns: minmax(0, 1fr); + } + + .watch-related, + #theater-mode-toggle:checked ~ .watch-content .watch-related { + grid-column: 1; + grid-row: 7; + margin-block-start: 1.25rem; + } + + .watch-related .pure-u-1 { + grid-template-columns: minmax(7rem, 11rem) minmax(0, 1fr); + } +} + +@media screen and (max-width: 520px) { + .watch-page { + padding-inline: 0.75rem; + } + + .watch-theater-toggle { + min-width: 2.75rem; + } + + .watch-title h1 { + font-size: 1.25rem; + } + + .watch-page .watch-content > .watch-channel { + display: flex; + align-items: flex-start; + gap: 0.75rem; + } + + .watch-page .watch-content > .watch-channel > div { + width: auto; + } + + .watch-page .watch-content > .watch-channel .flex-left { + min-width: 0; + } + + .watch-page .watch-content > .watch-channel .flex-right { + justify-content: flex-start; + margin: 0; + } + + .watch-utility-content { + grid-template-columns: minmax(0, 1fr); + } + + .watch-utility-content > form, + .watch-utility-content > #allowed_regions { + grid-column: 1; + } + + .watch-related .pure-u-1 { + grid-template-columns: minmax(6.5rem, 38vw) minmax(0, 1fr); + } +} + +@media (forced-colors: active) { + .watch-theater-toggle, + .watch-utility-disclosure { + border-color: CanvasText; + } +} + +/* Keep watch actions on one compact row; native disclosures hold form details. */ +.watch-page.has-player-theater-control .watch-theater-toggle, +.watch-page.has-player-theater-control #theater-mode-toggle { display: none; } +.watch-page .watch-theater-toggle { gap: .6rem; padding-inline: .75rem; } +.watch-page .watch-action-row { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; position: relative; padding-block: .35rem; } +.watch-action-row > p, .watch-action-row > span { margin: 0; } +.watch-action-row #watch-on-youtube { display: inline-flex; gap: 4px; } +.watch-action-row .watch-stat { display: inline-flex; align-items: center; gap: 6px; margin-inline-end: 12px; font-size: .85rem; color: var(--watch-muted); } +.watch-action-row .watch-action { display: inline-flex; align-items: center; justify-content: center; gap: 7px; box-sizing: border-box; min-width: 36px; min-height: 36px; padding: 6px 9px; border: 0; border-radius: 5px; color: var(--redesign-text); font-size: .85rem; line-height: 1.3; cursor: pointer; } +.watch-action-row .watch-action-icon { width: 36px; padding: 0; } +.watch-action-row .watch-action .icon { font-size: 18px; line-height: 1; } +.watch-action-row .watch-action:hover, .watch-action-menu[open] > summary { background: var(--watch-panel); } +.watch-action-menu > summary { list-style: none; } +.watch-action-menu > summary::-webkit-details-marker { display: none; } +.watch-action-row .watch-action-panel { position: absolute; z-index: 5; inset-inline-end: 0; top: 100%; width: min(320px, 100%); box-sizing: border-box; padding: 16px; background: var(--redesign-surface); color: var(--redesign-text); border: 1px solid var(--watch-border); border-radius: 6px; box-shadow: 0 6px 18px #0003; } +.watch-action-panel p { margin: 0 0 10px; font-size: .85rem; } +.watch-action-panel select { width: 100%; max-width: 100%; color: var(--redesign-text); background: var(--redesign-control); } +.watch-action-panel form { margin: 0; } +.redesign-ui #subscribe:not([data-type="unsubscribe"]) { background: #d92332; border-color: #d92332; color: #fff; } +.redesign-ui #subscribe:not([data-type="unsubscribe"]):hover { background: #bc1826; border-color: #bc1826; } +.redesign-ui .watch-channel .ion-md-checkmark-circle { color: #3ea6ff; } +.redesign-ui .channel-owner .ion-md-checkmark-circle { color: #fff; } + +.watch-page .watch-action-row { justify-content: space-between; gap: 8px 16px; padding-block: 8px; border-block: 1px solid var(--watch-border); } +.watch-action-stats, .watch-action-buttons { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; } +.watch-action-buttons { margin-inline-start: auto; } +.watch-action-row .watch-action { min-height: 36px; gap: 7px; padding: 8px 10px; font-weight: 500; } +.watch-action-row .watch-action-panel { top: calc(100% + 6px); } +.watch-info-menu .watch-action-panel .watch-action { justify-content: flex-start; width: 100%; } +.watch-info-menu .watch-visually-hidden { position: static; clip: auto; margin: 0; padding: 0; width: auto; height: auto; overflow: visible; white-space: normal; } +.watch-info-menu #watch-on-youtube { display: block; } +.watch-info-menu .watch-action-panel hr { border: 0; border-top: 1px solid var(--watch-border); margin-block: 12px; } +.redesign-ui .watch-action-panel select, .redesign-ui .watch-action-panel input:not([type=hidden]) { box-sizing: border-box; color: var(--redesign-text); background-color: var(--redesign-control); border: 1px solid var(--watch-border); border-radius: 4px; min-height: 38px; padding: 8px; width: 100%; margin-block: 8px; } +.redesign-ui .watch-action-panel button { background: var(--redesign-accent); color: var(--redesign-on-accent); border: 0; border-radius: 4px; padding: 9px 12px; cursor: pointer; margin: 0; } +.watch-share-choice { display: grid; grid-template-columns: 1fr auto; gap: 6px; margin-bottom: 16px; } +.watch-share-choice:last-child { margin-bottom: 0; } +.watch-share-choice .watch-share-link { grid-column: 1 / -1; } +.watch-share-choice input { min-width: 0; font-size: 12px; } +.watch-share-panel > .watch-share-link { display: block; padding: 10px 0; } +.watch-share-choice button { font-size: 12px; align-self: center; } +.watch-related { padding-top: 2px; } +#theater-mode-toggle:checked ~ .watch-content .watch-related { padding-top: 16px; } +.redesign-ui .watch-related .video-card-row a { color: var(--redesign-text); } +.redesign-ui .watch-related h5 a { color: var(--watch-muted); } +.watch-related .pure-u-1 { grid-template-columns: 8rem minmax(0,1fr); gap: 4px 12px; margin-bottom: 16px; } +.watch-related .thumbnail { border-radius: 5px; overflow: hidden; align-self: start; } +.watch-related .video-card-row p { font-size: .875rem; line-height: 1.4; } +.watch-related .pure-control-group { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 36px; font-size: .8125rem; color: var(--watch-muted); } +.watch-related .pure-control-group input { margin: 0; accent-color: var(--redesign-accent); } +.watch-related hr { border: 0; border-top: 1px solid var(--watch-border); margin: 8px 0 16px; } +.watch-related .length { font-size: .75rem; padding: 2px 4px; } +@media screen and (max-width: 520px) { + .watch-action-buttons { margin-inline-start: 0; } + .watch-action-stats { width: 100%; } + .watch-page .watch-channel { flex-wrap: nowrap; align-items: center; } + .watch-channel #subscribe { font-size: .8125rem; padding: 8px 10px; white-space: nowrap; } + .watch-channel #channel-name { font-size: 1rem; } +} + +.redesign-ui .watch-page a:hover, .redesign-ui .watch-page a:active { color: var(--redesign-text) !important; } +.redesign-ui .watch-page .channel-profile a:hover, .redesign-ui .watch-page .channel-profile:hover { color: var(--redesign-text); } +.redesign-ui .watch-related h5 a:hover { color: var(--watch-muted) !important; } +.redesign-ui .watch-channel a, .redesign-ui #comments h3 a { color: var(--redesign-text); } +.redesign-ui .watch-channel a:hover .ion-md-checkmark-circle { color: #3ea6ff; } + +.redesign-ui #subscribe:not([data-type="unsubscribe"]):hover, +.redesign-ui #subscribe:not([data-type="unsubscribe"]):focus, +.redesign-ui #subscribe:not([data-type="unsubscribe"]):active { background: #bc1826 !important; border-color: #bc1826 !important; color: #fff !important; } +.watch-action-panel [hidden] { display: none !important; } +.watch-action-row .watch-action-panel { max-height: min(420px, 65vh); overflow-y: auto; } +.watch-action-panel p { line-height: 1.5; } +.redesign-ui #comments > div:first-child { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 20px 0; } +.redesign-ui #comments h3 { margin: 0; font-size: 1.125rem; } +.redesign-ui #comments > div:first-child > b { font-size: .8125rem; font-weight: 400; } +.redesign-ui #comments .pure-g:has(> .channel-profile) { display: grid; grid-template-columns: 36px minmax(0,1fr); gap: 12px; padding: 0; margin-bottom: 24px; letter-spacing: normal; } +.redesign-ui #comments .pure-g > .channel-profile { width: 36px; } +.redesign-ui #comments .channel-profile img { width: 36px !important; height: 36px; object-fit: cover; margin: 0 !important; border-radius: 50%; } +.redesign-ui #comments .channel-profile + div { width: auto; min-width: 0; } +.redesign-ui #comments .channel-profile + div > p { margin: 0 0 8px; font-size: .875rem; line-height: 1.5; } +.redesign-ui #comments .channel-profile + div > p:first-child { font-size: .8125rem; line-height: 1.3; } +.redesign-ui #comments .channel-profile + div > p:has(> span[title]) { color: var(--watch-muted); font-size: .75rem; } +.redesign-ui #comments .channel-owner { background: #3a3d45; color: #fff; border-radius: 4px; padding: 3px 6px; display: inline-block; } +.redesign-ui #comments .channel-owner:hover { background: #3a3d45; color: #fff !important; } +.redesign-ui #comments .ion-md-checkmark-circle { color: #fff; } +.redesign-ui #comments [id="replies"] { margin-top: 8px; padding-left: 0; } +.redesign-ui #comments [id="replies"] > .pure-u-1-24 { display: none; } +.redesign-ui #comments [id="replies"] > .pure-u-23-24 { width: 100%; } +.redesign-ui #comments [id="replies"] p { margin: 4px 0 12px; font-size: .8125rem; } +.redesign-ui .watch-primary-content hr { border: 0; border-top: 1px solid var(--watch-border); } +@media screen and (max-width: 600px) { + .watch-action-buttons { gap: 2px; } + .watch-action-row .watch-action { font-size: .75rem; padding-inline: 7px; gap: 5px; } +} + +.watch-page { padding-top: 12px; } +.watch-page .video-js { border-radius: 12px; overflow: hidden; } +.watch-page video, .watch-page .vjs-poster { border-radius: 12px; } +.watch-page .video-js.vjs-fullscreen, .watch-page .vjs-fullscreen video { border-radius: 0; } +.light-theme .watch-page { --watch-muted: #555c68; --watch-border: #d5d9e0; } +.light-theme .watch-primary-content #description-box { border: 1px solid #e0e3e9; } +.light-theme .watch-action-row .watch-action:hover, .light-theme .watch-action-menu[open] > summary { background: #e8edf4; color: #17181b; } +.light-theme .watch-action-row .watch-action-panel { border-color: #c9ced8; box-shadow: 0 6px 18px #1720331a; } +.light-theme .watch-channel .ion-md-checkmark-circle { color: #0878d1; } +.light-theme .watch-page #comments .channel-owner { background: #4d5562; } diff --git a/assets/js/account-menu.js b/assets/js/account-menu.js new file mode 100644 index 000000000..f72d7de8a --- /dev/null +++ b/assets/js/account-menu.js @@ -0,0 +1,20 @@ +'use strict'; + +// Native details handles disclosure without scripts. These only add dismissal. +(function () { + var menu = document.querySelector('.redesign-account-menu'); + if (!menu) return; + var trigger = menu.querySelector('summary'); + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape' && menu.open) { + menu.open = false; + trigger.focus(); + } + }); + document.addEventListener('click', function (event) { + if (menu.open && !menu.contains(event.target)) menu.open = false; + }); + document.addEventListener('focusin', function (event) { + if (menu.open && !menu.contains(event.target)) menu.open = false; + }); +})(); diff --git a/assets/js/player-gestures.js b/assets/js/player-gestures.js new file mode 100644 index 000000000..b678250b3 --- /dev/null +++ b/assets/js/player-gestures.js @@ -0,0 +1,152 @@ +'use strict'; + +// Optional enhancements: native media controls remain available without this script. +(function (root, factory) { + if (typeof module === 'object' && module.exports) module.exports = factory; + else root.installPlayerGestures = factory; +})(typeof window === 'undefined' ? globalThis : window, function (player, environment) { + var env = environment || {}; + var win = env.window || window; + var doc = env.document || document; + var later = env.setTimeout || win.setTimeout.bind(win); + var cancelTimer = env.clearTimeout || win.clearTimeout.bind(win); + var el = player.el(); + var session = null; + var suppressClick = false; + var listeners = []; + + function blocked(event) { + return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || + !player.controls() || (event.target && event.target.closest && + event.target.closest('input, textarea, select, button, a, summary, [role="button"], [role="slider"], [role="textbox"], [contenteditable]:not([contenteditable="false"]), .vjs-control-bar, .vjs-menu')); + } + + function consume(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + + function play() { + var result = player.play(); + if (result && result.catch) result.catch(function () { finish(false); }); + } + + function toggle() { + if (player.paused()) play(); + else player.pause(); + } + + function showHold(active) { + if (env.onHold) env.onHold(active); + else el.classList.toggle('vjs-speed-hold', active); + } + + function begin(kind, event) { + if (session) return; + session = { kind: kind, pointerId: event.pointerId, x: event.clientX, y: event.clientY, + active: false, speed: player.playbackRate(), paused: player.paused() }; + session.timer = later(function () { + if (!session) return; + session.active = true; + showHold(true); + player.playbackRate(2); + play(); + }, 350); + } + + function finish(tap) { + if (!session) return false; + var current = session; + cancelTimer(current.timer); + // Keep the hold marked active while ratechange handlers restore preferences. + if (current.active) { + player.playbackRate(current.speed); + if (current.paused) player.pause(); + showHold(false); + } + session = null; + if (tap && !current.active && current.kind === 'keyboard') toggle(); + return current.active; + } + + function listen(target, type, handler) { + target.addEventListener(type, handler, true); + listeners.push([target, type, handler]); + } + + listen(win, 'keydown', function (event) { + var playButton = event.target && event.target.closest && + event.target.closest('.vjs-play-control, .vjs-big-play-button'); + var playerControl = event.key !== ' ' && event.target && event.target.closest && + event.target.closest('.vjs-control-bar') && + !event.target.closest('.vjs-menu, input, select, [role="slider"]'); + if (!el.contains(doc.activeElement) || (!playButton && !playerControl && blocked(event)) || + event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; + if (event.key === ' ') { + consume(event); + if (!event.repeat) begin('keyboard', event); + } else if (event.key === 'k') { + consume(event); + if (!event.repeat && !session) toggle(); + } else if (event.key === 'j' || event.key === 'l') { + consume(event); + var time = Math.max(0, player.currentTime() + (event.key === 'j' ? -10 : 10)); + var duration = player.duration(); + player.currentTime(Number.isFinite(duration) ? Math.min(duration, time) : time); + } + }); + + listen(win, 'keyup', function (event) { + if (event.key === ' ' && session && session.kind === 'keyboard') { + consume(event); + finish(el.contains(doc.activeElement)); + } + }); + + listen(el, 'pointerdown', function (event) { + suppressClick = false; + if (event.button !== 0 || event.pointerType !== 'mouse' || blocked(event)) return; + el.focus({ preventScroll: true }); + begin('pointer', event); + }); + + listen(win, 'pointermove', function (event) { + if (!session || session.kind !== 'pointer' || session.active || event.pointerId !== session.pointerId) return; + if (Math.abs(event.clientX - session.x) + Math.abs(event.clientY - session.y) > 12) finish(false); + }); + + listen(win, 'pointerup', function (event) { + if (!session || session.kind !== 'pointer' || event.pointerId !== session.pointerId) return; + if (finish(false)) { + // Browsers may deliver click in a later task. A new pointerdown clears + // this flag, so releasing outside cannot consume the next deliberate click. + suppressClick = true; + } + }); + + // Video.js toggles on mouseup; the subsequent browser click must also be + // consumed so other click handlers cannot turn a completed hold into a tap. + listen(el, 'mouseup', function (event) { + if (suppressClick && !blocked(event)) consume(event); + }); + + listen(el, 'click', function (event) { + if (suppressClick && event.detail !== 0 && !blocked(event)) { + consume(event); + suppressClick = false; + } + }); + + listen(win, 'pointercancel', function () { finish(false); }); + listen(win, 'blur', function () { finish(false); }); + listen(doc, 'visibilitychange', function () { if (doc.hidden) finish(false); }); + listen(doc, 'focusin', function (event) { if (!el.contains(event.target) || blocked(event)) finish(false); }); + + return { + isHolding: function () { return !!(session && session.active); }, + dispose: function () { + finish(false); + listeners.forEach(function (entry) { entry[0].removeEventListener(entry[1], entry[2], true); }); + } + }; +}); diff --git a/assets/js/player.js b/assets/js/player.js index 24f3379af..a1502b045 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -39,11 +39,6 @@ if (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; - var save_player_pos_key = 'save_player_pos'; videojs.Vhs.xhr.beforeRequest = function(options) { @@ -64,6 +59,13 @@ if (CONFIG.videojs.max_goal_buffer_length) { } var player = videojs('player', options); +var playerGestures = installPlayerGestures(player); +player.on('dispose', function () { playerGestures.dispose(); }); +var speedHoldIndicator = document.createElement('span'); +speedHoldIndicator.className = 'speed-hold-indicator'; +speedHoldIndicator.textContent = '2×'; +speedHoldIndicator.setAttribute('aria-hidden', 'true'); +player.el().appendChild(speedHoldIndicator); player.on('error', function () { if (video_data.params.quality === 'dash') return; @@ -185,22 +187,6 @@ player.on('timeupdate', function () { }); -var shareOptions = { - 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 ""; - } -}; - if (location.pathname.startsWith('/embed/')) { var overlay_content = '

' + player_data.title + '

'; player.overlay({ @@ -247,11 +233,8 @@ if (isMobile()) { 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. + // The HTTP source selector can't be fetched until the player is 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); @@ -351,7 +334,7 @@ function updateCookie(newVolume, newSpeed) { } player.on('ratechange', function () { - updateCookie(null, player.playbackRate()); + if (!playerGestures.isHolding()) updateCookie(null, player.playbackRate()); if (isMobile()) { player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); } @@ -451,9 +434,57 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') { player.vttThumbnails({ src: '/api/v1/storyboards/' + video_data.id + '?height=90', - showTimestamp: true }); +// The plugin concatenates root-relative sprite paths onto the API directory. +// Resolve URLs with browser semantics before its asynchronous VTT response arrives. +player.ready(function () { + var thumbnails = player.vttThumbnails; + thumbnails.getFullyQualifiedUrl = function (path, base) { + return new URL(path, base).href; + }; + var showThumbnail = thumbnails.showThumbnailHolder; + thumbnails.showThumbnailHolder = function () { + this.thumbnailHolder.style.display = 'block'; + showThumbnail.call(this); + }; + var hideThumbnail = thumbnails.hideThumbnailHolder; + thumbnails.hideThumbnailHolder = function () { + hideThumbnail.call(this); + this.thumbnailHolder.style.display = 'none'; + }; + var updateThumbnail = thumbnails.updateThumbnailStyle; + thumbnails.updateThumbnailStyle = function (x, width) { + updateThumbnail.call(this, x, width); + if (!this.thumbnailHolder) return; + var thumbWidth = this.thumbnailHolder.offsetWidth; + var center = Math.max(thumbWidth / 2, Math.min(width - thumbWidth / 2, x)); + this.thumbnailHolder.style.transform = 'translateX(' + center + 'px)'; + this.thumbnailHolder.setAttribute('data-time', videojs.formatTime( + Math.max(0, Math.min(1, x / width)) * player.duration(), player.duration())); + }; +}); + +var theaterToggle = document.getElementById('theater-mode-toggle'); +if (theaterToggle) { + var theaterLabel = document.querySelector('label[for="theater-mode-toggle"]'); + var theaterButton = player.controlBar.addChild('button', { + controlText: theaterLabel.textContent.trim() + }); + theaterButton.addClass('vjs-theater-control'); + player.controlBar.el().insertBefore(theaterButton.el(), player.controlBar.fullscreenToggle.el()); + theaterButton.el().setAttribute('title', theaterLabel.textContent.trim()); + theaterButton.el().setAttribute('aria-pressed', String(theaterToggle.checked)); + theaterButton.on('click', function () { + theaterToggle.checked = !theaterToggle.checked; + theaterToggle.dispatchEvent(new Event('change')); + }); + theaterToggle.addEventListener('change', function () { + theaterButton.el().setAttribute('aria-pressed', String(theaterToggle.checked)); + }); + theaterToggle.closest('.watch-page').classList.add('has-player-theater-control'); +} + // Enable annotations if (!video_data.params.listen && video_data.params.annotations) { addEventListener('load', function (e) { @@ -614,9 +645,38 @@ function update_captions() { } } -function toggle_fullscreen() { - player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); +var fullscreenFallbackTimer; +function exit_window_fullscreen() { + if (!player.isFullWindow) return; + player.exitFullWindow(); + player.isFullscreen(false); } +function toggle_fullscreen() { + clearTimeout(fullscreenFallbackTimer); + if (player.isFullWindow) { exit_window_fullscreen(); return; } + if (player.isFullscreen()) { player.exitFullscreen(); return; } + // Some embedded browsers leave the native fullscreen promise pending. + // Fall back to the same player's full-window mode without replacing media. + function useWindow() { + if (!document.fullscreenElement && !player.isDisposed()) { + player.enterFullWindow(); + player.isFullscreen(true); + } + } + fullscreenFallbackTimer = setTimeout(useWindow, 700); + var request = player.requestFullscreen(); + if (request && request.catch) request.catch(function () { + clearTimeout(fullscreenFallbackTimer); + useWindow(); + }); +} +player.controlBar.fullscreenToggle.handleClick = toggle_fullscreen; +player.controlBar.fullscreenToggle.off('click'); +player.controlBar.fullscreenToggle.on('click', toggle_fullscreen); +player.on('dispose', function () { clearTimeout(fullscreenFallbackTimer); }); +addEventListener('keydown', function (event) { + if (event.key === 'Escape' && player.isFullWindow) exit_window_fullscreen(); +}, true); function increase_playback_rate(steps) { const maxIndex = options.playbackRates.length - 1; @@ -655,7 +715,8 @@ function toggle_caption_opacity() { } addEventListener('keydown', function (e) { - if (e.target.tagName.toLowerCase() === 'input') { + if (!player.el().contains(document.activeElement) || !player.controls()) return; + if (e.target.closest('input, textarea, select, a, summary, [role="slider"], .vjs-menu, [contenteditable]:not([contenteditable="false"])')) { // Ignore input when focus is on certain elements, e.g. form fields. return; } @@ -676,8 +737,6 @@ addEventListener('keydown', function (e) { + (e.metaKey ? '+meta' : '') ; switch (decoratedKey) { - case ' ': - case 'k': case 'MediaPlayPause': action = toggle_play; break; @@ -705,12 +764,6 @@ addEventListener('keydown', function (e) { 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': @@ -794,9 +847,6 @@ addEventListener('keydown', function (e) { 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 () { diff --git a/assets/js/themes.js b/assets/js/themes.js index 84a9f6d9b..bcbbd9b67 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,13 +1,14 @@ 'use strict'; var toggle_theme = document.getElementById('toggle_theme'); -toggle_theme.href = 'javascript:void(0)'; +if (toggle_theme) toggle_theme.href = 'javascript:void(0)'; +var theme_choices = document.querySelectorAll('[data-theme]'); 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 () { +if (toggle_theme) toggle_theme.addEventListener('click', function () { const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK; setTheme(newTheme); @@ -15,18 +16,32 @@ toggle_theme.addEventListener('click', function () { helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); }); +theme_choices.forEach(function (choice) { + choice.addEventListener('click', function (event) { + event.preventDefault(); + var theme = choice.dataset.theme; + setTheme(theme); + helpers.storage.set(STORAGE_KEY_THEME, theme); + helpers.xhr('GET', '/toggle_theme?redirect=false&mode=' + theme, {}, {}); + }); +}); + /** @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 + theme_choices.forEach(function (choice) { + if (choice.dataset.theme === theme) choice.setAttribute('aria-current', 'true'); + else choice.removeAttribute('aria-current'); + }); + // Replace only the theme; preserve layout classes used by the rendered page. + document.body.classList.remove('no-theme', 'dark-theme', 'light-theme'); if (theme === THEME_DARK) { - toggle_theme.children[0].className = 'icon ion-ios-sunny'; - document.body.className = 'dark-theme'; + if (toggle_theme) toggle_theme.children[0].className = 'icon ion-ios-sunny'; + document.body.classList.add('dark-theme'); } else if (theme === THEME_LIGHT) { - toggle_theme.children[0].className = 'icon ion-ios-moon'; - document.body.className = 'light-theme'; + if (toggle_theme) toggle_theme.children[0].className = 'icon ion-ios-moon'; + document.body.classList.add('light-theme'); } else { - document.body.className = 'no-theme'; + document.body.classList.add('no-theme'); } } diff --git a/assets/js/watch-actions.js b/assets/js/watch-actions.js new file mode 100644 index 000000000..51730e316 --- /dev/null +++ b/assets/js/watch-actions.js @@ -0,0 +1,47 @@ +'use strict'; +(function () { + var menus = document.querySelectorAll('.watch-action-menu'); + menus.forEach(function (menu) { + menu.addEventListener('toggle', function () { + if (menu.open) menus.forEach(function (other) { + if (other !== menu && !other.contains(menu)) other.open = false; + }); + }); + }); + document.addEventListener('click', function (event) { + menus.forEach(function (menu) { + if (!menu.contains(event.target)) menu.open = false; + }); + }); + document.addEventListener('keydown', function (event) { + if (event.key !== 'Escape') return; + menus.forEach(function (menu) { + if (menu.open) { menu.open = false; menu.querySelector('summary').focus(); } + }); + }); + document.querySelectorAll('.watch-action').forEach(function (action) { + if (!action.title) action.title = action.textContent.trim(); + }); + document.querySelectorAll('.watch-share-link').forEach(function (link) { + var row = document.createElement('div'); + row.className = 'watch-share-choice'; + link.before(row); row.appendChild(link); + var input = document.createElement('input'); + input.readOnly = true; input.value = link.href; + input.setAttribute('aria-label', link.textContent.trim()); + input.addEventListener('focus', function () { input.select(); }); + row.appendChild(input); + if (!navigator.clipboard) return; + var button = document.createElement('button'); + button.type = 'button'; + var panel = row.closest('.watch-share-panel'); + button.textContent = panel.dataset.copyLabel; + button.addEventListener('click', function () { + navigator.clipboard.writeText(link.href).then(function () { + button.textContent = panel.dataset.copiedLabel; + setTimeout(function () { button.textContent = panel.dataset.copyLabel; }, 1800); + }).catch(function () { input.focus(); input.select(); }); + }); + row.appendChild(button); + }); +})(); diff --git a/assets/js/watch.js b/assets/js/watch.js index a2d8d26e7..8feb8b2eb 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -197,4 +197,5 @@ addEventListener('load', function (e) { } }); -document.getElementById("try-reddit-comments-link").onclick = swap_comments; +var redditCommentsLink = document.getElementById("try-reddit-comments-link"); +if (redditCommentsLink) redditCommentsLink.onclick = swap_comments; diff --git a/locales/en-US.json b/locales/en-US.json index 8032cf987..faba1cdd0 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -190,6 +190,7 @@ "playlist_button_add_items": "Add videos", "Show more": "Show more", "Show less": "Show less", + "Skip to content": "Skip to content", "Watch on YouTube": "Watch on YouTube", "Switch Invidious Instance": "Switch Invidious Instance", "search_message_no_results": "No results found.", @@ -406,6 +407,7 @@ "Fallback comments: ": "Fallback comments: ", "Popular": "Popular", "Search": "Search", + "Search results": "Search results", "Top": "Top", "About": "About", "Rating: ": "Rating: ", @@ -418,6 +420,12 @@ "News": "News", "Movies": "Movies", "Download": "Download", + "Share": "Share", + "Copy link": "Copy link", + "Copied": "Copied", + "More": "More", + "Video information": "Video information", + "Download and video options": "Download and video options", "Download as: ": "Download as: ", "Download is disabled": "Download is disabled", "%A %B %-d, %Y": "%A %B %-d, %Y", @@ -428,6 +436,7 @@ "Channel Sponsor": "Channel Sponsor", "Audio mode": "Audio mode", "Video mode": "Video mode", + "Theater mode": "Theater mode", "Playlists": "Playlists", "search_filters_title": "Filters", "search_filters_date_label": "Upload date", @@ -512,5 +521,12 @@ "timeline_parse_error_placeholder_heading": "Unable to parse item", "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", "timeline_parse_error_show_technical_details": "Show technical details", - "dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator." + "dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator.", + "No videos in this feed yet.": "No videos in this feed yet.", + "Channels": "Channels", + "Channel": "Channel", + "Feed menu": "Feed menu", + "Account": "Account", + "Light mode": "Light mode", + "Dark mode": "Dark mode" } diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 77ea02e91..5a4c46333 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -250,26 +250,19 @@ module Invidious::Routes::PreferencesRoute redirect ||= "true" redirect = redirect == "true" + requested_theme = env.params.query["mode"]? + requested_theme = nil unless {"light", "dark"}.includes?(requested_theme) + if user = env.get? "user" user = user.as(User) - case user.preferences.dark_mode - when "dark" - user.preferences.dark_mode = "light" - else - user.preferences.dark_mode = "dark" - end + user.preferences.dark_mode = requested_theme || (user.preferences.dark_mode == "dark" ? "light" : "dark") Invidious::Database::Users.update_preferences(user) else preferences = env.get("preferences").as(Preferences) - case preferences.dark_mode - when "dark" - preferences.dark_mode = "light" - else - preferences.dark_mode = "dark" - end + preferences.dark_mode = requested_theme || (preferences.dark_mode == "dark" ? "light" : "dark") host = env.get("header_x-forwarded-host") if alt = CONFIG.alternative_domains.index(host) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 686de6bd4..0e1a012f5 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -51,9 +51,7 @@ <%= rendered "components/channel_info" %> -
-
-
+ <%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index 9395000a4..3b4dadf25 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -1,3 +1,4 @@ +
<% if channel.banner %>
" alt="" /> @@ -13,7 +14,7 @@
- <%= author %><% if !channel.verified.nil? && channel.verified %> <% end %> + <%= author %><% if !channel.verified.nil? && channel.verified %><% end %> <% if !channel.pronouns.nil? %>
<%= channel.pronouns %><% end %>
@@ -37,28 +38,22 @@

<%= channel.description_html %>

-
-
+
+
-
-
- <% sort_options.each do |sort| %> -
- <% if sort_by == sort %> - <%= I18n.translate(locale, sort) %> - <% else %> - <%= I18n.translate(locale, sort) %> - <% end %> -
+ +
+ <% sort_options.each do |sort| %> + <% if sort_by == sort %> + <%= I18n.translate(locale, sort) %> + <% else %> + <%= I18n.translate(locale, sort) %> <% end %> -
+ <% end %>
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr index aeaf183b7..a722a2a82 100644 --- a/src/invidious/views/components/feed_menu.ecr +++ b/src/invidious/views/components/feed_menu.ecr @@ -1,11 +1,11 @@ -
+
+ diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index ece2efe8a..a1c1022a1 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -2,34 +2,50 @@ thin_mode = env.get("preferences").as(Preferences).thin_mode item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category | ProblematicTimelineItem) && env.get?("user").try &.as(User).watched.index(item.id) != nil author_verified = item.responds_to?(:author_verified) && item.author_verified + item_classes = if item.is_a?(SearchChannel) + "redesign-item redesign-channel-result" + elsif item.is_a?(SearchHashtag) + "redesign-item redesign-collection-result" + elsif item.is_a?(SearchPlaylist | InvidiousPlaylist) + "redesign-item redesign-video-result redesign-collection-result" + elsif item.is_a?(Category) + "redesign-item redesign-category-result" + elsif item.is_a?(ProblematicTimelineItem) + "redesign-item redesign-special-result" + else + "redesign-item redesign-video-result" + end -%> -
-
+
+
<% case item when %> <% when SearchChannel %> - <% if !thin_mode %> - -
- " alt="" /> -
-
- <%- else -%> -
- <% end %> +
+ <% if !thin_mode %> + + " alt="" /> + + <%- else -%> +
+ <% end %> -
- +
+ +

<%= HTML.escape(item.author) %> + <%- if author_verified %><% end -%> +

+
+ + + +
<%= item.description_html %>
+
- - <% if !item.channel_handle.nil? %>

<%= item.channel_handle %>

<% end %> -

<%= I18n.translate_count(locale, "generic_subscribers_count", item.subscriber_count, I18n::NumberFormatting::Separator) %>

- <% if !item.auto_generated && item.channel_handle.nil? %>

<%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %>

<% end %> -
<%= item.description_html %>
<% when SearchHashtag %> <% if !thin_mode %> @@ -77,21 +93,21 @@
-
+ -
+
<% if !item.ucid.to_s.empty? %>

<%= HTML.escape(item.author) %> - <%- if author_verified %> <% end -%> + <%- if author_verified %><% end -%>

<% else %>

<%= HTML.escape(item.author) %> - <%- if author_verified %> <% end -%> + <%- if author_verified %><% end -%>

<% end %>
@@ -175,43 +191,43 @@
-
+ -
+
<% if !item.ucid.to_s.empty? %>

<%= HTML.escape(item.author) %> - <%- if author_verified %> <% end -%> + <%- if author_verified %><% end -%>

<% else %>

<%= HTML.escape(item.author) %> - <%- if author_verified %> <% end -%> + <%- if author_verified %><% end -%>

<% end %>
- <%= rendered "components/video-context-buttons" %> -
- -
-
- <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> -

<%= I18n.translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>

- <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %> -

<%= I18n.translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>

- <% end %> -
- <% if item.responds_to?(:views) && item.views %>

<%= I18n.translate_count(locale, "generic_views_count", item.views || 0, I18n::NumberFormatting::Short) %>

<% end %>
+ +
+
+ <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> +

<%= I18n.translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>

+ <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %> +

<%= I18n.translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>

+ <% end %> +
+ + <%= rendered "components/video-context-buttons" %> +
<% end %> -
+
diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index 9af3899c4..884604caf 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -1,7 +1,6 @@ - @@ -11,7 +10,6 @@ - @@ -29,3 +27,5 @@ <% end %> + + diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 57e205260..bede1188f 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -5,29 +5,27 @@ <%= rendered "components/feed_menu" %> -
- - -
-

- -

+
+ +<% if videos.empty? && notifications.empty? %> +
+

<%= I18n.translate(locale, "No videos in this feed yet.") %>

+ <%= I18n.translate(locale, "Manage subscriptions") %> +
+<% end %> <% if CONFIG.enable_user_notifications %> -
+

<%= I18n.translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %> -

+

<% if !notifications.empty? %>
@@ -72,3 +70,5 @@ show_next: ((videos.size + notifications.size) == max_results) ) %> + +
diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index 0776b0d7d..96b97b191 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -107,20 +107,6 @@ - - - videojs-share.js - - - - Expat - - - - <%= I18n.translate(locale, "source") %> - - - videojs-vtt-thumbnails.js diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index ff0ced66e..1565f793a 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -9,18 +9,35 @@ <% end %> -<%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> -
+
"> + <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> -<%- if items.empty? -%> -
-
- <%= I18n.translate(locale, "search_message_no_results") %>

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

- <%= I18n.translate(locale, "search_message_use_another_instance", redirect_url) %> + + <%- if items.empty? -%> +
+
+ <%= I18n.translate(locale, "search_message_no_results") %>

+ <%= I18n.translate(locale, "search_message_change_filters_or_query") %>

+ <%= I18n.translate(locale, "search_message_use_another_instance", redirect_url) %> +
-
-<%- else -%> - <%= rendered "components/items_paginated" %> -<%- end -%> + <%- else -%> + <% channel_items = items.select { |item| item.is_a?(SearchChannel) } %> + <% unless channel_items.empty? %> +
+

<%= I18n.translate(locale, "Channels") %>

+
+ <% channel_items.each do |item| %> + <%= rendered "components/item" %> + <% end %> +
+
+ <% end %> + <% items = items.reject { |item| item.is_a?(SearchChannel) } %> +
"> + <% unless channel_items.empty? || items.empty? %>

<%= I18n.translate(locale, "Search results") %>

<% end %> + <%= rendered "components/items_paginated" %> +
+ <%- end -%> +
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 82d15958e..2e18b1afd 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -23,18 +23,22 @@ + --theme"> +-theme redesign-ui"> + <%= I18n.translate(locale, "Skip to content") %>
-