Add user import/export in client
This commit is contained in:
parent
35f0bb14be
commit
f9c89b98f7
|
@ -10,7 +10,7 @@ export class AdminConfigPage {
|
|||
}
|
||||
await go('/admin/config/edit-custom#' + tab)
|
||||
|
||||
await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed()
|
||||
await $('.section-left-column-title=' + waitTitles[tab]).waitForDisplayed()
|
||||
}
|
||||
|
||||
async updateNSFWSetting (newValue: 'do_not_list' | 'blur' | 'display') {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="row mt-5"> <!-- cache grid -->
|
||||
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">CACHE</h2>
|
||||
<h2 i18n class="section-left-column-title">CACHE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Some files are not federated, and fetched when necessary. Define their caching policies.
|
||||
</div>
|
||||
|
@ -74,7 +74,7 @@
|
|||
<div class="row mt-4"> <!-- cache grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="anchor" id="customizations"></div> <!-- customizations anchor -->
|
||||
<h2 i18n class="inner-form-title">CUSTOMIZATIONS</h2>
|
||||
<h2 i18n class="section-left-column-title">CUSTOMIZATIONS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<ng-container [formGroup]="form">
|
||||
<div class="row mt-5"> <!-- appearance grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">APPEARANCE</h2>
|
||||
<h2 i18n class="section-left-column-title">APPEARANCE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Use <a class="link-orange" routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-orange" routerLink="/admin/config/edit-custom" fragment="advanced-configuration">customizations</a>.
|
||||
</div>
|
||||
|
@ -91,7 +91,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- broadcast grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">BROADCAST MESSAGE</h2>
|
||||
<h2 i18n class="section-left-column-title">BROADCAST MESSAGE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Display a message on your instance
|
||||
</div>
|
||||
|
@ -147,7 +147,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- new users grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">NEW USERS</h2>
|
||||
<h2 i18n class="section-left-column-title">NEW USERS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-orange" routerLink="/admin/users">users</a> to set their quota individually.
|
||||
</div>
|
||||
|
@ -228,7 +228,7 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||
<my-user-real-quota-info class="mt-2 d-block small muted" [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||
|
||||
<div *ngIf="formErrors.user.videoQuota" class="form-error" role="alert">{{ formErrors.user.videoQuota }}</div>
|
||||
</div>
|
||||
|
@ -264,7 +264,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- videos grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">VIDEOS</h2>
|
||||
<h2 i18n class="section-left-column-title">VIDEOS</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -376,7 +376,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- video channels grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">VIDEO CHANNELS</h2>
|
||||
<h2 i18n class="section-left-column-title">VIDEO CHANNELS</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -398,7 +398,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- search grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">SEARCH</h2>
|
||||
<h2 i18n class="section-left-column-title">SEARCH</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -485,9 +485,85 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4"> <!-- import/export grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="section-left-column-title">USER IMPORT/EXPORT</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
<ng-container formGroupName="users">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to import a data archive"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Video quota is checked on import so the user doesn't upload a too big archive file</div>
|
||||
<div i18n>Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="export">
|
||||
|
||||
<ng-container formGroupName="users">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="exportUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to export their data"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Users can export their PeerTube data in a .zip for backup or re-import. Only one export at a time is allowed per user</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n for="exportUsersMaxUserVideoQuota">Max user video quota allowed to generate the export</label>
|
||||
|
||||
<span i18n class="ms-2 small muted">If the user decides to include the video files in the archive</span>
|
||||
|
||||
<my-select-custom-value
|
||||
id="exportUsersMaxUserVideoQuota"
|
||||
[items]="exportMaxUserVideoQuotaOptions"
|
||||
formControlName="maxUserVideoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors.export.users.maxUserVideoQuota" class="form-error" role="alert">{{ formErrors.export.users.maxUserVideoQuota }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n for="exportUsersExportExpiration">User export expiration</label>
|
||||
|
||||
<my-select-options
|
||||
labelForId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"
|
||||
bindLabel="label" bindValue="value" [clearable]="false" [searchable]="false"
|
||||
></my-select-options>
|
||||
|
||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>
|
||||
|
||||
<div *ngIf="formErrors.export.users.exportExpiration" class="form-error" role="alert">{{ formErrors.export.users.exportExpiration }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4"> <!-- federation grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">FEDERATION</h2>
|
||||
<h2 i18n class="section-left-column-title">FEDERATION</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-orange" routerLink="/admin/follows">relations</a> with other instances.
|
||||
</div>
|
||||
|
@ -566,7 +642,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- administrators grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">ADMINISTRATORS</h2>
|
||||
<h2 i18n class="section-left-column-title">ADMINISTRATORS</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -594,7 +670,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- Twitter grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">TWITTER</h2>
|
||||
<h2 i18n class="section-left-column-title">TWITTER</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Provide the Twitter account representing your instance to improve link previews.
|
||||
If you don't have a Twitter account, just leave the default value.
|
||||
|
|
|
@ -21,6 +21,9 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||
availableThemes: SelectOptionsItem[]
|
||||
|
||||
exportExpirationOptions: SelectOptionsItem[] = []
|
||||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||
|
||||
constructor (
|
||||
private configService: ConfigService,
|
||||
private menuService: MenuService,
|
||||
|
@ -33,6 +36,15 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
this.checkImportSyncField()
|
||||
|
||||
this.availableThemes = this.themeService.buildAvailableThemes()
|
||||
|
||||
this.exportExpirationOptions = [
|
||||
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
||||
{ id: 1000 * 3600 * 24 * 2, label: $localize`2 days` },
|
||||
{ id: 1000 * 3600 * 24 * 7, label: $localize`7 days` },
|
||||
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
||||
]
|
||||
|
||||
this.exportMaxUserVideoQuotaOptions = this.configService.videoQuotaOptions.filter(o => (o.id as number) >= 1)
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
|
@ -64,6 +76,14 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
return this.form.value['user']['videoQuota']
|
||||
}
|
||||
|
||||
isExportUsersEnabled () {
|
||||
return this.form.value['export']['users']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledExportUsersClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isExportUsersEnabled() }
|
||||
}
|
||||
|
||||
isSignupEnabled () {
|
||||
return this.form.value['signup']['enabled'] === true
|
||||
}
|
||||
|
|
|
@ -79,10 +79,6 @@ input[type=submit] {
|
|||
}
|
||||
}
|
||||
|
||||
.inner-form-title {
|
||||
@include settings-big-title;
|
||||
}
|
||||
|
||||
.inner-form-description {
|
||||
font-size: 15px;
|
||||
margin-bottom: 15px;
|
||||
|
@ -152,12 +148,6 @@ ngb-tabset:not(.previews) ::ng-deep {
|
|||
}
|
||||
}
|
||||
|
||||
my-user-real-quota-info {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
ADMIN_EMAIL_VALIDATOR,
|
||||
CACHE_SIZE_VALIDATOR,
|
||||
CONCURRENCY_VALIDATOR,
|
||||
EXPORT_EXPIRATION_VALIDATOR,
|
||||
EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||
INDEX_URL_VALIDATOR,
|
||||
INSTANCE_NAME_VALIDATOR,
|
||||
INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||
|
@ -149,6 +151,16 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
},
|
||||
videoChannelSynchronization: {
|
||||
enabled: null
|
||||
},
|
||||
users: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
export: {
|
||||
users: {
|
||||
enabled: null,
|
||||
maxUserVideoQuota: EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||
exportExpiration: EXPORT_EXPIRATION_VALIDATOR
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div class="homepage row mt-5"> <!-- homepage grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">INSTANCE HOMEPAGE</h2>
|
||||
<h2 i18n class="section-left-column-title">INSTANCE HOMEPAGE</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div class="row mt-5"> <!-- instance grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">INSTANCE</h2>
|
||||
<h2 i18n class="section-left-column-title">INSTANCE</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -81,7 +81,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- moderation & nsfw grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">MODERATION & NSFW</h2>
|
||||
<h2 i18n class="section-left-column-title">MODERATION & NSFW</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-orange" routerLink="/admin/users">users</a> to build a moderation team.
|
||||
</div>
|
||||
|
@ -159,7 +159,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- you and your instance grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">YOU AND YOUR INSTANCE</h2>
|
||||
<h2 i18n class="section-left-column-title">YOU AND YOUR INSTANCE</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -209,7 +209,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- other information grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">OTHER INFORMATION</h2>
|
||||
<h2 i18n class="section-left-column-title">OTHER INFORMATION</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<div class="row mt-5">
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">LIVE</h2>
|
||||
<h2 i18n class="section-left-column-title">LIVE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Enable users of your instance to stream live.
|
||||
</div>
|
||||
|
@ -89,7 +89,7 @@
|
|||
|
||||
<div class="row"> <!-- transcoding live streams grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">TRANSCODING</h2>
|
||||
<h2 i18n class="section-left-column-title">TRANSCODING</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some.
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<div class="row mt-4"> <!-- transcoding grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">TRANSCODING</h2>
|
||||
<h2 i18n class="section-left-column-title">TRANSCODING</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
|
||||
resources, this is a critical part of PeerTube, so tread carefully.
|
||||
|
@ -211,7 +211,7 @@
|
|||
|
||||
<div class="row mt-2"> <!-- video studio grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="inner-form-title">VIDEO STUDIO</h2>
|
||||
<h2 i18n class="section-left-column-title">VIDEO STUDIO</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Allows your users to edit their video (cut, add intro/outro, add a watermark etc)
|
||||
</div>
|
||||
|
|
|
@ -70,8 +70,8 @@
|
|||
<div class="row mt-4"> <!-- user grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="anchor" id="user"></div> <!-- user anchor -->
|
||||
<div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
|
||||
<div *ngIf="!isCreation() && user" class="account-title">
|
||||
<div *ngIf="isCreation()" class="section-left-column-title" i18n>NEW USER</div>
|
||||
<div *ngIf="!isCreation() && user" class="section-left-column-title">
|
||||
<my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -212,7 +212,7 @@
|
|||
<div *ngIf="displayDangerZone()" class="row mt-4"> <!-- danger zone grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="anchor" id="danger"></div> <!-- danger zone anchor -->
|
||||
<div i18n class="account-title account-title-danger">DANGER ZONE</div>
|
||||
<div i18n class="section-left-column-title section-left-column-title-danger">DANGER ZONE</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
@use 'sass:math';
|
||||
@use 'sass:color';
|
||||
|
||||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
$form-base-input-width: 340px;
|
||||
|
||||
.account-title {
|
||||
@include settings-big-title;
|
||||
|
||||
&.account-title-danger {
|
||||
color: color.adjust($color: #c54130, $lightness: 10%);
|
||||
}
|
||||
}
|
||||
|
||||
input:not([type=submit]) {
|
||||
@include peertube-input-text($form-base-input-width);
|
||||
display: block;
|
||||
|
@ -43,12 +34,6 @@ button {
|
|||
margin-top: 10px;
|
||||
}
|
||||
|
||||
my-user-real-quota-info {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
button {
|
||||
@include peertube-button;
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
<div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
|
||||
The video quota only takes into account <strong>original</strong> video size. <br />
|
||||
Since transcoding is enabled, videos size can be at most ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
|
||||
<div *ngIf="getQuotaAsNumber() > 0">
|
||||
|
||||
<ng-container i18n>
|
||||
The video quota only takes into account the size of <strong>uploaded</strong> videos, not transcoded files or user export archives (which may contain video files).
|
||||
</ng-container>
|
||||
|
||||
<br />
|
||||
|
||||
<ng-container i18n *ngIf="isTranscodingInformationDisplayed()">
|
||||
Transcoding is enabled so videos size can be at most ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export class UserRealQuotaInfoComponent implements OnInit {
|
|||
}
|
||||
|
||||
isTranscodingInformationDisplayed () {
|
||||
return this.serverConfig.transcoding.enabledResolutions.length !== 0 && this.getQuotaAsNumber() > 0
|
||||
return this.serverConfig.transcoding.enabledResolutions.length !== 0
|
||||
}
|
||||
|
||||
computeQuotaWithTranscoding () {
|
||||
|
@ -37,7 +37,7 @@ export class UserRealQuotaInfoComponent implements OnInit {
|
|||
return multiplier * this.getQuotaAsNumber()
|
||||
}
|
||||
|
||||
private getQuotaAsNumber () {
|
||||
getQuotaAsNumber () {
|
||||
return parseInt(this.videoQuota + '', 10)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ export class JobsComponent extends RestTable implements OnInit {
|
|||
'activitypub-refresher',
|
||||
'actor-keys',
|
||||
'after-video-channel-import',
|
||||
'create-user-export',
|
||||
'email',
|
||||
'federate-video',
|
||||
'generate-video-storyboard',
|
||||
|
|
|
@ -108,8 +108,6 @@ export class RunnerService {
|
|||
}
|
||||
})
|
||||
|
||||
console.log(filters)
|
||||
|
||||
return this.restService.addObjectParams(params, filters)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
<div class="row"> <!-- channel grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
|
||||
<div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
|
||||
<div *ngIf="isCreation()" class="section-left-column-title" i18n>NEW CHANNEL</div>
|
||||
<div *ngIf="!isCreation() && videoChannel" class="section-left-column-title" i18n>CHANNEL</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.video-channel-title {
|
||||
@include settings-big-title;
|
||||
}
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
|
||||
<div class="row"> <!-- built-in token grid -->
|
||||
|
||||
<div class="group col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="applications-title">SUBSCRIPTION FEED</h2>
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="section-left-column-title">SUBSCRIPTION FEED</h2>
|
||||
|
||||
<div i18n class="applications-description">
|
||||
Use third-party feed aggregators to retrieve the list of videos from
|
||||
channels you subscribed to.
|
||||
Use third-party feed aggregators to retrieve the list of videos from channels you subscribed to.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.applications-title {
|
||||
@include settings-big-title;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export * from './user-import-export.service'
|
||||
export * from './my-account-import.component'
|
||||
export * from './my-account-export.component'
|
||||
export * from './my-account-import-export.component'
|
|
@ -0,0 +1,120 @@
|
|||
<div class="row">
|
||||
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="section-left-column-title">EXPORT</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
||||
@if (isExportEnabled()) {
|
||||
|
||||
<p i18n>You can request an archive of your account containing:</p>
|
||||
|
||||
<ul>
|
||||
<li i18n>Your account settings with avatar file</li>
|
||||
<li i18n>Your channels with banner and avatar files</li>
|
||||
<li i18n>Your muted accounts and servers</li>
|
||||
<li i18n>Your comments</li>
|
||||
<li i18n>Your likes and dislikes</li>
|
||||
<li i18n>Your subscriptions and followers</li>
|
||||
<li i18n>Your video playlists with thumbnail files</li>
|
||||
<li i18n>Your videos with thumbnail, caption files. Video files can also be included in the archive</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>The exported data will contain multiple directories:</p>
|
||||
|
||||
<ul>
|
||||
<li i18n>A directory containing an export in ActivityPub format, readable by any compliant software</li>
|
||||
<li i18n>A directory containing an export in custom PeerTube JSON format that can be used to re-import your account on another PeerTube instance</li>
|
||||
<li i18n>A directory containing static files (thumbnails, avatars, video files etc.)</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>You can only request one archive at a time.</p>
|
||||
|
||||
@if (isEmailEnabled()) {
|
||||
<p i18n>An email will be sent when the export archive is available.</p>
|
||||
}
|
||||
|
||||
<table *ngIf="userExports && userExports.length !== 0">
|
||||
<tr>
|
||||
<th i18n scope="column">Date</th>
|
||||
<th i18n scope="column">State</th>
|
||||
<th i18n scope="column">Size</th>
|
||||
<th i18n scope="column">Expires on</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let export of userExports">
|
||||
<td>{{ export.createdAt | date: 'medium' }}</td>
|
||||
<td>{{ export.state.label }}</td>
|
||||
|
||||
<td>
|
||||
<ng-container *ngIf="export.size">{{ export.size | bytes }}</ng-container>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<ng-container *ngIf="export.expiresOn">{{ export.expiresOn | date: 'medium' }}</ng-container>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a i18n *ngIf="export.privateDownloadUrl" [href]="export.privateDownloadUrl" class="peertube-button-link grey-button">Download your archive</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="mt-3">
|
||||
<input
|
||||
class="peertube-button orange-button"
|
||||
[disabled]="isRequestArchiveDisabled()"
|
||||
(click)="openNewArchiveModal()"
|
||||
type="submit" i18n-value value="Request a new archive"
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<p i18n>User export is not enabled by your administrator.</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #exportModal let-hide="close">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" i18n>Archive settings</h4>
|
||||
|
||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div i18n class="alert alert-warning" *ngIf="hasAlreadyACompletedArchive()">
|
||||
You already have an active archive. Requesting a new export archive will remove the current one.
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorInModal" class="alert alert-danger">{{ errorInModal }}</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="exportWithVideos" [(ngModel)]="exportWithVideosFiles"
|
||||
i18n-labelText labelText="Include video files in archive file"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Including video files is required if you want to re-import your videos on another PeerTube website</div>
|
||||
<div *ngIf="archiveWeightEstimation" i18n>If you include video files, the archive file will weigh <strong>approximately {{ archiveWeightEstimation | bytes }}</strong></div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
|
||||
(click)="hide()" (key.enter)="hide()"
|
||||
>
|
||||
|
||||
<input
|
||||
type="submit" i18n-value value="Request an archive" class="peertube-button orange-button"
|
||||
(click)="requestNewArchive()"
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,9 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
table {
|
||||
td,
|
||||
th {
|
||||
@include padding-right(1rem);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
import { PeerTubeProblemDocument, ServerErrorCode, UserExport, UserExportState } from '@peertube/peertube-models'
|
||||
import { UserImportExportService } from './user-import-export.service'
|
||||
import { concatMap, first, from, of, switchMap, toArray } from 'rxjs'
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-export',
|
||||
templateUrl: './my-account-export.component.html',
|
||||
styleUrls: [ './my-account-export.component.scss' ]
|
||||
})
|
||||
export class MyAccountExportComponent implements OnInit {
|
||||
@ViewChild('exportModal', { static: true }) exportModal: NgbModal
|
||||
|
||||
@Input() videoQuotaUsed: number
|
||||
|
||||
userExports: UserExport[] = []
|
||||
|
||||
exportWithVideosFiles: boolean
|
||||
errorInModal: string
|
||||
|
||||
archiveWeightEstimation: number
|
||||
|
||||
private exportModalOpened: NgbModalRef
|
||||
private requestingArchive = false
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private server: ServerService,
|
||||
private userImportExportService: UserImportExportService,
|
||||
private modalService: NgbModal
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.archiveWeightEstimation = this.videoQuotaUsed
|
||||
this.authService.userInformationLoaded
|
||||
.pipe(first())
|
||||
.subscribe(() => this.reloadUserExports())
|
||||
}
|
||||
|
||||
isExportEnabled () {
|
||||
return this.server.getHTMLConfig().export.users.enabled
|
||||
}
|
||||
|
||||
isEmailEnabled () {
|
||||
return this.server.getHTMLConfig().email.enabled
|
||||
}
|
||||
|
||||
isRequestArchiveDisabled () {
|
||||
return this.userExports.some(e => {
|
||||
const id = e.state.id
|
||||
|
||||
return id === UserExportState.PENDING || id === UserExportState.PROCESSING
|
||||
})
|
||||
}
|
||||
|
||||
hasAlreadyACompletedArchive () {
|
||||
return this.userExports.some(e => e.state.id === UserExportState.COMPLETED)
|
||||
}
|
||||
|
||||
openNewArchiveModal () {
|
||||
this.exportWithVideosFiles = false
|
||||
this.errorInModal = undefined
|
||||
|
||||
this.exportModalOpened = this.modalService.open(this.exportModal, { centered: true })
|
||||
}
|
||||
|
||||
requestNewArchive () {
|
||||
if (this.requestingArchive) return
|
||||
this.requestingArchive = true
|
||||
|
||||
let baseObs = of<any>(true)
|
||||
|
||||
if (this.userExports.length !== 0) {
|
||||
baseObs = from(this.userExports.map(e => e.id))
|
||||
.pipe(
|
||||
concatMap(id => this.userImportExportService.deleteUserExport({ userId: this.getUserId(), userExportId: id })),
|
||||
toArray()
|
||||
)
|
||||
}
|
||||
|
||||
baseObs.pipe(
|
||||
switchMap(() => {
|
||||
return this.userImportExportService.requestNewUserExport({ withVideoFiles: this.exportWithVideosFiles, userId: this.getUserId() })
|
||||
})
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.reloadUserExports()
|
||||
|
||||
this.exportModalOpened.close()
|
||||
this.requestingArchive = false
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.requestingArchive = false
|
||||
|
||||
const error = err.body as PeerTubeProblemDocument
|
||||
|
||||
if (error.code === ServerErrorCode.MAX_USER_VIDEO_QUOTA_EXCEEDED_FOR_USER_EXPORT) {
|
||||
// eslint-disable-next-line max-len
|
||||
this.errorInModal = $localize`Video files cannot be included in the export because you have exceeded the maximum video quota allowed by your administrator to export this archive.`
|
||||
return
|
||||
}
|
||||
|
||||
this.errorInModal = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private reloadUserExports () {
|
||||
if (!this.isExportEnabled()) return
|
||||
|
||||
this.userImportExportService.listUserExports({ userId: this.authService.getUser().id })
|
||||
.subscribe(({ data }) => this.userExports = data)
|
||||
}
|
||||
|
||||
private getUserId () {
|
||||
return this.authService.getUser().id
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<h1>
|
||||
<my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Import/Export</ng-container>
|
||||
</h1>
|
||||
|
||||
<my-account-import #accountImport [videoQuotaUsed]="videoQuotaUsed" class="d-block mb-5"></my-account-import>
|
||||
|
||||
<my-account-export [videoQuotaUsed]="videoQuotaUsed"></my-account-export>
|
|
@ -0,0 +1,2 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
|
@ -0,0 +1,33 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { AuthService, CanComponentDeactivate, UserService } from '@app/core'
|
||||
import { MyAccountImportComponent } from './my-account-import.component'
|
||||
import { first } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-import-export',
|
||||
templateUrl: './my-account-import-export.component.html',
|
||||
styleUrls: [ './my-account-import-export.component.scss' ]
|
||||
})
|
||||
export class MyAccountImportExportComponent implements OnInit, CanComponentDeactivate {
|
||||
@ViewChild('accountImport') accountImport: MyAccountImportComponent
|
||||
|
||||
videoQuotaUsed: number
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.authService.userInformationLoaded
|
||||
.pipe(first())
|
||||
.subscribe(() => {
|
||||
this.userService.getMyVideoQuotaUsed()
|
||||
.subscribe(res => this.videoQuotaUsed = res.videoQuotaUsed)
|
||||
})
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return this.accountImport?.canDeactivate() || { canDeactivate: true }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<div class="row">
|
||||
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="section-left-column-title">IMPORT</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
||||
@if (isImportEnabled()) {
|
||||
<p i18n>
|
||||
You can import an archive created by another PeerTube website.
|
||||
|
||||
<p i18n>
|
||||
This is an <strong>import</strong> tool and not a migration tool.
|
||||
It's the reason why data (like channels or videos) is duplicated and not moved from your previous PeerTube website.
|
||||
</p>
|
||||
|
||||
<p i18n>The import process will automatically:</p>
|
||||
|
||||
<ul>
|
||||
<li i18n>Update your account metadata (display name, description, avatar...)</li>
|
||||
<li i18n>Update your user settings (autoplay or P2P policy, notification settings...). It does not update your user email, username or password.</li>
|
||||
<li i18n>Add accounts/server in your mute list</li>
|
||||
<li i18n>Add likes/dislikes</li>
|
||||
<li i18n>Send a follow request to your subscriptions</li>
|
||||
<li i18n>Create channels if they do not already exist</li>
|
||||
<li i18n>Create playlists if they do not already exist</li>
|
||||
<li i18n><strong>If the archive contains video files</strong>, create videos if they do not already exist</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>The following data objects are not imported:</p>
|
||||
|
||||
<ul>
|
||||
<li i18n>Comments</li>
|
||||
<li i18n>Followers (accounts will need to re-follow your channels)</li>
|
||||
</ul>
|
||||
|
||||
<p *ngIf="isEmailEnabled()" i18n>An email will be sent when the import process is complete.</p>
|
||||
|
||||
<div class="mb-3" *ngIf="latestImport">
|
||||
<div>
|
||||
<strong>Latest import on:</strong> {{ latestImport.createdAt | date: 'medium' }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Latest import state:</strong> {{ latestImport.state.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (hasPendingImport()) {
|
||||
<div i18n class="alert pt-alert-primary">
|
||||
You can't re-import an archive because you already have an import that is currently being processed by PeerTube.
|
||||
</div>
|
||||
} @else {
|
||||
<my-upload-progress
|
||||
[isUploading]="uploadingArchive" [uploadPercents]="uploadPercents" [error]="error" [uploaded]="archiveUploadFinished"
|
||||
[enableRetryAfterError]="enableRetryAfterError" (cancel)="cancelUpload()" (retry)="retryUpload()"
|
||||
>
|
||||
</my-upload-progress>
|
||||
|
||||
<div *ngIf="archiveUploadFinished && !error" class="alert pt-alert-primary" i18n>
|
||||
Upload completed. Your archive import will be processed as soon as possible.
|
||||
</div>
|
||||
|
||||
<div [hidden]="uploadingArchive || archiveUploadFinished" class="button-file form-control" i18n-ngbTooltip ngbTooltip="(extension: .zip)">
|
||||
<span i18n>Select the archive file to import</span>
|
||||
<input
|
||||
aria-label="Select the file to import"
|
||||
i18n-aria-label
|
||||
accept=".zip"
|
||||
(change)="onFileChange($event)"
|
||||
id="importfile"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<p i18n>User import is not enabled by your administrator.</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.button-file {
|
||||
@include peertube-button-file(max-content);
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.pt-alert-primary {
|
||||
width: fit-content;
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
|
||||
import { AuthService, ServerService, CanComponentDeactivate, Notifier } from '@app/core'
|
||||
import { Subscription, first, switchMap } from 'rxjs'
|
||||
import { UserImportExportService } from './user-import-export.service'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { buildHTTPErrorResponse, genericUploadErrorHandler, getUploadXRetryConfig } from '@app/helpers'
|
||||
import { HttpStatusCode, UserImport, UserImportState } from '@peertube/peertube-models'
|
||||
import { UploadxService, UploadState, UploaderX } from 'ngx-uploadx'
|
||||
import { BytesPipe } from '@app/shared/shared-main'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-import',
|
||||
templateUrl: './my-account-import.component.html',
|
||||
styleUrls: [ './my-account-import.component.scss' ]
|
||||
})
|
||||
export class MyAccountImportComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
@Input() videoQuotaUsed: number
|
||||
|
||||
uploadingArchive = false
|
||||
archiveUploadFinished = false
|
||||
|
||||
error: string
|
||||
enableRetryAfterError: boolean
|
||||
uploadPercents = 0
|
||||
|
||||
latestImport: UserImport
|
||||
|
||||
private fileToUpload: File
|
||||
private uploadServiceSubscription: Subscription
|
||||
private alreadyRefreshedToken = false
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private server: ServerService,
|
||||
private userImportExportService: UserImportExportService,
|
||||
private resumableUploadService: UploadxService,
|
||||
private notifier: Notifier
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.authService.userInformationLoaded
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap(() => this.userImportExportService.getLatestImport({ userId: this.authService.getUser().id }))
|
||||
)
|
||||
.subscribe(res => this.latestImport = res)
|
||||
|
||||
this.uploadServiceSubscription = this.resumableUploadService.events
|
||||
.subscribe(state => this.onUploadOngoing(state))
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.resumableUploadService.disconnect()
|
||||
|
||||
if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return {
|
||||
canDeactivate: !this.uploadingArchive,
|
||||
text: $localize`Your archive file is not uploaded yet, are you sure you want to leave this page?`
|
||||
}
|
||||
}
|
||||
|
||||
isImportEnabled () {
|
||||
return this.server.getHTMLConfig().import.users.enabled
|
||||
}
|
||||
|
||||
isEmailEnabled () {
|
||||
return this.server.getHTMLConfig().email.enabled
|
||||
}
|
||||
|
||||
onUploadOngoing (state: UploadState) {
|
||||
switch (state.status) {
|
||||
case 'error': {
|
||||
if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
|
||||
this.alreadyRefreshedToken = true
|
||||
|
||||
return this.refreshTokenAndRetryUpload()
|
||||
}
|
||||
|
||||
this.handleUploadError(buildHTTPErrorResponse(state))
|
||||
break
|
||||
}
|
||||
|
||||
case 'cancelled':
|
||||
this.uploadingArchive = false
|
||||
this.uploadPercents = 0
|
||||
|
||||
this.enableRetryAfterError = false
|
||||
this.error = ''
|
||||
break
|
||||
|
||||
case 'uploading':
|
||||
this.uploadPercents = state.progress
|
||||
break
|
||||
|
||||
case 'complete':
|
||||
this.archiveUploadFinished = true
|
||||
this.uploadPercents = 100
|
||||
this.uploadingArchive = false
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onFileChange (event: Event | { target: HTMLInputElement }) {
|
||||
const inputEl = event.target as HTMLInputElement
|
||||
const file = inputEl.files[0]
|
||||
if (!file) return
|
||||
|
||||
const user = this.authService.getUser()
|
||||
|
||||
if (user.videoQuota !== -1 && this.videoQuotaUsed + file.size > user.videoQuota) {
|
||||
const bytePipes = new BytesPipe()
|
||||
const fileSizeBytes = bytePipes.transform(file.size, 0)
|
||||
const videoQuotaUsedBytes = bytePipes.transform(this.videoQuotaUsed, 0)
|
||||
const videoQuotaBytes = bytePipes.transform(user.videoQuota, 0)
|
||||
|
||||
this.notifier.error(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`Cannot import this file as your video quota would be exceeded (import size: ${fileSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
||||
)
|
||||
|
||||
inputEl.value = ''
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.fileToUpload = file
|
||||
|
||||
this.uploadFile(file)
|
||||
}
|
||||
|
||||
cancelUpload () {
|
||||
this.resumableUploadService.control({ action: 'cancel' })
|
||||
}
|
||||
|
||||
retryUpload () {
|
||||
this.enableRetryAfterError = false
|
||||
this.error = ''
|
||||
this.uploadFile(this.fileToUpload)
|
||||
}
|
||||
|
||||
hasPendingImport () {
|
||||
if (!this.latestImport) return false
|
||||
|
||||
const state = this.latestImport.state.id
|
||||
return state === UserImportState.PENDING || state === UserImportState.PROCESSING
|
||||
}
|
||||
|
||||
private uploadFile (file: File) {
|
||||
this.resumableUploadService.handleFiles(file, {
|
||||
endpoint: `${UserImportExportService.BASE_USER_IMPORTS_URL}${this.authService.getUser().id}/imports/import-resumable`,
|
||||
multiple: false,
|
||||
|
||||
maxChunkSize: this.server.getHTMLConfig().client.videos.resumableUpload.maxChunkSize,
|
||||
|
||||
token: this.authService.getAccessToken(),
|
||||
|
||||
uploaderClass: UploaderX,
|
||||
|
||||
retryConfig: getUploadXRetryConfig(),
|
||||
|
||||
metadata: {
|
||||
filename: file.name
|
||||
}
|
||||
})
|
||||
|
||||
this.uploadingArchive = true
|
||||
}
|
||||
|
||||
private handleUploadError (err: HttpErrorResponse) {
|
||||
// Reset progress
|
||||
this.uploadPercents = 0
|
||||
this.enableRetryAfterError = true
|
||||
|
||||
this.error = genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`archive`,
|
||||
notifier: this.notifier,
|
||||
sticky: false
|
||||
})
|
||||
|
||||
if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
|
||||
this.cancelUpload()
|
||||
}
|
||||
}
|
||||
|
||||
private refreshTokenAndRetryUpload () {
|
||||
this.authService.refreshAccessToken()
|
||||
.subscribe(() => this.retryUpload())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import { catchError, map } from 'rxjs/operators'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { RestExtractor, ServerService } from '@app/core'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { HttpStatusCode, ResultList, UserExport, UserImport } from '@peertube/peertube-models'
|
||||
import { forkJoin, of } from 'rxjs'
|
||||
import { peertubeTranslate } from '@peertube/peertube-core-utils'
|
||||
|
||||
@Injectable()
|
||||
export class UserImportExportService {
|
||||
static BASE_USER_EXPORTS_URL = environment.apiUrl + '/api/v1/users/'
|
||||
static BASE_USER_IMPORTS_URL = environment.apiUrl + '/api/v1/users/'
|
||||
|
||||
constructor (
|
||||
private authHttp: HttpClient,
|
||||
private restExtractor: RestExtractor,
|
||||
private server: ServerService
|
||||
) { }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listUserExports (options: {
|
||||
userId: number
|
||||
}) {
|
||||
const { userId } = options
|
||||
|
||||
const url = UserImportExportService.BASE_USER_EXPORTS_URL + userId + '/exports'
|
||||
|
||||
return this.authHttp.get<ResultList<UserExport>>(url)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
requestNewUserExport (options: {
|
||||
userId: number
|
||||
withVideoFiles: boolean
|
||||
}) {
|
||||
const { userId, withVideoFiles } = options
|
||||
|
||||
const url = UserImportExportService.BASE_USER_EXPORTS_URL + userId + '/exports/request'
|
||||
|
||||
return this.authHttp.post(url, { withVideoFiles })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
deleteUserExport (options: {
|
||||
userId: number
|
||||
userExportId: number
|
||||
}) {
|
||||
const { userId, userExportId } = options
|
||||
|
||||
const url = UserImportExportService.BASE_USER_EXPORTS_URL + userId + '/exports/' + userExportId
|
||||
|
||||
return this.authHttp.delete(url)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getLatestImport (options: {
|
||||
userId: number
|
||||
}) {
|
||||
const { userId } = options
|
||||
|
||||
const url = UserImportExportService.BASE_USER_IMPORTS_URL + userId + '/imports/latest'
|
||||
|
||||
return forkJoin([
|
||||
this.authHttp.get<UserImport>(url),
|
||||
this.server.getServerLocale()
|
||||
]).pipe(
|
||||
map(([ latestImport, translations ]) => {
|
||||
latestImport.state.label = peertubeTranslate(latestImport.state.label, translations)
|
||||
|
||||
return latestImport
|
||||
}),
|
||||
catchError(err => {
|
||||
if (err.status === HttpStatusCode.NOT_FOUND_404) return of(undefined)
|
||||
|
||||
return this.restExtractor.handleError(err)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { LoginGuard } from '../core'
|
||||
import { CanDeactivateGuard, LoginGuard } from '../core'
|
||||
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
|
||||
import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
|
||||
|
@ -8,6 +8,7 @@ import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-acc
|
|||
import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
|
||||
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
||||
import { MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
|
||||
import { MyAccountImportExportComponent } from './my-account-import-export'
|
||||
import { MyAccountComponent } from './my-account.component'
|
||||
|
||||
const myAccountRoutes: Routes = [
|
||||
|
@ -137,6 +138,16 @@ const myAccountRoutes: Routes = [
|
|||
title: $localize`Applications`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'import-export',
|
||||
component: MyAccountImportExportComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Import/Export`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<div class="row mt-3"> <!-- profile settings grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="account-title">PROFILE SETTINGS</h2>
|
||||
<h2 i18n class="section-left-column-title">PROFILE SETTINGS</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -22,7 +22,7 @@
|
|||
|
||||
<div class="row mt-5"> <!-- interface grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="account-title">INTERFACE</h2>
|
||||
<h2 i18n class="section-left-column-title">INTERFACE</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -33,7 +33,7 @@
|
|||
<div class="row mt-5"> <!-- video settings grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="anchor" id="video-settings"></div> <!-- video settings anchor -->
|
||||
<h2 i18n class="account-title">VIDEO SETTINGS</h2>
|
||||
<h2 i18n class="section-left-column-title">VIDEO SETTINGS</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -44,7 +44,7 @@
|
|||
<div class="row mt-5"> <!-- notifications grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="anchor" id="notifications"></div> <!-- notifications anchor -->
|
||||
<h2 i18n class="account-title">NOTIFICATIONS</h2>
|
||||
<h2 i18n class="section-left-column-title">NOTIFICATIONS</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -54,7 +54,7 @@
|
|||
|
||||
<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- password grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="account-title">PASSWORD</h2>
|
||||
<h2 i18n class="section-left-column-title">PASSWORD</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -64,7 +64,7 @@
|
|||
|
||||
<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- two factor auth grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="account-title">Two-factor authentication</h2>
|
||||
<h2 i18n class="section-left-column-title">Two-factor authentication</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- email grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="account-title">EMAIL</h2>
|
||||
<h2 i18n class="section-left-column-title">EMAIL</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
@ -86,7 +86,7 @@
|
|||
|
||||
<div class="row mt-5"> <!-- danger zone grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<h2 i18n class="account-title account-title-danger">DANGER ZONE</h2>
|
||||
<h2 i18n class="section-left-column-title section-left-column-title-danger">DANGER ZONE</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -4,14 +4,6 @@
|
|||
@use '_mixins' as *;
|
||||
@use 'bootstrap/scss/functions' as *;
|
||||
|
||||
.account-title {
|
||||
@include settings-big-title;
|
||||
|
||||
&.account-title-danger {
|
||||
color: color.adjust($color: #c54130, $lightness: 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.row > div {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { AuthUser, ScreenService } from '@app/core'
|
||||
import { AuthUser, ScreenService, ServerService } from '@app/core'
|
||||
import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component'
|
||||
|
||||
@Component({
|
||||
|
@ -12,7 +12,8 @@ export class MyAccountComponent implements OnInit {
|
|||
user: AuthUser
|
||||
|
||||
constructor (
|
||||
private screenService: ScreenService
|
||||
private screenService: ScreenService,
|
||||
private server: ServerService
|
||||
) { }
|
||||
|
||||
get isBroadcastMessageDisplayed () {
|
||||
|
@ -56,6 +57,11 @@ export class MyAccountComponent implements OnInit {
|
|||
routerLink: '/my-account/notifications'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Import/Export`,
|
||||
routerLink: '/my-account/import-export'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Applications`,
|
||||
routerLink: '/my-account/applications'
|
||||
|
|
|
@ -28,6 +28,13 @@ import { MyAccountProfileComponent } from './my-account-settings/my-account-prof
|
|||
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
||||
import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
|
||||
import { MyAccountComponent } from './my-account.component'
|
||||
import {
|
||||
MyAccountImportExportComponent,
|
||||
MyAccountExportComponent,
|
||||
MyAccountImportComponent,
|
||||
UserImportExportService
|
||||
} from './my-account-import-export'
|
||||
import { UploadProgressComponent } from '@app/shared/standalone-upload'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -47,7 +54,9 @@ import { MyAccountComponent } from './my-account.component'
|
|||
SharedAbuseListModule,
|
||||
SharedShareModal,
|
||||
SharedActorImageModule,
|
||||
SharedActorImageEditModule
|
||||
SharedActorImageEditModule,
|
||||
|
||||
UploadProgressComponent
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
@ -68,14 +77,19 @@ import { MyAccountComponent } from './my-account.component'
|
|||
MyAccountNotificationsComponent,
|
||||
MyAccountNotificationPreferencesComponent,
|
||||
|
||||
MyAccountEmailPreferencesComponent
|
||||
MyAccountEmailPreferencesComponent,
|
||||
MyAccountImportExportComponent,
|
||||
MyAccountExportComponent,
|
||||
MyAccountImportComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
MyAccountComponent
|
||||
],
|
||||
|
||||
providers: []
|
||||
providers: [
|
||||
UserImportExportService
|
||||
]
|
||||
})
|
||||
export class MyAccountModule {
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="video-channel-sync-title" i18n>NEW SYNCHRONIZATION</div>
|
||||
<div class="section-left-column-title" i18n>NEW SYNCHRONIZATION</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -7,10 +7,6 @@ input[type=text] {
|
|||
@include peertube-input-text($form-base-input-width);
|
||||
}
|
||||
|
||||
.video-channel-sync-title {
|
||||
@include settings-big-title;
|
||||
}
|
||||
|
||||
my-select-channel {
|
||||
display: block;
|
||||
max-width: $form-base-input-width;
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
|
||||
<div class="row"> <!-- playlist grid -->
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div>
|
||||
<div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div>
|
||||
<div *ngIf="isCreation()" class="section-left-column-title" i18n>NEW PLAYLIST</div>
|
||||
<div *ngIf="!isCreation() && videoPlaylistToUpdate" class="section-left-column-title" i18n>PLAYLIST</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.video-playlist-title {
|
||||
@include settings-big-title;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
|||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
import { UploadProgressComponent } from './upload-progress.component'
|
||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||
import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
|
||||
import { VideoEditComponent } from './video-edit.component'
|
||||
|
@ -24,8 +23,7 @@ import { VideoUploadService } from './video-upload.service'
|
|||
declarations: [
|
||||
VideoEditComponent,
|
||||
VideoCaptionAddModalComponent,
|
||||
VideoCaptionEditModalContentComponent,
|
||||
UploadProgressComponent
|
||||
VideoCaptionEditModalContentComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -35,8 +33,7 @@ import { VideoUploadService } from './video-upload.service'
|
|||
SharedFormModule,
|
||||
SharedGlobalIconModule,
|
||||
|
||||
VideoEditComponent,
|
||||
UploadProgressComponent
|
||||
VideoEditComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { UploaderX, UploadState, UploadxOptions } from 'ngx-uploadx'
|
||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||
import { UploaderX, UploadxOptions } from 'ngx-uploadx'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { BytesPipe, VideoService } from '@app/shared/shared-main'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { UploaderXFormData } from './uploaderx-form-data'
|
||||
import { getUploadXRetryConfig } from '@app/helpers'
|
||||
|
||||
@Injectable()
|
||||
export class VideoUploadService {
|
||||
|
@ -73,31 +72,7 @@ export class VideoUploadService {
|
|||
|
||||
uploaderClass,
|
||||
|
||||
retryConfig: {
|
||||
maxAttempts: 30, // maximum attempts for 503 codes, otherwise set to 6, see below
|
||||
maxDelay: 120_000, // 2 min
|
||||
shouldRetry: (code: number, attempts: number) => {
|
||||
return code === HttpStatusCode.SERVICE_UNAVAILABLE_503 || ((code < 400 || code > 500) && attempts < 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
buildHTTPErrorResponse (state: UploadState): HttpErrorResponse {
|
||||
const error = state.response?.error?.message || state.response?.error || 'Unknown error'
|
||||
|
||||
return {
|
||||
error: new Error(error),
|
||||
name: 'HttpErrorResponse',
|
||||
message: error,
|
||||
ok: false,
|
||||
headers: new HttpHeaders(state.responseHeaders),
|
||||
status: +state.responseStatus,
|
||||
statusText: error,
|
||||
type: HttpEventType.Response,
|
||||
url: state.url
|
||||
retryConfig: getUploadXRetryConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
|
||||
<my-upload-progress
|
||||
[isUploadingVideo]="isUploadingVideo" [videoUploadPercents]="videoUploadPercents" [error]="error" [videoUploaded]="videoUploaded"
|
||||
[isUploading]="isUploadingVideo" [uploadPercents]="videoUploadPercents" [error]="error" [uploaded]="videoUploaded"
|
||||
[enableRetryAfterError]="enableRetryAfterError" (cancel)="cancelUpload()" (retry)="retryUpload()"
|
||||
>
|
||||
</my-upload-progress>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { HttpErrorResponse } from '@angular/common/http'
|
|||
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
|
||||
import { buildHTTPErrorResponse, genericUploadErrorHandler, scrollToTop } from '@app/helpers'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { Video, VideoCaptionService, VideoChapterService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
|
@ -140,7 +140,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
return this.refreshTokenAndRetryUpload()
|
||||
}
|
||||
|
||||
this.handleUploadError(this.videoUploadService.buildHTTPErrorResponse(state))
|
||||
this.handleUploadError(buildHTTPErrorResponse(state))
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { VideoImportUrlComponent } from './video-add-components/video-import-url
|
|||
import { VideoUploadComponent } from './video-add-components/video-upload.component'
|
||||
import { VideoAddRoutingModule } from './video-add-routing.module'
|
||||
import { VideoAddComponent } from './video-add.component'
|
||||
import { UploadProgressComponent } from '@app/shared/standalone-upload'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -15,7 +16,9 @@ import { VideoAddComponent } from './video-add.component'
|
|||
|
||||
VideoEditModule,
|
||||
|
||||
UploadxModule
|
||||
UploadxModule,
|
||||
|
||||
UploadProgressComponent
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
|
||||
<my-upload-progress
|
||||
[isUploadingVideo]="isReplacingVideoFile" [videoUploadPercents]="videoUploadPercents" [error]="uploadError" [videoUploaded]="updateDone"
|
||||
[isUploading]="isReplacingVideoFile" [uploadPercents]="videoUploadPercents" [error]="uploadError" [uploaded]="updateDone"
|
||||
[enableRetryAfterError]="false" (cancel)="cancelUpload()"
|
||||
>
|
||||
</my-upload-progress>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { HttpErrorResponse } from '@angular/common/http'
|
|||
import { Component, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, CanComponentDeactivate, ConfirmService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import { buildHTTPErrorResponse, genericUploadErrorHandler } from '@app/helpers'
|
||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import {
|
||||
Video,
|
||||
|
@ -329,7 +329,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit, OnDest
|
|||
return this.refreshTokenAndRetryUpload()
|
||||
}
|
||||
|
||||
this.handleUploadError(this.videoUploadService.buildHTTPErrorResponse(state))
|
||||
this.handleUploadError(buildHTTPErrorResponse(state))
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,15 @@ import { VideoEditModule } from './shared/video-edit.module'
|
|||
import { VideoUpdateRoutingModule } from './video-update-routing.module'
|
||||
import { VideoUpdateComponent } from './video-update.component'
|
||||
import { VideoUpdateResolver } from './video-update.resolver'
|
||||
import { UploadProgressComponent } from '@app/shared/standalone-upload'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
VideoUpdateRoutingModule,
|
||||
|
||||
VideoEditModule
|
||||
VideoEditModule,
|
||||
|
||||
UploadProgressComponent
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import * as debug from 'debug'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ConfirmService } from '@app/core/confirm'
|
||||
|
||||
export type CanComponentDeactivateResult = { text?: string, canDeactivate: Observable<boolean> | boolean }
|
||||
|
||||
const debugLogger = debug('peertube:routing:CanComponentDeactivate')
|
||||
|
||||
export interface CanComponentDeactivate {
|
||||
canDeactivate: () => CanComponentDeactivateResult
|
||||
}
|
||||
|
@ -15,6 +18,9 @@ export class CanDeactivateGuard {
|
|||
|
||||
canDeactivate (component: CanComponentDeactivate) {
|
||||
const result = component.canDeactivate()
|
||||
|
||||
debugLogger('Checking if component can deactivate', result)
|
||||
|
||||
const text = result.text || $localize`All unsaved data will be lost, are you sure you want to leave this page?`
|
||||
|
||||
return result.canDeactivate || this.confirmService.confirm(
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||
import { Notifier } from '@app/core'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { UploadState } from 'ngx-uploadx'
|
||||
|
||||
function genericUploadErrorHandler (options: {
|
||||
export function genericUploadErrorHandler (options: {
|
||||
err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
|
||||
name: string
|
||||
notifier?: Notifier
|
||||
|
@ -17,8 +18,30 @@ function genericUploadErrorHandler (options: {
|
|||
return message
|
||||
}
|
||||
|
||||
export {
|
||||
genericUploadErrorHandler
|
||||
export function getUploadXRetryConfig () {
|
||||
return {
|
||||
maxAttempts: 30, // maximum attempts for 503 codes, otherwise set to 6, see below
|
||||
maxDelay: 120_000, // 2 min
|
||||
shouldRetry: (code: number, attempts: number) => {
|
||||
return code === HttpStatusCode.SERVICE_UNAVAILABLE_503 || ((code < 400 || code > 500) && attempts < 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildHTTPErrorResponse (state: UploadState): HttpErrorResponse {
|
||||
const error = state.response?.error?.message || state.response?.error || 'Unknown error'
|
||||
|
||||
return {
|
||||
error: new Error(error),
|
||||
name: 'HttpErrorResponse',
|
||||
message: error,
|
||||
ok: false,
|
||||
headers: new HttpHeaders(state.responseHeaders),
|
||||
status: +state.responseStatus,
|
||||
statusText: error,
|
||||
type: HttpEventType.Response,
|
||||
url: state.url
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -11,7 +11,7 @@ export const INSTANCE_NAME_VALIDATOR: BuildFormValidator = {
|
|||
export const INSTANCE_SHORT_DESCRIPTION_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.maxLength(250) ],
|
||||
MESSAGES: {
|
||||
maxlength: $localize`Short description should not be longer than 250 characters.`
|
||||
maxlength: $localize`Short description must not be longer than 250 characters.`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ export const MAX_LIVE_DURATION_VALIDATOR: BuildFormValidator = {
|
|||
VALIDATORS: [ Validators.required, Validators.min(-1) ],
|
||||
MESSAGES: {
|
||||
required: $localize`Max live duration is required.`,
|
||||
min: $localize`Max live duration should be greater or equal to -1.`
|
||||
min: $localize`Max live duration must be greater or equal to -1.`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ export const MAX_INSTANCE_LIVES_VALIDATOR: BuildFormValidator = {
|
|||
VALIDATORS: [ Validators.required, Validators.min(-1) ],
|
||||
MESSAGES: {
|
||||
required: $localize`Max instance lives is required.`,
|
||||
min: $localize`Max instance lives should be greater or equal to -1.`
|
||||
min: $localize`Max instance lives must be greater or equal to -1.`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ export const MAX_USER_LIVES_VALIDATOR: BuildFormValidator = {
|
|||
VALIDATORS: [ Validators.required, Validators.min(-1) ],
|
||||
MESSAGES: {
|
||||
required: $localize`Max user lives is required.`,
|
||||
min: $localize`Max user lives should be greater or equal to -1.`
|
||||
min: $localize`Max user lives must be greater or equal to -1.`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,20 +102,35 @@ export const CONCURRENCY_VALIDATOR: BuildFormValidator = {
|
|||
VALIDATORS: [ Validators.required, Validators.min(1) ],
|
||||
MESSAGES: {
|
||||
required: $localize`Concurrency is required.`,
|
||||
min: $localize`Concurrency should be greater or equal to 1.`
|
||||
min: $localize`Concurrency must be greater or equal to 1.`
|
||||
}
|
||||
}
|
||||
|
||||
export const INDEX_URL_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.pattern(/^https:\/\//) ],
|
||||
MESSAGES: {
|
||||
pattern: $localize`Index URL should be a URL`
|
||||
pattern: $localize`Index URL must be a URL`
|
||||
}
|
||||
}
|
||||
|
||||
export const SEARCH_INDEX_URL_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.pattern(/^https?:\/\//) ],
|
||||
MESSAGES: {
|
||||
pattern: $localize`Search index URL should be a URL`
|
||||
pattern: $localize`Search index URL must be a URL`
|
||||
}
|
||||
}
|
||||
|
||||
export const EXPORT_EXPIRATION_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.required, Validators.min(1) ],
|
||||
MESSAGES: {
|
||||
required: $localize`Export expiration is required.`,
|
||||
min: $localize`Export expiration must be greater or equal to 1.`
|
||||
}
|
||||
}
|
||||
export const EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.required, Validators.min(1) ],
|
||||
MESSAGES: {
|
||||
required: $localize`Max user video quota is required.`,
|
||||
min: $localize`Max user video video quota must be greater or equal to 1.`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
|
|||
this.timestamp = timestamp
|
||||
|
||||
this.timestampString = secondsToTime({ seconds: this.timestamp, fullFormat: true, symbol: ':' })
|
||||
console.log(this.timestampString)
|
||||
}
|
||||
|
||||
registerOnChange (fn: (_: any) => void) {
|
||||
|
|
|
@ -112,6 +112,17 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th i18n class="label" colspan="2">Export</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th i18n class="sub-label" scope="row">Users can export their data</th>
|
||||
<td>
|
||||
<my-feature-boolean [value]="serverConfig.export.users.enabled"></my-feature-boolean>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th i18n class="label" colspan="2">Search</th>
|
||||
</tr>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { getBytes } from '@root-helpers/bytes'
|
|||
@Pipe({ name: 'bytes' })
|
||||
export class BytesPipe implements PipeTransform {
|
||||
|
||||
transform (value: number, precision?: number | undefined): string | number {
|
||||
transform (value: number, precision = 0): string | number {
|
||||
return getBytes(value, precision)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './upload-progress.component'
|
|
@ -1,17 +1,18 @@
|
|||
<!-- Upload progress/cancel/error/success header -->
|
||||
<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
|
||||
<div class="progress" i18n-title title="Total video uploaded">
|
||||
<div *ngIf="isUploading && !error" class="upload-progress-cancel">
|
||||
<div class="progress" i18n-title title="Total uploaded">
|
||||
<div
|
||||
class="progress-bar" role="progressbar"
|
||||
[style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"
|
||||
[style]="{ width: uploadPercents + '%' }" [attr.aria-valuenow]="uploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"
|
||||
>
|
||||
<span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span>
|
||||
<span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span>
|
||||
<span *ngIf="uploadPercents === 100 && uploaded === false" i18n>Processing…</span>
|
||||
<span *ngIf="uploadPercents !== 100 || uploaded">{{ uploadPercents }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
*ngIf="videoUploaded === false"
|
||||
type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancel.emit()"
|
||||
*ngIf="uploaded === false"
|
||||
type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload" value="Cancel" (click)="cancel.emit()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -22,8 +23,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Retry failed upload of a video" value="Retry" (click)="retry.emit()" />
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancel.emit()" />
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Retry failed upload" value="Retry" (click)="retry.emit()" />
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload" value="Cancel" (click)="cancel.emit()" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="error && !enableRetryAfterError" class="alert alert-danger">
|
|
@ -1,15 +1,18 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-upload-progress',
|
||||
templateUrl: './upload-progress.component.html',
|
||||
styleUrls: [ './upload-progress.component.scss' ]
|
||||
styleUrls: [ './upload-progress.component.scss' ],
|
||||
imports: [ CommonModule ],
|
||||
standalone: true
|
||||
})
|
||||
export class UploadProgressComponent {
|
||||
@Input() isUploadingVideo: boolean
|
||||
@Input() videoUploadPercents: number
|
||||
@Input() isUploading: boolean
|
||||
@Input() uploadPercents: number
|
||||
@Input() error: string
|
||||
@Input() videoUploaded: boolean
|
||||
@Input() uploaded: boolean
|
||||
@Input() enableRetryAfterError: boolean
|
||||
|
||||
@Output() cancel = new EventEmitter()
|
|
@ -2,7 +2,8 @@ const dictionary: { max: number, type: string }[] = [
|
|||
{ max: 1024, type: 'B' },
|
||||
{ max: 1048576, type: 'KB' },
|
||||
{ max: 1073741824, type: 'MB' },
|
||||
{ max: 1.0995116e12, type: 'GB' }
|
||||
{ max: 1.0995116e12, type: 'GB' },
|
||||
{ max: 1.125899906842624e15, type: 'TB' }
|
||||
]
|
||||
|
||||
function getBytes (value: number, precision?: number | undefined): string | number {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use 'sass:color';
|
||||
|
||||
@use '_badges' as *;
|
||||
@use '_icons' as *;
|
||||
@use '_variables' as *;
|
||||
|
@ -23,6 +25,20 @@
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.section-left-column-title {
|
||||
text-transform: uppercase;
|
||||
color: pvar(--mainColor);
|
||||
font-weight: $font-bold;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.section-left-column-title-danger {
|
||||
color: color.adjust($color: #c54130, $lightness: 10%);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.muted {
|
||||
@include muted;
|
||||
}
|
||||
|
|
|
@ -598,14 +598,6 @@
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
@mixin settings-big-title {
|
||||
text-transform: uppercase;
|
||||
color: pvar(--mainColor);
|
||||
font-weight: $font-bold;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@mixin row-blocks ($column-responsive: true, $min-height: 130px, $separator: true) {
|
||||
display: flex;
|
||||
min-height: $min-height;
|
||||
|
|
Loading…
Reference in New Issue