From be4bf80883f693b701789b6c0167576da1f57daa Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 14 Aug 2024 08:52:02 +0200 Subject: [PATCH] Add transcription widget --- CREDITS.md | 2 + client/package.json | 1 + client/src/app/+search/search.component.html | 4 +- .../action-buttons.component.html | 18 +- .../action-buttons.component.ts | 50 ++-- .../app/+videos/+video-watch/shared/index.ts | 1 - .../player-widget.component.scss | 37 +++ .../video-transcription.component.html | 62 +++++ .../video-transcription.component.scss | 26 ++ .../video-transcription.component.ts | 241 ++++++++++++++++++ .../video-watch-playlist.component.html | 8 +- .../video-watch-playlist.component.scss | 24 -- .../video-watch-playlist.component.ts | 2 +- .../+video-watch/shared/playlist/index.ts | 1 - .../+video-watch/video-watch.component.html | 23 +- .../+video-watch/video-watch.component.scss | 10 +- .../+video-watch/video-watch.component.ts | 38 ++- .../select/select-options.component.ts | 9 +- .../shared-icons/global-icon.component.ts | 3 +- .../video-actions-dropdown.component.ts | 75 +++++- ...-playlist-element-miniature.component.scss | 2 - client/src/assets/images/feather/filter.svg | 2 +- client/src/assets/player/peertube-player.ts | 12 + client/yarn.lock | 5 + 24 files changed, 566 insertions(+), 90 deletions(-) create mode 100644 client/src/app/+videos/+video-watch/shared/player-widgets/player-widget.component.scss create mode 100644 client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.html create mode 100644 client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.scss create mode 100644 client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.ts rename client/src/app/+videos/+video-watch/shared/{playlist => player-widgets}/video-watch-playlist.component.html (93%) rename client/src/app/+videos/+video-watch/shared/{playlist => player-widgets}/video-watch-playlist.component.scss (65%) rename client/src/app/+videos/+video-watch/shared/{playlist => player-widgets}/video-watch-playlist.component.ts (98%) delete mode 100644 client/src/app/+videos/+video-watch/shared/playlist/index.ts diff --git a/CREDITS.md b/CREDITS.md index 733ec5afc8c..97e9e56bb0a 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -754,11 +754,13 @@ * [Olivier Massain](https://dribbble.com/omassain) * [Marie-Cécile Godwin Paccard](https://mcgodwin.com/) + * [La Coopérative des Internets](https://www.lacooperativedesinternets.fr/) # Icons * [Feather Icons](https://feathericons.com) (MIT) + * [Lucide Icons](https://lucide.dev/) (ISC) * `playlist add`, `history`, `subscriptions`, `miscellaneous-services.svg`, `tip` by Material UI (Apache 2.0) * `support` by Chocobozzz (CC-BY) * `language` by Aaron Jin (CC-BY) diff --git a/client/package.json b/client/package.json index 1d34cf39cdc..eb1a53f7d3c 100644 --- a/client/package.json +++ b/client/package.json @@ -64,6 +64,7 @@ "@peertube/p2p-media-loader-core": "^1.0.20", "@peertube/p2p-media-loader-hlsjs": "^1.0.20", "@peertube/xliffmerge": "^2.0.3", + "@plussub/srt-vtt-parser": "^2.0.5", "@popperjs/core": "^2.11.5", "@types/chart.js": "^2.9.37", "@types/core-js": "^2.5.2", diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index a6a28b35e65..7e154e953dc 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html @@ -12,7 +12,7 @@ -
+
{{ error }}
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html index 4a527760644..2382ae620ab 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html @@ -34,7 +34,7 @@
- + @if (!isUserLoggedIn && !video.isLive) { + + + + + +
+
+ +
+
+
+ + + +
+
+
+ + + + @if (search && segments.length === 0) { +
No results for your search
+ } +
+ +
+ {{ segment.startFormatted }} + {{ segment.text }} +
+
+ + diff --git a/client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.scss b/client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.scss new file mode 100644 index 00000000000..c3122dc010d --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.scss @@ -0,0 +1,26 @@ +@use '_variables' as *; +@use '_mixins' as *; + +.segment { + &.active, + &:hover { + background: pvar(--mainBackgroundHoverColor); + } +} + +input[type=text] { + @include peertube-input-text(100%); +} + +.settings-button my-global-icon { + width: 18px; + height: 18px; +} + +.settings-panel { + position: absolute; + width: 100%; + padding: 0 1.5rem; + left: 0; + right: 0; +} diff --git a/client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.ts b/client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.ts new file mode 100644 index 00000000000..89f21100566 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-transcription.component.ts @@ -0,0 +1,241 @@ +import { NgClass, NgFor, NgIf } from '@angular/common' +import { + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { Notifier } from '@app/core' +import { durationToString, isInViewport } from '@app/helpers' +import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component' +import { VideoCaptionService } from '@app/shared/shared-main/video-caption/video-caption.service' +import { NgbCollapse, NgbTooltip } from '@ng-bootstrap/ng-bootstrap' +import { Video, VideoCaption } from '@peertube/peertube-models' +import { parse } from '@plussub/srt-vtt-parser' +import debug from 'debug' +import { debounceTime, distinctUntilChanged, Subject } from 'rxjs' +import { SelectOptionsItem } from 'src/types' +import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component' + +const debugLogger = debug('peertube:watch:VideoTranscriptionComponent') + +type Segment = { + start: number + startFormatted: string + + end: number + + text: string +} + +@Component({ + selector: 'my-video-transcription', + templateUrl: './video-transcription.component.html', + styleUrls: [ './player-widget.component.scss', './video-transcription.component.scss' ], + standalone: true, + imports: [ + NgIf, + NgClass, + NgbTooltip, + GlobalIconComponent, + NgFor, + NgbCollapse, + FormsModule, + SelectOptionsComponent + ] +}) +export class VideoTranscriptionComponent implements OnInit, OnChanges { + @ViewChild('settingsPanel') settingsPanel: ElementRef + + @Input() video: Video + @Input() captions: VideoCaption[] + @Input() currentTime: number + + // Output the duration clicked + @Output() segmentClicked = new EventEmitter() + @Output() closeTranscription = new EventEmitter() + + currentCaption: VideoCaption + segments: Segment[] = [] + activeSegment: Segment + + search = '' + + currentLanguage: string + languagesOptions: SelectOptionsItem[] = [] + + isSettingsPanelCollapsed: boolean + // true when collapsed has been shown (after the transition) + settingsPanelShown: boolean + + private segmentsStore: Segment[] = [] + private searchSubject = new Subject() + + constructor ( + private notifier: Notifier, + private captionService: VideoCaptionService + ) { + } + + @HostListener('document:click', [ '$event' ]) + clickout (event: Event) { + if (!this.settingsPanelShown) return + + if (!this.settingsPanel?.nativeElement.contains(event.target)) { + this.isSettingsPanelCollapsed = true + } + } + + ngOnInit () { + this.searchSubject.asObservable() + .pipe( + debounceTime(100), + distinctUntilChanged() + ) + .subscribe(search => this.filterSegments(search)) + } + + ngOnChanges (changes: SimpleChanges) { + if (changes['video'] || changes['captions']) { + this.load() + return + } + + if (changes['currentTime']) { + this.findActiveSegment() + } + } + + getSegmentClasses (segment: Segment) { + return { active: this.activeSegment === segment, ['segment-' + segment.start]: true } + } + + updateCurrentCaption () { + this.currentCaption = this.captions.find(c => c.language.id === this.currentLanguage) + + this.parseCurrentCaption() + } + + private load () { + this.search = '' + + this.segmentsStore = [] + this.segments = [] + + this.activeSegment = undefined + this.currentCaption = undefined + + this.isSettingsPanelCollapsed = true + this.settingsPanelShown = false + + this.languagesOptions = [] + + if (!this.video || !this.captions || this.captions.length === 0) return + + this.currentLanguage = this.captions.some(c => c.language.id === this.video.language.id) + ? this.video.language.id + : this.captions[0].language.id + + this.languagesOptions = this.captions.map(c => ({ + id: c.language.id, + label: c.automaticallyGenerated + ? $localize`${c.language.label} (automatically generated)` + : c.language.label + })) + + this.updateCurrentCaption() + } + + private parseCurrentCaption () { + this.captionService.getCaptionContent({ captionPath: this.currentCaption.captionPath }) + .subscribe({ + next: content => { + try { + const entries = parse(content).entries + + this.segmentsStore = entries.map(({ from, to, text }) => { + const start = Math.ceil(from / 1000) + const end = Math.ceil(to / 1000) + + return { + start, + startFormatted: durationToString(start), + end, + text + } + }) + + this.segments = this.segmentsStore + } catch (err) { + this.notifier.error($localize`Cannot load transcript: ${err.message}`) + } + }, + + error: err => this.notifier.error(err.message) + }) + } + + // --------------------------------------------------------------------------- + + onSearchChange (event: Event) { + const target = event.target as HTMLInputElement + + this.searchSubject.next(target.value) + } + + onSegmentClick (event: Event, segment: Segment) { + event.preventDefault() + + this.segmentClicked.emit(segment.start) + } + + // --------------------------------------------------------------------------- + + private filterSegments (search: string) { + this.search = search + + const searchLowercase = search.toLocaleLowerCase() + + this.segments = this.segmentsStore.filter(s => { + return s.text.toLocaleLowerCase().includes(searchLowercase) + }) + } + + private findActiveSegment () { + const lastActiveSegment = this.activeSegment + this.activeSegment = undefined + + if (isNaN(this.currentTime)) return + + for (let i = this.segmentsStore.length - 1; i >= 0; i--) { + const current = this.segmentsStore[i] + + if (current.start < this.currentTime) { + this.activeSegment = current + break + } + } + + if (lastActiveSegment !== this.activeSegment) { + setTimeout(() => { + const element = document.querySelector('.segment-' + this.activeSegment.start) + if (!element) return // Can happen with a search + + const container = document.querySelector('.widget-root') + + if (isInViewport(element, container)) return + + container.scrollTop = element.offsetTop + + debugLogger(`Set transcription segment ${this.activeSegment.start} in viewport`) + }) + } + } +} diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html similarity index 93% rename from client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html rename to client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html index c0f45c977ef..3cbfa33ad88 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html @@ -1,9 +1,9 @@
-
-
+ > +
+
{{ playlist.displayName }} Unlisted diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss similarity index 65% rename from client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss rename to client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss index cb56a1cd77e..24c9fb5c068 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss @@ -4,30 +4,6 @@ @use '_miniature' as *; .playlist { - position: relative; - min-width: 200px; - width: 25vw; - max-width: 470px; - height: 66vh; - background-color: pvar(--mainBackgroundColor); - overflow-y: auto; - border-bottom: 1px solid $separator-border-color; - - .playlist-info { - padding: 5px 30px; - background-color: pvar(--greyBackgroundColor); - } - - .playlist-display-name { - font-size: 18px; - font-weight: $font-semibold; - margin-bottom: 5px; - - .pt-badge { - @include margin-left(5px); - } - } - .playlist-by-index { color: pvar(--greyForegroundColor); display: flex; diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts similarity index 98% rename from client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts rename to client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts index 70e040b2b1c..85601fd8afe 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts @@ -17,7 +17,7 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl @Component({ selector: 'my-video-watch-playlist', templateUrl: './video-watch-playlist.component.html', - styleUrls: [ './video-watch-playlist.component.scss' ], + styleUrls: [ './player-widget.component.scss', './video-watch-playlist.component.scss' ], standalone: true, imports: [ NgIf, InfiniteScrollerDirective, NgClass, NgbTooltip, GlobalIconComponent, NgFor, VideoPlaylistElementMiniatureComponent ] }) diff --git a/client/src/app/+videos/+video-watch/shared/playlist/index.ts b/client/src/app/+videos/+video-watch/shared/playlist/index.ts deleted file mode 100644 index 539705508f3..00000000000 --- a/client/src/app/+videos/+video-watch/shared/playlist/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './video-watch-playlist.component' diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index d9e2f698c74..d692cc1c03c 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html @@ -13,10 +13,20 @@
- +
+ + + @if (transcriptionWidgetOpened) { + + } +
@@ -53,8 +63,11 @@

{{ video.name }}

diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss index d38679a029f..61d5ef156d9 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.scss +++ b/client/src/app/+videos/+video-watch/video-watch.component.scss @@ -7,7 +7,7 @@ $video-default-height: 66vh; $video-max-height: calc(100vh - #{$header-height} - #{$theater-bottom-space}); -@mixin playlist-below-player { +@mixin player-widget-below-player { width: 100% !important; height: auto !important; max-height: 300px !important; @@ -43,8 +43,8 @@ $video-max-height: calc(100vh - #{$header-height} - #{$theater-bottom-space}); --player-height: #{$video-max-height}; } - my-video-watch-playlist ::ng-deep .playlist { - @include playlist-below-player; + .player-widget-component ::ng-deep .widget-root { + @include player-widget-below-player; } } } @@ -233,8 +233,8 @@ my-video-comments { flex-direction: column; justify-content: center; - my-video-watch-playlist ::ng-deep .playlist { - @include playlist-below-player; + .player-widget-component ::ng-deep .widget-root { + @include player-widget-below-player; } } diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 2bc23a2bea4..d6dc5914ae0 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -45,6 +45,7 @@ import { } from '@peertube/peertube-models' import { logger } from '@root-helpers/logger' import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video' +import debug from 'debug' import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' import { HLSOptions, @@ -60,7 +61,6 @@ import { DateToggleComponent } from '../../shared/shared-main/date/date-toggle.c import { PluginPlaceholderComponent } from '../../shared/shared-main/plugins/plugin-placeholder.component' import { VideoViewsCounterComponent } from '../../shared/shared-video/video-views-counter.component' import { PlayerStylesComponent } from './player-styles.component' -import { VideoWatchPlaylistComponent } from './shared' import { ActionButtonsComponent } from './shared/action-buttons/action-buttons.component' import { VideoCommentsComponent } from './shared/comment/video-comments.component' import { PrivacyConcernsComponent } from './shared/information/privacy-concerns.component' @@ -68,8 +68,12 @@ import { VideoAlertComponent } from './shared/information/video-alert.component' import { VideoAttributesComponent } from './shared/metadata/video-attributes.component' import { VideoAvatarChannelComponent } from './shared/metadata/video-avatar-channel.component' import { VideoDescriptionComponent } from './shared/metadata/video-description.component' +import { VideoTranscriptionComponent } from './shared/player-widgets/video-transcription.component' +import { VideoWatchPlaylistComponent } from './shared/player-widgets/video-watch-playlist.component' import { RecommendedVideosComponent } from './shared/recommendations/recommended-videos.component' +const debugLogger = debug('peertube:watch:VideoWatchComponent') + type URLOptions = { playerMode: PlayerMode @@ -112,7 +116,9 @@ type URLOptions = { VideoCommentsComponent, RecommendedVideosComponent, PrivacyConcernsComponent, - PlayerStylesComponent + PlayerStylesComponent, + VideoWatchPlaylistComponent, + VideoTranscriptionComponent ] }) export class VideoWatchComponent implements OnInit, OnDestroy { @@ -136,6 +142,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { remoteServerDown = false noPlaylistVideoFound = false + transcriptionWidgetOpened = false + private nextRecommendedVideoUUID = '' private nextRecommendedVideoTitle = '' @@ -239,13 +247,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.nextRecommendedVideoTitle = video.name } + // --------------------------------------------------------------------------- + handleTimestampClicked (timestamp: number) { if (!this.peertubePlayer || this.video.isLive) return - this.peertubePlayer.getPlayer().currentTime(timestamp) + const player = this.peertubePlayer.getPlayer() + if (!player) return + + this.peertubePlayer.setCurrentTime(timestamp) + scrollToTop() } + // --------------------------------------------------------------------------- + onPlaylistVideoFound (videoId: string) { this.loadVideo({ videoId, forceAutoplay: false }) } @@ -309,7 +325,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { const start = queryParams['start'] if (this.peertubePlayer?.getPlayer() && start) { - this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10)) + this.peertubePlayer.setCurrentTime(parseInt(start, 10)) } }) } @@ -492,6 +508,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.remoteServerDown = false this.currentTime = undefined + if (this.transcriptionWidgetOpened && this.videoCaptions.length === 0) { + this.transcriptionWidgetOpened = false + } + if (this.isVideoBlur(this.video)) { const res = await this.confirmService.confirm( $localize`This video contains mature or explicit content. Are you sure you want to watch it?`, @@ -556,8 +576,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { const player = this.peertubePlayer.getPlayer() player.on('timeupdate', () => { - // Don't need to trigger angular change for this variable, that is sent to children components on click - this.currentTime = Math.floor(player.currentTime()) + const newTime = Math.floor(player.currentTime()) + + // Update only if we have at least 1 second difference + if (!this.currentTime || Math.abs(newTime - this.currentTime) >= 1) { + debugLogger('Updating current time to ' + newTime) + + this.zone.run(() => this.currentTime = newTime) + } }) if (this.video.isLive) { diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts index 62df5c80e72..d699ac28304 100644 --- a/client/src/app/shared/shared-forms/select/select-options.component.ts +++ b/client/src/app/shared/shared-forms/select/select-options.component.ts @@ -1,4 +1,4 @@ -import { Component, forwardRef, HostListener, Input } from '@angular/core' +import { booleanAttribute, Component, forwardRef, HostListener, Input } from '@angular/core' import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms' import { SelectOptionsItem } from '../../../../types/select-options-item.model' import { NgIf } from '@angular/common' @@ -20,10 +20,13 @@ import { NgSelectModule } from '@ng-select/ng-select' }) export class SelectOptionsComponent implements ControlValueAccessor { @Input() items: SelectOptionsItem[] = [] - @Input() clearable = false - @Input() searchable = false + + @Input({ transform: booleanAttribute }) clearable = false + @Input({ transform: booleanAttribute }) searchable = false + @Input() groupBy: string @Input() labelForId: string + @Input() searchFn: any selectedId: number | string diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index 7f6b49952fd..ee7e4389070 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts @@ -20,7 +20,7 @@ const icons = { 'flame': require('../../../assets/images/misc/flame.svg'), 'local': require('../../../assets/images/misc/local.svg'), - // feather icons + // feather/lucide icons 'copy': require('../../../assets/images/feather/copy.svg'), 'flag': require('../../../assets/images/feather/flag.svg'), 'playlists': require('../../../assets/images/feather/list.svg'), @@ -78,6 +78,7 @@ const icons = { 'codesandbox': require('../../../assets/images/feather/codesandbox.svg'), 'award': require('../../../assets/images/feather/award.svg'), 'stats': require('../../../assets/images/feather/stats.svg'), + 'filter': require('../../../assets/images/feather/filter.svg'), 'shield': require('../../../assets/images/misc/shield.svg') } diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index 12ffb9713ff..bdae2183aed 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts @@ -39,6 +39,7 @@ export type VideoActionsDisplayType = { studio?: boolean stats?: boolean generateTranscription?: boolean + transcriptionWidget?: boolean } @Component({ @@ -84,7 +85,9 @@ export class VideoActionsDropdownComponent implements OnChanges { removeFiles: false, transcoding: false, studio: true, - stats: true + stats: true, + generateTranscription: false, + transcriptionWidget: false } @Input() placement = 'auto' @Input() moreActions: DropdownAction<{ video: Video }>[][] = [] @@ -96,6 +99,8 @@ export class VideoActionsDropdownComponent implements OnChanges { @Input() buttonSize: DropdownButtonSize = 'normal' @Input() buttonDirection: DropdownDirection = 'vertical' + @Input() transcriptionWidgetOpened: boolean + @Output() videoFilesRemoved = new EventEmitter() @Output() videoRemoved = new EventEmitter() @Output() videoUnblocked = new EventEmitter() @@ -104,6 +109,9 @@ export class VideoActionsDropdownComponent implements OnChanges { @Output() transcodingCreated = new EventEmitter() @Output() modalOpened = new EventEmitter() + @Output() showTranscriptionWidget = new EventEmitter() + @Output() hideTranscriptionWidget = new EventEmitter() + videoActions: DropdownAction<{ video: Video }>[][] = [] private loaded = false @@ -140,14 +148,16 @@ export class VideoActionsDropdownComponent implements OnChanges { } loadDropdownInformation () { - if (!this.isUserLoggedIn() || this.loaded === true) return + if (this.loaded === true) return this.loaded = true if (this.displayOptions.playlist) this.playlistAdd.load() } - /* Show modals */ + // --------------------------------------------------------------------------- + // Show modals + // --------------------------------------------------------------------------- showDownloadModal () { this.modalOpened.emit() @@ -179,37 +189,55 @@ export class VideoActionsDropdownComponent implements OnChanges { this.liveStreamInformationModal.show(video) } - /* Actions checker */ + // --------------------------------------------------------------------------- + // Actions checker + // --------------------------------------------------------------------------- isVideoUpdatable () { + if (!this.user) return false + return this.video.isUpdatableBy(this.user) } isVideoEditable () { + if (!this.user) return false + return this.video.isEditableBy(this.user, this.serverService.getHTMLConfig().videoStudio.enabled) } isVideoStatsAvailable () { + if (!this.user) return false + return this.video.isLocal && this.video.isOwnerOrHasSeeAllVideosRight(this.user) } isVideoRemovable () { + if (!this.user) return false + return this.video.isRemovableBy(this.user) } isVideoBlockable () { + if (!this.user) return false + return this.video.isBlockableBy(this.user) } isVideoUnblockable () { + if (!this.user) return false + return this.video.isUnblockableBy(this.user) } isVideoLiveInfoAvailable () { + if (!this.user) return false + return this.video.isLiveInfoAvailableBy(this.user) } canGenerateTranscription () { + if (!this.user) return false + return this.video.canGenerateTranscription(this.user, this.serverService.getHTMLConfig().videoTranscription.enabled) } @@ -225,6 +253,8 @@ export class VideoActionsDropdownComponent implements OnChanges { } isVideoDownloadableByUser () { + if (!this.user) return false + return ( this.video && this.video.isLive !== true && @@ -235,22 +265,32 @@ export class VideoActionsDropdownComponent implements OnChanges { // --------------------------------------------------------------------------- canVideoBeDuplicated () { + if (!this.user) return false + return !this.video.isLive && this.video.canBeDuplicatedBy(this.user) } isVideoAccountMutable () { + if (!this.user) return false + return this.video.account.id !== this.user.account.id } canRemoveVideoFiles () { + if (!this.user) return false + return this.video.canRemoveAllHLSOrWebFiles(this.user) } canRunTranscoding () { + if (!this.user) return false + return this.video.canRunTranscoding(this.user) } - /* Action handlers */ + // --------------------------------------------------------------------------- + // Action handlers + // --------------------------------------------------------------------------- async unblockVideo () { const confirmMessage = $localize`Do you really want to unblock ${this.video.name}? It will be available again in the videos list.` @@ -400,7 +440,7 @@ export class VideoActionsDropdownComponent implements OnChanges { iconName: 'playlist-add' } ], - [ // actions regarding the video + [ // public actions regarding the video { label: $localize`Download`, handler: () => this.showDownloadModal(), @@ -417,6 +457,29 @@ export class VideoActionsDropdownComponent implements OnChanges { return $localize`This option is visible only to you` } }, + { + label: $localize`Show transcription`, + handler: () => this.showTranscriptionWidget.emit(), + isDisplayed: () => { + if (!this.displayOptions.transcriptionWidget) return false + if (this.transcriptionWidgetOpened) return false + + return Array.isArray(this.videoCaptions) && this.videoCaptions.length !== 0 + }, + iconName: 'video-lang' + }, + { + label: $localize`Hide transcription`, + handler: () => this.hideTranscriptionWidget.emit(), + isDisplayed: () => { + if (!this.displayOptions.transcriptionWidget) return false + + return this.transcriptionWidgetOpened === true + }, + iconName: 'video-lang' + } + ], + [ // private actions regarding the video { label: $localize`Display live information`, handler: ({ video }) => this.showLiveInfoModal(video), diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss index d4acae86bc0..5e7e2b43654 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss @@ -46,8 +46,6 @@ my-video-thumbnail, cursor: pointer; .position { - @include margin-right(10px); - font-weight: $font-semibold; color: pvar(--greyForegroundColor); min-width: 25px; diff --git a/client/src/assets/images/feather/filter.svg b/client/src/assets/images/feather/filter.svg index 38a47e04379..17a9574e9bd 100644 --- a/client/src/assets/images/feather/filter.svg +++ b/client/src/assets/images/feather/filter.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts index 1805a07c408..d4d4d4a7be8 100644 --- a/client/src/assets/player/peertube-player.ts +++ b/client/src/assets/player/peertube-player.ts @@ -151,6 +151,18 @@ export class PeerTubePlayer { (this.player.el() as HTMLElement).style.pointerEvents = 'none' } + setCurrentTime (currentTime: number) { + if (this.player.paused()) { + this.currentLoadOptions.startTime = currentTime + + this.player.play() + return + } + + this.player.currentTime(currentTime) + this.player.userActive(true) + } + private async loadP2PMediaLoader () { const hlsOptionsBuilder = new HLSOptionsBuilder({ ...pick(this.options, [ 'pluginsManager', 'serverUrl', 'authorizationHeader' ]), diff --git a/client/yarn.lock b/client/yarn.lock index 257e3546f6b..107a86ed6e0 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2436,6 +2436,11 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@plussub/srt-vtt-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@plussub/srt-vtt-parser/-/srt-vtt-parser-2.0.5.tgz#4836d1fe9c912b4f48b8c0ce6a9c0c9755b1c66e" + integrity sha512-cOedEgu7gyea9k+ixkPCQGf8ABBctFWWsBYnVCzzmuoHz45awc9vKtveHzn7VugR36fzFqgkXaLEn2HdZnzFdQ== + "@polka/parse@^1.0.0-next.0": version "1.0.0-next.0" resolved "https://registry.yarnpkg.com/@polka/parse/-/parse-1.0.0-next.0.tgz#3551d792acdf4ad0b053072e57498cbe32e45a94"