mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-02 17:29:29 +02:00
Add ability to customize player settings
This commit is contained in:
parent
b742dbc0fc
commit
74e97347bb
133 changed files with 2809 additions and 783 deletions
|
@ -244,7 +244,7 @@
|
|||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "120kb"
|
||||
"maximumError": "140kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
|
|
|
@ -40,6 +40,7 @@ my-select-videos-sort,
|
|||
my-select-videos-scope,
|
||||
my-select-checkbox,
|
||||
my-select-options,
|
||||
my-select-player-theme,
|
||||
my-select-custom-value {
|
||||
display: block;
|
||||
|
||||
|
|
|
@ -7,26 +7,32 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="theme">
|
||||
<div class="form-group">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
<div class="form-group" formGroupName="theme">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
</ng-container>
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="miniature">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName"
|
||||
formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText
|
||||
labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group" formGroupName="miniature">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName"
|
||||
formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText
|
||||
labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="player">
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPlayerTheme">Player Theme</label>
|
||||
|
||||
<my-select-player-theme mode="instance" formControlName="theme" inputId="defaultsPlayerTheme"></my-select-player-theme>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/shared-forms/peertube-che
|
|||
import { SelectCustomValueComponent } from '@app/shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { CustomConfig, PlayerTheme } from '@peertube/peertube-models'
|
||||
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
||||
import debug from 'debug'
|
||||
|
@ -20,6 +20,7 @@ import { AdminConfigService } from '../../../shared/shared-admin/admin-config.se
|
|||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
import { SelectPlayerThemeComponent } from '@app/shared/shared-forms/select/select-player-theme.component'
|
||||
|
||||
const debugLogger = debug('peertube:config')
|
||||
|
||||
|
@ -65,6 +66,12 @@ type Form = {
|
|||
inputBorderRadius: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
|
||||
defaults: FormGroup<{
|
||||
player: FormGroup<{
|
||||
theme: FormControl<PlayerTheme>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
type FieldType = 'color' | 'radius'
|
||||
|
@ -84,7 +91,8 @@ type FieldType = 'color' | 'radius'
|
|||
SelectOptionsComponent,
|
||||
HelpComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
SelectCustomValueComponent
|
||||
SelectCustomValueComponent,
|
||||
SelectPlayerThemeComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
|
@ -108,6 +116,7 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can
|
|||
}[] = []
|
||||
|
||||
availableThemes: SelectOptionsItem[]
|
||||
availablePlayerThemes: SelectOptionsItem<PlayerTheme>[] = []
|
||||
|
||||
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
||||
private customConfig: CustomConfig
|
||||
|
@ -164,6 +173,11 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can
|
|||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.availablePlayerThemes = [
|
||||
{ id: 'galaxy', label: $localize`Galaxy`, description: $localize`Original theme` },
|
||||
{ id: 'lucide', label: $localize`Lucide`, description: $localize`A clean and modern theme` }
|
||||
]
|
||||
|
||||
this.buildForm()
|
||||
this.subscribeToCustomizationChanges()
|
||||
|
||||
|
@ -265,6 +279,11 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can
|
|||
headerBackgroundColor: null,
|
||||
inputBorderRadius: null
|
||||
}
|
||||
},
|
||||
defaults: {
|
||||
player: {
|
||||
theme: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="form-group" formGroupName="instance">
|
||||
<label i18n id="instanceDefaultClientRouteLabel" for="instanceDefaultClientRoute">Landing page</label>
|
||||
|
||||
|
@ -43,13 +42,14 @@
|
|||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
|
||||
<ng-container formGroupName="menu">
|
||||
<ng-container formGroupName="login">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientMenuLoginRedirectOnSingleExternalAuth" formControlName="redirectOnSingleExternalAuth"
|
||||
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
inputName="clientMenuLoginRedirectOnSingleExternalAuth"
|
||||
formControlName="redirectOnSingleExternalAuth"
|
||||
i18n-labelText
|
||||
labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
@if (countExternalAuth() === 0) {
|
||||
|
@ -58,12 +58,11 @@
|
|||
<span i18n>⚠️ You have multiple external auth plugins enabled</span>
|
||||
}
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -76,20 +75,22 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="broadcastMessage">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable broadcast message"
|
||||
inputName="broadcastMessageEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable broadcast message"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageDismissable" formControlName="dismissable"
|
||||
i18n-labelText labelText="Allow users to dismiss the broadcast message "
|
||||
inputName="broadcastMessageDismissable"
|
||||
formControlName="dismissable"
|
||||
i18n-labelText
|
||||
labelText="Allow users to dismiss the broadcast message "
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
|
@ -111,31 +112,28 @@
|
|||
<label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="broadcastMessageMessage" formControlName="message"
|
||||
[formError]="formErrors.broadcastMessage.message" markdownType="to-unsafe-html"
|
||||
inputId="broadcastMessageMessage"
|
||||
formControlName="message"
|
||||
[formError]="formErrors.broadcastMessage.message"
|
||||
markdownType="to-unsafe-html"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.broadcastMessage.message" class="form-error" role="alert">{{ formErrors.broadcastMessage.message }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- new users grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<!-- new users grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>NEW USERS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="signup">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="signupEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable Signup"
|
||||
>
|
||||
<my-peertube-checkbox inputName="signupEnabled" formControlName="enabled" i18n-labelText labelText="Enable Signup">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
|
||||
|
||||
|
@ -144,16 +142,22 @@
|
|||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresApproval" formControlName="requiresApproval"
|
||||
i18n-labelText labelText="Signup requires approval by moderators"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresApproval"
|
||||
formControlName="requiresApproval"
|
||||
i18n-labelText
|
||||
labelText="Signup requires approval by moderators"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
||||
i18n-labelText labelText="Signup requires email verification"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresEmailVerification"
|
||||
formControlName="requiresEmailVerification"
|
||||
i18n-labelText
|
||||
labelText="Signup requires email verification"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
|
@ -163,8 +167,12 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="-1" id="signupLimit" class="form-control"
|
||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors.signup.limit }"
|
||||
type="number"
|
||||
min="-1"
|
||||
id="signupLimit"
|
||||
class="form-control"
|
||||
formControlName="limit"
|
||||
[ngClass]="{ 'input-error': formErrors.signup.limit }"
|
||||
>
|
||||
<span i18n>{form.value.signup.limit, plural, =1 {user} other {users}}</span>
|
||||
</div>
|
||||
|
@ -179,8 +187,12 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="signupMinimumAge" class="form-control"
|
||||
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors.signup.minimumAge }"
|
||||
type="number"
|
||||
min="1"
|
||||
id="signupMinimumAge"
|
||||
class="form-control"
|
||||
formControlName="minimumAge"
|
||||
[ngClass]="{ 'input-error': formErrors.signup.minimumAge }"
|
||||
>
|
||||
<span i18n>{form.value.signup.minimumAge, plural, =1 {year old} other {years old}}</span>
|
||||
</div>
|
||||
|
@ -201,7 +213,9 @@
|
|||
inputId="userVideoQuota"
|
||||
[items]="getVideoQuotaOptions()"
|
||||
formControlName="videoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
i18n-inputSuffix
|
||||
inputSuffix="bytes"
|
||||
inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
|
@ -218,7 +232,9 @@
|
|||
inputId="userVideoQuotaDaily"
|
||||
[items]="getVideoQuotaDailyOptions()"
|
||||
formControlName="videoQuotaDaily"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
i18n-inputSuffix
|
||||
inputSuffix="bytes"
|
||||
inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
|
@ -228,15 +244,16 @@
|
|||
<ng-container formGroupName="history">
|
||||
<ng-container formGroupName="videos">
|
||||
<my-peertube-checkbox
|
||||
inputName="videosHistoryEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically enable video history for a new user"
|
||||
inputName="videosHistoryEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Automatically enable video history for a new user"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -246,11 +263,8 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
|
||||
<ng-container formGroupName="videos">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="importConcurrency">Import jobs concurrency</label>
|
||||
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart</span>
|
||||
|
@ -265,39 +279,46 @@
|
|||
|
||||
<div class="form-group" formGroupName="http">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosHttpEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||
inputName="importVideosHttpEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
<span i18n
|
||||
>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security"
|
||||
>a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrent">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosTorrentEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
|
||||
inputName="importVideosTorrentEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow import with a torrent file or a magnet URI"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ We don't recommend to enable this feature if you don't trust your users</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ We don't recommend to enable this feature if you don't trust your users</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoChannelSynchronization">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importSynchronizationEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow channel synchronization with channel of other platforms like YouTube"
|
||||
inputName="importSynchronizationEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow channel synchronization with channel of other platforms like YouTube"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
⛔ You need to allow import with HTTP URL to be able to activate this feature.
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
⛔ You need to allow import with HTTP URL to be able to activate this feature.
|
||||
</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
|
@ -306,16 +327,21 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
type="number"
|
||||
min="1"
|
||||
id="videoChannelSynchronizationMaxPerUser"
|
||||
class="form-control"
|
||||
formControlName="maxPerUser"
|
||||
[ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
>
|
||||
<span i18n>{form.value.import.videoChannelSynchronization.maxPerUser, plural, =1 {sync} other {syncs}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors.import.videoChannelSynchronization.maxPerUser }}</div>
|
||||
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">
|
||||
{{ formErrors.import.videoChannelSynchronization.maxPerUser }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -354,22 +380,21 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="autoBlacklist">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="ofUsers">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Block new videos automatically"
|
||||
inputName="autoBlacklistVideosOfUsersEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Block new videos automatically"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -378,8 +403,10 @@
|
|||
<ng-container formGroupName="update">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoFileUpdateEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow users to upload a new version of their video"
|
||||
inputName="videoFileUpdateEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow users to upload a new version of their video"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -388,10 +415,7 @@
|
|||
|
||||
<ng-container formGroupName="storyboards">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="storyboardsEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable video storyboards"
|
||||
>
|
||||
<my-peertube-checkbox inputName="storyboardsEnabled" formControlName="enabled" i18n-labelText labelText="Enable video storyboards">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video</span>
|
||||
</ng-container>
|
||||
|
@ -415,19 +439,19 @@
|
|||
|
||||
<ng-container formGroupName="videoTranscription">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoTranscriptionEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable video transcription"
|
||||
>
|
||||
<my-peertube-checkbox inputName="videoTranscriptionEnabled" formControlName="enabled" i18n-labelText labelText="Enable video transcription">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n><a href="https://docs.joinpeertube.org/admin/configuration#automatic-transcription" target="_blank">Automatically create subtitles</a> for uploaded/imported VOD videos</span>
|
||||
<span i18n><a href="https://docs.joinpeertube.org/admin/configuration#automatic-transcription" target="_blank">Automatically create subtitles</a>
|
||||
for uploaded/imported VOD videos</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getTranscriptionRunnerDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoTranscriptionRemoteRunnersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable remote runners for transcription"
|
||||
inputName="videoTranscriptionRemoteRunnersEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable remote runners for transcription"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks.</div>
|
||||
|
@ -442,7 +466,6 @@
|
|||
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="publish">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPublishPrivacy">Default video privacy</label>
|
||||
|
||||
|
@ -482,8 +505,12 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelsMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors.videoChannels.maxPerUser }"
|
||||
type="number"
|
||||
min="1"
|
||||
id="videoChannelsMaxPerUser"
|
||||
class="form-control"
|
||||
formControlName="maxPerUser"
|
||||
[ngClass]="{ 'input-error': formErrors.videoChannels.maxPerUser }"
|
||||
>
|
||||
<span i18n>{form.value.videoChannels.maxPerUser, plural, =1 {channel} other {channels}}</span>
|
||||
</div>
|
||||
|
@ -502,12 +529,13 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="videoComments">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoCommentsAcceptRemoteComments" formControlName="acceptRemoteComments"
|
||||
i18n-labelText labelText="Accept comments made on remote platforms"
|
||||
inputName="videoCommentsAcceptRemoteComments"
|
||||
formControlName="acceptRemoteComments"
|
||||
i18n-labelText
|
||||
labelText="Accept comments made on remote platforms"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current comments from remote platforms will not be deleted</span>
|
||||
|
@ -520,22 +548,25 @@
|
|||
<ng-container formGroupName="channels">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersChannelsEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Remote actors can follow channels of your platform"
|
||||
inputName="followersChannelsEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Remote actors can follow channels of your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current followers of channels of your platform will not be affected</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Remote actors can follow your platform"
|
||||
inputName="followersInstanceEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Remote actors can follow your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current followers of your platform will not be affected</span>
|
||||
|
@ -545,8 +576,10 @@
|
|||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
||||
i18n-labelText labelText="Manually approve new followers that follow your platform"
|
||||
inputName="followersInstanceManualApproval"
|
||||
formControlName="manualApproval"
|
||||
i18n-labelText
|
||||
labelText="Manually approve new followers that follow your platform"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -554,12 +587,13 @@
|
|||
|
||||
<ng-container formGroupName="followings">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<ng-container formGroupName="autoFollowBack">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow back followers that follow your platform"
|
||||
inputName="followingsInstanceAutoFollowBackEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Automatically follow back followers that follow your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
|
||||
|
@ -571,14 +605,21 @@
|
|||
<ng-container formGroupName="autoFollowIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow platforms of a public index"
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Automatically follow platforms of a public index"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||
|
||||
<span i18n>
|
||||
See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
See <a
|
||||
class="link-primary"
|
||||
href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>the documentation</a> for more information about the expected URL
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
|
@ -586,19 +627,22 @@
|
|||
<div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
|
||||
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
||||
<input
|
||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors.followings.instance.autoFollowIndex.indexUrl }"
|
||||
type="text"
|
||||
id="followingsInstanceAutoFollowIndexUrl"
|
||||
class="form-control"
|
||||
formControlName="indexUrl"
|
||||
[ngClass]="{ 'input-error': formErrors.followings.instance.autoFollowIndex.indexUrl }"
|
||||
>
|
||||
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">
|
||||
{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -609,27 +653,34 @@
|
|||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="player">
|
||||
|
||||
<div class="form-group" formGroupName="player">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsPlayerAutoplay" formControlName="autoPlay"
|
||||
i18n-labelText labelText="Automatically play videos in the player"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsPlayerAutoplay"
|
||||
formControlName="autoPlay"
|
||||
i18n-labelText
|
||||
labelText="Automatically play videos in the player"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="p2p">
|
||||
|
||||
<div class="form-group" formGroupName="webapp">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsP2PWebappEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable P2P streaming by default on your platform"
|
||||
inputName="defaultsP2PWebappEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable P2P streaming by default on your platform"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="embed">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsP2PEmbedEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable P2P streaming by default for videos embedded on external websites"
|
||||
inputName="defaultsP2PEmbedEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable P2P streaming by default for videos embedded on external websites"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -643,14 +694,14 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="search">
|
||||
<ng-container formGroupName="remoteUri">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchRemoteUriUsers" formControlName="users"
|
||||
i18n-labelText labelText="Allow users to do remote URI/handle search"
|
||||
inputName="searchRemoteUriUsers"
|
||||
formControlName="users"
|
||||
i18n-labelText
|
||||
labelText="Allow users to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||
|
@ -660,23 +711,21 @@
|
|||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchRemoteUriAnonymous" formControlName="anonymous"
|
||||
i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
|
||||
inputName="searchRemoteUriAnonymous"
|
||||
formControlName="anonymous"
|
||||
i18n-labelText
|
||||
labelText="Allow anonymous to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="searchIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable global search"
|
||||
>
|
||||
<my-peertube-checkbox inputName="searchIndexEnabled" formControlName="enabled" i18n-labelText labelText="Enable global search">
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality depends heavily on the moderation of platforms followed by the search index you select</div>
|
||||
</ng-container>
|
||||
|
@ -690,39 +739,44 @@
|
|||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="searchIndexUrl" class="form-control"
|
||||
formControlName="url" [ngClass]="{ 'input-error': formErrors.search.searchIndex.url }"
|
||||
type="text"
|
||||
id="searchIndexUrl"
|
||||
class="form-control"
|
||||
formControlName="url"
|
||||
[ngClass]="{ 'input-error': formErrors.search.searchIndex.url }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.search.searchIndex.url" class="form-error" role="alert">{{ formErrors.search.searchIndex.url }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
|
||||
i18n-labelText labelText="Disable local search in search bar"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexDisableLocalSearch"
|
||||
formControlName="disableLocalSearch"
|
||||
i18n-labelText
|
||||
labelText="Disable local search in search bar"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
|
||||
i18n-labelText labelText="Search bar uses the global search index by default"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexIsDefaultSearch"
|
||||
formControlName="isDefaultSearch"
|
||||
i18n-labelText
|
||||
labelText="Search bar uses the global search index by default"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Otherwise, the local search will be used by default</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -732,14 +786,10 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
<ng-container formGroupName="users">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to import a data archive"
|
||||
>
|
||||
<my-peertube-checkbox inputName="importUsersEnabled" formControlName="enabled" i18n-labelText labelText="Allow your users to import a data archive">
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Video quota is checked on import so the user doesn't upload a too big archive file</div>
|
||||
<div i18n>Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import</div>
|
||||
|
@ -750,20 +800,14 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="export">
|
||||
|
||||
<ng-container formGroupName="users">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="exportUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to export their data"
|
||||
>
|
||||
<my-peertube-checkbox inputName="exportUsersEnabled" formControlName="enabled" i18n-labelText labelText="Allow your users to export their data">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Users can export their PeerTube data in a .zip for backup or re-import. Only one export at a time is allowed per user</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n id="exportUsersMaxUserVideoQuota" for="exportUsersMaxUserVideoQuota">Max user video quota allowed to generate the export</label>
|
||||
|
||||
|
@ -774,7 +818,9 @@
|
|||
inputId="exportUsersMaxUserVideoQuota"
|
||||
[items]="exportMaxUserVideoQuotaOptions"
|
||||
formControlName="maxUserVideoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
i18n-inputSuffix
|
||||
inputSuffix="bytes"
|
||||
inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
|
@ -784,20 +830,21 @@
|
|||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n for="exportUsersExportExpiration">User export expiration</label>
|
||||
|
||||
<my-select-options inputId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"></my-select-options>
|
||||
<my-select-options
|
||||
inputId="exportUsersExportExpiration"
|
||||
[items]="exportExpirationOptions"
|
||||
formControlName="exportExpiration"
|
||||
></my-select-options>
|
||||
|
||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period</div>
|
||||
|
||||
<div *ngIf="formErrors.export.users.exportExpiration" class="form-error" role="alert">{{ formErrors.export.users.exportExpiration }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
|
|
@ -24,7 +24,14 @@ import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@a
|
|||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { BroadcastMessageLevel, CustomConfig, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import {
|
||||
BroadcastMessageLevel,
|
||||
CustomConfig,
|
||||
PlayerTheme,
|
||||
VideoCommentPolicyType,
|
||||
VideoConstant,
|
||||
VideoPrivacyType
|
||||
} from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
|
||||
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { SearchService } from '@app/shared/shared-search/search.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
|
@ -8,11 +9,11 @@ import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
|||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import { OverviewService } from '../+video-list'
|
||||
import { VideoRecommendationService } from './shared'
|
||||
import { VideoWatchComponent } from './video-watch.component'
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -30,7 +31,8 @@ export default [
|
|||
AbuseService,
|
||||
UserAdminService,
|
||||
BulkService,
|
||||
VideoStateMessageService
|
||||
VideoStateMessageService,
|
||||
PlayerSettingsService
|
||||
],
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -29,12 +29,16 @@ import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription/s
|
|||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { getVideoWatchRSSFeeds, timeToInt } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HTMLServerConfig,
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
PeerTubeProblemDocument,
|
||||
PlayerMode,
|
||||
PlayerTheme,
|
||||
PlayerVideoSettings,
|
||||
ServerErrorCode,
|
||||
Storyboard,
|
||||
VideoCaption,
|
||||
|
@ -51,8 +55,6 @@ import {
|
|||
PeerTubePlayer,
|
||||
PeerTubePlayerConstructorOptions,
|
||||
PeerTubePlayerLoadOptions,
|
||||
PeerTubePlayerTheme,
|
||||
PlayerMode,
|
||||
videojs,
|
||||
VideojsPlayer
|
||||
} from '@peertube/player'
|
||||
|
@ -80,7 +82,7 @@ const debugLogger = debug('peertube:watch:VideoWatchComponent')
|
|||
|
||||
type URLOptions = {
|
||||
playerMode: PlayerMode
|
||||
playerTheme?: PeerTubePlayerTheme
|
||||
playerTheme?: PlayerTheme
|
||||
|
||||
startTime: number | string
|
||||
stopTime: number | string
|
||||
|
@ -140,6 +142,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
private zone = inject(NgZone)
|
||||
private videoCaptionService = inject(VideoCaptionService)
|
||||
private videoChapterService = inject(VideoChapterService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private hotkeysService = inject(HotkeysService)
|
||||
private hooks = inject(HooksService)
|
||||
private pluginService = inject(PluginService)
|
||||
|
@ -163,6 +166,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
liveVideo: LiveVideo
|
||||
videoPassword: string
|
||||
storyboards: Storyboard[] = []
|
||||
playerSettings: PlayerVideoSettings
|
||||
|
||||
playlistPosition: number
|
||||
playlist: VideoPlaylist = null
|
||||
|
@ -374,9 +378,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.videoCaptionService.listCaptions(videoId, videoPassword),
|
||||
this.videoChapterService.getChapters({ videoId, videoPassword }),
|
||||
this.videoService.getStoryboards(videoId, videoPassword),
|
||||
this.playerSettingsService.getVideoSettings({ videoId, videoPassword, raw: false }),
|
||||
this.userService.getAnonymousOrLoggedUser()
|
||||
]).subscribe({
|
||||
next: ([ { video, live, videoFileToken }, captionsResult, chaptersResult, storyboards, loggedInOrAnonymousUser ]) => {
|
||||
next: ([ { video, live, videoFileToken }, captionsResult, chaptersResult, storyboards, playerSettings, loggedInOrAnonymousUser ]) => {
|
||||
this.onVideoFetched({
|
||||
video,
|
||||
live,
|
||||
|
@ -385,6 +390,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
storyboards,
|
||||
videoFileToken,
|
||||
videoPassword,
|
||||
playerSettings,
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay
|
||||
}).catch(err => {
|
||||
|
@ -491,6 +497,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
storyboards: Storyboard[]
|
||||
videoFileToken: string
|
||||
videoPassword: string
|
||||
playerSettings: PlayerVideoSettings
|
||||
|
||||
loggedInOrAnonymousUser: User
|
||||
forceAutoplay: boolean
|
||||
|
@ -503,6 +510,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
storyboards,
|
||||
videoFileToken,
|
||||
videoPassword,
|
||||
playerSettings,
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay
|
||||
} = options
|
||||
|
@ -516,6 +524,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.videoFileToken = videoFileToken
|
||||
this.videoPassword = videoPassword
|
||||
this.storyboards = storyboards
|
||||
this.playerSettings = playerSettings
|
||||
|
||||
// Re init attributes
|
||||
this.remoteServerDown = false
|
||||
|
@ -579,6 +588,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
liveVideo: this.liveVideo,
|
||||
videoFileToken: this.videoFileToken,
|
||||
videoPassword: this.videoPassword,
|
||||
playerSettings: this.playerSettings,
|
||||
urlOptions: this.getUrlOptions(),
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay,
|
||||
|
@ -727,6 +737,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
videoCaptions: VideoCaption[]
|
||||
videoChapters: VideoChapter[]
|
||||
storyboards: Storyboard[]
|
||||
playerSettings: PlayerVideoSettings
|
||||
|
||||
videoFileToken: string
|
||||
videoPassword: string
|
||||
|
@ -747,7 +758,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
videoPassword,
|
||||
urlOptions,
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay
|
||||
forceAutoplay,
|
||||
playerSettings
|
||||
} = options
|
||||
|
||||
let mode: PlayerMode
|
||||
|
@ -816,7 +828,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
return {
|
||||
mode,
|
||||
theme: urlOptions.playerTheme || 'default',
|
||||
theme: urlOptions.playerTheme || playerSettings.theme as PlayerTheme,
|
||||
|
||||
autoplay: this.isAutoplay(video, loggedInOrAnonymousUser),
|
||||
forceAutoplay,
|
||||
|
|
|
@ -8,6 +8,8 @@ import { manageRoutes } from '../shared-manage/routes'
|
|||
import { VideoStudioService } from '../shared-manage/studio/video-studio.service'
|
||||
import { VideoManageComponent } from './video-manage.component'
|
||||
import { VideoManageResolver } from './video-manage.resolver'
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -16,12 +18,14 @@ export default [
|
|||
canActivate: [ LoginGuard ],
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
providers: [
|
||||
VideoManageController,
|
||||
VideoManageResolver,
|
||||
LiveVideoService,
|
||||
I18nPrimengCalendarService,
|
||||
VideoUploadService,
|
||||
VideoStudioService,
|
||||
VideoStateMessageService
|
||||
VideoStateMessageService,
|
||||
PlayerSettingsService
|
||||
],
|
||||
resolve: {
|
||||
resolverData: VideoManageResolver
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<div class="margin-content">
|
||||
<my-video-manage-container
|
||||
*ngIf="loaded"
|
||||
canUpdate="true" canWatch="true" cancelLink="/my-library/videos" (videoUpdated)="onVideoUpdated()"
|
||||
></my-video-manage-container>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/cor
|
|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||
import { VideoEdit } from '../shared-manage/common/video-edit.model'
|
||||
import { VideoManageContainerComponent } from '../shared-manage/video-manage-container.component'
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { VideoManageResolverData } from './video-manage.resolver'
|
||||
|
@ -16,8 +15,7 @@ import { VideoManageResolverData } from './video-manage.resolver'
|
|||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
VideoManageContainerComponent
|
||||
],
|
||||
providers: [ VideoManageController ]
|
||||
]
|
||||
})
|
||||
export class VideoManageComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private route = inject(ActivatedRoute)
|
||||
|
@ -29,18 +27,9 @@ export class VideoManageComponent implements OnInit, OnDestroy, CanComponentDeac
|
|||
isUpdatingVideo = false
|
||||
loaded = false
|
||||
|
||||
async ngOnInit () {
|
||||
ngOnInit () {
|
||||
const data = this.route.snapshot.data.resolverData as VideoManageResolverData
|
||||
const { video, userChannels, captions, chapters, videoSource, live, videoPasswords, userQuota, privacies } = data
|
||||
|
||||
const videoEdit = await VideoEdit.createFromAPI(this.serverService.getHTMLConfig(), {
|
||||
video,
|
||||
captions,
|
||||
chapters,
|
||||
live,
|
||||
videoSource,
|
||||
videoPasswords: videoPasswords.map(p => p.password)
|
||||
})
|
||||
const { userChannels, userQuota, privacies, videoEdit } = data
|
||||
|
||||
this.manageController.setStore({
|
||||
videoEdit,
|
||||
|
@ -50,8 +39,6 @@ export class VideoManageComponent implements OnInit, OnDestroy, CanComponentDeac
|
|||
})
|
||||
|
||||
this.manageController.setConfig({ manageType: 'update', serverConfig: this.serverService.getHTMLConfig() })
|
||||
|
||||
this.loaded = true
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { VideoService } from '@app/shared/shared-main/video/video.service'
|
|||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import {
|
||||
LiveVideo,
|
||||
PlayerVideoSettings,
|
||||
UserVideoQuota,
|
||||
VideoCaption,
|
||||
VideoChapter,
|
||||
|
@ -22,6 +23,8 @@ import {
|
|||
import { forkJoin, of } from 'rxjs'
|
||||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from '../../../types'
|
||||
import { VideoEdit } from '../shared-manage/common/video-edit.model'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
export type VideoManageResolverData = {
|
||||
video: VideoDetails
|
||||
|
@ -33,6 +36,8 @@ export type VideoManageResolverData = {
|
|||
videoPasswords: VideoPassword[]
|
||||
userQuota: UserVideoQuota
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
videoEdit: VideoEdit
|
||||
playerSettings: PlayerVideoSettings
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
@ -45,6 +50,7 @@ export class VideoManageResolver {
|
|||
private videoPasswordService = inject(VideoPasswordService)
|
||||
private userService = inject(UserService)
|
||||
private serverService = inject(ServerService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
resolve (route: ActivatedRouteSnapshot) {
|
||||
const uuid: string = route.params['uuid']
|
||||
|
@ -52,18 +58,32 @@ export class VideoManageResolver {
|
|||
return this.videoService.getVideo({ videoId: uuid })
|
||||
.pipe(
|
||||
switchMap(video => forkJoin(this.buildObservables(video))),
|
||||
map(([ video, videoSource, userChannels, captions, chapters, live, videoPasswords, userQuota, privacies ]) =>
|
||||
({
|
||||
video,
|
||||
userChannels,
|
||||
captions,
|
||||
chapters,
|
||||
videoSource,
|
||||
live,
|
||||
videoPasswords,
|
||||
userQuota,
|
||||
privacies
|
||||
}) as VideoManageResolverData
|
||||
switchMap(
|
||||
async ([ video, videoSource, userChannels, captions, chapters, live, videoPasswords, userQuota, privacies, playerSettings ]) => {
|
||||
const videoEdit = await VideoEdit.createFromAPI(this.serverService.getHTMLConfig(), {
|
||||
video,
|
||||
captions,
|
||||
chapters,
|
||||
live,
|
||||
videoSource,
|
||||
playerSettings,
|
||||
videoPasswords: videoPasswords.map(p => p.password)
|
||||
})
|
||||
|
||||
return {
|
||||
video,
|
||||
userChannels,
|
||||
captions,
|
||||
chapters,
|
||||
videoSource,
|
||||
live,
|
||||
videoPasswords,
|
||||
userQuota,
|
||||
privacies,
|
||||
videoEdit,
|
||||
playerSettings
|
||||
} satisfies VideoManageResolverData
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -94,11 +114,13 @@ export class VideoManageResolver {
|
|||
|
||||
video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
|
||||
? this.videoPasswordService.getVideoPasswords({ videoUUID: video.uuid })
|
||||
: of([]),
|
||||
: of([] as VideoPassword[]),
|
||||
|
||||
this.userService.getMyVideoQuotaUsed(),
|
||||
|
||||
this.serverService.getVideoPrivacies()
|
||||
]
|
||||
this.serverService.getVideoPrivacies(),
|
||||
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true })
|
||||
] as const
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { inject } from '@angular/core'
|
||||
import { RedirectCommand, Router, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import debug from 'debug'
|
||||
import { I18nPrimengCalendarService } from '../shared-manage/common/i18n-primeng-calendar.service'
|
||||
import { VideoUploadService } from '../shared-manage/common/video-upload.service'
|
||||
import { manageRoutes } from '../shared-manage/routes'
|
||||
|
@ -8,9 +12,6 @@ import { VideoStudioService } from '../shared-manage/studio/video-studio.service
|
|||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { VideoPublishComponent } from './video-publish.component'
|
||||
import { VideoPublishResolver } from './video-publish.resolver'
|
||||
import { inject } from '@angular/core'
|
||||
import debug from 'debug'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
|
||||
const debugLogger = debug('peertube:video-publish')
|
||||
|
||||
|
@ -43,6 +44,7 @@ export default [
|
|||
providers: [
|
||||
VideoPublishResolver,
|
||||
VideoManageController,
|
||||
PlayerSettingsService,
|
||||
VideoStateMessageService,
|
||||
LiveVideoService,
|
||||
I18nPrimengCalendarService,
|
||||
|
|
|
@ -41,8 +41,7 @@ import { VideoPublishResolverData } from './video-publish.resolver'
|
|||
VideoImportUrlComponent,
|
||||
VideoUploadComponent,
|
||||
HelpComponent
|
||||
],
|
||||
providers: [ VideoManageController ]
|
||||
]
|
||||
})
|
||||
export class VideoPublishComponent implements OnInit, CanComponentDeactivate {
|
||||
private auth = inject(AuthService)
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
LiveVideoCreate,
|
||||
LiveVideoUpdate,
|
||||
NSFWFlag,
|
||||
PlayerVideoSettings,
|
||||
PlayerVideoSettingsUpdate,
|
||||
VideoCaption,
|
||||
VideoChapter,
|
||||
VideoCreate,
|
||||
|
@ -65,6 +67,8 @@ type StudioForm = {
|
|||
'add-watermark'?: { file?: File }
|
||||
}
|
||||
|
||||
type PlayerSettingsForm = PlayerVideoSettingsUpdate
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type LoadFromPublishOptions = Required<Pick<VideoCreate, 'channelId' | 'support'>> & Partial<Pick<VideoCreate, 'name'>>
|
||||
|
@ -115,6 +119,7 @@ type UpdateFromAPIOptions = {
|
|||
captions?: VideoCaption[]
|
||||
videoPasswords?: string[]
|
||||
videoSource?: VideoSource
|
||||
playerSettings?: PlayerVideoSettings
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -143,6 +148,7 @@ export class VideoEdit {
|
|||
private live: LiveUpdate
|
||||
private replaceFile: File
|
||||
private studioTasks: VideoStudioTask[] = []
|
||||
private playerSettings: PlayerVideoSettings
|
||||
|
||||
private videoImport: Pick<VideoImportCreate, 'magnetUri' | 'torrentfile' | 'targetUrl'>
|
||||
|
||||
|
@ -185,6 +191,7 @@ export class VideoEdit {
|
|||
previewfile?: { size: number }
|
||||
|
||||
live?: LiveUpdate
|
||||
playerSettings?: PlayerVideoSettings
|
||||
|
||||
pluginData?: any
|
||||
pluginDefaults?: Record<string, string | boolean>
|
||||
|
@ -294,12 +301,13 @@ export class VideoEdit {
|
|||
}
|
||||
|
||||
async loadFromAPI (options: UpdateFromAPIOptions & { loadPrivacy?: boolean }) {
|
||||
const { video, videoPasswords, live, chapters, captions, videoSource, loadPrivacy = true } = options
|
||||
const { video, videoPasswords, live, chapters, captions, videoSource, playerSettings, loadPrivacy = true } = options
|
||||
|
||||
debugLogger('Load from API', options)
|
||||
|
||||
this.loadVideo({ video, videoPasswords, saveInStore: true, loadPrivacy })
|
||||
this.loadLive(live)
|
||||
this.loadPlayerSettings(playerSettings)
|
||||
|
||||
if (captions !== undefined) {
|
||||
this.captions = captions
|
||||
|
@ -449,6 +457,17 @@ export class VideoEdit {
|
|||
this.metadata.live = pick(live, [ 'rtmpUrl', 'rtmpsUrl', 'streamKey' ])
|
||||
}
|
||||
|
||||
private loadPlayerSettings (playerSettings: UpdateFromAPIOptions['playerSettings']) {
|
||||
const buildObj = () => {
|
||||
return {
|
||||
theme: playerSettings.theme
|
||||
}
|
||||
}
|
||||
|
||||
this.playerSettings = buildObj()
|
||||
this.saveStore.playerSettings = buildObj()
|
||||
}
|
||||
|
||||
loadAfterPublish (options: {
|
||||
video: Pick<VideoDetails, 'id' | 'uuid' | 'shortUUID'>
|
||||
}) {
|
||||
|
@ -797,6 +816,26 @@ export class VideoEdit {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
loadFromPlayerSettingsForm (values: PlayerSettingsForm) {
|
||||
this.playerSettings = values
|
||||
}
|
||||
|
||||
toPlayerSettingsFormPatch (): Required<PlayerSettingsForm> {
|
||||
return {
|
||||
theme: this.playerSettings?.theme ?? 'channel-default'
|
||||
}
|
||||
}
|
||||
|
||||
toPlayerSettingsUpdate (): PlayerVideoSettingsUpdate {
|
||||
if (!this.playerSettings) return undefined
|
||||
|
||||
return {
|
||||
theme: this.playerSettings.theme
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getVideoSource () {
|
||||
return this.metadata.videoSource
|
||||
}
|
||||
|
@ -825,6 +864,10 @@ export class VideoEdit {
|
|||
return this.studioTasks
|
||||
}
|
||||
|
||||
getPlayerSettings () {
|
||||
return this.playerSettings
|
||||
}
|
||||
|
||||
getStudioTasksSummary () {
|
||||
return this.getStudioTasks().map(t => {
|
||||
if (t.name === 'add-intro') {
|
||||
|
@ -941,6 +984,21 @@ export class VideoEdit {
|
|||
return changes
|
||||
}
|
||||
|
||||
hasPlayerSettingsChanges () {
|
||||
if (!this.playerSettings) return false
|
||||
if (!this.saveStore.playerSettings) return true
|
||||
|
||||
const changes = !this.areSameObjects(this.playerSettings, this.saveStore.playerSettings)
|
||||
|
||||
debugLogger('Check if player settings has changes', {
|
||||
playerSettings: this.playerSettings,
|
||||
savePlayerSettings: this.saveStore.playerSettings,
|
||||
changes
|
||||
})
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
hasPendingChanges () {
|
||||
|
@ -950,7 +1008,8 @@ export class VideoEdit {
|
|||
this.hasStudioTasks() ||
|
||||
this.hasChaptersChanges() ||
|
||||
this.hasCommonChanges() ||
|
||||
this.hasPluginDataChanges()
|
||||
this.hasPluginDataChanges() ||
|
||||
this.hasPlayerSettingsChanges()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -31,8 +31,15 @@
|
|||
</div>
|
||||
|
||||
<p-datepicker
|
||||
inputId="originallyPublishedAt" formControlName="originallyPublishedAt" [dateFormat]="calendarDateFormat" [firstDayOfWeek]="0"
|
||||
[showTime]="true" [hideOnDateTimeSelect]="true" [monthNavigator]="true" [yearNavigator]="true" [yearRange]="myYearRange"
|
||||
inputId="originallyPublishedAt"
|
||||
formControlName="originallyPublishedAt"
|
||||
[dateFormat]="calendarDateFormat"
|
||||
[firstDayOfWeek]="0"
|
||||
[showTime]="true"
|
||||
[hideOnDateTimeSelect]="true"
|
||||
[monthNavigator]="true"
|
||||
[yearNavigator]="true"
|
||||
[yearRange]="myYearRange"
|
||||
baseZIndex="20000"
|
||||
>
|
||||
</p-datepicker>
|
||||
|
@ -42,10 +49,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="downloadEnabled" formControlName="downloadEnabled"
|
||||
i18n-labelText labelText="Enable download"
|
||||
></my-peertube-checkbox>
|
||||
<my-peertube-checkbox inputName="downloadEnabled" formControlName="downloadEnabled" i18n-labelText labelText="Enable download"></my-peertube-checkbox>
|
||||
|
||||
<div class="form-group" formGroupName="playerSettings">
|
||||
<label i18n for="playerSettingsTheme">Player Theme</label>
|
||||
|
||||
<my-select-player-theme formControlName="theme" inputId="playerSettingsTheme" mode="video" [channel]="videoChannel">
|
||||
</my-select-player-theme>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ServerService } from '@app/core'
|
||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||
import { BuildFormArgumentTyped } from '@app/shared/form-validators/form-validator.model'
|
||||
import { VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
||||
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { FormReactiveErrors, FormReactiveMessages, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { SelectPlayerThemeComponent } from '@app/shared/shared-forms/select/select-player-theme.component'
|
||||
import { HTMLServerConfig, PlayerVideoSettings, VideoChannel } from '@peertube/peertube-models'
|
||||
import debug from 'debug'
|
||||
import { DatePickerModule } from 'primeng/datepicker'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
@ -19,6 +20,10 @@ const debugLogger = debug('peertube:video-manage')
|
|||
type Form = {
|
||||
downloadEnabled: FormControl<boolean>
|
||||
originallyPublishedAt: FormControl<Date>
|
||||
|
||||
playerSettings: FormGroup<{
|
||||
theme: FormControl<PlayerVideoSettings['theme']>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -28,12 +33,13 @@ type Form = {
|
|||
],
|
||||
templateUrl: './video-customization.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgIf,
|
||||
DatePickerModule,
|
||||
PeertubeCheckboxComponent,
|
||||
GlobalIconComponent
|
||||
GlobalIconComponent,
|
||||
SelectPlayerThemeComponent
|
||||
]
|
||||
})
|
||||
export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
||||
|
@ -47,6 +53,7 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
|||
validationMessages: FormReactiveMessages = {}
|
||||
|
||||
videoEdit: VideoEdit
|
||||
videoChannel: Pick<VideoChannel, 'name' | 'displayName'>
|
||||
|
||||
calendarDateFormat: string
|
||||
myYearRange: string
|
||||
|
@ -63,17 +70,24 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
|||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
|
||||
const { videoEdit } = this.manageController.getStore()
|
||||
const { videoEdit, userChannels } = this.manageController.getStore()
|
||||
this.videoEdit = videoEdit
|
||||
|
||||
const channelItem = userChannels.find(c => c.id === videoEdit.toCommonFormPatch().channelId)
|
||||
this.videoChannel = { name: channelItem.name, displayName: channelItem.label }
|
||||
|
||||
this.buildForm()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const defaultValues = this.videoEdit.toCommonFormPatch()
|
||||
const obj: BuildFormArgument = {
|
||||
const defaultValues = { ...this.videoEdit.toCommonFormPatch(), playerSettings: this.videoEdit.toPlayerSettingsFormPatch() }
|
||||
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
downloadEnabled: null,
|
||||
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR
|
||||
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
|
||||
playerSettings: {
|
||||
theme: null
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -93,12 +107,18 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
|||
debugLogger('Updating form values', formValues)
|
||||
|
||||
this.videoEdit.loadFromCommonForm(formValues)
|
||||
this.videoEdit.loadFromPlayerSettingsForm({
|
||||
theme: formValues.playerSettings.theme
|
||||
})
|
||||
})
|
||||
|
||||
this.formReactiveService.markAllAsDirty(this.form.controls)
|
||||
|
||||
this.updatedSub = this.manageController.getUpdatedObs().subscribe(() => {
|
||||
this.form.patchValue(this.videoEdit.toCommonFormPatch())
|
||||
this.form.patchValue({
|
||||
...this.videoEdit.toCommonFormPatch(),
|
||||
...this.videoEdit.toPlayerSettingsFormPatch()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VideoChapterService } from '@app/shared/shared-main/video/video-chapter
|
|||
import { VideoPasswordService } from '@app/shared/shared-main/video/video-password.service'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import {
|
||||
HTMLServerConfig,
|
||||
|
@ -47,6 +48,7 @@ export class VideoManageController implements OnDestroy {
|
|||
private formReactiveService = inject(FormReactiveService)
|
||||
private videoStudio = inject(VideoStudioService)
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
private videoEdit: VideoEdit
|
||||
private userChannels: SelectChannelItem[]
|
||||
|
@ -245,6 +247,16 @@ export class VideoManageController implements OnDestroy {
|
|||
|
||||
return this.videoChapterService.updateChapters(videoAttributes.uuid, this.videoEdit.getChaptersEdit())
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (!this.videoEdit.hasPlayerSettingsChanges()) return of(true)
|
||||
|
||||
debugLogger('Update player settings')
|
||||
|
||||
return this.playerSettingsService.updateVideoSettings({
|
||||
videoId: videoAttributes.uuid,
|
||||
settings: this.videoEdit.getPlayerSettings()
|
||||
})
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (!isLive || !this.videoEdit.hasLiveChanges()) return of(true)
|
||||
|
||||
|
@ -283,16 +295,19 @@ export class VideoManageController implements OnDestroy {
|
|||
|
||||
!isLive
|
||||
? this.videoCaptionService.listCaptions(videoAttributes.uuid)
|
||||
: of(undefined)
|
||||
: of(undefined),
|
||||
|
||||
this.playerSettingsService.getVideoSettings({ videoId: videoAttributes.uuid, raw: true })
|
||||
])
|
||||
}),
|
||||
switchMap(([ video, videoPasswords, live, chaptersRes, captionsRes ]) => {
|
||||
switchMap(([ video, videoPasswords, live, chaptersRes, captionsRes, playerSettings ]) => {
|
||||
return this.videoEdit.loadFromAPI({
|
||||
video,
|
||||
videoPasswords: videoPasswords.map(p => p.password),
|
||||
live,
|
||||
chapters: chaptersRes?.chapters,
|
||||
captions: captionsRes?.data
|
||||
captions: captionsRes?.data,
|
||||
playerSettings
|
||||
})
|
||||
}),
|
||||
first(), // To complete
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="actor" *ngIf="actor">
|
||||
<div class="position-relative me-3">
|
||||
<my-actor-avatar [actor]="actor" [actorType]="actorType()" [previewImage]="preview" size="100"></my-actor-avatar>
|
||||
<my-actor-avatar [actor]="actor" [actorType]="actorType()" [previewImage]="previewUrl" size="100"></my-actor-avatar>
|
||||
|
||||
@if (editable()) {
|
||||
@if (hasAvatar()) {
|
||||
|
|
|
@ -41,7 +41,7 @@ export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
|||
maxAvatarSize = 0
|
||||
avatarExtensions = ''
|
||||
|
||||
preview: string
|
||||
previewUrl: string
|
||||
|
||||
actor: ActorAvatarInput
|
||||
|
||||
|
@ -55,6 +55,8 @@ export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.previewUrl = undefined
|
||||
|
||||
this.actor = {
|
||||
avatars: this.avatars(),
|
||||
name: this.username()
|
||||
|
@ -73,16 +75,23 @@ export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
|||
this.avatarChange.emit(formData)
|
||||
|
||||
if (this.previewImage()) {
|
||||
imageToDataURL(avatarfile).then(result => this.preview = result)
|
||||
imageToDataURL(avatarfile).then(result => this.previewUrl = result)
|
||||
}
|
||||
}
|
||||
|
||||
deleteAvatar () {
|
||||
this.preview = undefined
|
||||
if (this.previewImage()) {
|
||||
this.previewUrl = null
|
||||
this.actor.avatars = []
|
||||
}
|
||||
|
||||
this.avatarDelete.emit()
|
||||
}
|
||||
|
||||
hasAvatar () {
|
||||
return !!this.preview || this.avatars().length !== 0
|
||||
// User deleted the avatar
|
||||
if (this.previewUrl === null) return false
|
||||
|
||||
return !!this.previewUrl || this.avatars().length !== 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
<div class="actor">
|
||||
<div class="actor-img-edit-container">
|
||||
<div class="banner-placeholder">
|
||||
<img *ngIf="hasBanner()" [src]="preview || bannerUrl()" alt="Banner" />
|
||||
<img *ngIf="hasBanner()" [src]="getBannerUrl()" alt="Banner" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasBanner()" class="actor-img-edit-button button-file primary-button button-focus-within" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasBanner()" ngbDropdown placement="right">
|
||||
<button type="button" class="actor-img-edit-button button-file primary-button" ngbDropdownToggle>
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<span i18n>Change your banner</span>
|
||||
</button>
|
||||
|
||||
<div ngbDropdownMenu>
|
||||
<div class="dropdown-item dropdown-file button-focus-within" [ngbTooltip]="bannerFormat">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dropdown-item" (click)="deleteBanner()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove banner</span>
|
||||
</button>
|
||||
@if (!hasBanner()) {
|
||||
<div class="actor-img-edit-button button-file primary-button button-focus-within" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div ngbDropdown placement="right">
|
||||
<button type="button" class="actor-img-edit-button button-file primary-button" ngbDropdownToggle>
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<span i18n>Change your banner</span>
|
||||
</button>
|
||||
|
||||
<div ngbDropdownMenu>
|
||||
<div class="dropdown-item dropdown-file button-focus-within" [ngbTooltip]="bannerFormat">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dropdown-item" (click)="deleteBanner()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove banner</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { NgIf, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, ElementRef, OnInit, inject, input, output, viewChild } from '@angular/core'
|
||||
import { CommonModule, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, ElementRef, OnInit, booleanAttribute, inject, input, output, viewChild } from '@angular/core'
|
||||
import { SafeResourceUrl } from '@angular/platform-browser'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle, NgbPopover, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbDropdownModule, NgbPopover, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getBytes } from '@root-helpers/bytes'
|
||||
import { imageToDataURL } from '@root-helpers/images'
|
||||
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
||||
|
@ -14,7 +14,7 @@ import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
|||
'./actor-image-edit.scss',
|
||||
'./actor-banner-edit.component.scss'
|
||||
],
|
||||
imports: [ NgIf, NgbTooltip, NgTemplateOutlet, NgbDropdown, NgbDropdownToggle, GlobalIconComponent, NgbDropdownMenu ]
|
||||
imports: [ CommonModule, NgbTooltipModule, NgTemplateOutlet, NgbDropdownModule, GlobalIconComponent ]
|
||||
})
|
||||
export class ActorBannerEditComponent implements OnInit {
|
||||
private serverService = inject(ServerService)
|
||||
|
@ -23,8 +23,8 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
readonly bannerfileInput = viewChild<ElementRef<HTMLInputElement>>('bannerfileInput')
|
||||
readonly bannerPopover = viewChild<NgbPopover>('bannerPopover')
|
||||
|
||||
readonly bannerUrl = input<string>(undefined)
|
||||
readonly previewImage = input(false)
|
||||
readonly bannerUrl = input<string>()
|
||||
readonly previewImage = input(false, { transform: booleanAttribute })
|
||||
|
||||
readonly bannerChange = output<FormData>()
|
||||
readonly bannerDelete = output()
|
||||
|
@ -63,11 +63,23 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
}
|
||||
|
||||
deleteBanner () {
|
||||
this.preview = undefined
|
||||
if (this.previewImage()) {
|
||||
this.preview = null
|
||||
}
|
||||
|
||||
this.bannerDelete.emit()
|
||||
}
|
||||
|
||||
hasBanner () {
|
||||
// User deleted the avatar
|
||||
if (this.preview === null) return false
|
||||
|
||||
return !!this.preview || !!this.bannerUrl()
|
||||
}
|
||||
|
||||
getBannerUrl () {
|
||||
if (this.preview === null) return ''
|
||||
|
||||
return this.preview || this.bannerUrl()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, forwardRef, inject, input, OnInit } from '@angular/core'
|
||||
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { ServerService } from '@app/core'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { PlayerChannelSettings, PlayerTheme, PlayerVideoSettings, VideoChannel } from '@peertube/peertube-models'
|
||||
import { of } from 'rxjs'
|
||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||
import { SelectOptionsComponent } from './select-options.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-select-player-theme',
|
||||
template: `
|
||||
<my-select-options
|
||||
[inputId]="inputId()"
|
||||
|
||||
[items]="themes"
|
||||
|
||||
[(ngModel)]="selectedId"
|
||||
(ngModelChange)="onModelChange()"
|
||||
filter="false"
|
||||
></my-select-options>
|
||||
`,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SelectPlayerThemeComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
imports: [ FormsModule, CommonModule, SelectOptionsComponent ]
|
||||
})
|
||||
export class SelectPlayerThemeComponent implements ControlValueAccessor, OnInit {
|
||||
private serverService = inject(ServerService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
readonly inputId = input.required<string>()
|
||||
readonly mode = input.required<'instance' | 'video' | 'channel'>()
|
||||
|
||||
readonly channel = input<Pick<VideoChannel, 'name' | 'displayName'>>()
|
||||
|
||||
themes: SelectOptionsItem<PlayerVideoSettings['theme']>[]
|
||||
selectedId: PlayerTheme
|
||||
|
||||
ngOnInit () {
|
||||
if (this.mode() === 'video' && !this.channel()) {
|
||||
throw new Error('Channel must be specified in video mode')
|
||||
}
|
||||
|
||||
this.buildOptions()
|
||||
}
|
||||
|
||||
propagateChange = (_: any) => {
|
||||
// empty
|
||||
}
|
||||
|
||||
writeValue (id: PlayerTheme) {
|
||||
this.selectedId = id
|
||||
}
|
||||
|
||||
registerOnChange (fn: (_: any) => void) {
|
||||
this.propagateChange = fn
|
||||
}
|
||||
|
||||
registerOnTouched () {
|
||||
// Unused
|
||||
}
|
||||
|
||||
onModelChange () {
|
||||
this.propagateChange(this.selectedId)
|
||||
}
|
||||
|
||||
private buildOptions () {
|
||||
const config = this.serverService.getHTMLConfig()
|
||||
const instanceName = config.instance.name
|
||||
const instancePlayerTheme = this.getLabelOf(config.defaults.player.theme)
|
||||
|
||||
this.themes = []
|
||||
|
||||
if (this.mode() === 'channel' || this.mode() === 'video') {
|
||||
this.themes.push(
|
||||
{ id: 'instance-default', label: $localize`${instanceName} setting (${instancePlayerTheme})` }
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mode() === 'video') {
|
||||
this.themes.push(
|
||||
{ id: 'channel-default', label: $localize`${this.channel().displayName} setting` }
|
||||
)
|
||||
|
||||
this.scheduleChannelUpdate()
|
||||
}
|
||||
|
||||
this.themes = this.themes.concat(this.getPlayerThemes())
|
||||
}
|
||||
|
||||
private scheduleChannelUpdate () {
|
||||
this.playerSettingsService.getChannelSettings({ channelHandle: this.channel().name, raw: true }).subscribe({
|
||||
next: settings => {
|
||||
this.themes.find(t => t.id === 'channel-default').label = this.buildChannelLabel(settings)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private buildChannelLabel (channelRawPlayerSettings: PlayerChannelSettings) {
|
||||
const config = this.serverService.getHTMLConfig()
|
||||
const instanceName = config.instance.name
|
||||
const instancePlayerTheme = this.getLabelOf(config.defaults.player.theme)
|
||||
|
||||
const channelRawTheme = channelRawPlayerSettings.theme
|
||||
|
||||
const channelPlayerTheme = channelRawTheme === 'instance-default'
|
||||
? $localize`from ${instanceName} setting\: ${instancePlayerTheme}`
|
||||
: this.getLabelOf(channelRawTheme)
|
||||
|
||||
return $localize`${this.channel().displayName} channel setting (${channelPlayerTheme})`
|
||||
}
|
||||
|
||||
private getLabelOf (playerTheme: PlayerTheme) {
|
||||
return this.getPlayerThemes().find(t => t.id === playerTheme)?.label
|
||||
}
|
||||
|
||||
private getPlayerThemes (): SelectOptionsItem<PlayerVideoSettings['theme']>[] {
|
||||
return [
|
||||
{ id: 'galaxy', label: $localize`Galaxy`, description: $localize`Original theme` },
|
||||
{ id: 'lucide', label: $localize`Lucide`, description: $localize`A clean and modern theme` }
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { AfterViewChecked, booleanAttribute, Directive, ElementRef, inject, input, OnDestroy, OnInit, output } from '@angular/core'
|
||||
import { PeerTubeRouterService } from '@app/core'
|
||||
import { fromEvent, Observable, Subscription } from 'rxjs'
|
||||
import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
|
||||
|
||||
|
@ -8,7 +7,6 @@ import { distinctUntilChanged, filter, map, share, startWith, throttleTime } fro
|
|||
standalone: true
|
||||
})
|
||||
export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewChecked {
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private el = inject(ElementRef)
|
||||
|
||||
readonly percentLimit = input(70)
|
||||
|
@ -18,7 +16,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
|
|||
readonly nearOfBottom = output()
|
||||
|
||||
private decimalLimit = 0
|
||||
private lastCurrentBottom = -1
|
||||
private lastCurrentBottom: number
|
||||
private scrollDownSub: Subscription
|
||||
private container: HTMLElement
|
||||
|
||||
|
@ -98,6 +96,11 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
|
|||
}
|
||||
|
||||
private isScrollingDown (current: number) {
|
||||
if (this.lastCurrentBottom === undefined) {
|
||||
this.lastCurrentBottom = current
|
||||
return false
|
||||
}
|
||||
|
||||
const result = this.lastCurrentBottom < current
|
||||
|
||||
this.lastCurrentBottom = current
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { RestExtractor } from '@app/core'
|
||||
import {
|
||||
PlayerChannelSettings,
|
||||
PlayerChannelSettingsUpdate,
|
||||
PlayerVideoSettings,
|
||||
PlayerVideoSettingsUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { catchError } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { VideoPasswordService } from '../shared-main/video/video-password.service'
|
||||
|
||||
@Injectable()
|
||||
export class PlayerSettingsService {
|
||||
static BASE_PLAYER_SETTINGS_URL = environment.apiUrl + '/api/v1/player-settings/'
|
||||
|
||||
private authHttp = inject(HttpClient)
|
||||
private restExtractor = inject(RestExtractor)
|
||||
|
||||
getVideoSettings (options: {
|
||||
videoId: string
|
||||
videoPassword?: string
|
||||
raw: boolean
|
||||
}) {
|
||||
const headers = VideoPasswordService.buildVideoPasswordHeader(options.videoPassword)
|
||||
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'videos/' + options.videoId
|
||||
|
||||
let params = new HttpParams()
|
||||
if (options.raw) params = params.set('raw', 'true')
|
||||
|
||||
return this.authHttp.get<PlayerVideoSettings>(path, { params, headers })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
updateVideoSettings (options: {
|
||||
videoId: string
|
||||
settings: PlayerVideoSettingsUpdate
|
||||
}) {
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'videos/' + options.videoId
|
||||
|
||||
return this.authHttp.put<PlayerVideoSettings>(path, options.settings)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getChannelSettings (options: {
|
||||
channelHandle: string
|
||||
raw: boolean
|
||||
}) {
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'video-channels/' + options.channelHandle
|
||||
|
||||
let params = new HttpParams()
|
||||
if (options.raw) params = params.set('raw', 'true')
|
||||
|
||||
return this.authHttp.get<PlayerChannelSettings>(path, { params })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
updateChannelSettings (options: {
|
||||
channelHandle: string
|
||||
settings: PlayerChannelSettingsUpdate
|
||||
}) {
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'video-channels/' + options.channelHandle
|
||||
|
||||
return this.authHttp.put<PlayerChannelSettings>(path, options.settings)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
|
@ -1,144 +1,98 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { AfterViewInit, Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AfterViewInit, Component, inject } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, HooksService, Notifier } from '@app/core'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
} from '@app/shared/form-validators/video-channel-validators'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { HttpStatusCode, VideoChannelCreate } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, PlayerChannelSettings, VideoChannelCreate } from '@peertube/peertube-models'
|
||||
import { of } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import { ActorAvatarEditComponent } from '../shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { MarkdownTextareaComponent } from '../shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { HelpComponent } from '../shared-main/buttons/help.component'
|
||||
import { MarkdownHintComponent } from '../shared-main/text/markdown-hint.component'
|
||||
import { VideoChannelEdit } from './video-channel-edit'
|
||||
import { PlayerSettingsService } from '../shared-video/player-settings.service'
|
||||
import { FormValidatedOutput, VideoChannelEditComponent } from './video-channel-edit.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './video-channel-edit.component.html',
|
||||
styleUrls: [ './video-channel-edit.component.scss' ],
|
||||
template: `
|
||||
<my-video-channel-edit
|
||||
mode="create" [channel]="channel" [rawPlayerSettings]="rawPlayerSettings" [error]="error"
|
||||
(formValidated)="onFormValidated($event)"
|
||||
>
|
||||
</my-video-channel-edit>
|
||||
`,
|
||||
imports: [
|
||||
NgIf,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorBannerEditComponent,
|
||||
ActorAvatarEditComponent,
|
||||
NgClass,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
AlertComponent,
|
||||
MarkdownHintComponent
|
||||
VideoChannelEditComponent
|
||||
],
|
||||
providers: [
|
||||
PlayerSettingsService
|
||||
]
|
||||
})
|
||||
export class VideoChannelCreateComponent extends VideoChannelEdit implements OnInit, AfterViewInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
export class VideoChannelCreateComponent implements AfterViewInit {
|
||||
private authService = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
private router = inject(Router)
|
||||
private videoChannelService = inject(VideoChannelService)
|
||||
private hooks = inject(HooksService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
error: string
|
||||
videoChannel = new VideoChannel({})
|
||||
|
||||
private avatar: FormData
|
||||
private banner: FormData
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
'name': VIDEO_CHANNEL_NAME_VALIDATOR,
|
||||
'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
'description': VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
'support': VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
})
|
||||
channel = new VideoChannel({})
|
||||
rawPlayerSettings: PlayerChannelSettings = {
|
||||
theme: 'instance-default'
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.hooks.runAction('action:video-channel-create.init', 'video-channel')
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
onFormValidated (output: FormValidatedOutput) {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoChannelCreate: VideoChannelCreate = {
|
||||
name: body.name,
|
||||
displayName: body['display-name'],
|
||||
description: body.description || null,
|
||||
support: body.support || null
|
||||
const channelCreate: VideoChannelCreate = {
|
||||
name: output.channel.name,
|
||||
displayName: output.channel.displayName,
|
||||
description: output.channel.description,
|
||||
support: output.channel.support
|
||||
}
|
||||
|
||||
this.videoChannelService.createVideoChannel(videoChannelCreate)
|
||||
this.videoChannelService.createVideoChannel(channelCreate)
|
||||
.pipe(
|
||||
switchMap(() => this.uploadAvatar()),
|
||||
switchMap(() => this.uploadBanner())
|
||||
switchMap(() => {
|
||||
return this.playerSettingsService.updateChannelSettings({
|
||||
channelHandle: output.channel.name,
|
||||
settings: {
|
||||
theme: output.playerSettings.theme
|
||||
}
|
||||
})
|
||||
}),
|
||||
switchMap(() => this.uploadAvatar(output.channel.name, output.avatar)),
|
||||
switchMap(() => this.uploadBanner(output.channel.name, output.banner))
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.authService.refreshUserInformation()
|
||||
|
||||
this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`)
|
||||
this.notifier.success($localize`Video channel ${channelCreate.displayName} created.`)
|
||||
this.router.navigate([ '/my-library', 'video-channels' ])
|
||||
},
|
||||
|
||||
error: err => {
|
||||
let message = err.message
|
||||
|
||||
if (err.status === HttpStatusCode.CONFLICT_409) {
|
||||
this.error = $localize`This name already exists on this platform.`
|
||||
return
|
||||
message = $localize`Channel name "${channelCreate.name}" already exists on this platform.`
|
||||
}
|
||||
|
||||
this.error = err.message
|
||||
this.notifier.error(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.avatar = formData
|
||||
private uploadAvatar (username: string, avatar?: FormData) {
|
||||
if (!avatar) return of(undefined)
|
||||
|
||||
return this.videoChannelService.changeVideoChannelImage(username, avatar, 'avatar')
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.avatar = null
|
||||
}
|
||||
private uploadBanner (username: string, banner?: FormData) {
|
||||
if (!banner) return of(undefined)
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.banner = formData
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.banner = null
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return true
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return $localize`Create your channel`
|
||||
}
|
||||
|
||||
getUsername () {
|
||||
return this.form.value.name
|
||||
}
|
||||
|
||||
private uploadAvatar () {
|
||||
if (!this.avatar) return of(undefined)
|
||||
|
||||
return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.avatar, 'avatar')
|
||||
}
|
||||
|
||||
private uploadBanner () {
|
||||
if (!this.banner) return of(undefined)
|
||||
|
||||
return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.banner, 'banner')
|
||||
return this.videoChannelService.changeVideoChannelImage(username, banner, 'banner')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<my-alert *ngIf="error" type="danger">{{ error }}</my-alert>
|
||||
<my-alert *ngIf="error()" type="danger">{{ error() }}</my-alert>
|
||||
|
||||
<div class="pt-4">
|
||||
<form (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
<form (ngSubmit)="onFormValidated()" [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols"> <!-- channel grid -->
|
||||
<div class="title-col">
|
||||
@if (isCreation()) {
|
||||
@if (mode() === 'create') {
|
||||
<h2 i18n>NEW CHANNEL</h2>
|
||||
} @else {
|
||||
<h2 i18n>UPDATE CHANNEL</h2>
|
||||
|
@ -14,40 +14,40 @@
|
|||
|
||||
<div class="content-col">
|
||||
<my-actor-banner-edit
|
||||
*ngIf="videoChannel" [previewImage]="isCreation()" class="d-block mb-4"
|
||||
[bannerUrl]="videoChannel?.bannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
*ngIf="channel()" previewImage="true" class="d-block mb-4"
|
||||
[bannerUrl]="channel()?.bannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
*ngIf="videoChannel" class="d-block mb-4" actorType="channel"
|
||||
[displayName]="videoChannel.displayName" [previewImage]="isCreation()" [avatars]="videoChannel.avatars"
|
||||
[username]="!isCreation() && videoChannel.name" [subscribers]="!isCreation() && videoChannel.followersCount"
|
||||
*ngIf="channel()" class="d-block mb-4" actorType="channel"
|
||||
[displayName]="channel().displayName" previewImage="true" [avatars]="channel().avatars"
|
||||
[username]="mode() === 'update' && channel().name" [subscribers]="mode() === 'update' && channel().followersCount"
|
||||
(avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||
></my-actor-avatar-edit>
|
||||
|
||||
<div class="form-group" *ngIf="isCreation()">
|
||||
<div class="form-group" *ngIf="mode() === 'create'">
|
||||
<label i18n for="name">Name</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control w-auto flex-grow-1 d-block"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors.name }" class="form-control w-auto flex-grow-1 d-block"
|
||||
>
|
||||
<div class="input-group-text">@{{ instanceHost }}</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors['name']" class="form-error" role="alert">
|
||||
{{ formErrors['name'] }}
|
||||
<div *ngIf="formErrors.name" class="form-error" role="alert">
|
||||
{{ formErrors.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="display-name">Display name</label>
|
||||
<label i18n for="displayName">Display name</label>
|
||||
<input
|
||||
type="text" id="display-name" class="form-control"
|
||||
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
|
||||
type="text" id="displayName" class="form-control"
|
||||
formControlName="displayName" [ngClass]="{ 'input-error': formErrors.displayName }"
|
||||
>
|
||||
<div *ngIf="formErrors['display-name']" class="form-error" role="alert">
|
||||
{{ formErrors['display-name'] }}
|
||||
<div *ngIf="formErrors.displayName" class="form-error" role="alert">
|
||||
{{ formErrors.displayName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -58,7 +58,7 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="description" formControlName="description"
|
||||
markdownType="enhanced" [formError]="formErrors['description']" withEmoji="true" withHtml="true"
|
||||
markdownType="enhanced" [formError]="formErrors.description" withEmoji="true" withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.description" class="form-error" role="alert">
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="support" formControlName="support"
|
||||
markdownType="enhanced" [formError]="formErrors['support']" withEmoji="true" withHtml="true"
|
||||
markdownType="enhanced" [formError]="formErrors.support" withEmoji="true" withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
|
@ -86,6 +86,13 @@
|
|||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="playerTheme">Player Theme</label>
|
||||
|
||||
<my-select-player-theme formControlName="playerTheme" inputId="playerTheme" mode="channel">
|
||||
</my-select-player-theme>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="peertube-button primary-button mt-4" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
input[type="text"] {
|
||||
@include peertube-input-text(340px);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
input[type="submit"] {
|
||||
@include margin-left(auto);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,8 @@ input[type=submit] {
|
|||
max-width: 500px;
|
||||
}
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container(340px);
|
||||
my-select-player-theme {
|
||||
display: block;
|
||||
|
||||
@include responsive-width(340px);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, input, OnInit, output } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
import { PlayerChannelSettings } from '@peertube/peertube-models'
|
||||
import { BuildFormArgumentTyped, FormReactiveErrorsTyped, FormReactiveMessagesTyped } from '../form-validators/form-validator.model'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
} from '../form-validators/video-channel-validators'
|
||||
import { ActorAvatarEditComponent } from '../shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { FormReactiveService } from '../shared-forms/form-reactive.service'
|
||||
import { MarkdownTextareaComponent } from '../shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { SelectPlayerThemeComponent } from '../shared-forms/select/select-player-theme.component'
|
||||
import { HelpComponent } from '../shared-main/buttons/help.component'
|
||||
import { AlertComponent } from '../shared-main/common/alert.component'
|
||||
import { MarkdownHintComponent } from '../shared-main/text/markdown-hint.component'
|
||||
|
||||
type Form = {
|
||||
name: FormControl<string>
|
||||
displayName: FormControl<string>
|
||||
description: FormControl<string>
|
||||
support: FormControl<string>
|
||||
playerTheme: FormControl<PlayerChannelSettings['theme']>
|
||||
bulkVideosSupportUpdate: FormControl<boolean>
|
||||
}
|
||||
|
||||
export type FormValidatedOutput = {
|
||||
avatar: FormData
|
||||
banner: FormData
|
||||
|
||||
playerSettings: {
|
||||
theme: PlayerChannelSettings['theme']
|
||||
}
|
||||
|
||||
channel: {
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
support: string
|
||||
bulkVideosSupportUpdate: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-edit',
|
||||
templateUrl: './video-channel-edit.component.html',
|
||||
styleUrls: [ './video-channel-edit.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorBannerEditComponent,
|
||||
ActorAvatarEditComponent,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
AlertComponent,
|
||||
MarkdownHintComponent,
|
||||
SelectPlayerThemeComponent
|
||||
]
|
||||
})
|
||||
export class VideoChannelEditComponent implements OnInit {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
|
||||
readonly mode = input.required<'create' | 'update'>()
|
||||
readonly channel = input.required<VideoChannel>()
|
||||
readonly rawPlayerSettings = input.required<PlayerChannelSettings>()
|
||||
readonly error = input<string>()
|
||||
|
||||
readonly formValidated = output<FormValidatedOutput>()
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
private avatar: FormData
|
||||
private banner: FormData
|
||||
private oldSupportField: string
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm()
|
||||
|
||||
this.oldSupportField = this.channel().support
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
name: this.mode() === 'create'
|
||||
? VIDEO_CHANNEL_NAME_VALIDATOR
|
||||
: null,
|
||||
displayName: VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
support: VIDEO_CHANNEL_SUPPORT_VALIDATOR,
|
||||
bulkVideosSupportUpdate: null,
|
||||
playerTheme: null
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
displayName: this.channel().displayName,
|
||||
description: this.channel().description,
|
||||
support: this.channel().support,
|
||||
playerTheme: this.rawPlayerSettings().theme
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
if (this.mode() === 'update') {
|
||||
return $localize`Update ${this.channel().name}`
|
||||
}
|
||||
|
||||
return $localize`Create your channel`
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.avatar = formData
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.avatar = null
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.banner = formData
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.banner = null
|
||||
}
|
||||
|
||||
get instanceHost () {
|
||||
return window.location.host
|
||||
}
|
||||
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
if (this.mode() === 'create') return false
|
||||
|
||||
if (this.oldSupportField === undefined) return false
|
||||
|
||||
return this.oldSupportField !== this.form.value.support
|
||||
}
|
||||
|
||||
onFormValidated () {
|
||||
const body = this.form.value
|
||||
|
||||
this.formValidated.emit({
|
||||
avatar: this.avatar,
|
||||
banner: this.banner,
|
||||
playerSettings: {
|
||||
theme: body.playerTheme
|
||||
},
|
||||
channel: {
|
||||
name: body.name,
|
||||
displayName: body.displayName,
|
||||
description: body.description || null,
|
||||
support: body.support || null,
|
||||
|
||||
bulkVideosSupportUpdate: this.mode() === 'update'
|
||||
? body.bulkVideosSupportUpdate || false
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
|
||||
export abstract class VideoChannelEdit extends FormReactive {
|
||||
videoChannel: VideoChannel
|
||||
|
||||
abstract isCreation (): boolean
|
||||
abstract getFormButtonTitle (): string
|
||||
|
||||
get instanceHost () {
|
||||
return window.location.host
|
||||
}
|
||||
|
||||
// Should be implemented by the child
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,92 +1,65 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AfterViewInit, Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, HooksService, Notifier, RedirectService } from '@app/core'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
} from '@app/shared/form-validators/video-channel-validators'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { shallowCopy } from '@peertube/peertube-core-utils'
|
||||
import { VideoChannelUpdate } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ActorAvatarEditComponent } from '../shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { MarkdownTextareaComponent } from '../shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { HelpComponent } from '../shared-main/buttons/help.component'
|
||||
import { MarkdownHintComponent } from '../shared-main/text/markdown-hint.component'
|
||||
import { VideoChannelEdit } from './video-channel-edit'
|
||||
import { PlayerChannelSettings, VideoChannelUpdate } from '@peertube/peertube-models'
|
||||
import { catchError, forkJoin, Subscription, switchMap, tap, throwError } from 'rxjs'
|
||||
import { VideoChannel } from '../shared-main/channel/video-channel.model'
|
||||
import { PlayerSettingsService } from '../shared-video/player-settings.service'
|
||||
import { FormValidatedOutput, VideoChannelEditComponent } from './video-channel-edit.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-update',
|
||||
templateUrl: './video-channel-edit.component.html',
|
||||
styleUrls: [ './video-channel-edit.component.scss' ],
|
||||
template: `
|
||||
@if (channel && rawPlayerSettings) {
|
||||
<my-video-channel-edit
|
||||
mode="update" [channel]="channel" [rawPlayerSettings]="rawPlayerSettings" [error]="error"
|
||||
(formValidated)="onFormValidated($event)"
|
||||
>
|
||||
</my-video-channel-edit>
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
NgIf,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorBannerEditComponent,
|
||||
ActorAvatarEditComponent,
|
||||
NgClass,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
AlertComponent,
|
||||
MarkdownHintComponent
|
||||
VideoChannelEditComponent
|
||||
],
|
||||
providers: [
|
||||
PlayerSettingsService
|
||||
]
|
||||
})
|
||||
export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnInit, AfterViewInit, OnDestroy {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
export class VideoChannelUpdateComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private authService = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
private route = inject(ActivatedRoute)
|
||||
private videoChannelService = inject(VideoChannelService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private redirectService = inject(RedirectService)
|
||||
private hooks = inject(HooksService)
|
||||
|
||||
channel: VideoChannel
|
||||
rawPlayerSettings: PlayerChannelSettings
|
||||
error: string
|
||||
|
||||
private paramsSub: Subscription
|
||||
private oldSupportField: string
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
'description': VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
'support': VIDEO_CHANNEL_SUPPORT_VALIDATOR,
|
||||
'bulkVideosSupportUpdate': null
|
||||
})
|
||||
|
||||
this.paramsSub = this.route.params.subscribe(routeParams => {
|
||||
const videoChannelName = routeParams['videoChannelName']
|
||||
|
||||
this.videoChannelService.getVideoChannel(videoChannelName)
|
||||
.subscribe({
|
||||
next: videoChannelToUpdate => {
|
||||
this.videoChannel = videoChannelToUpdate
|
||||
forkJoin([
|
||||
this.videoChannelService.getVideoChannel(videoChannelName),
|
||||
this.playerSettingsService.getChannelSettings({ channelHandle: videoChannelName, raw: true })
|
||||
]).subscribe({
|
||||
next: ([ channel, rawPlayerSettings ]) => {
|
||||
this.channel = channel
|
||||
this.rawPlayerSettings = rawPlayerSettings
|
||||
|
||||
this.hooks.runAction('action:video-channel-update.video-channel.loaded', 'video-channel', { videoChannel: this.videoChannel })
|
||||
this.hooks.runAction('action:video-channel-update.video-channel.loaded', 'video-channel', { videoChannel: this.channel })
|
||||
},
|
||||
|
||||
this.oldSupportField = videoChannelToUpdate.support
|
||||
|
||||
this.form.patchValue({
|
||||
'display-name': videoChannelToUpdate.displayName,
|
||||
'description': videoChannelToUpdate.description,
|
||||
'support': videoChannelToUpdate.support
|
||||
})
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -98,112 +71,84 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
|
|||
if (this.paramsSub) this.paramsSub.unsubscribe()
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
onFormValidated (output: FormValidatedOutput) {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoChannelUpdate: VideoChannelUpdate = {
|
||||
displayName: body['display-name'],
|
||||
description: body.description || null,
|
||||
support: body.support || null,
|
||||
bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false
|
||||
displayName: output.channel.displayName,
|
||||
description: output.channel.description,
|
||||
support: output.channel.support,
|
||||
bulkVideosSupportUpdate: output.channel.bulkVideosSupportUpdate
|
||||
}
|
||||
|
||||
this.videoChannelService.updateVideoChannel(this.videoChannel.name, videoChannelUpdate)
|
||||
this.videoChannelService.updateVideoChannel(this.channel.name, videoChannelUpdate)
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.playerSettingsService.updateChannelSettings({
|
||||
channelHandle: this.channel.name,
|
||||
settings: {
|
||||
theme: output.playerSettings.theme
|
||||
}
|
||||
})
|
||||
}),
|
||||
switchMap(() => this.updateOrDeleteAvatar(output.avatar)),
|
||||
switchMap(() => this.updateOrDeleteBanner(output.banner))
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
// So my-actor-avatar component detects changes
|
||||
this.channel = shallowCopy(this.channel)
|
||||
|
||||
this.authService.refreshUserInformation()
|
||||
|
||||
this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
|
||||
|
||||
this.redirectService.redirectToPreviousRoute('/c/' + this.videoChannel.name)
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'avatar')
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.notifier.success($localize`Avatar changed.`)
|
||||
|
||||
this.videoChannel.updateAvatar(data.avatars)
|
||||
|
||||
// So my-actor-avatar component detects changes
|
||||
this.videoChannel = shallowCopy(this.videoChannel)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) =>
|
||||
genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`avatar`,
|
||||
notifier: this.notifier
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'avatar')
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Avatar deleted.`)
|
||||
|
||||
this.videoChannel.resetAvatar()
|
||||
|
||||
// So my-actor-avatar component detects changes
|
||||
this.videoChannel = shallowCopy(this.videoChannel)
|
||||
this.redirectService.redirectToPreviousRoute('/c/' + this.channel.name)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'banner')
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
private updateOrDeleteAvatar (avatar: FormData) {
|
||||
if (!avatar) {
|
||||
return this.videoChannelService.deleteVideoChannelImage(this.channel.name, 'avatar')
|
||||
.pipe(tap(() => this.channel.resetAvatar()))
|
||||
}
|
||||
|
||||
this.videoChannel.updateBanner(data.banners)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) =>
|
||||
genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`banner`,
|
||||
notifier: this.notifier
|
||||
return this.videoChannelService.changeVideoChannelImage(this.channel.name, avatar, 'avatar')
|
||||
.pipe(
|
||||
tap(data => this.channel.updateAvatar(data.avatars)),
|
||||
catchError(err =>
|
||||
throwError(() => {
|
||||
return new Error(genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`avatar`,
|
||||
notifier: this.notifier
|
||||
}))
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'banner')
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Banner deleted.`)
|
||||
private updateOrDeleteBanner (banner: FormData) {
|
||||
if (!banner) {
|
||||
return this.videoChannelService.deleteVideoChannelImage(this.channel.name, 'banner')
|
||||
.pipe(tap(() => this.channel.resetBanner()))
|
||||
}
|
||||
|
||||
this.videoChannel.resetBanner()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return false
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return $localize`Update ${this.videoChannel?.name}`
|
||||
}
|
||||
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
if (this.oldSupportField === undefined) return false
|
||||
|
||||
return this.oldSupportField !== this.form.value['support']
|
||||
return this.videoChannelService.changeVideoChannelImage(this.channel.name, banner, 'banner')
|
||||
.pipe(
|
||||
tap(data => this.channel.updateBanner(data.banners)),
|
||||
catchError(err =>
|
||||
throwError(() => {
|
||||
return new Error(genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`banner`,
|
||||
notifier: this.notifier
|
||||
}))
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ export class PeerTubePlayer {
|
|||
|
||||
await this.buildPlayerIfNeeded()
|
||||
|
||||
for (const theme of [ 'default', 'lucide' ]) {
|
||||
for (const theme of [ 'galaxy', 'lucide' ]) {
|
||||
this.player.removeClass('vjs-peertube-theme-' + theme)
|
||||
}
|
||||
|
||||
|
|
|
@ -313,11 +313,11 @@ $chapter-marker-size: 9px;
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PeerTube Default Theme
|
||||
// PeerTube Galaxy (original) Theme
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Play/pause animations
|
||||
.video-js.vjs-peertube-theme-default.vjs-has-started .vjs-play-control {
|
||||
.video-js.vjs-peertube-theme-galaxy.vjs-has-started .vjs-play-control {
|
||||
&.vjs-playing {
|
||||
animation: remove-pause-button 0.25s ease;
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ $chapter-marker-size: 9px;
|
|||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-peertube-theme-default .vjs-control-bar {
|
||||
.video-js.vjs-peertube-theme-galaxy .vjs-control-bar {
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6));
|
||||
box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2);
|
||||
|
||||
|
|
|
@ -166,16 +166,16 @@ body {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default theme
|
||||
// Galaxy (original) theme
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.video-js.vjs-peertube-theme-default {
|
||||
.video-js.vjs-peertube-theme-galaxy {
|
||||
.vjs-big-play-button {
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-peertube-theme-default.vjs-size-570 {
|
||||
.video-js.vjs-peertube-theme-galaxy.vjs-size-570 {
|
||||
.vjs-big-play-button {
|
||||
--big-play-button-size: 78px;
|
||||
--big-play-button-icon-size: 32px;
|
||||
|
@ -184,7 +184,7 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-peertube-theme-default.vjs-size-350 {
|
||||
.video-js.vjs-peertube-theme-galaxy.vjs-size-350 {
|
||||
.vjs-big-play-button {
|
||||
--big-play-button-size: 46px;
|
||||
--big-play-button-icon-size: 20px;
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import debug from 'debug'
|
||||
import videojs from 'video.js'
|
||||
import MenuButton from 'video.js/dist/types/menu/menu-button'
|
||||
import { VideojsComponent, VideojsMenu, VideojsMenuItem, VideojsMenuItemOptions, VideojsPlayer } from '../../types'
|
||||
import { toTitleCase } from '../common'
|
||||
import { SettingsDialog } from './settings-dialog'
|
||||
import { SettingsButton } from './settings-menu-button'
|
||||
import { SettingsPanel } from './settings-panel'
|
||||
import { SettingsPanelChild } from './settings-panel-child'
|
||||
import Button from 'video.js/dist/types/button'
|
||||
import MenuButton from 'video.js/dist/types/menu/menu-button'
|
||||
|
||||
const debugLogger = debug('peertube:player:settings')
|
||||
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { LiveVideoLatencyModeType, VideoChapter, VideoFile } from '@peertube/peertube-models'
|
||||
import { LiveVideoLatencyModeType, PlayerMode, PlayerTheme, VideoChapter, VideoFile } from '@peertube/peertube-models'
|
||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
||||
import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
|
||||
import { PlaylistPluginOptions, VideoJSCaption, VideojsPlayer, VideoJSStoryboard } from './peertube-videojs-typings'
|
||||
|
||||
export type PlayerMode = 'web-video' | 'p2p-media-loader'
|
||||
export type PeerTubePlayerTheme = 'default' | 'lucide'
|
||||
|
||||
export type PeerTubePlayerConstructorOptions = {
|
||||
playerElement: () => HTMLVideoElement
|
||||
|
||||
|
@ -53,7 +50,7 @@ export type PeerTubePlayerConstructorOptions = {
|
|||
export type PeerTubePlayerLoadOptions = {
|
||||
mode: PlayerMode
|
||||
|
||||
theme: PeerTubePlayerTheme
|
||||
theme: PlayerTheme
|
||||
|
||||
startTime?: number | string
|
||||
stopTime?: number | string
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VideoChapter, VideoFile, VideoPlaylist, VideoPlaylistElement } from '@peertube/peertube-models'
|
||||
import { PlayerMode, VideoChapter, VideoFile, VideoPlaylist, VideoPlaylistElement } from '@peertube/peertube-models'
|
||||
import type { HlsConfig, Level, Loader, LoaderContext } from 'hls.js'
|
||||
import type { CoreConfig } from 'p2p-media-loader-core'
|
||||
import type { HlsJsP2PEngine } from 'p2p-media-loader-hlsjs'
|
||||
|
@ -34,12 +34,11 @@ import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator'
|
|||
import { PeerTubePlugin } from '../shared/peertube/peertube-plugin'
|
||||
import { PlaylistPlugin } from '../shared/playlist/playlist-plugin'
|
||||
import { PeerTubeResolutionsPlugin } from '../shared/resolutions/peertube-resolutions-plugin'
|
||||
import { SettingsButton } from '../shared/settings/settings-menu-button'
|
||||
import { StatsCardOptions } from '../shared/stats/stats-card'
|
||||
import { StatsForNerdsPlugin } from '../shared/stats/stats-plugin'
|
||||
import { UpNextPlugin } from '../shared/upnext/upnext-plugin'
|
||||
import { WebVideoPlugin } from '../shared/web-video/web-video-plugin'
|
||||
import { PlayerMode } from './peertube-player-options'
|
||||
import { SettingsButton } from '../shared/settings/settings-menu-button'
|
||||
|
||||
declare module 'video.js' {
|
||||
export interface VideoJsPlayer {
|
||||
|
|
|
@ -69,7 +69,7 @@ export class PeerTubeEmbed {
|
|||
this.peertubePlugin = new PeerTubePlugin(this.http)
|
||||
this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin)
|
||||
this.playerHTML = new PlayerHTML(videoWrapperId)
|
||||
this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
|
||||
this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin, this.config)
|
||||
this.liveManager = new LiveManager(this.playerHTML)
|
||||
this.requiresPassword = false
|
||||
|
||||
|
@ -220,10 +220,18 @@ export class PeerTubeEmbed {
|
|||
videoResponse,
|
||||
captionsPromise,
|
||||
chaptersPromise,
|
||||
storyboardsPromise
|
||||
storyboardsPromise,
|
||||
playerSettingsPromise
|
||||
} = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
|
||||
|
||||
return this.buildVideoPlayer({ videoResponse, captionsPromise, chaptersPromise, storyboardsPromise, forceAutoplay })
|
||||
return this.buildVideoPlayer({
|
||||
videoResponse,
|
||||
captionsPromise,
|
||||
chaptersPromise,
|
||||
storyboardsPromise,
|
||||
playerSettingsPromise,
|
||||
forceAutoplay
|
||||
})
|
||||
} catch (err) {
|
||||
if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
|
||||
else this.playerHTML.displayError(err.message, await this.translationsPromise)
|
||||
|
@ -235,9 +243,10 @@ export class PeerTubeEmbed {
|
|||
storyboardsPromise: Promise<Response>
|
||||
captionsPromise: Promise<Response>
|
||||
chaptersPromise: Promise<Response>
|
||||
playerSettingsPromise: Promise<Response>
|
||||
forceAutoplay: boolean
|
||||
}) {
|
||||
const { videoResponse, captionsPromise, chaptersPromise, storyboardsPromise, forceAutoplay } = options
|
||||
const { videoResponse, captionsPromise, chaptersPromise, storyboardsPromise, playerSettingsPromise, forceAutoplay } = options
|
||||
|
||||
const videoInfoPromise = videoResponse.json()
|
||||
.then(async (videoInfo: VideoDetails) => {
|
||||
|
@ -259,13 +268,15 @@ export class PeerTubeEmbed {
|
|||
translations,
|
||||
captionsResponse,
|
||||
chaptersResponse,
|
||||
storyboardsResponse
|
||||
storyboardsResponse,
|
||||
playerSettingsResponse
|
||||
] = await Promise.all([
|
||||
videoInfoPromise,
|
||||
this.translationsPromise,
|
||||
captionsPromise,
|
||||
chaptersPromise,
|
||||
storyboardsPromise,
|
||||
playerSettingsPromise,
|
||||
this.buildPlayerIfNeeded()
|
||||
])
|
||||
|
||||
|
@ -283,6 +294,7 @@ export class PeerTubeEmbed {
|
|||
video,
|
||||
captionsResponse,
|
||||
chaptersResponse,
|
||||
playerSettingsResponse,
|
||||
|
||||
config: this.config,
|
||||
translations,
|
||||
|
|
|
@ -2,6 +2,9 @@ import { peertubeTranslate } from '@peertube/peertube-core-utils'
|
|||
import {
|
||||
HTMLServerConfig,
|
||||
LiveVideo,
|
||||
PlayerMode,
|
||||
PlayerTheme,
|
||||
PlayerVideoSettings,
|
||||
Storyboard,
|
||||
Video,
|
||||
VideoCaption,
|
||||
|
@ -24,14 +27,7 @@ import {
|
|||
UserLocalStorageKeys,
|
||||
videoRequiresUserAuth
|
||||
} from '../../../root-helpers'
|
||||
import {
|
||||
HLSOptions,
|
||||
PeerTubePlayerConstructorOptions,
|
||||
PeerTubePlayerLoadOptions,
|
||||
PeerTubePlayerTheme,
|
||||
PlayerMode,
|
||||
VideoJSCaption
|
||||
} from '../../player'
|
||||
import { HLSOptions, PeerTubePlayerConstructorOptions, PeerTubePlayerLoadOptions, VideoJSCaption } from '../../player'
|
||||
import { PeerTubePlugin } from './peertube-plugin'
|
||||
import { PlayerHTML } from './player-html'
|
||||
import { PlaylistTracker } from './playlist-tracker'
|
||||
|
@ -59,7 +55,7 @@ export class PlayerOptionsBuilder {
|
|||
private p2pEnabled: boolean
|
||||
private bigPlayBackgroundColor: string
|
||||
private foregroundColor: string
|
||||
private playerTheme: PeerTubePlayerTheme
|
||||
private playerTheme: PlayerTheme
|
||||
|
||||
private waitPasswordFromEmbedAPI = false
|
||||
|
||||
|
@ -69,7 +65,8 @@ export class PlayerOptionsBuilder {
|
|||
constructor (
|
||||
private readonly playerHTML: PlayerHTML,
|
||||
private readonly videoFetcher: VideoFetcher,
|
||||
private readonly peertubePlugin: PeerTubePlugin
|
||||
private readonly peertubePlugin: PeerTubePlugin,
|
||||
private readonly serverConfig: HTMLServerConfig
|
||||
) {}
|
||||
|
||||
hasAPIEnabled () {
|
||||
|
@ -150,7 +147,7 @@ export class PlayerOptionsBuilder {
|
|||
this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor')
|
||||
this.foregroundColor = getParamString(params, 'foregroundColor')
|
||||
|
||||
this.playerTheme = getParamString(params, 'playerTheme', 'default') as PeerTubePlayerTheme
|
||||
this.playerTheme = getParamString(params, 'playerTheme') as PlayerTheme
|
||||
} catch (err) {
|
||||
logger.error('Cannot get params from URL.', err)
|
||||
}
|
||||
|
@ -238,6 +235,8 @@ export class PlayerOptionsBuilder {
|
|||
|
||||
chaptersResponse: Response
|
||||
|
||||
playerSettingsResponse: Response
|
||||
|
||||
live?: LiveVideo
|
||||
|
||||
alreadyPlayed: boolean
|
||||
|
@ -271,13 +270,15 @@ export class PlayerOptionsBuilder {
|
|||
live,
|
||||
storyboardsResponse,
|
||||
chaptersResponse,
|
||||
config
|
||||
config,
|
||||
playerSettingsResponse
|
||||
} = options
|
||||
|
||||
const [ videoCaptions, storyboard, chapters ] = await Promise.all([
|
||||
const [ videoCaptions, storyboard, chapters, playerSettings ] = await Promise.all([
|
||||
this.buildCaptions(captionsResponse, translations),
|
||||
this.buildStoryboard(storyboardsResponse),
|
||||
this.buildChapters(chaptersResponse)
|
||||
this.buildChapters(chaptersResponse),
|
||||
playerSettingsResponse.json() as Promise<PlayerVideoSettings>
|
||||
])
|
||||
|
||||
const nsfwWarn = isVideoNSFWWarnedForUser(video, config, null) || isVideoNSFWHiddenForUser(video, config, null)
|
||||
|
@ -285,7 +286,7 @@ export class PlayerOptionsBuilder {
|
|||
|
||||
return {
|
||||
mode: this.mode,
|
||||
theme: this.playerTheme,
|
||||
theme: this.playerTheme || playerSettings.theme as PlayerTheme,
|
||||
|
||||
autoplay: !nsfwWarn && (forceAutoplay || alreadyPlayed || this.autoplay),
|
||||
forceAutoplay,
|
||||
|
|
|
@ -5,9 +5,7 @@ import { AuthHTTP } from './auth-http'
|
|||
import { getBackendUrl } from './url'
|
||||
|
||||
export class VideoFetcher {
|
||||
|
||||
constructor (private readonly http: AuthHTTP) {
|
||||
|
||||
}
|
||||
|
||||
async loadVideo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }) {
|
||||
|
@ -39,8 +37,9 @@ export class VideoFetcher {
|
|||
const captionsPromise = this.loadVideoCaptions({ videoId, videoPassword })
|
||||
const chaptersPromise = this.loadVideoChapters({ videoId, videoPassword })
|
||||
const storyboardsPromise = this.loadStoryboards(videoId)
|
||||
const playerSettingsPromise = this.loadPlayerSettings({ videoId, videoPassword })
|
||||
|
||||
return { captionsPromise, chaptersPromise, storyboardsPromise, videoResponse }
|
||||
return { captionsPromise, chaptersPromise, storyboardsPromise, videoResponse, playerSettingsPromise }
|
||||
}
|
||||
|
||||
loadLive (video: VideoDetails) {
|
||||
|
@ -70,10 +69,18 @@ export class VideoFetcher {
|
|||
return this.http.fetch(this.getVideoUrl(videoId) + '/chapters', { optionalAuth: true }, videoPassword)
|
||||
}
|
||||
|
||||
private loadPlayerSettings ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> {
|
||||
return this.http.fetch(this.getPlayerSettingsUrl(videoId), { optionalAuth: true }, videoPassword)
|
||||
}
|
||||
|
||||
private getVideoUrl (id: string) {
|
||||
return getBackendUrl() + '/api/v1/videos/' + id
|
||||
}
|
||||
|
||||
private getPlayerSettingsUrl (id: string) {
|
||||
return getBackendUrl() + '/api/v1/player-settings/videos/' + id
|
||||
}
|
||||
|
||||
private getLiveUrl (videoId: string) {
|
||||
return getBackendUrl() + '/api/v1/videos/live/' + videoId
|
||||
}
|
||||
|
|
|
@ -1176,6 +1176,8 @@ defaults:
|
|||
enabled: true
|
||||
|
||||
player:
|
||||
theme: 'galaxy' # 'galaxy' | 'lucide'
|
||||
|
||||
# By default, playback starts automatically when opening a video
|
||||
auto_play: true
|
||||
|
||||
|
|
|
@ -1186,6 +1186,8 @@ defaults:
|
|||
enabled: true
|
||||
|
||||
player:
|
||||
theme: 'galaxy' # 'galaxy' | 'lucide'
|
||||
|
||||
# By default, playback starts automatically when opening a video
|
||||
auto_play: true
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
ActivityObject,
|
||||
APObjectId,
|
||||
CacheFileObject,
|
||||
PlayerSettingsObject,
|
||||
PlaylistObject,
|
||||
VideoCommentObject,
|
||||
VideoObject,
|
||||
|
@ -12,7 +13,7 @@ import {
|
|||
} from './objects/index.js'
|
||||
|
||||
export type ActivityUpdateObject =
|
||||
| Extract<ActivityObject, VideoObject | CacheFileObject | PlaylistObject | ActivityPubActor | string>
|
||||
| Extract<ActivityObject, VideoObject | CacheFileObject | PlaylistObject | ActivityPubActor | PlayerSettingsObject | string>
|
||||
| ActivityPubActor
|
||||
|
||||
// Cannot Extract from Activity because of circular reference
|
||||
|
|
|
@ -38,4 +38,7 @@ export interface ActivityPubActor {
|
|||
// Used by the user export feature
|
||||
likes?: string
|
||||
dislikes?: string
|
||||
|
||||
// On channels only
|
||||
playerSettings?: string
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
export type ContextType =
|
||||
'Video' |
|
||||
'Comment' |
|
||||
'Playlist' |
|
||||
'Follow' |
|
||||
'Reject' |
|
||||
'Accept' |
|
||||
'View' |
|
||||
'Announce' |
|
||||
'CacheFile' |
|
||||
'Delete' |
|
||||
'Rate' |
|
||||
'Flag' |
|
||||
'Actor' |
|
||||
'Collection' |
|
||||
'WatchAction' |
|
||||
'Chapters' |
|
||||
'ApproveReply' |
|
||||
'RejectReply'
|
||||
| 'Video'
|
||||
| 'Comment'
|
||||
| 'Playlist'
|
||||
| 'Follow'
|
||||
| 'Reject'
|
||||
| 'Accept'
|
||||
| 'View'
|
||||
| 'Announce'
|
||||
| 'CacheFile'
|
||||
| 'Delete'
|
||||
| 'Rate'
|
||||
| 'Flag'
|
||||
| 'Actor'
|
||||
| 'Collection'
|
||||
| 'WatchAction'
|
||||
| 'Chapters'
|
||||
| 'ApproveReply'
|
||||
| 'RejectReply'
|
||||
| 'PlayerSettings'
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { AbuseObject } from './abuse-object.js'
|
||||
import { CacheFileObject } from './cache-file-object.js'
|
||||
import { PlayerSettingsObject } from './player-settings-object.js'
|
||||
import { PlaylistObject } from './playlist-object.js'
|
||||
import { VideoCommentObject } from './video-comment-object.js'
|
||||
import { VideoObject } from './video-object.js'
|
||||
import { WatchActionObject } from './watch-action-object.js'
|
||||
|
||||
export type ActivityObject =
|
||||
VideoObject |
|
||||
AbuseObject |
|
||||
VideoCommentObject |
|
||||
CacheFileObject |
|
||||
PlaylistObject |
|
||||
WatchActionObject |
|
||||
string
|
||||
| VideoObject
|
||||
| AbuseObject
|
||||
| VideoCommentObject
|
||||
| CacheFileObject
|
||||
| PlaylistObject
|
||||
| WatchActionObject
|
||||
| PlayerSettingsObject
|
||||
| string
|
||||
|
||||
export type APObjectId = string | { id: string }
|
||||
|
|
|
@ -8,4 +8,5 @@ export * from './video-caption-object.js'
|
|||
export * from './video-chapters-object.js'
|
||||
export * from './video-comment-object.js'
|
||||
export * from './video-object.js'
|
||||
export * from './player-settings-object.js'
|
||||
export * from './watch-action-object.js'
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { PlayerThemeChannelSetting, PlayerThemeVideoSetting } from '../../player/player-theme.type.js'
|
||||
|
||||
export interface PlayerSettingsObject {
|
||||
type: 'PlayerSettings'
|
||||
id: string
|
||||
object: string
|
||||
theme: PlayerThemeVideoSetting | PlayerThemeChannelSetting
|
||||
}
|
|
@ -64,6 +64,7 @@ export interface VideoObject {
|
|||
shares: string
|
||||
comments: string
|
||||
hasParts: string | VideoChapterObject[]
|
||||
playerSettings: string
|
||||
|
||||
attributedTo: ActivityPubAttributedTo[]
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { PlayerThemeChannelSetting } from '../../player/player-theme.type.js'
|
||||
import { UserActorImageJSON } from './actor-export.model.js'
|
||||
|
||||
export interface ChannelExportJSON {
|
||||
|
@ -15,6 +16,10 @@ export interface ChannelExportJSON {
|
|||
avatars: UserActorImageJSON[]
|
||||
banners: UserActorImageJSON[]
|
||||
|
||||
playerSettings?: {
|
||||
theme: PlayerThemeChannelSetting
|
||||
}
|
||||
|
||||
archiveFiles: {
|
||||
avatar: string | null
|
||||
banner: string | null
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { PlayerThemeVideoSetting } from '../../player/player-theme.type.js'
|
||||
import {
|
||||
LiveVideoLatencyModeType,
|
||||
VideoCommentPolicyType,
|
||||
|
@ -108,6 +109,10 @@ export interface VideoExportJSON {
|
|||
metadata: VideoFileMetadata
|
||||
}
|
||||
|
||||
playerSettings?: {
|
||||
theme: PlayerThemeVideoSetting
|
||||
}
|
||||
|
||||
archiveFiles: {
|
||||
videoFile: string | null
|
||||
thumbnail: string | null
|
||||
|
|
|
@ -11,6 +11,7 @@ export * from './metrics/index.js'
|
|||
export * from './moderation/index.js'
|
||||
export * from './nodeinfo/index.js'
|
||||
export * from './overviews/index.js'
|
||||
export * from './player/index.js'
|
||||
export * from './plugins/index.js'
|
||||
export * from './redundancy/index.js'
|
||||
export * from './runners/index.js'
|
||||
|
|
2
packages/models/src/player/index.ts
Normal file
2
packages/models/src/player/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './player-mode.type.js'
|
||||
export * from './player-theme.type.js'
|
1
packages/models/src/player/player-mode.type.ts
Normal file
1
packages/models/src/player/player-mode.type.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type PlayerMode = 'web-video' | 'p2p-media-loader'
|
3
packages/models/src/player/player-theme.type.ts
Normal file
3
packages/models/src/player/player-theme.type.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type PlayerTheme = 'galaxy' | 'lucide'
|
||||
export type PlayerThemeChannelSetting = 'instance-default' | PlayerTheme
|
||||
export type PlayerThemeVideoSetting = 'channel-default' | PlayerThemeChannelSetting
|
|
@ -1,3 +1,4 @@
|
|||
import { PlayerTheme } from '../player/player-theme.type.js'
|
||||
import { VideoCommentPolicyType, VideoPrivacyType } from '../videos/index.js'
|
||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
||||
import { BroadcastMessageLevel } from './broadcast-message-level.type.js'
|
||||
|
@ -370,6 +371,7 @@ export interface CustomConfig {
|
|||
}
|
||||
|
||||
player: {
|
||||
theme: PlayerTheme
|
||||
autoPlay: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ActorImage, LogoType, VideoCommentPolicyType } from '../index.js'
|
||||
import { ActorImage, LogoType, PlayerTheme, VideoCommentPolicyType } from '../index.js'
|
||||
import { ClientScriptJSON } from '../plugins/plugin-package-json.model.js'
|
||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
||||
import { VideoPrivacyType } from '../videos/video-privacy.enum.js'
|
||||
|
@ -103,6 +103,7 @@ export interface ServerConfig {
|
|||
}
|
||||
|
||||
player: {
|
||||
theme: PlayerTheme
|
||||
autoPlay: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ export * from './chapter/index.js'
|
|||
|
||||
export * from './nsfw-flag.enum.js'
|
||||
export * from './nsfw-policy.type.js'
|
||||
export * from './player-settings.js'
|
||||
export * from './player-settings-update.js'
|
||||
|
||||
export * from './storyboard.model.js'
|
||||
export * from './thumbnail.type.js'
|
||||
|
|
9
packages/models/src/videos/player-settings-update.ts
Normal file
9
packages/models/src/videos/player-settings-update.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { PlayerThemeChannelSetting, PlayerThemeVideoSetting } from '../player/player-theme.type.js'
|
||||
|
||||
export interface PlayerVideoSettingsUpdate {
|
||||
theme: PlayerThemeVideoSetting
|
||||
}
|
||||
|
||||
export interface PlayerChannelSettingsUpdate {
|
||||
theme: PlayerThemeChannelSetting
|
||||
}
|
9
packages/models/src/videos/player-settings.ts
Normal file
9
packages/models/src/videos/player-settings.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { PlayerThemeChannelSetting, PlayerThemeVideoSetting } from '../player/player-theme.type.js'
|
||||
|
||||
export interface PlayerVideoSettings {
|
||||
theme: PlayerThemeVideoSetting
|
||||
}
|
||||
|
||||
export interface PlayerChannelSettings {
|
||||
theme: PlayerThemeChannelSetting
|
||||
}
|
|
@ -3,6 +3,7 @@ import { Video, VideoChannel, VideoChannelSync, VideoCreateResult, VideoDetails
|
|||
import { parallelTests, root } from '@peertube/peertube-node-utils'
|
||||
import { ChildProcess, fork } from 'child_process'
|
||||
import { copy } from 'fs-extra/esm'
|
||||
import merge from 'lodash-es/merge.js'
|
||||
import { join } from 'path'
|
||||
import { BulkCommand } from '../bulk/index.js'
|
||||
import { CLICommand } from '../cli/index.js'
|
||||
|
@ -36,6 +37,7 @@ import {
|
|||
CommentsCommand,
|
||||
HistoryCommand,
|
||||
LiveCommand,
|
||||
PlayerSettingsCommand,
|
||||
PlaylistsCommand,
|
||||
ServicesCommand,
|
||||
StoryboardCommand,
|
||||
|
@ -58,7 +60,6 @@ import { PluginsCommand } from './plugins-command.js'
|
|||
import { RedundancyCommand } from './redundancy-command.js'
|
||||
import { ServersCommand } from './servers-command.js'
|
||||
import { StatsCommand } from './stats-command.js'
|
||||
import merge from 'lodash-es/merge.js'
|
||||
|
||||
export type RunServerOptions = {
|
||||
autoEnableImportProxy?: boolean
|
||||
|
@ -154,6 +155,7 @@ export class PeerTubeServer {
|
|||
videoToken?: VideoTokenCommand
|
||||
registrations?: RegistrationsCommand
|
||||
videoPasswords?: VideoPasswordsCommand
|
||||
playerSettings?: PlayerSettingsCommand
|
||||
|
||||
storyboard?: StoryboardCommand
|
||||
chapters?: ChaptersCommand
|
||||
|
@ -460,6 +462,8 @@ export class PeerTubeServer {
|
|||
this.videoToken = new VideoTokenCommand(this)
|
||||
this.registrations = new RegistrationsCommand(this)
|
||||
|
||||
this.playerSettings = new PlayerSettingsCommand(this)
|
||||
|
||||
this.storyboard = new StoryboardCommand(this)
|
||||
this.chapters = new ChaptersCommand(this)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export * from './channels-command.js'
|
|||
export * from './chapters-command.js'
|
||||
export * from './channel-syncs-command.js'
|
||||
export * from './comments-command.js'
|
||||
export * from './player-settings-command.js'
|
||||
export * from './history-command.js'
|
||||
export * from './video-imports-command.js'
|
||||
export * from './live-command.js'
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, PlayerChannelSettings, PlayerVideoSettings, PlayerVideoSettingsUpdate } from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/requests.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class PlayerSettingsCommand extends AbstractCommand {
|
||||
getForVideo (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
raw?: boolean
|
||||
videoPassword?: string
|
||||
}
|
||||
) {
|
||||
const path = '/api/v1/player-settings/videos/' + options.videoId
|
||||
|
||||
return this.get<PlayerVideoSettings>({ ...options, path })
|
||||
}
|
||||
|
||||
getForChannel (
|
||||
options: OverrideCommandOptions & {
|
||||
channelHandle: string
|
||||
raw?: boolean
|
||||
}
|
||||
) {
|
||||
const path = '/api/v1/player-settings/video-channels/' + options.channelHandle
|
||||
|
||||
return this.get<PlayerChannelSettings>({ ...options, path })
|
||||
}
|
||||
|
||||
private get<T = (PlayerVideoSettings | PlayerChannelSettings)> (
|
||||
options: OverrideCommandOptions & {
|
||||
path: string
|
||||
videoPassword?: string
|
||||
|
||||
raw?: boolean
|
||||
}
|
||||
) {
|
||||
const headers = this.buildVideoPasswordHeader(options.videoPassword)
|
||||
|
||||
return this.getRequestBody<T>({
|
||||
...options,
|
||||
|
||||
headers,
|
||||
|
||||
query: options.raw
|
||||
? { raw: options.raw }
|
||||
: undefined,
|
||||
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateForVideo (
|
||||
options: OverrideCommandOptions & PlayerVideoSettingsUpdate & {
|
||||
videoId: number | string
|
||||
}
|
||||
) {
|
||||
const path = '/api/v1/player-settings/videos/' + options.videoId
|
||||
|
||||
return this.update<PlayerVideoSettings>({ ...options, path })
|
||||
}
|
||||
|
||||
updateForChannel (
|
||||
options: OverrideCommandOptions & PlayerVideoSettingsUpdate & {
|
||||
channelHandle: string
|
||||
}
|
||||
) {
|
||||
const path = '/api/v1/player-settings/video-channels/' + options.channelHandle
|
||||
|
||||
return this.update<PlayerChannelSettings>({ ...options, path })
|
||||
}
|
||||
|
||||
private update<T = (PlayerVideoSettings | PlayerChannelSettings)> (
|
||||
options: OverrideCommandOptions & PlayerVideoSettingsUpdate & {
|
||||
path: string
|
||||
}
|
||||
) {
|
||||
return unwrapBody<T>(this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
fields: pick(options, [ 'theme' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import './live.js'
|
|||
import './logs.js'
|
||||
import './metrics.js'
|
||||
import './my-user.js'
|
||||
import './player-settings.js'
|
||||
import './plugins.js'
|
||||
import './redundancy.js'
|
||||
import './registrations.js'
|
||||
|
|
138
packages/tests/src/api/check-params/player-settings.ts
Normal file
138
packages/tests/src/api/check-params/player-settings.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, PlayerSettings, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test player settings API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
let ownerAccessToken: string
|
||||
let userAccessToken: string
|
||||
let video: VideoCreateResult
|
||||
let privateVideo: VideoCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
ownerAccessToken = await server.users.generateUserAndToken('owner')
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
video = await server.videos.upload({ token: ownerAccessToken })
|
||||
privateVideo = await server.videos.upload({ token: ownerAccessToken, attributes: { privacy: VideoPrivacy.PRIVATE } })
|
||||
})
|
||||
|
||||
it('Should fail to get video player settings if the video does not exist', async function () {
|
||||
await server.playerSettings.getForVideo({ videoId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should check video privacy before getting player settings of a video', async function () {
|
||||
const videoId = privateVideo.uuid
|
||||
|
||||
await server.playerSettings.getForVideo({ token: server.accessToken, videoId })
|
||||
await server.playerSettings.getForVideo({ token: ownerAccessToken, videoId })
|
||||
await server.playerSettings.getForVideo({ token: userAccessToken, videoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await server.playerSettings.getForVideo({ token: null, videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail to get channel player settings if the channel does not exist', async function () {
|
||||
await server.playerSettings.getForChannel({
|
||||
token: ownerAccessToken,
|
||||
channelHandle: 'unknown',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should only allow to get raw player settings of a channel by owner/moderators', async function () {
|
||||
const channelHandle = 'owner_channel'
|
||||
const videoId = video.uuid
|
||||
|
||||
await server.playerSettings.getForChannel({ token: server.accessToken, channelHandle, raw: true })
|
||||
await server.playerSettings.getForChannel({ token: ownerAccessToken, channelHandle, raw: true })
|
||||
await server.playerSettings.getForChannel({
|
||||
token: userAccessToken,
|
||||
channelHandle,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
raw: true
|
||||
})
|
||||
await server.playerSettings.getForChannel({ token: null, channelHandle, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, raw: true })
|
||||
|
||||
await server.playerSettings.getForVideo({ token: server.accessToken, videoId, raw: true })
|
||||
await server.playerSettings.getForVideo({ token: ownerAccessToken, videoId, raw: true })
|
||||
await server.playerSettings.getForVideo({ token: userAccessToken, videoId, expectedStatus: HttpStatusCode.FORBIDDEN_403, raw: true })
|
||||
await server.playerSettings.getForVideo({ token: null, videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, raw: true })
|
||||
})
|
||||
|
||||
it('Should only allow to update player settings of a video by owner/moderators', async function () {
|
||||
const videoId = video.uuid
|
||||
const playerSettings: PlayerSettings = { theme: 'lucide' }
|
||||
|
||||
await server.playerSettings.updateForVideo({ token: server.accessToken, videoId, ...playerSettings })
|
||||
await server.playerSettings.updateForVideo({ token: ownerAccessToken, videoId, ...playerSettings })
|
||||
|
||||
await server.playerSettings.updateForVideo({
|
||||
token: userAccessToken,
|
||||
videoId,
|
||||
...playerSettings,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
|
||||
await server.playerSettings.updateForVideo({ token: null, videoId, ...playerSettings, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should only allow to update player settings of a channel by owner/moderators', async function () {
|
||||
const channelHandle = 'owner_channel'
|
||||
const playerSettings: PlayerSettings = { theme: 'lucide' }
|
||||
|
||||
await server.playerSettings.updateForChannel({ token: server.accessToken, channelHandle, ...playerSettings })
|
||||
await server.playerSettings.updateForChannel({ token: ownerAccessToken, channelHandle, ...playerSettings })
|
||||
|
||||
await server.playerSettings.updateForChannel({
|
||||
token: userAccessToken,
|
||||
channelHandle,
|
||||
...playerSettings,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
|
||||
await server.playerSettings.updateForChannel({
|
||||
token: null,
|
||||
channelHandle,
|
||||
...playerSettings,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to update player settings with invalid settings', async function () {
|
||||
const videoId = video.uuid
|
||||
const channelHandle = 'owner_channel'
|
||||
|
||||
{
|
||||
const playerSettings = { theme: 'invalid' } as any
|
||||
|
||||
await server.playerSettings.updateForVideo({ videoId, ...playerSettings, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.playerSettings.updateForChannel({ channelHandle, ...playerSettings, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
|
||||
{
|
||||
const playerSettings = { theme: 'channel-default' } as any
|
||||
|
||||
await server.playerSettings.updateForVideo({ videoId, ...playerSettings })
|
||||
await server.playerSettings.updateForChannel({ channelHandle, ...playerSettings, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
|
@ -48,7 +48,7 @@ describe('Test video passwords validator', function () {
|
|||
},
|
||||
import: {
|
||||
videos: {
|
||||
http:{
|
||||
http: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,6 @@ describe('Test video passwords validator', function () {
|
|||
}
|
||||
|
||||
function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') {
|
||||
|
||||
it('Should fail with a password protected privacy without providing a password', async function () {
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
|
@ -268,7 +267,17 @@ describe('Test video passwords validator', function () {
|
|||
token?: string
|
||||
videoPassword?: string
|
||||
expectedStatus: HttpStatusCodeType
|
||||
mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token'
|
||||
mode:
|
||||
| 'get'
|
||||
| 'getWithPassword'
|
||||
| 'getWithToken'
|
||||
| 'listCaptions'
|
||||
| 'createThread'
|
||||
| 'listThreads'
|
||||
| 'replyThread'
|
||||
| 'rate'
|
||||
| 'token'
|
||||
| 'getPlayerSettings'
|
||||
}) {
|
||||
const { server, token = null, videoPassword, expectedStatus, mode } = options
|
||||
|
||||
|
@ -351,6 +360,15 @@ describe('Test video passwords validator', function () {
|
|||
})
|
||||
}
|
||||
|
||||
if (mode === 'getPlayerSettings') {
|
||||
return server.playerSettings.getForVideo({
|
||||
videoId: video.id,
|
||||
token,
|
||||
expectedStatus,
|
||||
videoPassword
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'token') {
|
||||
return server.videoToken.create({
|
||||
videoId: video.id,
|
||||
|
@ -380,9 +398,12 @@ describe('Test video passwords validator', function () {
|
|||
expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') {
|
||||
function validateVideoAccess (
|
||||
mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'getPlayerSettings' | 'rate' | 'token'
|
||||
) {
|
||||
const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode)
|
||||
let tokens: string[]
|
||||
|
||||
if (!requiresUserAuth) {
|
||||
it('Should fail without providing a password for an unlogged user', async function () {
|
||||
const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode })
|
||||
|
@ -482,7 +503,6 @@ describe('Test video passwords validator', function () {
|
|||
}
|
||||
|
||||
describe('When accessing password protected video', function () {
|
||||
|
||||
describe('For getting a password protected video', function () {
|
||||
validateVideoAccess('get')
|
||||
})
|
||||
|
@ -507,6 +527,10 @@ describe('Test video passwords validator', function () {
|
|||
validateVideoAccess('listCaptions')
|
||||
})
|
||||
|
||||
describe('For getting player settings', function () {
|
||||
validateVideoAccess('getPlayerSettings')
|
||||
})
|
||||
|
||||
describe('For creating video file token', function () {
|
||||
validateVideoAccess('token')
|
||||
})
|
||||
|
|
|
@ -26,7 +26,6 @@ describe('Test config defaults', function () {
|
|||
})
|
||||
|
||||
describe('Default publish values', function () {
|
||||
|
||||
before(async function () {
|
||||
const overrideConfig = {
|
||||
defaults: {
|
||||
|
@ -123,9 +122,7 @@ describe('Test config defaults', function () {
|
|||
})
|
||||
|
||||
describe('Default P2P values', function () {
|
||||
|
||||
describe('Webapp default value', function () {
|
||||
|
||||
before(async function () {
|
||||
const overrideConfig = {
|
||||
defaults: {
|
||||
|
@ -167,7 +164,6 @@ describe('Test config defaults', function () {
|
|||
})
|
||||
|
||||
describe('Embed default value', function () {
|
||||
|
||||
before(async function () {
|
||||
const overrideConfig = {
|
||||
defaults: {
|
||||
|
@ -213,11 +209,11 @@ describe('Test config defaults', function () {
|
|||
})
|
||||
|
||||
describe('Default player value', function () {
|
||||
|
||||
before(async function () {
|
||||
const overrideConfig = {
|
||||
defaults: {
|
||||
player: {
|
||||
theme: 'lucide',
|
||||
auto_play: false
|
||||
}
|
||||
},
|
||||
|
@ -230,9 +226,10 @@ describe('Test config defaults', function () {
|
|||
await server.run(overrideConfig)
|
||||
})
|
||||
|
||||
it('Should have appropriate autoplay config', async function () {
|
||||
it('Should have appropriate player config', async function () {
|
||||
const config = await server.config.getConfig()
|
||||
|
||||
expect(config.defaults.player.theme).to.equal('lucide')
|
||||
expect(config.defaults.player.autoPlay).to.be.false
|
||||
})
|
||||
|
||||
|
@ -255,7 +252,6 @@ describe('Test config defaults', function () {
|
|||
})
|
||||
|
||||
describe('Default user attributes', function () {
|
||||
|
||||
it('Should create a user and register a user with the default config', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
|
@ -265,7 +261,7 @@ describe('Test config defaults', function () {
|
|||
enabled: true
|
||||
}
|
||||
},
|
||||
videoQuota : -1,
|
||||
videoQuota: -1,
|
||||
videoQuotaDaily: -1
|
||||
},
|
||||
signup: {
|
||||
|
@ -305,7 +301,7 @@ describe('Test config defaults', function () {
|
|||
enabled: false
|
||||
}
|
||||
},
|
||||
videoQuota : 5242881,
|
||||
videoQuota: 5242881,
|
||||
videoQuotaDaily: 318742
|
||||
},
|
||||
signup: {
|
||||
|
@ -330,7 +326,6 @@ describe('Test config defaults', function () {
|
|||
expect(user.videoQuotaDaily).to.equal(318742)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
|
|
|
@ -159,6 +159,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
|||
expect(data.defaults.publish.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
expect(data.defaults.p2p.embed.enabled).to.be.true
|
||||
expect(data.defaults.p2p.webapp.enabled).to.be.true
|
||||
expect(data.defaults.player.theme).to.equal('galaxy')
|
||||
expect(data.defaults.player.autoPlay).to.be.true
|
||||
|
||||
expect(data.email.body.signature).to.equal('')
|
||||
|
@ -473,7 +474,8 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
|||
}
|
||||
},
|
||||
player: {
|
||||
autoPlay: false
|
||||
autoPlay: false,
|
||||
theme: 'lucide'
|
||||
}
|
||||
},
|
||||
email: {
|
||||
|
|
|
@ -455,6 +455,7 @@ function runTest (withObjectStorage: boolean) {
|
|||
expect(secondaryChannel.displayName).to.equal('noah display name')
|
||||
expect(secondaryChannel.description).to.equal('noah description')
|
||||
expect(secondaryChannel.support).to.equal('noah support')
|
||||
expect(secondaryChannel.playerSettings.theme).to.equal('galaxy')
|
||||
|
||||
expect(secondaryChannel.avatars).to.have.lengthOf(4)
|
||||
expect(secondaryChannel.banners).to.have.lengthOf(2)
|
||||
|
@ -554,6 +555,8 @@ function runTest (withObjectStorage: boolean) {
|
|||
expect(publicVideo.source.metadata?.streams).to.exist
|
||||
expect(publicVideo.source.resolution).to.equal(720)
|
||||
expect(publicVideo.source.size).to.equal(218910)
|
||||
|
||||
expect(publicVideo.playerSettings.theme).to.equal('lucide')
|
||||
}
|
||||
|
||||
{
|
||||
|
|
|
@ -193,11 +193,25 @@ function runTest (withObjectStorage: boolean) {
|
|||
expect(importedMain.avatars).to.have.lengthOf(0)
|
||||
expect(importedMain.banners).to.have.lengthOf(0)
|
||||
|
||||
const playerSettingMain = await remoteServer.playerSettings.getForChannel({
|
||||
channelHandle: 'noah_remote_channel',
|
||||
token: remoteServer.accessToken,
|
||||
raw: true
|
||||
})
|
||||
expect(playerSettingMain.theme).to.equal('instance-default')
|
||||
|
||||
const importedSecond = await remoteServer.channels.get({ token: remoteNoahToken, channelName: 'noah_second_channel' })
|
||||
expect(importedSecond.displayName).to.equal('noah display name')
|
||||
expect(importedSecond.description).to.equal('noah description')
|
||||
expect(importedSecond.support).to.equal('noah support')
|
||||
|
||||
const playerSettingSecond = await remoteServer.playerSettings.getForChannel({
|
||||
channelHandle: 'noah_second_channel',
|
||||
token: remoteServer.accessToken,
|
||||
raw: true
|
||||
})
|
||||
expect(playerSettingSecond.theme).to.equal('galaxy')
|
||||
|
||||
for (const banner of importedSecond.banners) {
|
||||
await testImage({ url: banner.fileUrl, name: `banner-user-import-resized-${banner.width}.jpg` })
|
||||
}
|
||||
|
@ -376,6 +390,13 @@ function runTest (withObjectStorage: boolean) {
|
|||
expect(publicVideo).to.exist
|
||||
expect(publicVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
|
||||
|
||||
const playerSetting = await remoteServer.playerSettings.getForVideo({
|
||||
videoId: publicVideo.uuid,
|
||||
token: remoteServer.accessToken,
|
||||
raw: true
|
||||
})
|
||||
expect(playerSetting.theme).to.equal('lucide')
|
||||
|
||||
// Federated
|
||||
await server.videos.get({ id: publicVideo.uuid })
|
||||
}
|
||||
|
@ -385,6 +406,13 @@ function runTest (withObjectStorage: boolean) {
|
|||
expect(passwordVideo).to.exist
|
||||
expect(passwordVideo.privacy.id).to.equal(VideoPrivacy.PASSWORD_PROTECTED)
|
||||
|
||||
const playerSetting = await remoteServer.playerSettings.getForVideo({
|
||||
videoId: passwordVideo.uuid,
|
||||
token: remoteServer.accessToken,
|
||||
raw: true
|
||||
})
|
||||
expect(playerSetting.theme).to.equal('channel-default')
|
||||
|
||||
const { data: passwords } = await remoteServer.videoPasswords.list({ videoId: passwordVideo.uuid })
|
||||
expect(passwords.map(p => p.password).sort()).to.deep.equal([ 'password1', 'password2' ])
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import './channel-import-videos.js'
|
||||
import './generate-download.js'
|
||||
import './multiple-servers.js'
|
||||
import './player-settings.js'
|
||||
import './resumable-upload.js'
|
||||
import './single-server.js'
|
||||
import './video-captions.js'
|
||||
|
|
283
packages/tests/src/api/videos/player-settings.ts
Normal file
283
packages/tests/src/api/videos/player-settings.ts
Normal file
|
@ -0,0 +1,283 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
import { VideoCreateResult } from '../../../../models/src/videos/video-create-result.model.js'
|
||||
|
||||
describe('Test player settings', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let video: VideoCreateResult
|
||||
let otherVideo: VideoCreateResult
|
||||
let otherVideoAndChannel: VideoCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
video = await servers[0].videos.upload()
|
||||
otherVideo = await servers[0].videos.upload()
|
||||
|
||||
const otherChannel = await servers[0].channels.create({ attributes: { name: 'other_channel' } })
|
||||
otherVideoAndChannel = await servers[0].videos.upload({ attributes: { channelId: otherChannel.id } })
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
async function check (options: {
|
||||
server: PeerTubeServer
|
||||
videoId: number | string
|
||||
channelHandle: string
|
||||
expectedVideo: string
|
||||
expectedChannel: string
|
||||
expectedRawVideo?: string
|
||||
expectedRawChannel?: string
|
||||
}) {
|
||||
const { server, expectedRawVideo, expectedRawChannel, expectedVideo, expectedChannel } = options
|
||||
|
||||
// Raw mode
|
||||
{
|
||||
if (expectedRawVideo) {
|
||||
const { theme } = await server.playerSettings.getForVideo({
|
||||
token: server.accessToken,
|
||||
videoId: options.videoId,
|
||||
raw: true
|
||||
})
|
||||
expect(theme).to.equal(expectedRawVideo)
|
||||
}
|
||||
|
||||
if (expectedRawChannel) {
|
||||
const { theme } = await server.playerSettings.getForChannel({
|
||||
token: server.accessToken,
|
||||
channelHandle: options.channelHandle,
|
||||
raw: true
|
||||
})
|
||||
expect(theme).to.equal(expectedRawChannel)
|
||||
}
|
||||
}
|
||||
|
||||
// Interpreted settings mode
|
||||
{
|
||||
{
|
||||
const { theme } = await server.playerSettings.getForVideo({ videoId: options.videoId })
|
||||
expect(theme).to.equal(expectedVideo)
|
||||
}
|
||||
|
||||
{
|
||||
const { theme } = await server.playerSettings.getForChannel({ channelHandle: options.channelHandle, raw: false })
|
||||
expect(theme).to.equal(expectedChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('Should return default player settings for the instance', async function () {
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'channel-default',
|
||||
expectedRawChannel: 'instance-default',
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should update instance settings and return the updated settings', async function () {
|
||||
await servers[0].config.updateExistingConfig({ newConfig: { defaults: { player: { theme: 'lucide' } } } })
|
||||
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'channel-default',
|
||||
expectedRawChannel: 'instance-default',
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'lucide'
|
||||
})
|
||||
|
||||
// Instance 2 keeps its own instance default
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
// Update instance 2 default theme to observe changes
|
||||
await servers[1].config.updateExistingConfig({ newConfig: { defaults: { player: { theme: 'lucide' } } } })
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'lucide'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should update player settings of the channel and return the updated settings', async function () {
|
||||
const { theme } = await servers[0].playerSettings.updateForChannel({ channelHandle: 'root_channel', theme: 'galaxy' })
|
||||
expect(theme).to.equal('galaxy')
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'channel-default',
|
||||
expectedRawChannel: 'galaxy',
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should update player settings of the video and return the updated settings', async function () {
|
||||
const { theme } = await servers[0].playerSettings.updateForVideo({ videoId: video.id, theme: 'lucide' })
|
||||
expect(theme).to.equal('lucide')
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'lucide',
|
||||
expectedRawChannel: 'galaxy',
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should choose the default instance player theme', async function () {
|
||||
const { theme } = await servers[0].playerSettings.updateForVideo({ videoId: video.id, theme: 'instance-default' })
|
||||
expect(theme).to.equal('instance-default')
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'instance-default',
|
||||
expectedRawChannel: 'galaxy',
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should choose the default channel player theme', async function () {
|
||||
const { theme } = await servers[0].playerSettings.updateForVideo({ videoId: video.id, theme: 'channel-default' })
|
||||
expect(theme).to.equal('channel-default')
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'channel-default',
|
||||
expectedRawChannel: 'galaxy',
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: video.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should keep default settings for the other video', async function () {
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: otherVideo.uuid,
|
||||
channelHandle: 'root_channel',
|
||||
expectedRawVideo: 'channel-default',
|
||||
expectedRawChannel: 'galaxy',
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: otherVideo.uuid,
|
||||
channelHandle: 'root_channel@' + servers[0].host,
|
||||
expectedVideo: 'galaxy',
|
||||
expectedChannel: 'galaxy'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should keep default settings for the other channel', async function () {
|
||||
await check({
|
||||
server: servers[0],
|
||||
videoId: otherVideoAndChannel.uuid,
|
||||
channelHandle: 'other_channel',
|
||||
expectedRawVideo: 'channel-default',
|
||||
expectedRawChannel: 'instance-default',
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'lucide'
|
||||
})
|
||||
|
||||
await check({
|
||||
server: servers[1],
|
||||
videoId: otherVideoAndChannel.uuid,
|
||||
channelHandle: 'other_channel@' + servers[0].host,
|
||||
expectedVideo: 'lucide',
|
||||
expectedChannel: 'lucide'
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
|
@ -211,13 +211,17 @@ export async function prepareImportExportTests (options: {
|
|||
fixture: 'avatar.png',
|
||||
type: 'avatar'
|
||||
})
|
||||
await server.playerSettings.updateForChannel({ channelHandle: 'noah_second_channel', theme: 'galaxy' })
|
||||
|
||||
// Videos
|
||||
const externalVideo = await remoteServer.videos.quickUpload({ name: 'external video', privacy: VideoPrivacy.PUBLIC })
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const noahPrivateVideo = await server.videos.quickUpload({ name: 'noah private video', token: noahToken, privacy: VideoPrivacy.PRIVATE })
|
||||
|
||||
const noahVideo = await server.videos.quickUpload({ name: 'noah public video', token: noahToken, privacy: VideoPrivacy.PUBLIC })
|
||||
await server.playerSettings.updateForVideo({ videoId: noahVideo.uuid, theme: 'lucide' })
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const noahVideo2 = await server.videos.upload({
|
||||
token: noahToken,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getContextFilter } from '@server/lib/activitypub/context.js'
|
|||
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
|
||||
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models/index.js'
|
||||
import cors from 'cors'
|
||||
|
@ -177,6 +178,13 @@ activityPubClientRouter.get(
|
|||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoDislikesController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/player-settings',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoPlayerSettingsController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -228,6 +236,13 @@ activityPubClientRouter.get(
|
|||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelPlaylistsController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/player-settings',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(channelPlayerSettingsController)
|
||||
)
|
||||
|
||||
activityPubClientRouter.get(
|
||||
'/redundancy/streaming-playlists/:streamingPlaylistType/:videoId',
|
||||
|
@ -399,6 +414,30 @@ async function videoCommentsController (req: express.Request, res: express.Respo
|
|||
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function videoPlayerSettingsController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const settings = await PlayerSettingModel.loadByVideoId(video.id)
|
||||
const json = PlayerSettingModel.formatAPPlayerSetting({ channel: undefined, video, settings })
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'PlayerSettings', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function channelPlayerSettingsController (req: express.Request, res: express.Response) {
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const settings = await PlayerSettingModel.loadByChannelId(channel.id)
|
||||
const json = PlayerSettingModel.formatAPPlayerSetting({ channel, video: undefined, settings })
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'PlayerSettings', getContextFilter()), res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function videoChannelController (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
|
|
|
@ -605,6 +605,7 @@ function customConfig (): CustomConfig {
|
|||
}
|
||||
},
|
||||
player: {
|
||||
theme: CONFIG.DEFAULTS.PLAYER.THEME,
|
||||
autoPlay: CONFIG.DEFAULTS.PLAYER.AUTO_PLAY
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ import { jobsRouter } from './jobs.js'
|
|||
import { metricsRouter } from './metrics.js'
|
||||
import { oauthClientsRouter } from './oauth-clients.js'
|
||||
import { overviewsRouter } from './overviews.js'
|
||||
import { playerSettingsRouter } from './player-settings.js'
|
||||
import { pluginRouter } from './plugins.js'
|
||||
import { runnersRouter } from './runners/index.js'
|
||||
import { searchRouter } from './search/index.js'
|
||||
|
@ -48,6 +49,7 @@ apiRouter.use('/jobs', jobsRouter)
|
|||
apiRouter.use('/metrics', metricsRouter)
|
||||
apiRouter.use('/search', searchRouter)
|
||||
apiRouter.use('/overviews', overviewsRouter)
|
||||
apiRouter.use('/player-settings', playerSettingsRouter)
|
||||
apiRouter.use('/plugins', pluginRouter)
|
||||
apiRouter.use('/custom-pages', customPageRouter)
|
||||
apiRouter.use('/blocklist', blocklistRouter)
|
||||
|
|
112
server/core/controllers/api/player-settings.ts
Normal file
112
server/core/controllers/api/player-settings.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { PlayerChannelSettingsUpdate, PlayerVideoSettingsUpdate } from '@peertube/peertube-models'
|
||||
import { upsertPlayerSettings } from '@server/lib/player-settings.js'
|
||||
import {
|
||||
getChannelPlayerSettingsValidator,
|
||||
getVideoPlayerSettingsValidator,
|
||||
updatePlayerSettingsValidatorFactory,
|
||||
updateVideoPlayerSettingsValidator
|
||||
} from '@server/middlewares/validators/player-settings.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import express from 'express'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
asyncMiddleware,
|
||||
authenticate,
|
||||
optionalAuthenticate,
|
||||
videoChannelsHandleValidatorFactory
|
||||
} from '../../middlewares/index.js'
|
||||
import { sendUpdateChannelPlayerSettings, sendUpdateVideoPlayerSettings } from '@server/lib/activitypub/send/send-update.js'
|
||||
|
||||
const playerSettingsRouter = express.Router()
|
||||
|
||||
playerSettingsRouter.use(apiRateLimiter)
|
||||
|
||||
playerSettingsRouter.get(
|
||||
'/videos/:videoId',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(getVideoPlayerSettingsValidator),
|
||||
asyncMiddleware(getVideoPlayerSettings)
|
||||
)
|
||||
|
||||
playerSettingsRouter.put(
|
||||
'/videos/:videoId',
|
||||
authenticate,
|
||||
asyncMiddleware(updateVideoPlayerSettingsValidator),
|
||||
updatePlayerSettingsValidatorFactory('video'),
|
||||
asyncMiddleware(updateVideoPlayerSettings)
|
||||
)
|
||||
|
||||
playerSettingsRouter.get(
|
||||
'/video-channels/:handle',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
getChannelPlayerSettingsValidator,
|
||||
asyncMiddleware(getChannelPlayerSettings)
|
||||
)
|
||||
|
||||
playerSettingsRouter.put(
|
||||
'/video-channels/:handle',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
updatePlayerSettingsValidatorFactory('channel'),
|
||||
asyncMiddleware(updateChannelPlayerSettings)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
playerSettingsRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getVideoPlayerSettings (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo || res.locals.videoAll
|
||||
|
||||
const { videoSetting, channelSetting } = await PlayerSettingModel.loadByVideoIdOrChannelId({
|
||||
channelId: video.channelId,
|
||||
videoId: video.id
|
||||
})
|
||||
|
||||
if (req.query.raw === true) {
|
||||
return res.json(PlayerSettingModel.formatVideoPlayerRawSetting(videoSetting))
|
||||
}
|
||||
|
||||
return res.json(PlayerSettingModel.formatVideoPlayerSetting({ videoSetting, channelSetting }))
|
||||
}
|
||||
|
||||
async function getChannelPlayerSettings (req: express.Request, res: express.Response) {
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const channelSetting = await PlayerSettingModel.loadByChannelId(channel.id)
|
||||
|
||||
if (req.query.raw === true) {
|
||||
return res.json(PlayerSettingModel.formatChannelPlayerRawSetting(channelSetting))
|
||||
}
|
||||
|
||||
return res.json(PlayerSettingModel.formatChannelPlayerSetting({ channelSetting }))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function updateVideoPlayerSettings (req: express.Request, res: express.Response) {
|
||||
const body: PlayerVideoSettingsUpdate = req.body
|
||||
const video = res.locals.videoAll
|
||||
|
||||
const setting = await upsertPlayerSettings({ settings: body, channel: undefined, video })
|
||||
|
||||
await sendUpdateVideoPlayerSettings(video, setting, undefined)
|
||||
|
||||
return res.json(PlayerSettingModel.formatVideoPlayerRawSetting(setting))
|
||||
}
|
||||
|
||||
async function updateChannelPlayerSettings (req: express.Request, res: express.Response) {
|
||||
const body: PlayerChannelSettingsUpdate = req.body
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const settings = await upsertPlayerSettings({ settings: body, channel, video: undefined })
|
||||
|
||||
await sendUpdateChannelPlayerSettings(channel, settings, undefined)
|
||||
|
||||
return res.json(PlayerSettingModel.formatChannelPlayerRawSetting(settings))
|
||||
}
|
|
@ -2,10 +2,10 @@ import { arrayify } from '@peertube/peertube-core-utils'
|
|||
import { ContextType } from '@peertube/peertube-models'
|
||||
import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
|
||||
import { isArray } from './custom-validators/misc.js'
|
||||
import { logger } from './logger.js'
|
||||
import { buildDigest } from './peertube-crypto.js'
|
||||
import type { signJsonLDObject } from './peertube-jsonld.js'
|
||||
import { doJSONRequest } from './requests.js'
|
||||
import { logger } from './logger.js'
|
||||
|
||||
export type ContextFilter = <T>(arg: T) => Promise<T>
|
||||
|
||||
|
@ -75,6 +75,8 @@ type ContextValue = { [id: string]: string | { '@type': string, '@id': string }
|
|||
|
||||
const contextStore: { [id in ContextType]: (string | { [id: string]: string })[] } = {
|
||||
Video: buildContext({
|
||||
...getPlayerSettingsTypeContext(),
|
||||
|
||||
Hashtag: 'as:Hashtag',
|
||||
category: 'sc:category',
|
||||
licence: 'sc:license',
|
||||
|
@ -99,6 +101,7 @@ const contextStore: { [id in ContextType]: (string | { [id: string]: string })[]
|
|||
},
|
||||
|
||||
Infohash: 'pt:Infohash',
|
||||
|
||||
SensitiveTag: 'pt:SensitiveTag',
|
||||
|
||||
tileWidth: {
|
||||
|
@ -131,6 +134,8 @@ const contextStore: { [id in ContextType]: (string | { [id: string]: string })[]
|
|||
|
||||
hasParts: 'sc:hasParts',
|
||||
|
||||
playerSettings: 'pt:playerSettings',
|
||||
|
||||
views: {
|
||||
'@type': 'sc:Number',
|
||||
'@id': 'pt:views'
|
||||
|
@ -236,6 +241,8 @@ const contextStore: { [id in ContextType]: (string | { [id: string]: string })[]
|
|||
}),
|
||||
|
||||
Actor: buildContext({
|
||||
...getPlayerSettingsTypeContext(),
|
||||
|
||||
playlists: {
|
||||
'@id': 'pt:playlists',
|
||||
'@type': '@id'
|
||||
|
@ -303,9 +310,24 @@ const contextStore: { [id in ContextType]: (string | { [id: string]: string })[]
|
|||
hasPart: 'sc:hasPart',
|
||||
endOffset: 'sc:endOffset',
|
||||
startOffset: 'sc:startOffset'
|
||||
}),
|
||||
|
||||
PlayerSettings: buildContext({
|
||||
...getPlayerSettingsTypeContext(),
|
||||
|
||||
theme: 'pt:theme'
|
||||
})
|
||||
}
|
||||
|
||||
function getPlayerSettingsTypeContext () {
|
||||
return {
|
||||
PlayerSettings: {
|
||||
'@type': '@id',
|
||||
'@id': 'pt:PlayerSettings'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let allContext: (string | ContextValue)[]
|
||||
export function getAllContext () {
|
||||
if (allContext) return allContext
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import validator from 'validator'
|
||||
import { Activity, ActivityType } from '@peertube/peertube-models'
|
||||
import validator from 'validator'
|
||||
import { isAbuseReasonValid } from '../abuses.js'
|
||||
import { exists } from '../misc.js'
|
||||
import { sanitizeAndCheckActorObject } from './actor.js'
|
||||
import { isCacheFileObjectValid } from './cache-file.js'
|
||||
import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc.js'
|
||||
import { sanitizeAndCheckPlayerSettingsObject } from './player-settings.js'
|
||||
import { isPlaylistObjectValid } from './playlist.js'
|
||||
import { sanitizeAndCheckVideoCommentObject } from './video-comments.js'
|
||||
import { sanitizeAndCheckVideoTorrentObject } from './videos.js'
|
||||
|
@ -28,7 +29,7 @@ function isActivity (activity: any) {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
|
||||
const activityCheckers: { [P in ActivityType]: (activity: Activity) => boolean } = {
|
||||
Create: isCreateActivityValid,
|
||||
Update: isUpdateActivityValid,
|
||||
Delete: isDeleteActivityValid,
|
||||
|
@ -88,7 +89,6 @@ export function isCreateActivityValid (activity: any) {
|
|||
isFlagActivityValid(activity.object) ||
|
||||
isPlaylistObjectValid(activity.object) ||
|
||||
isWatchActionObjectValid(activity.object) ||
|
||||
|
||||
isCacheFileObjectValid(activity.object) ||
|
||||
sanitizeAndCheckVideoCommentObject(activity.object) ||
|
||||
sanitizeAndCheckVideoTorrentObject(activity.object)
|
||||
|
@ -101,7 +101,9 @@ export function isUpdateActivityValid (activity: any) {
|
|||
isCacheFileObjectValid(activity.object) ||
|
||||
isPlaylistObjectValid(activity.object) ||
|
||||
sanitizeAndCheckVideoTorrentObject(activity.object) ||
|
||||
sanitizeAndCheckActorObject(activity.object)
|
||||
sanitizeAndCheckActorObject(activity.object) ||
|
||||
sanitizeAndCheckPlayerSettingsObject(activity.object, 'video') ||
|
||||
sanitizeAndCheckPlayerSettingsObject(activity.object, 'channel')
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { PlayerSettingsObject } from '@peertube/peertube-models'
|
||||
import { isPlayerChannelThemeSettingValid, isPlayerVideoThemeSettingValid } from '../player-settings.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
|
||||
export function sanitizeAndCheckPlayerSettingsObject (settings: PlayerSettingsObject, target: 'video' | 'channel') {
|
||||
if (!settings) return false
|
||||
|
||||
if (settings.type !== 'PlayerSettings') return false
|
||||
if (target === 'video' && !isPlayerVideoThemeSettingValid(settings.theme)) return false
|
||||
if (target === 'channel' && !isPlayerChannelThemeSettingValid(settings.theme)) return false
|
||||
|
||||
return isActivityPubUrlValid(settings.id)
|
||||
}
|
16
server/core/helpers/custom-validators/player-settings.ts
Normal file
16
server/core/helpers/custom-validators/player-settings.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { PlayerTheme, PlayerThemeVideoSetting } from '@peertube/peertube-models'
|
||||
import { DEFAULT_CHANNEL_PLAYER_SETTING_VALUE, DEFAULT_INSTANCE_PLAYER_SETTING_VALUE } from '@server/initializers/constants.js'
|
||||
|
||||
export function isPlayerVideoThemeSettingValid (name: PlayerThemeVideoSetting) {
|
||||
return isPlayerChannelThemeSettingValid(name) || name === DEFAULT_CHANNEL_PLAYER_SETTING_VALUE
|
||||
}
|
||||
|
||||
export function isPlayerChannelThemeSettingValid (name: PlayerThemeVideoSetting) {
|
||||
return name === DEFAULT_INSTANCE_PLAYER_SETTING_VALUE || isPlayerThemeValid(name as PlayerTheme)
|
||||
}
|
||||
|
||||
const availableThemes = new Set<PlayerTheme>([ 'galaxy', 'lucide' ])
|
||||
|
||||
export function isPlayerThemeValid (name: PlayerTheme) {
|
||||
return availableThemes.has(name)
|
||||
}
|
|
@ -135,6 +135,7 @@ export function checkMissedConfig () {
|
|||
'defaults.publish.privacy',
|
||||
'defaults.publish.licence',
|
||||
'defaults.player.auto_play',
|
||||
'defaults.player.theme',
|
||||
'instance.name',
|
||||
'instance.short_description',
|
||||
'instance.default_language',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
BroadcastMessageLevel,
|
||||
NSFWPolicyType,
|
||||
PlayerTheme,
|
||||
VideoCommentPolicyType,
|
||||
VideoPrivacyType,
|
||||
VideoRedundancyConfigFilter,
|
||||
|
@ -172,6 +173,9 @@ const CONFIG = {
|
|||
}
|
||||
},
|
||||
PLAYER: {
|
||||
get THEME () {
|
||||
return config.get<PlayerTheme>('defaults.player.theme')
|
||||
},
|
||||
get AUTO_PLAY () {
|
||||
return config.get<boolean>('defaults.player.auto_play')
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
FollowState,
|
||||
JobType,
|
||||
NSFWPolicyType,
|
||||
PlayerThemeChannelSetting,
|
||||
PlayerThemeVideoSetting,
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
UploadImageType,
|
||||
|
@ -50,7 +52,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const LAST_MIGRATION_VERSION = 925
|
||||
export const LAST_MIGRATION_VERSION = 930
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -1177,7 +1179,9 @@ export const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL
|
|||
export let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
|
||||
|
||||
export const DEFAULT_THEME_NAME = 'default'
|
||||
export const DEFAULT_USER_THEME_NAME = 'instance-default'
|
||||
export const DEFAULT_INSTANCE_THEME_NAME = 'instance-default'
|
||||
export const DEFAULT_CHANNEL_PLAYER_SETTING_VALUE: PlayerThemeVideoSetting = 'channel-default'
|
||||
export const DEFAULT_INSTANCE_PLAYER_SETTING_VALUE: PlayerThemeVideoSetting | PlayerThemeChannelSetting = 'instance-default'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ import { VideoTagModel } from '../models/video/video-tag.js'
|
|||
import { VideoModel } from '../models/video/video.js'
|
||||
import { VideoViewModel } from '../models/view/video-view.js'
|
||||
import { CONFIG } from './config.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
|
||||
pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||
|
||||
|
@ -189,7 +190,8 @@ export async function initDatabaseModels (silent: boolean) {
|
|||
WatchedWordsListModel,
|
||||
AccountAutomaticTagPolicyModel,
|
||||
UploadImageModel,
|
||||
VideoLiveScheduleModel
|
||||
VideoLiveScheduleModel,
|
||||
PlayerSettingModel
|
||||
])
|
||||
|
||||
// Check extensions exist in the database
|
||||
|
|
28
server/core/initializers/migrations/0930-player-settings.ts
Normal file
28
server/core/initializers/migrations/0930-player-settings.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
}): Promise<void> {
|
||||
const query = `CREATE TABLE IF NOT EXISTS "playerSetting" (
|
||||
"id" SERIAL,
|
||||
"theme" VARCHAR(255) NOT NULL DEFAULT 'instance-default',
|
||||
"videoId" INTEGER REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"channelId" INTEGER REFERENCES "videoChannel" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);`
|
||||
|
||||
await utils.sequelize.query(query, { transaction: utils.transaction })
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Op, Transaction } from 'sequelize'
|
||||
import { ActivityPubActor, ActorImageType, ActorImageType_Type } from '@peertube/peertube-models'
|
||||
import { isAccountActor, isChannelActor } from '@server/helpers/actors.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
|
@ -15,18 +15,17 @@ import {
|
|||
MChannel,
|
||||
MServer
|
||||
} from '@server/types/models/index.js'
|
||||
import { Op, Transaction } from 'sequelize'
|
||||
import { upsertAPPlayerSettings } from '../../player-settings.js'
|
||||
import { updateActorImages } from '../image.js'
|
||||
import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes.js'
|
||||
import { fetchActorFollowsCount } from './url-to-object.js'
|
||||
import { isAccountActor, isChannelActor } from '@server/helpers/actors.js'
|
||||
|
||||
export class APActorCreator {
|
||||
|
||||
constructor (
|
||||
private readonly actorObject: ActivityPubActor,
|
||||
private readonly ownerActor?: MActorFullActor
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
async create (): Promise<MActorFullActor> {
|
||||
|
@ -34,7 +33,7 @@ export class APActorCreator {
|
|||
|
||||
const actorInstance = new ActorModel(getActorAttributesFromObject(this.actorObject, followersCount, followingCount))
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const actor = await sequelizeTypescript.transaction(async t => {
|
||||
const server = await this.setServer(actorInstance, t)
|
||||
|
||||
const { actorCreated, created } = await this.saveActor(actorInstance, t)
|
||||
|
@ -58,6 +57,17 @@ export class APActorCreator {
|
|||
|
||||
return actorCreated
|
||||
})
|
||||
|
||||
if (isChannelActor(actor.type) && typeof this.actorObject.playerSettings === 'string') {
|
||||
await upsertAPPlayerSettings({
|
||||
settingsObject: this.actorObject.playerSettings,
|
||||
video: undefined,
|
||||
channel: actor.VideoChannel,
|
||||
contextUrl: actor.url
|
||||
})
|
||||
}
|
||||
|
||||
return actor
|
||||
}
|
||||
|
||||
private async setServer (actor: MActor, t: Transaction) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { logger } from '@server/helpers/logger.js'
|
|||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models/index.js'
|
||||
import { upsertAPPlayerSettings } from '../player-settings.js'
|
||||
import { getOrCreateAPOwner } from './get.js'
|
||||
import { updateActorImages } from './image.js'
|
||||
import { fetchActorFollowsCount } from './shared/index.js'
|
||||
|
@ -36,6 +37,15 @@ export class APActorUpdater {
|
|||
this.accountOrChannel.Account = owner.Account as AccountModel
|
||||
|
||||
this.accountOrChannel.support = this.actorObject.support
|
||||
|
||||
if (typeof this.actorObject.playerSettings === 'string') {
|
||||
await upsertAPPlayerSettings({
|
||||
settingsObject: this.actorObject.playerSettings,
|
||||
video: undefined,
|
||||
channel: this.accountOrChannel,
|
||||
contextUrl: this.actor.url
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await runInReadCommittedTransaction(async t => {
|
||||
|
|
44
server/core/lib/activitypub/player-settings.ts
Normal file
44
server/core/lib/activitypub/player-settings.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { PlayerSettingsObject } from '@peertube/peertube-models'
|
||||
import { sanitizeAndCheckPlayerSettingsObject } from '@server/helpers/custom-validators/activitypub/player-settings.js'
|
||||
import { MChannelId, MChannelUrl, MVideoIdUrl } from '../../types/models/index.js'
|
||||
import { upsertPlayerSettings } from '../player-settings.js'
|
||||
import { fetchAPObjectIfNeeded } from './activity.js'
|
||||
import { checkUrlsSameHost } from './url.js'
|
||||
|
||||
export async function upsertAPPlayerSettings (options: {
|
||||
video: MVideoIdUrl
|
||||
channel: MChannelUrl & MChannelId
|
||||
settingsObject: PlayerSettingsObject | string
|
||||
contextUrl: string
|
||||
}) {
|
||||
const { video, channel, contextUrl } = options
|
||||
|
||||
if (!video && !channel) throw new Error('Video or channel must be specified')
|
||||
|
||||
const settingsObject = await fetchAPObjectIfNeeded<PlayerSettingsObject>(options.settingsObject)
|
||||
|
||||
if (!sanitizeAndCheckPlayerSettingsObject(settingsObject, video ? 'video' : 'channel')) {
|
||||
throw new Error(`Player settings ${settingsObject.id} object is not valid`)
|
||||
}
|
||||
|
||||
if (!checkUrlsSameHost(settingsObject.id, contextUrl)) {
|
||||
throw new Error(`Player settings ${settingsObject.id} object is not on the same host as context URL ${contextUrl}`)
|
||||
}
|
||||
|
||||
const objectUrl = video?.url || channel?.Actor.url
|
||||
if (!checkUrlsSameHost(settingsObject.id, objectUrl)) {
|
||||
throw new Error(`Player settings ${settingsObject.id} object is not on the same host as context URL ${contextUrl}`)
|
||||
}
|
||||
|
||||
await upsertPlayerSettings({ settings: getPlayerSettingsAttributesFromObject(settingsObject), channel, video })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getPlayerSettingsAttributesFromObject (settingsObject: PlayerSettingsObject) {
|
||||
return {
|
||||
theme: settingsObject.theme
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import {
|
|||
ActivityUpdate,
|
||||
ActivityUpdateObject,
|
||||
CacheFileObject,
|
||||
PlayerSettingsObject,
|
||||
PlaylistObject,
|
||||
VideoObject
|
||||
} from '@peertube/peertube-models'
|
||||
|
@ -17,13 +18,15 @@ import { logger } from '../../../helpers/logger.js'
|
|||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { ActorModel } from '../../../models/actor/actor.js'
|
||||
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
|
||||
import { MActorFull, MActorSignature } from '../../../types/models/index.js'
|
||||
import { MActorAccountChannelId, MActorFull, MActorSignature } from '../../../types/models/index.js'
|
||||
import { fetchAPObjectIfNeeded } from '../activity.js'
|
||||
import { getOrCreateAPActor } from '../actors/get.js'
|
||||
import { APActorUpdater } from '../actors/updater.js'
|
||||
import { createOrUpdateCacheFile } from '../cache-file.js'
|
||||
import { upsertAPPlayerSettings } from '../player-settings.js'
|
||||
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
|
||||
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
|
||||
import { APVideoUpdater, canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'
|
||||
import { APVideoUpdater, canVideoBeFederated, getOrCreateAPVideo, maybeGetOrCreateAPVideo } from '../videos/index.js'
|
||||
|
||||
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -51,6 +54,10 @@ async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate
|
|||
return retryTransactionWrapper(processUpdatePlaylist, byActor, activity, object)
|
||||
}
|
||||
|
||||
if (objectType === 'PlayerSettings') {
|
||||
return retryTransactionWrapper(processUpdatePlayerSettings, byActor, object)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
@ -130,3 +137,34 @@ async function processUpdatePlaylist (
|
|||
|
||||
await createOrUpdateVideoPlaylist({ playlistObject, contextUrl: byActor.url, to: arrayify(activity.to) })
|
||||
}
|
||||
|
||||
async function processUpdatePlayerSettings (
|
||||
byActor: MActorSignature,
|
||||
settingsObject: PlayerSettingsObject
|
||||
) {
|
||||
let actor: MActorAccountChannelId
|
||||
|
||||
const { video } = await maybeGetOrCreateAPVideo({ videoObject: settingsObject.object })
|
||||
|
||||
if (!video) {
|
||||
try {
|
||||
actor = await getOrCreateAPActor(settingsObject.object)
|
||||
} catch {
|
||||
actor = undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (!video && !actor?.VideoChannel) {
|
||||
logger.warn(`Do not process update player settings on unknown video/channel`)
|
||||
return
|
||||
}
|
||||
|
||||
await upsertAPPlayerSettings({
|
||||
settingsObject,
|
||||
contextUrl: byActor.url,
|
||||
video,
|
||||
channel: actor
|
||||
? Object.assign(actor.VideoChannel, { Actor: actor })
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { MPlayerSetting } from '@server/types/models/video/player-setting.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { AccountModel } from '../../../models/account/account.js'
|
||||
|
@ -11,11 +13,12 @@ import {
|
|||
MActorLight,
|
||||
MChannelDefault,
|
||||
MVideoAPLight,
|
||||
MVideoFullLight,
|
||||
MVideoPlaylistFull,
|
||||
MVideoRedundancyVideo
|
||||
} from '../../../types/models/index.js'
|
||||
import { audiencify, getPlaylistAudience, getPublicAudience, getVideoAudience } from '../audience.js'
|
||||
import { getUpdateActivityPubUrl } from '../url.js'
|
||||
import { getLocalChannelPlayerSettingsActivityPubUrl, getLocalVideoPlayerSettingsActivityPubUrl, getUpdateActivityPubUrl } from '../url.js'
|
||||
import { canVideoBeFederated } from '../videos/federate.js'
|
||||
import { getActorsInvolvedInVideo } from './shared/index.js'
|
||||
import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils.js'
|
||||
|
@ -58,21 +61,10 @@ export async function sendUpdateActor (accountOrChannel: MChannelDefault | MAcco
|
|||
const audience = getPublicAudience(byActor)
|
||||
const updateActivity = buildUpdateActivity(url, byActor, accountOrChannelObject, audience)
|
||||
|
||||
let actorsInvolved: MActor[]
|
||||
if (accountOrChannel instanceof AccountModel) {
|
||||
// Actors that shared my videos are involved too
|
||||
actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, transaction)
|
||||
} else {
|
||||
// Actors that shared videos of my channel are involved too
|
||||
actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, transaction)
|
||||
}
|
||||
|
||||
actorsInvolved.push(byActor)
|
||||
|
||||
return broadcastToFollowers({
|
||||
data: updateActivity,
|
||||
byActor,
|
||||
toFollowersOf: actorsInvolved,
|
||||
toFollowersOf: await getToFollowersOfForActor(accountOrChannel, transaction),
|
||||
transaction,
|
||||
contextType: 'Actor'
|
||||
})
|
||||
|
@ -127,6 +119,53 @@ export async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull
|
|||
})
|
||||
}
|
||||
|
||||
export async function sendUpdateVideoPlayerSettings (video: MVideoFullLight, settings: MPlayerSetting, transaction: Transaction) {
|
||||
if (!canVideoBeFederated(video, false)) return
|
||||
|
||||
const byActor = video.VideoChannel.Account.Actor
|
||||
const settingsUrl = getLocalVideoPlayerSettingsActivityPubUrl(video)
|
||||
|
||||
logger.info('Creating job to update video player settings ' + settingsUrl)
|
||||
|
||||
const updateUrl = getUpdateActivityPubUrl(settingsUrl, settings.updatedAt.toISOString())
|
||||
|
||||
const object = PlayerSettingModel.formatAPPlayerSetting({ settings, video, channel: undefined })
|
||||
const audience = getVideoAudience(byActor, video.privacy)
|
||||
|
||||
const updateActivity = buildUpdateActivity(updateUrl, byActor, object, audience)
|
||||
|
||||
const toFollowersOf = await getActorsInvolvedInVideo(video, transaction)
|
||||
|
||||
return broadcastToFollowers({
|
||||
data: updateActivity,
|
||||
byActor,
|
||||
toFollowersOf,
|
||||
transaction,
|
||||
contextType: 'PlayerSettings'
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendUpdateChannelPlayerSettings (channel: MChannelDefault, settings: MPlayerSetting, transaction: Transaction) {
|
||||
const byActor = channel.Actor
|
||||
const settingsUrl = getLocalChannelPlayerSettingsActivityPubUrl(channel.Actor.preferredUsername)
|
||||
|
||||
logger.info('Creating job to update channel player settings actor ' + settingsUrl)
|
||||
|
||||
const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
|
||||
const object = PlayerSettingModel.formatAPPlayerSetting({ settings, video: undefined, channel })
|
||||
|
||||
const audience = getPublicAudience(byActor)
|
||||
const updateActivity = buildUpdateActivity(url, byActor, object, audience)
|
||||
|
||||
return broadcastToFollowers({
|
||||
data: updateActivity,
|
||||
byActor,
|
||||
toFollowersOf: await getToFollowersOfForActor(channel, transaction),
|
||||
transaction,
|
||||
contextType: 'PlayerSettings'
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -149,3 +188,18 @@ function buildUpdateActivity (
|
|||
audience
|
||||
)
|
||||
}
|
||||
|
||||
async function getToFollowersOfForActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction?: Transaction) {
|
||||
let actorsInvolved: MActor[]
|
||||
if (accountOrChannel instanceof AccountModel) {
|
||||
// Actors that shared my videos are involved too
|
||||
actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(accountOrChannel.Actor.id, transaction)
|
||||
} else {
|
||||
// Actors that shared videos of my channel are involved too
|
||||
actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, transaction)
|
||||
}
|
||||
|
||||
actorsInvolved.push(accountOrChannel.Actor)
|
||||
|
||||
return actorsInvolved
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ import {
|
|||
MActorFollow,
|
||||
MActorId,
|
||||
MActorUrl,
|
||||
MCommentId, MLocalVideoViewer,
|
||||
MCommentId,
|
||||
MLocalVideoViewer,
|
||||
MVideoId,
|
||||
MVideoPlaylistElement,
|
||||
MVideoUUID,
|
||||
|
@ -40,6 +41,10 @@ export function getLocalVideoChannelActivityPubUrl (videoChannelName: string) {
|
|||
return WEBSERVER.URL + '/video-channels/' + videoChannelName
|
||||
}
|
||||
|
||||
export function getLocalChannelPlayerSettingsActivityPubUrl (videoChannelName: string) {
|
||||
return WEBSERVER.URL + '/video-channels/' + videoChannelName + '/player-settings'
|
||||
}
|
||||
|
||||
export function getLocalAccountActivityPubUrl (accountName: string) {
|
||||
return WEBSERVER.URL + '/accounts/' + accountName
|
||||
}
|
||||
|
@ -76,6 +81,10 @@ export function getLocalVideoChaptersActivityPubUrl (video: MVideoUrl) {
|
|||
return video.url + '/chapters'
|
||||
}
|
||||
|
||||
export function getLocalVideoPlayerSettingsActivityPubUrl (video: MVideoUrl) {
|
||||
return video.url + '/player-settings'
|
||||
}
|
||||
|
||||
export function getLocalVideoLikesActivityPubUrl (video: MVideoUrl) {
|
||||
return video.url + '/likes'
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
import { CreationAttributes, Transaction } from 'sequelize'
|
||||
import { fetchAP } from '../../activity.js'
|
||||
import { findOwner, getOrCreateAPActor } from '../../actors/index.js'
|
||||
import { upsertAPPlayerSettings } from '../../player-settings.js'
|
||||
import {
|
||||
getCaptionAttributesFromObject,
|
||||
getFileAttributesFromUrl,
|
||||
|
@ -160,7 +161,7 @@ export abstract class APVideoAbstractBuilder {
|
|||
video.VideoFiles = await Promise.all(upsertTasks)
|
||||
}
|
||||
|
||||
protected async updateChaptersOutsideTransaction (video: MVideoFullLight) {
|
||||
protected async updateChapters (video: MVideoFullLight) {
|
||||
if (!this.videoObject.hasParts || typeof this.videoObject.hasParts !== 'string') return
|
||||
|
||||
const { body } = await fetchAP<VideoChaptersObject>(this.videoObject.hasParts)
|
||||
|
@ -180,6 +181,17 @@ export abstract class APVideoAbstractBuilder {
|
|||
})
|
||||
}
|
||||
|
||||
protected async upsertPlayerSettings (video: MVideoFullLight) {
|
||||
if (typeof this.videoObject.playerSettings !== 'string') return
|
||||
|
||||
await upsertAPPlayerSettings({
|
||||
settingsObject: this.videoObject.playerSettings,
|
||||
video,
|
||||
channel: undefined,
|
||||
contextUrl: video.url
|
||||
})
|
||||
}
|
||||
|
||||
protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
|
||||
const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject)
|
||||
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
|
||||
|
|
|
@ -62,7 +62,8 @@ export class APVideoCreator extends APVideoAbstractBuilder {
|
|||
return { autoBlacklisted, videoCreated }
|
||||
})
|
||||
|
||||
await this.updateChaptersOutsideTransaction(videoCreated)
|
||||
await this.updateChapters(videoCreated)
|
||||
await this.upsertPlayerSettings(videoCreated)
|
||||
|
||||
return { autoBlacklisted, videoCreated }
|
||||
}
|
||||
|
|
|
@ -79,7 +79,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
|
|||
|
||||
await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t))
|
||||
|
||||
await this.updateChaptersOutsideTransaction(videoUpdated)
|
||||
await this.updateChapters(videoUpdated)
|
||||
await this.upsertPlayerSettings(videoUpdated)
|
||||
|
||||
await autoBlacklistVideoIfNeeded({
|
||||
video: videoUpdated,
|
||||
|
|
31
server/core/lib/player-settings.ts
Normal file
31
server/core/lib/player-settings.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { PlayerChannelSettings, PlayerVideoSettings } from '../../../packages/models/src/videos/player-settings.js'
|
||||
import { MChannelId, MVideoId } from '@server/types/models/index.js'
|
||||
|
||||
export async function upsertPlayerSettings (options: {
|
||||
settings: PlayerVideoSettings | PlayerChannelSettings
|
||||
channel: MChannelId
|
||||
video: MVideoId
|
||||
}) {
|
||||
const { settings, channel, video } = options
|
||||
|
||||
if (!channel && !video) throw new Error('channel or video must be specified')
|
||||
|
||||
return retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async transaction => {
|
||||
const setting = channel
|
||||
? await PlayerSettingModel.loadByChannelId(channel.id, transaction)
|
||||
: await PlayerSettingModel.loadByVideoId(video.id, transaction)
|
||||
|
||||
if (setting) await setting.destroy({ transaction })
|
||||
|
||||
return PlayerSettingModel.create({
|
||||
theme: settings.theme,
|
||||
channelId: channel?.id,
|
||||
videoId: video?.id
|
||||
}, { transaction })
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME } from '../../initializers/constants.js'
|
||||
import { DEFAULT_THEME_NAME, DEFAULT_INSTANCE_THEME_NAME } from '../../initializers/constants.js'
|
||||
import { PluginManager } from './plugin-manager.js'
|
||||
import { CONFIG } from '../../initializers/config.js'
|
||||
import { ServerConfigManager } from '../server-config-manager.js'
|
||||
|
@ -13,7 +13,7 @@ export function getThemeOrDefault (name: string, defaultTheme: string) {
|
|||
}
|
||||
|
||||
export function isThemeRegistered (name: string) {
|
||||
if (name === DEFAULT_THEME_NAME || name === DEFAULT_USER_THEME_NAME) return true
|
||||
if (name === DEFAULT_THEME_NAME || name === DEFAULT_INSTANCE_THEME_NAME) return true
|
||||
|
||||
return PluginManager.Instance.getRegisteredThemes().some(r => r.name === name) ||
|
||||
ServerConfigManager.Instance.getBuiltInThemes().some(r => r.name === name)
|
||||
|
|
|
@ -116,6 +116,7 @@ class ServerConfigManager {
|
|||
}
|
||||
},
|
||||
player: {
|
||||
theme: CONFIG.DEFAULTS.PLAYER.THEME,
|
||||
autoPlay: CONFIG.DEFAULTS.PLAYER.AUTO_PLAY
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { ChannelExportJSON, PlayerChannelSettings } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { ExportResult } from './abstract-user-exporter.js'
|
||||
import { ChannelExportJSON } from '@peertube/peertube-models'
|
||||
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
|
||||
import { MPlayerSetting } from '@server/types/models/video/player-setting.js'
|
||||
import { ExportResult } from './abstract-user-exporter.js'
|
||||
import { ActorExporter } from './actor-exporter.js'
|
||||
|
||||
export class ChannelsExporter extends ActorExporter <ChannelExportJSON> {
|
||||
|
||||
export class ChannelsExporter extends ActorExporter<ChannelExportJSON> {
|
||||
async export () {
|
||||
const channelsJSON: ChannelExportJSON['channels'] = []
|
||||
let staticFiles: ExportResult<ChannelExportJSON>['staticFiles'] = []
|
||||
|
@ -31,12 +32,15 @@ export class ChannelsExporter extends ActorExporter <ChannelExportJSON> {
|
|||
}
|
||||
|
||||
private async exportChannel (channelId: number) {
|
||||
const channel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
||||
const [ channel, playerSettings ] = await Promise.all([
|
||||
VideoChannelModel.loadAndPopulateAccount(channelId),
|
||||
PlayerSettingModel.loadByChannelId(channelId)
|
||||
])
|
||||
|
||||
const { relativePathsFromJSON, staticFiles } = this.exportActorFiles(channel.Actor)
|
||||
|
||||
return {
|
||||
json: this.exportChannelJSON(channel, relativePathsFromJSON),
|
||||
json: this.exportChannelJSON(channel, playerSettings, relativePathsFromJSON),
|
||||
staticFiles
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +49,7 @@ export class ChannelsExporter extends ActorExporter <ChannelExportJSON> {
|
|||
|
||||
private exportChannelJSON (
|
||||
channel: MChannelBannerAccountDefault,
|
||||
playerSettings: MPlayerSetting,
|
||||
archiveFiles: { avatar: string, banner: string }
|
||||
): ChannelExportJSON['channels'][0] {
|
||||
return {
|
||||
|
@ -54,6 +59,8 @@ export class ChannelsExporter extends ActorExporter <ChannelExportJSON> {
|
|||
description: channel.description,
|
||||
support: channel.support,
|
||||
|
||||
playerSettings: this.exportPlayerSettingsJSON(playerSettings),
|
||||
|
||||
updatedAt: channel.updatedAt.toISOString(),
|
||||
createdAt: channel.createdAt.toISOString(),
|
||||
|
||||
|
@ -61,4 +68,11 @@ export class ChannelsExporter extends ActorExporter <ChannelExportJSON> {
|
|||
}
|
||||
}
|
||||
|
||||
private exportPlayerSettingsJSON (playerSettings: MPlayerSetting) {
|
||||
if (!playerSettings) return null
|
||||
|
||||
return {
|
||||
theme: playerSettings.theme as PlayerChannelSettings['theme']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '@server/lib/object-storage/videos.js'
|
||||
import { VideoDownload } from '@server/lib/video-download.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
MVideoLiveWithSettingSchedules,
|
||||
MVideoPassword
|
||||
} from '@server/types/models/index.js'
|
||||
import { MPlayerSetting } from '@server/types/models/video/player-setting.js'
|
||||
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||
import Bluebird from 'bluebird'
|
||||
import { createReadStream } from 'fs'
|
||||
|
@ -80,11 +82,12 @@ export class VideosExporter extends AbstractUserExporter<VideoExportJSON> {
|
|||
}
|
||||
|
||||
private async exportVideo (videoId: number) {
|
||||
const [ video, captions, source, chapters ] = await Promise.all([
|
||||
const [ video, captions, source, chapters, playerSettings ] = await Promise.all([
|
||||
VideoModel.loadFull(videoId),
|
||||
VideoCaptionModel.listVideoCaptions(videoId),
|
||||
VideoSourceModel.loadLatest(videoId),
|
||||
VideoChapterModel.listChaptersOfVideo(videoId)
|
||||
VideoChapterModel.listChaptersOfVideo(videoId),
|
||||
PlayerSettingModel.loadByVideoId(videoId)
|
||||
])
|
||||
|
||||
const passwords = video.privacy === VideoPrivacy.PASSWORD_PROTECTED
|
||||
|
@ -101,7 +104,16 @@ export class VideosExporter extends AbstractUserExporter<VideoExportJSON> {
|
|||
const { relativePathsFromJSON, staticFiles, exportedVideoFileOrSource } = await this.exportVideoFiles({ video, captions })
|
||||
|
||||
return {
|
||||
json: this.exportVideoJSON({ video, captions, live, passwords, source, chapters, archiveFiles: relativePathsFromJSON }),
|
||||
json: this.exportVideoJSON({
|
||||
video,
|
||||
captions,
|
||||
live,
|
||||
passwords,
|
||||
source,
|
||||
chapters,
|
||||
playerSettings,
|
||||
archiveFiles: relativePathsFromJSON
|
||||
}),
|
||||
staticFiles,
|
||||
relativePathsFromJSON,
|
||||
activityPubOutbox: await this.exportVideoAP(videoAP, chapters, exportedVideoFileOrSource)
|
||||
|
@ -116,10 +128,11 @@ export class VideosExporter extends AbstractUserExporter<VideoExportJSON> {
|
|||
live: MVideoLiveWithSettingSchedules
|
||||
passwords: MVideoPassword[]
|
||||
source: MVideoSource
|
||||
playerSettings: MPlayerSetting
|
||||
chapters: MVideoChapter[]
|
||||
archiveFiles: VideoExportJSON['videos'][0]['archiveFiles']
|
||||
}): VideoExportJSON['videos'][0] {
|
||||
const { video, captions, live, passwords, source, chapters, archiveFiles } = options
|
||||
const { video, captions, live, passwords, source, chapters, playerSettings, archiveFiles } = options
|
||||
|
||||
return {
|
||||
uuid: video.uuid,
|
||||
|
@ -182,6 +195,8 @@ export class VideosExporter extends AbstractUserExporter<VideoExportJSON> {
|
|||
|
||||
source: this.exportVideoSourceJSON(source),
|
||||
|
||||
playerSettings: this.exportPlayerSettingsJSON(playerSettings),
|
||||
|
||||
archiveFiles
|
||||
}
|
||||
}
|
||||
|
@ -261,6 +276,14 @@ export class VideosExporter extends AbstractUserExporter<VideoExportJSON> {
|
|||
}
|
||||
}
|
||||
|
||||
private exportPlayerSettingsJSON (playerSettings: MPlayerSetting) {
|
||||
if (!playerSettings) return null
|
||||
|
||||
return {
|
||||
theme: playerSettings.theme
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async exportVideoAP (
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue