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