Merge branch 'Chocobozzz:develop' into feature/Remember-user-table-pagination-in-admin

This commit is contained in:
Wicklow 2023-10-16 07:18:39 +00:00 committed by GitHub
commit 6cd05dc963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 44156 additions and 42411 deletions

View File

@ -1,7 +1,10 @@
<ng-template #modal>
<div class="modal-header">
<h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1>
<my-global-icon iconName="cross" aria-label="Close" tabindex="0" role="button" (click)="hide()" (keydown.enter)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -2,7 +2,9 @@
<div class="modal-header">
<h4 i18n class="modal-title">Follow</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -5,7 +5,9 @@
<ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container>
</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<form novalidate [formGroup]="form" (ngSubmit)="processRegistration()">

View File

@ -112,11 +112,13 @@
<li *ngFor="let file of video.files">
<a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }}
<my-global-icon
*ngIf="canRemoveOneFile(video)"
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
<button
*ngIf="canRemoveOneFile(video)" class="border-0 p-0"
i18n-title title="Delete this file"
(click)="removeVideoFile(video, file, 'web-videos')"
></my-global-icon>
>
<my-global-icon iconName="delete"></my-global-icon>
</button>
</li>
</ul>
</div>
@ -128,11 +130,13 @@
<li *ngFor="let file of video.streamingPlaylists[0].files">
<a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }}
<my-global-icon
*ngIf="canRemoveOneFile(video)"
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
<button
*ngIf="canRemoveOneFile(video)" class="border-0 p-0"
i18n-title title="Delete this file"
(click)="removeVideoFile(video, file, 'hls')"
></my-global-icon>
>
<my-global-icon iconName="delete"></my-global-icon>
</button>
</li>
</ul>
</div>

View File

@ -1,7 +1,7 @@
<h1 i18n class="title-page-v2">
<h1 class="title-page-v2">
<strong class="underline-orange">{{ instanceName }}</strong>
>
Login
<ng-container i18n>Login</ng-container>
</h1>
<div class="margin-content">
@ -120,7 +120,9 @@
<div class="modal-header">
<h4 i18n class="modal-title">Forgot your password</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hideForgotPasswordModal()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body text-start">

View File

@ -2,7 +2,9 @@
<div class="modal-header">
<h1 i18n class="modal-title">Accept ownership</h1>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body" [formGroup]="form">

View File

@ -2,14 +2,20 @@
<div class="modal-header">
<h4 i18n class="modal-title">Change ownership</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body" [formGroup]="form">
<div class="form-group">
<label i18n for="next-ownership-username">Select the next owner</label>
<p-autoComplete formControlName="username" [suggestions]="usernamePropositions"
(completeMethod)="search($event)" id="next-ownership-username"></p-autoComplete>
<p-autoComplete
formControlName="username" [suggestions]="usernamePropositions"
(completeMethod)="search($event)" id="next-ownership-username"
></p-autoComplete>
<div *ngIf="formErrors.username" class="form-error" role="alert">
{{ formErrors.username }}
</div>

View File

@ -3,7 +3,9 @@
<div class="modal-header">
<h4 i18n class="modal-title">Add caption</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-container [formGroup]="form">
<div class="modal-header">
<h4 i18n class="modal-title">Edit caption</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -3,11 +3,13 @@
<my-actor-avatar [actor]="user?.account" [actorType]="getAvatarActorType()" size="25"></my-actor-avatar>
<div class="textarea-wrapper">
<textarea i18n-placeholder placeholder="Add comment..." myAutoResize
[readonly]="(user === null) ? true : false"
(click)="openVisitorModal($event)"
formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
(keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea>
<textarea
i18n-placeholder placeholder="Add comment..." myAutoResize
[readonly]="(user === null) ? true : false"
(click)="openVisitorModal($event)"
formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
(keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea
>
</textarea>
<my-help
@ -57,7 +59,10 @@
<ng-template #visitorModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideModals()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hideModals()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">
@ -81,8 +86,12 @@
<ng-template #emojiModal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Markdown Emoji List</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideModals()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hideModals()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">
<div class="emoji-flex">
<div class="emoji-flex-item" *ngFor="let emojiMarkup of getEmojiMarkupList()">

View File

@ -19,23 +19,21 @@
</div>
<div class="playlist-controls">
<my-global-icon
iconName="videos"
[class.active]="autoPlayNextVideoPlaylist"
(click)="switchAutoPlayNextVideoPlaylist()"
[ngbTooltip]="autoPlayNextVideoPlaylistSwitchText"
placement="bottom auto"
container="body"
></my-global-icon>
<button
class="border-0 p-0 me-2" [ngClass]="{ active: autoPlayNextVideoPlaylist }" (click)="switchAutoPlayNextVideoPlaylist()"
[ngbTooltip]="autoPlayNextVideoPlaylistSwitchText" [ariaLabel]="autoPlayNextVideoPlaylistSwitchText"
placement="bottom auto" container="body"
>
<my-global-icon iconName="videos"></my-global-icon>
</button>
<my-global-icon
iconName="repeat"
[class.active]="loopPlaylist"
(click)="switchLoopPlaylist()"
[ngbTooltip]="loopPlaylistSwitchText"
placement="bottom auto"
container="body"
></my-global-icon>
<button
class="border-0 p-0" [ngClass]="{ active: loopPlaylist }" (click)="switchLoopPlaylist()"
[ngbTooltip]="loopPlaylistSwitchText" [ariaLabel]="loopPlaylistSwitchText"
placement="bottom auto" container="body"
>
<my-global-icon iconName="repeat"></my-global-icon>
</button>
</div>
</div>

View File

@ -46,18 +46,8 @@
display: flex;
margin: 10px 0;
my-global-icon:not(:last-child) {
@include margin-right(.5rem);
}
my-global-icon {
&:not(.active) {
opacity: .5;
}
::ng-deep {
cursor: pointer;
}
button:not(.active) {
opacity: .5;
}
}

View File

@ -40,7 +40,9 @@
<div [innerHTML]="broadcastMessage.message"></div>
<button
*ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()" class="border-0" title="Close this message" i18n-title>
*ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()"
class="border-0" title="Close this message" i18n-title
>
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 i18n class="modal-title">Change the language</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 i18n class="modal-title">Welcome to {{ instanceName }}, dear user!</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 i18n class="modal-title">Welcome to PeerTube, dear administrator!</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -3,7 +3,9 @@
<div class="modal-header">
<h4 class="modal-title">{{ title }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body" >

View File

@ -1,7 +1,10 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 class="modal-title">{{title}}</h4>
<my-global-icon *ngIf="close" iconName="cross" aria-label="Close" role="button" (click)="onCloseClick()"></my-global-icon>
<button *ngIf="close" class="border-0 p-0" title="Close this modal" i18n-title (click)="onCloseClick()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body" [innerHTML]="content"></div>

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 i18n class="modal-title">Configuration warning!</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 i18n class="modal-title">My settings</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -5,7 +5,9 @@
<ng-container i18n *ngIf="!isAdminView">Messages with the moderation team</ng-container>
</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -2,7 +2,9 @@
<div class="modal-header">
<h4 i18n class="modal-title">Moderation comment</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,12 +1,12 @@
<button class="feed border-0 p-0" *ngIf="syndicationItems && syndicationItems.length !== 0">
<my-global-icon
role="button" aria-label="Open syndication dropdown" i18n-aria-label
*ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom left auto"
class="icon-syndication" iconName="syndication"
>
</my-global-icon>
<button
*ngIf="syndicationItems && syndicationItems.length !== 0"
[ngbPopover]="feedsList" [autoClose]="true" placement="bottom left auto"
class="feed border-0 p-0"
title="Open syndication dropdown" i18n-title
>
<my-global-icon iconName="syndication"></my-global-icon>
<ng-template #feedsList>
<a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
<a *ngFor="let item of syndicationItems" class="feed-link" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
</ng-template>
</button>

View File

@ -4,15 +4,15 @@
.feed {
width: 100%;
color: inherit;
}
a {
color: pvar(--mainForegroundColor);
display: block;
min-width: 100px;
.feed-link {
color: pvar(--mainForegroundColor);
display: block;
min-width: 100px;
&:hover {
text-decoration: underline;
}
&:hover {
text-decoration: underline;
}
}

View File

@ -2,7 +2,9 @@
<div class="modal-header">
<h4 class="modal-title">{{ action }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal>
<div class="modal-header">
<h4 class="modal-title">{{ modalTitle }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Report video "{{ video.name }}"</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -2,7 +2,9 @@
<div class="modal-header">
<h4 i18n class="modal-title">{{ getModalTitle() }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -9,7 +9,9 @@
<h4 i18n class="modal-title" *ngIf="getSingleVideo().isLive">Block live "{{ getSingleVideo().name }}"</h4>
</ng-container>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close">
<div class="modal-header">
<h4 i18n class="modal-title">Support {{ displayName }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body" [innerHTML]="htmlSupport"></div>

View File

@ -2,7 +2,9 @@
<div class="modal-header">
<h4 i18n class="modal-title">Live information</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body" *ngIf="live">

View File

@ -11,7 +11,9 @@
</div>
</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div>
<div class="modal-body">

View File

@ -1,6 +1,6 @@
<div class="margin-content">
<div class="videos-header">
<h1 *ngIf="displayTitle" class="title" placement="bottom" [ngbTooltip]="titleTooltip" container="body">
<div class="videos-header pt-4 mb-4">
<h1 *ngIf="displayTitle" class="title mb-1" placement="bottom" [ngbTooltip]="titleTooltip" container="body">
{{ title }}
</h1>
@ -10,7 +10,7 @@
<my-feed [syndicationItems]="syndicationItems"></my-feed>
</div>
<div class="action-block">
<div *ngIf="headerActions.length !== 0" class="action-block mt-3">
<ng-container *ngFor="let action of headerActions">
<a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active">
<ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>

View File

@ -4,12 +4,11 @@
@use '_miniature' as *;
// Cannot set margin top to videos-header because of the main header fixed position
$margin-top: 30px;
$margin-top: 2rem;
.videos-header {
display: grid;
grid-template-columns: auto 1fr auto;
margin-bottom: 30px;
.title,
.title-subscription {
@ -21,9 +20,6 @@ $margin-top: 30px;
color: pvar(--mainForegroundColor);
display: inline-block;
font-weight: $font-semibold;
margin-top: $margin-top;
margin-bottom: 0;
}
.title-subscription {
@ -39,7 +35,6 @@ $margin-top: 30px;
.action-block {
grid-column: 3;
grid-row: 1/3;
margin-top: $margin-top;
}
my-feed {
@ -77,15 +72,15 @@ $margin-top: 30px;
@include margin-right(pvar(--horizontalMarginContent));
.video-wrapper {
margin-bottom: 15px;
margin-bottom: 1rem;
}
}
@media screen and (max-width: $mobile-view) {
.videos-header,
my-video-filters-header {
@include margin-left(15px);
@include margin-right(15px);
@include margin-left(1rem);
@include margin-right(1rem);
display: inline-block;
}
@ -95,9 +90,8 @@ $margin-top: 30px;
}
.videos-header {
flex-direction: column;
align-items: center;
height: auto;
margin-bottom: 10px;
text-align: center;
width: 100%;
margin-bottom: 1rem;
}
}

View File

@ -53,10 +53,13 @@
<my-edit-button *ngIf="owned && touchScreenEditButton" [ptRouterLink]="[ '/my-library', 'video-playlists', playlist.uuid ]"></my-edit-button>
<div *ngIf="owned" class="more dropdown-root" ngbDropdown #moreDropdown="ngbDropdown" placement="left auto"
(openChange)="onDropdownOpenChange()" autoClose="outside" container="body"
<div
*ngIf="owned" class="more dropdown-root" ngbDropdown #moreDropdown="ngbDropdown" placement="left auto"
(openChange)="onDropdownOpenChange()" autoClose="outside" container="body"
>
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon>
<button class="border-0 p-0 more-button" (click)="$event.preventDefault()" ngbDropdownToggle>
<my-global-icon iconName="more-vertical"></my-global-icon>
</button>
<div ngbDropdownMenu>
<ng-container *ngIf="playlistElement.video">

View File

@ -29,24 +29,6 @@ my-video-thumbnail,
padding: 10px;
border-bottom: 1px solid $separator-border-color;
.more {
display: flex;
}
&:hover {
background-color: rgba(0, 0, 0, 0.05);
.more {
opacity: 1;
}
}
@media not all and (hover: hover) and (pointer: fine) {
.more {
opacity: 1 !important;
}
}
&.playing {
background-color: rgba(0, 0, 0, 0.02);
}
@ -87,20 +69,38 @@ my-video-thumbnail,
}
.more {
display: flex;
align-items: center;
}
.more-button {
opacity: 0;
&.show {
opacity: 1;
&::after {
border: 0;
}
.icon-more {
my-global-icon {
@include apply-svg-color(pvar(--greyForegroundColor));
}
}
display: flex;
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 0.05);
}
&::after {
border: 0;
}
&:hover,
&:focus-within,
.show {
.more-button {
opacity: 1;
}
}
@media not all and (hover: hover) and (pointer: fine) {
.more-button {
opacity: 1 !important;
}
}
}
@ -183,7 +183,7 @@ my-video-thumbnail,
my-edit-button {
display: none;
+ .more {
+ .more-button {
display: inline-flex;
}
}
@ -204,7 +204,7 @@ my-video-thumbnail,
}
}
my-edit-button + .more {
my-edit-button + .more-button {
display: none;
}
}

View File

@ -17,16 +17,14 @@ class ChaptersPlugin extends Plugin {
this.player.ready(() => {
player.addClass('vjs-chapters')
this.player.one('durationchange', () => {
for (const chapter of this.chapters) {
if (chapter.timecode === 0) continue
for (const chapter of this.chapters) {
if (chapter.timecode === 0) continue
const marker = new ProgressBarMarkerComponent(player, { timecode: chapter.timecode })
const marker = new ProgressBarMarkerComponent(player, { timecode: chapter.timecode })
this.markers.push(marker)
this.getSeekBar().addChild(marker)
}
})
this.markers.push(marker)
this.getSeekBar().addChild(marker)
}
})
}
@ -34,6 +32,8 @@ class ChaptersPlugin extends Plugin {
for (const marker of this.markers) {
this.getSeekBar().removeChild(marker)
}
super.dispose()
}
getChapter (timecode: number) {

View File

@ -9,16 +9,25 @@ export class ProgressBarMarkerComponent extends Component {
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (player: videojs.Player, options?: ProgressBarMarkerComponentOptions & videojs.ComponentOptions) {
super(player, options)
const updateMarker = () => {
(this.el() as HTMLElement).style.setProperty('left', this.buildLeftStyle())
}
this.player().on('durationchange', updateMarker)
this.one('dispose', () => this.player().off('durationchange', updateMarker))
}
createEl () {
const left = (this.options_.timecode / this.player().duration()) * 100
return videojs.dom.createEl('span', {
className: 'vjs-marker',
style: `left: ${left}%`
style: `left: ${this.buildLeftStyle()}`
}) as HTMLButtonElement
}
private buildLeftStyle () {
return `${(this.options_.timecode / this.player().duration()) * 100}%`
}
}
videojs.registerComponent('ProgressBarMarkerComponent', ProgressBarMarkerComponent)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -115,7 +115,7 @@
"bencode": "^4.0.0",
"bittorrent-tracker": "^10.0.12",
"bluebird": "^3.5.0",
"bullmq": "^3.6.6",
"bullmq": "^4.12.3",
"bytes": "^3.0.0",
"chokidar": "^3.4.2",
"commander": "^11.0.0",

View File

@ -172,15 +172,21 @@ async function getVideoStream (path: string, existingProbe?: FfprobeData) {
// Chapters
// ---------------------------------------------------------------------------
async function getChaptersFromContainer (path: string, existingProbe?: FfprobeData) {
const metadata = existingProbe || await ffprobePromise(path)
async function getChaptersFromContainer (options: {
path: string
maxTitleLength: number
ffprobe?: FfprobeData
}) {
const { path, maxTitleLength, ffprobe } = options
const metadata = ffprobe || await ffprobePromise(path)
if (!Array.isArray(metadata?.chapters)) return []
return metadata.chapters
.map(c => ({
timecode: c.start_time,
title: c['TAG:title']
timecode: Math.round(c.start_time),
title: (c['TAG:title'] || '').slice(0, maxTitleLength)
}))
}

View File

@ -3,7 +3,7 @@ import { VideoStateType } from '../videos/index.js'
import { VideoStudioTaskCut } from '../videos/studio/index.js'
import { SendEmailOptions } from './emailer.model.js'
export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' | 'waiting-children'
export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' | 'waiting-children' | 'prioritized'
export type JobType =
| 'activitypub-cleaner'

View File

@ -19,7 +19,7 @@ describe('Test videos chapters API validator', function () {
// ---------------------------------------------------------------
before(async function () {
this.timeout(30000)
this.timeout(60000)
server = await createSingleServer(1)

View File

@ -199,7 +199,7 @@ describe('Test video studio API validator', function () {
})
it('Should fail with a video that is already waiting for edition', async function () {
this.timeout(120000)
this.timeout(360000)
await command.createEditionTasks({
videoId: videoUUID,
@ -257,7 +257,7 @@ describe('Test video studio API validator', function () {
})
it('Should succeed with the correct params', async function () {
this.timeout(120000)
this.timeout(360000)
await cut(0, 2, HttpStatusCode.NO_CONTENT_204)
@ -291,7 +291,7 @@ describe('Test video studio API validator', function () {
})
it('Should succeed with the correct params', async function () {
this.timeout(120000)
this.timeout(360000)
await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204)
@ -337,7 +337,7 @@ describe('Test video studio API validator', function () {
})
it('Should succeed with the correct params', async function () {
this.timeout(120000)
this.timeout(360000)
await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204)
await waitJobs([ server ])
@ -347,7 +347,7 @@ describe('Test video studio API validator', function () {
})
it('Should check total quota when creating the task', async function () {
this.timeout(120000)
this.timeout(360000)
const user = await server.users.create({ username: 'user_quota_1' })
const token = await server.login.getAccessToken('user_quota_1')

View File

@ -66,7 +66,7 @@ describe('Test admin notifications', function () {
joinPeerTubeServer.setLatestVersion('1.4.2')
await wait(3000)
await wait(4500)
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' })
})
@ -75,14 +75,14 @@ describe('Test admin notifications', function () {
joinPeerTubeServer.setLatestVersion('15.4.2')
await wait(3000)
await wait(4500)
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' })
})
it('Should not send the same notification to admins', async function () {
this.timeout(30000)
await wait(3000)
await wait(4500)
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
})
@ -97,7 +97,7 @@ describe('Test admin notifications', function () {
joinPeerTubeServer.setLatestVersion('15.4.3')
await wait(3000)
await wait(4500)
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' })
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
})

View File

@ -39,7 +39,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new comment notification after a comment on another video', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -51,7 +51,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new comment notification if I comment my own video', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -63,7 +63,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new comment notification if the account is muted', async function () {
this.timeout(30000)
this.timeout(60000)
await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
@ -79,7 +79,7 @@ describe('Test comments notifications', function () {
})
it('Should send a new comment notification after a local comment on my video', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -91,7 +91,7 @@ describe('Test comments notifications', function () {
})
it('Should send a new comment notification after a remote comment on my video', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -109,7 +109,7 @@ describe('Test comments notifications', function () {
})
it('Should send a new comment notification after a local reply on my video', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -122,7 +122,7 @@ describe('Test comments notifications', function () {
})
it('Should send a new comment notification after a remote reply on my video', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
await waitJobs(servers)
@ -148,7 +148,7 @@ describe('Test comments notifications', function () {
})
it('Should convert markdown in comment to html', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } })
@ -178,7 +178,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new mention comment notification if I mention the video owner', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -189,7 +189,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new mention comment notification if I mention myself', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -200,7 +200,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new mention notification if the account is muted', async function () {
this.timeout(30000)
this.timeout(60000)
await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
@ -215,7 +215,7 @@ describe('Test comments notifications', function () {
})
it('Should not send a new mention notification if the remote account mention a local account', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -229,7 +229,7 @@ describe('Test comments notifications', function () {
})
it('Should send a new mention notification after local comments', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -245,7 +245,7 @@ describe('Test comments notifications', function () {
})
it('Should send a new mention notification after remote comments', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -277,7 +277,7 @@ describe('Test comments notifications', function () {
})
it('Should convert markdown in comment to html', async function () {
this.timeout(30000)
this.timeout(60000)
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } })

View File

@ -233,7 +233,7 @@ describe('Test user notifications', function () {
})
it('Should not send a notification if the wait transcoding is false', async function () {
this.timeout(100_000)
this.timeout(240000)
await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false })
await waitJobs(servers)
@ -245,7 +245,7 @@ describe('Test user notifications', function () {
})
it('Should send a notification even if the video is not transcoded in other resolutions', async function () {
this.timeout(100_000)
this.timeout(240000)
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' })
await waitJobs(servers)
@ -254,7 +254,7 @@ describe('Test user notifications', function () {
})
it('Should send a notification with a transcoded video', async function () {
this.timeout(100_000)
this.timeout(240000)
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
await waitJobs(servers)
@ -263,7 +263,7 @@ describe('Test user notifications', function () {
})
it('Should send a notification when an imported video is transcoded', async function () {
this.timeout(120000)
this.timeout(240000)
const name = 'video import ' + buildUUID()

View File

@ -232,7 +232,7 @@ describe('Test stats (excluding redundancy)', function () {
})
it('Should have the correct AP stats', async function () {
this.timeout(120000)
this.timeout(240000)
await servers[0].config.disableTranscoding()

View File

@ -489,7 +489,7 @@ describe('Test video transcoding', function () {
})
it('Should downscale to the closest divisor standard framerate', async function () {
this.timeout(200_000)
this.timeout(360_000)
let tempFixturePath: string

View File

@ -74,7 +74,7 @@ describe('Test video playlists', function () {
let commands: PlaylistsCommand[]
before(async function () {
this.timeout(240000)
this.timeout(360000)
servers = await createMultipleServers(3)

View File

@ -19,7 +19,7 @@ import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@peertube
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { MIMETYPES } from '../../../initializers/constants.js'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { Hooks } from '../../../lib/plugins/hooks.js'
import { generateLocalVideoMiniature } from '../../../lib/thumbnail.js'
@ -145,7 +145,10 @@ async function addVideo (options: {
const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' })
const originalFilename = videoPhysicalFile.originalname
const containerChapters = await getChaptersFromContainer(videoPhysicalFile.path)
const containerChapters = await getChaptersFromContainer({
path: videoPhysicalFile.path,
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max
})
logger.debug(`Got ${containerChapters.length} chapters from video "${video.name}" container`, { containerChapters, ...lTags(video.uuid) })
// Move physical file

View File

@ -2,10 +2,10 @@ import { JobState } from '@peertube/peertube-models'
import { jobTypes } from '@server/lib/job-queue/job-queue.js'
import { exists } from './misc.js'
const jobStates: JobState[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children' ]
const jobStates = new Set<JobState>([ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children', 'prioritized' ])
function isValidJobState (value: JobState) {
return exists(value) && jobStates.includes(value)
return exists(value) && jobStates.has(value)
}
function isValidJobType (value: any) {

View File

@ -41,7 +41,7 @@ import {
import { logger } from '../../../helpers/logger.js'
import { getSecureTorrentName } from '../../../helpers/utils.js'
import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent.js'
import { JOB_TTL } from '../../../initializers/constants.js'
import { CONSTRAINTS_FIELDS, JOB_TTL } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { VideoFileModel } from '../../../models/video/video-file.js'
import { VideoImportModel } from '../../../models/video/video-import.js'
@ -143,16 +143,20 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
throw new Error('The user video quota is exceeded with this video to import.')
}
const probe = await ffprobePromise(tempVideoPath)
const ffprobe = await ffprobePromise(tempVideoPath)
const { resolution } = await isAudioFile(tempVideoPath, probe)
const { resolution } = await isAudioFile(tempVideoPath, ffprobe)
? { resolution: VideoResolution.H_NOVIDEO }
: await getVideoStreamDimensionsInfo(tempVideoPath, probe)
: await getVideoStreamDimensionsInfo(tempVideoPath, ffprobe)
const fps = await getVideoStreamFPS(tempVideoPath, probe)
const duration = await getVideoStreamDuration(tempVideoPath, probe)
const fps = await getVideoStreamFPS(tempVideoPath, ffprobe)
const duration = await getVideoStreamDuration(tempVideoPath, ffprobe)
const containerChapters = await getChaptersFromContainer(tempVideoPath, probe)
const containerChapters = await getChaptersFromContainer({
path: tempVideoPath,
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
ffprobe
})
// Prepare video file object for creation in database
const fileExt = getLowercaseExtension(tempVideoPath)

View File

@ -257,6 +257,9 @@ class JobQueue {
queue.on('error', err => { logger.error('Error in job queue %s.', handlerName, { err }) })
this.queues[handlerName] = queue
queue.removeDeprecatedPriorityKey()
.catch(err => logger.error('Cannot remove bullmq deprecated priority keys of ' + handlerName, { err }))
}
private buildQueueEvent (handlerName: JobType) {
@ -455,12 +458,15 @@ class JobQueue {
}
private buildStateFilter (state?: JobState) {
if (!state) return jobStates
if (!state) return Array.from(jobStates)
const states = [ state ]
// Include parent if filtering on waiting
if (state === 'waiting') states.push('waiting-children')
// Include parent and prioritized if filtering on waiting
if (state === 'waiting') {
states.push('waiting-children')
states.push('prioritized')
}
return states
}

View File

@ -1,4 +1,4 @@
import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@peertube/peertube-core-utils'
import { buildVideoEmbedPath, buildVideoWatchPath, pick, wait } from '@peertube/peertube-core-utils'
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg'
import {
ResultList,
@ -1925,7 +1925,20 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
? getHLSRedundancyDirectory(this)
: getHLSDirectory(this)
await remove(directoryPath)
try {
await remove(directoryPath)
} catch (err) {
// If it's a live, ffmpeg may have added another file while fs-extra is removing the directory
// So wait a little bit and retry
if (err.code === 'ENOTEMPTY') {
await wait(1000)
await remove(directoryPath)
return
}
throw err
}
if (isRedundancy !== true) {
const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo

View File

@ -3572,17 +3572,18 @@ builtins@^5.0.1:
dependencies:
semver "^7.0.0"
bullmq@^3.6.6:
version "3.15.8"
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-3.15.8.tgz#e8ec5b46b0b7d7ce57e509280d03745109411e05"
integrity sha512-k3uimHGhl5svqD7SEak+iI6c5DxeLOaOXzCufI9Ic0ST3nJr69v71TGR4cXCTXdgCff3tLec5HgoBnfyWjgn5A==
bullmq@^4.12.3:
version "4.12.3"
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-4.12.3.tgz#0c649b9a5e48227519c526ee9edd96b982eee22d"
integrity sha512-4uPp4NQTALFF+eFK7g8VJM+rt0aiduQdzBomgiEO1OK4OE+TdgC6cjGXooKI/asuB8iDhSZ+pSnGYy5Xyr6qRA==
dependencies:
cron-parser "^4.6.0"
glob "^8.0.3"
ioredis "^5.3.2"
lodash "^4.17.21"
msgpackr "^1.6.2"
semver "^7.3.7"
node-abort-controller "^3.1.1"
semver "^7.5.4"
tslib "^2.0.0"
uuid "^9.0.0"
@ -7458,6 +7459,11 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-abort-controller@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
node-addon-api@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@ -8911,7 +8917,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==