Merge remote-tracking branch 'origin' into feat-4769-my-videos-contained-playlists
This commit is contained in:
commit
e72bb69350
|
@ -7,11 +7,11 @@ root = true
|
|||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.yml]
|
||||
[*.{yml,html}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{client,server,shared}/**.{ts,json,js}]
|
||||
[{client,server,shared,scripts}/**.{ts,json,js}]
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
|
|
3
.github/CONTRIBUTING.md
vendored
3
.github/CONTRIBUTING.md
vendored
|
@ -116,6 +116,9 @@ $ sudo -u postgres psql -c "CREATE EXTENSION pg_trgm;" peertube_dev
|
|||
$ sudo -u postgres psql -c "CREATE EXTENSION unaccent;" peertube_dev
|
||||
```
|
||||
|
||||
Peertube also requires a running redis server, no special setup is needed for
|
||||
this.
|
||||
|
||||
In dev mode, administrator username is **root** and password is **test**.
|
||||
|
||||
### Online development
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -75,6 +75,8 @@ jobs:
|
|||
- name: Run Test
|
||||
# external-plugins tests only run on schedule
|
||||
if: github.event_name == 'schedule' || matrix.test_suite != 'external-plugins'
|
||||
env:
|
||||
AKISMET_KEY: ${{ secrets.AKISMET_KEY }}
|
||||
run: npm run ci -- ${{ matrix.test_suite }}
|
||||
|
||||
- name: Display errors
|
||||
|
|
132
CHANGELOG.md
132
CHANGELOG.md
|
@ -1,5 +1,137 @@
|
|||
# Changelog
|
||||
|
||||
## v4.3.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Redis **<** 5.x is not supported anymore
|
||||
* FFmpeg **<** 4.3 is not supported anymore
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Use `yt-dlp` by default instead of `youtube-dl` for new installations (because of much more dev activity)
|
||||
* Support NodeJS 18
|
||||
* Improved PeerTube logs:
|
||||
* Reduce amount of PeerTube error logs
|
||||
* Introduce `log.log_tracker_unknown_infohash` setting to disable "Unknown infoHash" warnings
|
||||
* Web browsers send their error logs to the server that writes them in its own logs. Can be disabled by `log.accept_client_log` setting
|
||||
* Introduce experimental support of [OpenTelemetry](https://opentelemetry.io/)
|
||||
* Enable metrics export using a Prometheus exporter
|
||||
* Enable tracing export using a Jaeger exporter
|
||||
* Automatically rebuild native plugin modules on NodeJS ABI change
|
||||
|
||||
### Docker
|
||||
|
||||
* Add ability to easily use the docker compose stack on localhost
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* Theme:
|
||||
* Removed unused `--secondaryColor` CSS variable
|
||||
* Add client plugin hooks (https://docs.joinpeertube.org/api-plugins):
|
||||
* `filter:api.my-library.video-playlist-elements.list.params` & `filter:api.my-library.video-playlist-elements.list.result` [#5098](https://github.com/Chocobozzz/PeerTube/pull/5098)
|
||||
* `action:video-channel-create.init`
|
||||
* `action:video-channel-update.init` & `action:video-channel-update.video-channel.loaded`
|
||||
* `action:video-channel-videos.init` & `action:video-channel-videos.video-channel.loaded` & `action:video-channel-videos.videos.loaded`
|
||||
* `action:video-channel-playlists.init` & `action:video-channel-playlists.video-channel.loaded` & `action:video-channel-playlists.playlists.loaded`
|
||||
* `filter:share.video-embed-code.build.params` & `filter:share.video-embed-code.build.result` & `filter:share.video-playlist-embed-code.build.params` & `filter:share.video-playlist-embed-code.build.result`
|
||||
* `filter:share.video-embed-url.build.params` & `filter:share.video-embed-url.build.result` & `filter:share.video-playlist-embed-url.build.params` & `filter:share.video-playlist-embed-url.build.result`
|
||||
* `filter:share.video-url.build.params` & `filter:share.video-url.build.result` & `filter:share.video-playlist-url.build.params` & `filter:share.video-playlist-url.build.result`
|
||||
* `action:modal.share.shown`
|
||||
* Add server plugin hooks (https://docs.joinpeertube.org/api-plugins):
|
||||
* `filter:job-queue.process.params` & `filter:job-queue.process.result`
|
||||
* `filter:transcoding.manual.resolutions-to-transcode.result` & `filter:transcoding.auto.resolutions-to-transcode.result`
|
||||
* `action:api.video-channel.created` & `action:api.video-channel.updated` & `action:api.video-channel.deleted`
|
||||
* `action:notifier.notification.created`
|
||||
* Add HTML placeholder (https://docs.joinpeertube.org/contribute-plugins?id=html-placeholder-elements):
|
||||
* `share-modal-playlist-settings` & `share-modal-video-settings`
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: Add ability for users to synchronize a remote channel [#5135](https://github.com/Chocobozzz/PeerTube/pull/5135) :tada:
|
||||
* Automatically import all videos of a remote channel in your PeerTube channel
|
||||
* PeerTube will watch for new publications and automatically import these new videos
|
||||
* UI:
|
||||
* Redesigned *Create an account* steps
|
||||
* Improved *Login* page
|
||||
* Use a lighter font color
|
||||
* Use a bigger font size
|
||||
* Don't display form errors in red while typing but only when we unfocus the input
|
||||
* Display an error message when the user is unauthorized to view a page [#5097](https://github.com/Chocobozzz/PeerTube/pull/5097)
|
||||
* Display latest upload date for captions
|
||||
* Add an information if the live will be saved as a replay when displaying live sessions
|
||||
* Move search bar at the center of the header
|
||||
* Add *Toki Pona* and *Croatian* locales in client
|
||||
* Embed:
|
||||
* Display a message and automatically start live streams in embed
|
||||
* Use the instance name instead of "PeerTube" in embed control bar
|
||||
* Reuse current watch page query parameters for embed when using OEmbed [#5023](https://github.com/Chocobozzz/PeerTube/pull/5023)
|
||||
* Instance follows:
|
||||
* Introduce a *Rejected* state for follow requests to not reprocess already rejected follow requests
|
||||
* Add bulk actions on instance following/followers ()
|
||||
* Admins:
|
||||
* Add ability to disable original resolution transcoding of the uploaded video/live stream
|
||||
* Add ability to delete a specific video file in videos overview
|
||||
* Display *Last Login* column by default in users overview
|
||||
* Remember last selected columns in users overview
|
||||
* Add ability to set a custom video import timeout
|
||||
* Add ability to set the default feed (Atom, RSS...) items count
|
||||
* Admins and moderators now bypass API rate limits
|
||||
* Add ability to list comments on local videos in comments overview
|
||||
* Limit video import resolution depending on enabled VOD transcoding resolutions
|
||||
* Store and display the uploaded video original filename [#4885](https://github.com/Chocobozzz/PeerTube/pull/4885)
|
||||
* Add *Total views* in the my channels list [#5007](https://github.com/Chocobozzz/PeerTube/pull/5007)
|
||||
* Add *Original Publication Date* video sort option [#4959](https://github.com/Chocobozzz/PeerTube/pull/4959)
|
||||
* Performance:
|
||||
* Optimized view/watching endpoint
|
||||
* Optimized video feed SQL query
|
||||
* Process images (resize, convert...) in a dedicated worker thread
|
||||
* Optimized emoji markup list rendering in client
|
||||
* Use a worker thread to send ActivityPub Broadcast requests
|
||||
* Suffix external auth username/channel name on conflict instead of throwing an exception
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix users overview *Last login* sort in admin
|
||||
* More robust *move to object storage* job failure
|
||||
* Fix comment add avatar with a unauthenticated user
|
||||
* Fix fetching unlisted video in client
|
||||
* Fix comments/download enabled attributes when importing a video
|
||||
* Fix total instance views stats
|
||||
* Fix HLS player infinite buffering on seek
|
||||
* Reset table pagination on search
|
||||
* *Host* search filter can also search into channels and playlists in global search
|
||||
* Fix *My videos* invalid counter
|
||||
* Prevent error on highlighted thread
|
||||
* Fix *Jobs*, *Account blocklist* and *Server blocklist* hidden columns on Safari
|
||||
* Fix live stream max bitrate
|
||||
* Fix incompatibility with OpenSSL 3
|
||||
* Don't crash on redis connection error
|
||||
* Transcoding:
|
||||
* Fix failed transcoding with a mp3 file that contains a cover image
|
||||
* Prevent duplicated HLS playlist when running transcoding
|
||||
* Regenerate video file names when running transcoding manually
|
||||
* Prevent job failures resulting in broken videos on concurrent transcoding
|
||||
* Fix transcoding of videos with quad audio channels
|
||||
* ActivityPub
|
||||
* Fix random invalid HTTP signature generation
|
||||
* Use unique AP id for *Accept*/*Reject* activities
|
||||
* Correctly handle remote actors that don't have follow counters
|
||||
* Correctly handle unknown remote actor image size
|
||||
* Add years in graph legend when grouping video views stats by month
|
||||
* Prevent creating multiple lives when clicking multiple times on the "Go Live" button
|
||||
* Fix *undefined" resolution in player *Stats for nerds*
|
||||
* Fix not displayed error message in administrator web config
|
||||
* More robust S3 upload [#5231](https://github.com/Chocobozzz/PeerTube/pull/5231)
|
||||
* Fix broken saved live stream with only one resolution
|
||||
* Fix `removeEventListener` player embed api
|
||||
* Progressively cleanup actor images without width from the database
|
||||
* Fix broken dates on localized pages
|
||||
* Prevent job queue to be started before plugins
|
||||
* Fix old database enum names
|
||||
* Don't display remove file icon in admin videos overviews if we can't delete the file
|
||||
|
||||
|
||||
## v4.2.2
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*",
|
||||
"node_modules/"
|
||||
"node_modules/",
|
||||
"src/standalone/player/dist"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
@ -82,7 +82,7 @@ module.exports = {
|
|||
{
|
||||
browserName: 'Chrome',
|
||||
|
||||
...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S6', '5.0')
|
||||
...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S8', '7.0')
|
||||
},
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "4.2.2",
|
||||
"version": "4.3.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
@ -27,11 +27,11 @@
|
|||
"typings": "*.d.ts",
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^14.0.1",
|
||||
"@angular-eslint/builder": "14.0.2",
|
||||
"@angular-eslint/eslint-plugin": "14.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "14.0.2",
|
||||
"@angular-eslint/schematics": "14.0.2",
|
||||
"@angular-eslint/template-parser": "14.0.2",
|
||||
"@angular-eslint/builder": "^14.0.2",
|
||||
"@angular-eslint/eslint-plugin": "^14.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "^14.0.2",
|
||||
"@angular-eslint/schematics": "^14.0.2",
|
||||
"@angular-eslint/template-parser": "^14.0.2",
|
||||
"@angular/animations": "^14.0.1",
|
||||
"@angular/cdk": "^14.0.1",
|
||||
"@angular/cli": "^14.0.1",
|
||||
|
@ -47,7 +47,7 @@
|
|||
"@angular/service-worker": "^14.0.1",
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^12.1.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^13.0.0",
|
||||
"@ng-select/ng-select": "^9.0.1",
|
||||
"@ngx-loading-bar/core": "^6.0.0",
|
||||
"@ngx-loading-bar/http-client": "^6.0.0",
|
||||
|
@ -69,8 +69,8 @@
|
|||
"@types/sha.js": "^2.4.0",
|
||||
"@types/video.js": "^7.3.40",
|
||||
"@types/webtorrent": "^0.109.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.31.0",
|
||||
"@typescript-eslint/parser": "5.31.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||
"@typescript-eslint/parser": "^5.31.0",
|
||||
"@wdio/browserstack-service": "^7.20.2",
|
||||
"@wdio/cli": "^7.20.2",
|
||||
"@wdio/local-runner": "^7.20.2",
|
||||
|
@ -84,7 +84,7 @@
|
|||
"cache-chunk-store": "^3.0.0",
|
||||
"chart.js": "^3.8.0",
|
||||
"chartjs-plugin-zoom": "^1.2.1",
|
||||
"chromedriver": "^103.0.0",
|
||||
"chromedriver": "^105.0.0",
|
||||
"core-js": "^3.22.8",
|
||||
"css-loader": "^6.2.0",
|
||||
"debug": "^4.3.1",
|
||||
|
@ -96,7 +96,7 @@
|
|||
"expect-webdriverio": "^3.4.0",
|
||||
"focus-visible": "^5.0.2",
|
||||
"geckodriver": "^3.0.1",
|
||||
"hls.js": "1.2.0",
|
||||
"hls.js": "1.2.2",
|
||||
"html-loader": "^4.1.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
|
@ -128,7 +128,7 @@
|
|||
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||
"ts-loader": "^9.3.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "~4.7.3",
|
||||
"typescript": "~4.8.3",
|
||||
"url": "^0.11.0",
|
||||
"video.js": "^7.19.2",
|
||||
"videostream": "~3.2.1",
|
||||
|
@ -137,7 +137,7 @@
|
|||
"webpack": "^5.73.0",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webtorrent": "^1.8.22",
|
||||
"webtorrent": "1.8.26",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
|
|
|
@ -49,6 +49,7 @@ import {
|
|||
PluginSearchComponent,
|
||||
PluginShowInstalledComponent
|
||||
} from './plugins'
|
||||
import { SharedAdminModule } from './shared'
|
||||
import { JobService, LogsComponent, LogsService } from './system'
|
||||
import { DebugComponent, DebugService } from './system/debug'
|
||||
import { JobsComponent } from './system/jobs/jobs.component'
|
||||
|
@ -69,6 +70,7 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
|||
SharedVideoMiniatureModule,
|
||||
SharedTablesModule,
|
||||
SharedUsersModule,
|
||||
SharedAdminModule,
|
||||
|
||||
TableModule,
|
||||
ChartModule
|
||||
|
|
|
@ -218,6 +218,8 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||
|
||||
<div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
|
||||
</div>
|
||||
|
||||
|
@ -285,6 +287,22 @@
|
|||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoChannelSynchronization">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importSynchronizationEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow channel synchronization with channel of other platforms like YouTube (requires allowing import with HTTP URL)"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
⛔ You need to allow import with HTTP URL to be able to activate this feature.
|
||||
</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="autoBlacklist">
|
||||
|
|
|
@ -25,11 +25,12 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
private configService: ConfigService,
|
||||
private menuService: MenuService,
|
||||
private themeService: ThemeService
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildLandingPageOptions()
|
||||
this.checkSignupField()
|
||||
this.checkImportSyncField()
|
||||
|
||||
this.availableThemes = this.themeService.buildAvailableThemes()
|
||||
}
|
||||
|
@ -59,6 +60,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
return !!enabled.find((e: string) => e === algorithm)
|
||||
}
|
||||
|
||||
getUserVideoQuota () {
|
||||
return this.form.value['user']['videoQuota']
|
||||
}
|
||||
|
||||
isSignupEnabled () {
|
||||
return this.form.value['signup']['enabled'] === true
|
||||
}
|
||||
|
@ -67,6 +72,14 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
||||
}
|
||||
|
||||
isImportVideosHttpEnabled (): boolean {
|
||||
return this.form.value['import']['videos']['http']['enabled'] === true
|
||||
}
|
||||
|
||||
importSynchronizationChecked () {
|
||||
return this.isImportVideosHttpEnabled() && this.form.value['import']['videoChannelSynchronization']['enabled']
|
||||
}
|
||||
|
||||
hasUnlimitedSignup () {
|
||||
return this.form.value['signup']['limit'] === -1
|
||||
}
|
||||
|
@ -97,6 +110,21 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
return this.themeService.getDefaultThemeLabel()
|
||||
}
|
||||
|
||||
private checkImportSyncField () {
|
||||
const importSyncControl = this.form.get('import.videoChannelSynchronization.enabled')
|
||||
const importVideosHttpControl = this.form.get('import.videos.http.enabled')
|
||||
|
||||
importVideosHttpControl.valueChanges
|
||||
.subscribe((httpImportEnabled) => {
|
||||
importSyncControl.setValue(httpImportEnabled && importSyncControl.value)
|
||||
if (httpImportEnabled) {
|
||||
importSyncControl.enable()
|
||||
} else {
|
||||
importSyncControl.disable()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private checkSignupField () {
|
||||
const signupControl = this.form.get('signup.enabled')
|
||||
|
||||
|
|
|
@ -36,6 +36,10 @@ input[type=number] {
|
|||
position: absolute;
|
||||
top: 0.2em;
|
||||
right: 2.5rem;
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
|
@ -146,3 +150,9 @@ ngb-tabset:not(.previews) ::ng-deep {
|
|||
padding: 0 .3em;
|
||||
}
|
||||
}
|
||||
|
||||
my-user-real-quota-info {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
|
|
@ -144,6 +144,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
torrent: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoChannelSynchronization: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
|
@ -289,6 +292,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
formValidated () {
|
||||
this.forceCheck()
|
||||
if (!this.form.valid) return
|
||||
|
||||
const value: ComponentCustomConfig = this.form.getRawValue()
|
||||
|
||||
forkJoin([
|
||||
|
@ -378,8 +384,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
||||
|
||||
this.updateForm()
|
||||
// Force form validation
|
||||
this.forceCheck()
|
||||
this.markAllAsDirty()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
</ng-container>
|
||||
|
||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||
<label i18n>Resolutions to generate per enabled format</label>
|
||||
<label i18n>Resolutions to generate</label>
|
||||
|
||||
<div class="ms-2 d-flex flex-column">
|
||||
<my-peertube-checkbox
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h1>
|
||||
<my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Your instance subscriptions</ng-container>
|
||||
<ng-container i18n>Subscriptions of your instance</ng-container>
|
||||
</h1>
|
||||
|
||||
<p-table
|
||||
|
@ -45,8 +45,8 @@
|
|||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pSelectableRow="follow" pTemplate="body" let-follow>
|
||||
<tr>
|
||||
<ng-template pTemplate="body" let-follow>
|
||||
<tr pSelectableRow="follow">
|
||||
<td class="checkbox-cell">
|
||||
<p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
||||
</td>
|
||||
|
|
|
@ -145,15 +145,15 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
getVideoEmbed (entry: VideoBlacklist) {
|
||||
return buildVideoOrPlaylistEmbed(
|
||||
decorateVideoLink({
|
||||
return buildVideoOrPlaylistEmbed({
|
||||
embedUrl: decorateVideoLink({
|
||||
url: buildVideoEmbedLink(entry.video, environment.originServerUrl),
|
||||
|
||||
title: false,
|
||||
warningTitle: false
|
||||
}),
|
||||
entry.video.name
|
||||
)
|
||||
embedTitle: entry.video.name
|
||||
})
|
||||
}
|
||||
|
||||
protected reloadData () {
|
||||
|
|
|
@ -54,6 +54,10 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
|
|||
{
|
||||
value: 'local:false',
|
||||
label: $localize`Remote comments`
|
||||
},
|
||||
{
|
||||
value: 'localVideo:true',
|
||||
label: $localize`Comments on local videos`
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -152,10 +152,7 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
|
||||
Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
|
||||
At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
|
||||
</div>
|
||||
<my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||
|
||||
<div *ngIf="formErrors.videoQuota" class="form-error">
|
||||
{{ formErrors.videoQuota }}
|
||||
|
|
|
@ -41,7 +41,8 @@ button {
|
|||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.transcoding-information {
|
||||
my-user-real-quota-info {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ConfigService } from '@app/+admin/config/shared/config.service'
|
|||
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||
import { FormReactive } from '@app/shared/shared-forms'
|
||||
import { USER_ROLE_LABELS } from '@shared/core-utils/users'
|
||||
import { HTMLServerConfig, UserAdminFlag, UserRole, VideoResolution } from '@shared/models'
|
||||
import { HTMLServerConfig, UserAdminFlag, UserRole } from '@shared/models'
|
||||
import { SelectOptionsItem } from '../../../../../types/select-options-item.model'
|
||||
|
||||
@Directive()
|
||||
|
@ -60,33 +60,14 @@ export abstract class UserEdit extends FormReactive implements OnInit {
|
|||
]
|
||||
}
|
||||
|
||||
isTranscodingInformationDisplayed () {
|
||||
const formVideoQuota = parseInt(this.form.value['videoQuota'], 10)
|
||||
|
||||
return this.serverConfig.transcoding.enabledResolutions.length !== 0 &&
|
||||
formVideoQuota > 0
|
||||
}
|
||||
|
||||
computeQuotaWithTranscoding () {
|
||||
const transcodingConfig = this.serverConfig.transcoding
|
||||
|
||||
const resolutions = transcodingConfig.enabledResolutions
|
||||
const higherResolution = VideoResolution.H_4K
|
||||
let multiplier = 0
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
multiplier += resolution / higherResolution
|
||||
}
|
||||
|
||||
if (transcodingConfig.hls.enabled) multiplier *= 2
|
||||
|
||||
return multiplier * parseInt(this.form.value['videoQuota'], 10)
|
||||
}
|
||||
|
||||
resetPassword () {
|
||||
return
|
||||
}
|
||||
|
||||
getUserVideoQuota () {
|
||||
return this.form.value['videoQuota']
|
||||
}
|
||||
|
||||
protected buildAdminFlags (formValue: any) {
|
||||
return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
||||
|
||||
<my-global-icon
|
||||
*ngIf="canRemoveOneFile(video)"
|
||||
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
||||
(click)="removeVideoFile(video, file, 'webtorrent')"
|
||||
></my-global-icon>
|
||||
|
@ -124,6 +125,7 @@
|
|||
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
||||
|
||||
<my-global-icon
|
||||
*ngIf="canRemoveOneFile(video)"
|
||||
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
||||
(click)="removeVideoFile(video, file, 'hls')"
|
||||
></my-global-icon>
|
||||
|
|
|
@ -166,6 +166,10 @@ export class VideoListComponent extends RestTable implements OnInit {
|
|||
return video.files.length !== 0
|
||||
}
|
||||
|
||||
canRemoveOneFile (video: Video) {
|
||||
return video.canRemoveOneFile(this.authUser)
|
||||
}
|
||||
|
||||
getFilesSize (video: Video) {
|
||||
let files = video.files
|
||||
|
||||
|
|
|
@ -15,10 +15,15 @@
|
|||
|
||||
<my-button
|
||||
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
|
||||
[label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)" [responsiveLabel]="true"
|
||||
[attr.disabled]="isUpdating(plugin) || isUninstalling(plugin)"
|
||||
[label]="getUpdateLabel(plugin)" icon="refresh" [responsiveLabel]="true"
|
||||
></my-button>
|
||||
|
||||
<my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label [responsiveLabel]="true"></my-delete-button>
|
||||
<my-delete-button
|
||||
(click)="uninstall(plugin)"
|
||||
label="Uninstall" i18n-label [responsiveLabel]="true"
|
||||
[disabled]="isUpdating(plugin) || isUninstalling(plugin)"
|
||||
></my-delete-button>
|
||||
</div>
|
||||
</my-plugin-card>
|
||||
</ng-container>
|
||||
|
|
|
@ -24,6 +24,7 @@ export class PluginListInstalledComponent implements OnInit {
|
|||
|
||||
plugins: PeerTubePlugin[] = []
|
||||
updating: { [name: string]: boolean } = {}
|
||||
uninstalling: { [name: string]: boolean } = {}
|
||||
|
||||
onDataSubject = new Subject<any[]>()
|
||||
|
||||
|
@ -99,7 +100,11 @@ export class PluginListInstalledComponent implements OnInit {
|
|||
}
|
||||
|
||||
isUpdating (plugin: PeerTubePlugin) {
|
||||
return !!this.updating[this.getUpdatingKey(plugin)]
|
||||
return !!this.updating[this.getPluginKey(plugin)]
|
||||
}
|
||||
|
||||
isUninstalling (plugin: PeerTubePlugin) {
|
||||
return !!this.uninstall[this.getPluginKey(plugin)]
|
||||
}
|
||||
|
||||
isTheme (plugin: PeerTubePlugin) {
|
||||
|
@ -107,12 +112,17 @@ export class PluginListInstalledComponent implements OnInit {
|
|||
}
|
||||
|
||||
async uninstall (plugin: PeerTubePlugin) {
|
||||
const pluginKey = this.getPluginKey(plugin)
|
||||
if (this.uninstalling[pluginKey]) return
|
||||
|
||||
const res = await this.confirmService.confirm(
|
||||
$localize`Do you really want to uninstall ${plugin.name}?`,
|
||||
$localize`Uninstall`
|
||||
)
|
||||
if (res === false) return
|
||||
|
||||
this.uninstalling[pluginKey] = true
|
||||
|
||||
this.pluginApiService.uninstall(plugin.name, plugin.type)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
|
@ -120,15 +130,20 @@ export class PluginListInstalledComponent implements OnInit {
|
|||
|
||||
this.plugins = this.plugins.filter(p => p.name !== plugin.name)
|
||||
this.pagination.totalItems--
|
||||
|
||||
this.uninstalling[pluginKey] = false
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
error: err => {
|
||||
this.notifier.error(err.message)
|
||||
this.uninstalling[pluginKey] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async update (plugin: PeerTubePlugin) {
|
||||
const updatingKey = this.getUpdatingKey(plugin)
|
||||
if (this.updating[updatingKey]) return
|
||||
const pluginKey = this.getPluginKey(plugin)
|
||||
if (this.updating[pluginKey]) return
|
||||
|
||||
if (this.isMajorUpgrade(plugin)) {
|
||||
const res = await this.confirmService.confirm(
|
||||
|
@ -140,20 +155,23 @@ export class PluginListInstalledComponent implements OnInit {
|
|||
if (res === false) return
|
||||
}
|
||||
|
||||
this.updating[updatingKey] = true
|
||||
this.updating[pluginKey] = true
|
||||
|
||||
this.pluginApiService.update(plugin.name, plugin.type)
|
||||
.pipe()
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.updating[updatingKey] = false
|
||||
this.updating[pluginKey] = false
|
||||
|
||||
this.notifier.success($localize`${plugin.name} updated.`)
|
||||
|
||||
Object.assign(plugin, res)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
error: err => {
|
||||
this.notifier.error(err.message)
|
||||
this.updating[pluginKey] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -165,7 +183,7 @@ export class PluginListInstalledComponent implements OnInit {
|
|||
return this.pluginApiService.getPluginOrThemeHref(this.pluginType, name)
|
||||
}
|
||||
|
||||
private getUpdatingKey (plugin: PeerTubePlugin) {
|
||||
private getPluginKey (plugin: PeerTubePlugin) {
|
||||
return plugin.name + plugin.type
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
></my-edit-button>
|
||||
|
||||
<my-button
|
||||
class="update-button" *ngIf="plugin.installed === false" (click)="install(plugin)"
|
||||
*ngIf="plugin.installed === false" (click)="install(plugin)"
|
||||
[loading]="isInstalling(plugin)" label="Install" [responsiveLabel]="true"
|
||||
icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
|
||||
></my-button>
|
||||
|
|
|
@ -145,7 +145,11 @@ export class PluginSearchComponent implements OnInit {
|
|||
plugin.installed = true
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
error: err => {
|
||||
this.installing[plugin.npmName] = false
|
||||
|
||||
this.notifier.error(err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
3
client/src/app/+admin/shared/index.ts
Normal file
3
client/src/app/+admin/shared/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './user-real-quota-info.component'
|
||||
|
||||
export * from './shared-admin.module'
|
20
client/src/app/+admin/shared/shared-admin.module.ts
Normal file
20
client/src/app/+admin/shared/shared-admin.module.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
|
||||
import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedMainModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
UserRealQuotaInfoComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
UserRealQuotaInfoComponent
|
||||
],
|
||||
|
||||
providers: []
|
||||
})
|
||||
export class SharedAdminModule { }
|
|
@ -0,0 +1,4 @@
|
|||
<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>
|
|
@ -0,0 +1,2 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
|
@ -0,0 +1,44 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { ServerService } from '@app/core'
|
||||
import { HTMLServerConfig, VideoResolution } from '@shared/models/index'
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-real-quota-info',
|
||||
templateUrl: './user-real-quota-info.component.html',
|
||||
styleUrls: [ './user-real-quota-info.component.scss' ]
|
||||
})
|
||||
export class UserRealQuotaInfoComponent implements OnInit {
|
||||
@Input() videoQuota: number | string
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
constructor (private server: ServerService) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.serverConfig = this.server.getHTMLConfig()
|
||||
}
|
||||
|
||||
isTranscodingInformationDisplayed () {
|
||||
return this.serverConfig.transcoding.enabledResolutions.length !== 0 && this.getQuotaAsNumber() > 0
|
||||
}
|
||||
|
||||
computeQuotaWithTranscoding () {
|
||||
const transcodingConfig = this.serverConfig.transcoding
|
||||
|
||||
const resolutions = transcodingConfig.enabledResolutions
|
||||
const higherResolution = VideoResolution.H_4K
|
||||
let multiplier = 0
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
multiplier += resolution / higherResolution
|
||||
}
|
||||
|
||||
if (transcodingConfig.hls.enabled) multiplier *= 2
|
||||
|
||||
return multiplier * this.getQuotaAsNumber()
|
||||
}
|
||||
|
||||
private getQuotaAsNumber () {
|
||||
return parseInt(this.videoQuota + '', 10)
|
||||
}
|
||||
}
|
|
@ -34,9 +34,7 @@ export class JobService {
|
|||
|
||||
return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL + `/${jobState || ''}`, { params })
|
||||
.pipe(
|
||||
map(res => {
|
||||
return this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ])
|
||||
}),
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ], 'precise')),
|
||||
map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData)),
|
||||
map(res => this.restExtractor.applyToResultListData(res, this.buildUniqId)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
<ng-container *ngIf="hasProgress(job)">{{ getProgress(job) }}</ng-container>
|
||||
</td>
|
||||
|
||||
<td class="job-date c-hand" [pRowToggler]="job">{{ job.createdAt | date: 'short' }}</td>
|
||||
<td class="job-date c-hand" [pRowToggler]="job">{{ job.createdAt }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
|
|
|
@ -23,22 +23,28 @@ export class JobsComponent extends RestTable implements OnInit {
|
|||
jobTypes: JobTypeClient[] = [
|
||||
'all',
|
||||
|
||||
'activitypub-cleaner',
|
||||
'activitypub-follow',
|
||||
'activitypub-http-broadcast',
|
||||
'activitypub-http-broadcast-parallel',
|
||||
'activitypub-http-broadcast',
|
||||
'activitypub-http-fetcher',
|
||||
'activitypub-http-unicast',
|
||||
'activitypub-refresher',
|
||||
'activitypub-cleaner',
|
||||
'actor-keys',
|
||||
'after-video-channel-import',
|
||||
'email',
|
||||
'federate-video',
|
||||
'manage-video-torrent',
|
||||
'move-to-object-storage',
|
||||
'notify',
|
||||
'video-channel-import',
|
||||
'video-file-import',
|
||||
'video-import',
|
||||
'video-live-ending',
|
||||
'video-redundancy',
|
||||
'video-studio-edition',
|
||||
'video-transcoding',
|
||||
'videos-views-stats',
|
||||
'move-to-object-storage'
|
||||
'videos-views-stats'
|
||||
]
|
||||
|
||||
jobs: Job[] = []
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="support">Support</label>
|
||||
<label i18n for="support">Support</label>
|
||||
<my-help
|
||||
helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
|
||||
When a video is uploaded in this channel, the video support field will be automatically filled by this text."
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<strong>{{ user.pendingEmail }}</strong> is awaiting email verification
|
||||
</div>
|
||||
|
||||
<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form" *ngIf="user.pluginAuth === null">
|
||||
<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="new-email">Change your email</label>
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5"> <!-- email grid -->
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
<h1>
|
||||
<span>
|
||||
<my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My channels</ng-container>
|
||||
<span *ngIf="totalItems" class="pt-badge badge-secondary">{{ totalItems }}</span>
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<a routerLink="/my-library/video-channel-syncs" class="button-link">
|
||||
<my-global-icon iconName="repeat" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My synchronizations</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<my-channels-setup-message [hideLink]="true"></my-channels-setup-message>
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
h1 my-global-icon {
|
||||
h1 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
my-global-icon {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
@include peertube-button-link;
|
||||
@include grey-button;
|
||||
@include button-with-icon(18px, 3px, -1px);
|
||||
}
|
||||
}
|
||||
|
||||
.create-button {
|
||||
|
|
|
@ -6,6 +6,8 @@ import { MySubscriptionsComponent } from './my-follows/my-subscriptions.componen
|
|||
import { MyHistoryComponent } from './my-history/my-history.component'
|
||||
import { MyLibraryComponent } from './my-library.component'
|
||||
import { MyOwnershipComponent } from './my-ownership/my-ownership.component'
|
||||
import { MyVideoChannelSyncsComponent } from './my-video-channel-syncs/my-video-channel-syncs.component'
|
||||
import { VideoChannelSyncEditComponent } from './my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component'
|
||||
import { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component'
|
||||
import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
|
||||
import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component'
|
||||
|
@ -131,6 +133,26 @@ const myLibraryRoutes: Routes = [
|
|||
key: 'my-videos-history-list'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channel-syncs',
|
||||
component: MyVideoChannelSyncsComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`My synchronizations`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channel-syncs/create',
|
||||
component: VideoChannelSyncEditComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Create new synchronization`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-pl
|
|||
import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component'
|
||||
import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
|
||||
import { MyVideosComponent } from './my-videos/my-videos.component'
|
||||
import { MyVideoChannelSyncsComponent } from './my-video-channel-syncs/my-video-channel-syncs.component'
|
||||
import { VideoChannelSyncEditComponent } from './my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -63,6 +65,8 @@ import { MyVideosComponent } from './my-videos/my-videos.component'
|
|||
MyOwnershipComponent,
|
||||
MyAcceptOwnershipComponent,
|
||||
MyVideoImportsComponent,
|
||||
MyVideoChannelSyncsComponent,
|
||||
VideoChannelSyncEditComponent,
|
||||
MySubscriptionsComponent,
|
||||
MyFollowersComponent,
|
||||
MyHistoryComponent,
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<h1>
|
||||
<my-global-icon iconName="refresh" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My synchronizations</ng-container>
|
||||
</h1>
|
||||
|
||||
<div *ngIf="!syncEnabled()">
|
||||
<p class="muted" i18n>⚠️ The instance doesn't allow channel synchronization</p>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
*ngIf="syncEnabled()" [value]="channelSyncs" [lazy]="true"
|
||||
[paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
|
||||
[first]="pagination.start" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} synchronizations"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="left-buttons">
|
||||
<a class="add-sync" routerLink="{{ getSyncCreateLink() }}">
|
||||
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Add synchronization</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 10%"></th>
|
||||
<th style="width: 25%" i18n pSortableColumn="externalChannelUrl">External Channel <p-sortIcon field="externalChannelUrl"></p-sortIcon></th>
|
||||
<th style="width: 25%" i18n pSortableColumn="videoChannel">Channel <p-sortIcon field="videoChannel"></p-sortIcon></th>
|
||||
<th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-videoChannelSync>
|
||||
<tr>
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown
|
||||
container="body"
|
||||
[actions]="videoChannelSyncActions" [entry]="videoChannelSync"
|
||||
></my-action-dropdown>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a [href]="videoChannelSync.externalChannelUrl" target="_blank" rel="noopener noreferrer">{{ videoChannelSync.externalChannelUrl }}</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="actor">
|
||||
<my-actor-avatar
|
||||
class="channel"
|
||||
[actor]="videoChannelSync.channel" actorType="channel"
|
||||
[internalHref]="[ '/c', videoChannelSync.channel.name ]"
|
||||
size="25"
|
||||
></my-actor-avatar>
|
||||
|
||||
<div class="actor-info">
|
||||
<a [routerLink]="[ '/c', videoChannelSync.channel.name ]" class="actor-names" i18n-title title="Channel page">
|
||||
<div class="actor-display-name">{{ videoChannelSync.channel.displayName }}</div>
|
||||
<div class="actor-name">{{ videoChannelSync.channel.name }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span [ngClass]="getSyncStateClass(videoChannelSync.state.id)">
|
||||
{{ videoChannelSync.state.label }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>{{ videoChannelSync.createdAt | date: 'short' }}</td>
|
||||
<td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td>
|
||||
|
||||
<td>
|
||||
<a i18n routerLink="/my-library/video-imports" [queryParams]="{ search: 'videoChannelSyncId:' + videoChannelSync.id }" class="peertube-button-link grey-button">
|
||||
List imports
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
|
@ -0,0 +1,14 @@
|
|||
@use '_mixins' as *;
|
||||
@use '_variables' as *;
|
||||
@use '_actor' as *;
|
||||
|
||||
.add-sync {
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
.actor {
|
||||
@include actor-row($min-height: auto, $separator: true);
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { AuthService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
|
||||
import { DropdownAction, VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main'
|
||||
import { HTMLServerConfig } from '@shared/models/server'
|
||||
import { VideoChannelSync, VideoChannelSyncState } from '@shared/models/videos'
|
||||
import { SortMeta } from 'primeng/api'
|
||||
import { mergeMap } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
templateUrl: './my-video-channel-syncs.component.html',
|
||||
styleUrls: [ './my-video-channel-syncs.component.scss' ]
|
||||
})
|
||||
export class MyVideoChannelSyncsComponent extends RestTable implements OnInit {
|
||||
error: string
|
||||
|
||||
channelSyncs: VideoChannelSync[] = []
|
||||
totalRecords = 0
|
||||
|
||||
videoChannelSyncActions: DropdownAction<VideoChannelSync>[][] = []
|
||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
private static STATE_CLASS_BY_ID = {
|
||||
[VideoChannelSyncState.FAILED]: 'badge-red',
|
||||
[VideoChannelSyncState.PROCESSING]: 'badge-blue',
|
||||
[VideoChannelSyncState.SYNCED]: 'badge-green',
|
||||
[VideoChannelSyncState.WAITING_FIRST_RUN]: 'badge-yellow'
|
||||
}
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
constructor (
|
||||
private videoChannelsSyncService: VideoChannelSyncService,
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier,
|
||||
private authService: AuthService,
|
||||
private videoChannelService: VideoChannelService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
this.initialize()
|
||||
|
||||
this.videoChannelSyncActions = [
|
||||
[
|
||||
{
|
||||
label: $localize`Delete`,
|
||||
iconName: 'delete',
|
||||
handler: videoChannelSync => this.deleteSync(videoChannelSync)
|
||||
},
|
||||
{
|
||||
label: $localize`Fully synchronize the channel`,
|
||||
description: $localize`This fetches any missing videos on the local channel`,
|
||||
iconName: 'refresh',
|
||||
handler: videoChannelSync => this.fullySynchronize(videoChannelSync)
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
protected reloadData () {
|
||||
this.error = undefined
|
||||
|
||||
this.authService.userInformationLoaded
|
||||
.pipe(mergeMap(() => {
|
||||
const user = this.authService.getUser()
|
||||
return this.videoChannelsSyncService.listAccountVideoChannelsSyncs({
|
||||
sort: this.sort,
|
||||
account: user.account,
|
||||
pagination: this.pagination
|
||||
})
|
||||
}))
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.channelSyncs = res.data
|
||||
this.totalRecords = res.total
|
||||
},
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
syncEnabled () {
|
||||
return this.serverConfig.import.videoChannelSynchronization.enabled
|
||||
}
|
||||
|
||||
deleteSync (videoChannelSync: VideoChannelSync) {
|
||||
this.videoChannelsSyncService.deleteSync(videoChannelSync.id)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Synchronization removed successfully for ${videoChannelSync.channel.displayName}.`)
|
||||
this.reloadData()
|
||||
},
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fullySynchronize (videoChannelSync: VideoChannelSync) {
|
||||
this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Full synchronization requested successfully for ${videoChannelSync.channel.displayName}.`)
|
||||
},
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getSyncCreateLink () {
|
||||
return '/my-library/video-channel-syncs/create'
|
||||
}
|
||||
|
||||
getSyncStateClass (stateId: number) {
|
||||
return [ 'pt-badge', MyVideoChannelSyncsComponent.STATE_CLASS_BY_ID[stateId] ]
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'MyVideoChannelsSyncComponent'
|
||||
}
|
||||
|
||||
getChannelUrl (name: string) {
|
||||
return '/c/' + name
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<div class="margin-content">
|
||||
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-4 col-xl-3">
|
||||
<div class="video-channel-sync-title" i18n>NEW SYNCHRONIZATION</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
<div class="form-group">
|
||||
<label i18n for="externalChannelUrl">Remote channel URL</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="externalChannelUrl"
|
||||
i18n-placeholder
|
||||
placeholder="Example: https://youtube.com/channel/UC_fancy_channel"
|
||||
formControlName="externalChannelUrl"
|
||||
[ngClass]="{ 'input-error': formErrors['externalChannelUrl'] }"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors['externalChannelUrl']" class="form-error">
|
||||
{{ formErrors['externalChannelUrl'] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="videoChannel">Video Channel</label>
|
||||
<my-select-channel required [items]="userVideoChannels" formControlName="videoChannel"></my-select-channel>
|
||||
|
||||
<div *ngIf="formErrors['videoChannel']" class="form-error">
|
||||
{{ formErrors['videoChannel'] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="existingVideoStrategy" i18n>Options for existing videos on remote channel:</label>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input type="radio" name="existingVideoStrategy" id="import" value="import" formControlName="existingVideoStrategy" required />
|
||||
<label for="import" i18n>Import all and watch for new publications</label>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input type="radio" name="existingVideoStrategy" id="doNothing" value="nothing" formControlName="existingVideoStrategy" required />
|
||||
<label for="doNothing" i18n>Only watch for new publications</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row"> <!-- submit placement block -->
|
||||
<div class="col-md-7 col-xl-5"></div>
|
||||
<div class="col-md-5 col-xl-5 d-inline-flex">
|
||||
<input type="submit" class="peertube-button orange-button ms-auto" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,17 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
$form-base-input-width: 480px;
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { mergeMap } from 'rxjs'
|
||||
import { SelectChannelItem } from 'src/types'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import { VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
|
||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main'
|
||||
import { VideoChannelSyncCreate } from '@shared/models/videos'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-sync-edit',
|
||||
templateUrl: './video-channel-sync-edit.component.html',
|
||||
styleUrls: [ './video-channel-sync-edit.component.scss' ]
|
||||
})
|
||||
export class VideoChannelSyncEditComponent extends FormReactive implements OnInit {
|
||||
error: string
|
||||
userVideoChannels: SelectChannelItem[] = []
|
||||
existingVideosStrategy: string
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private notifier: Notifier,
|
||||
private videoChannelSyncService: VideoChannelSyncService,
|
||||
private videoChannelService: VideoChannelService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
externalChannelUrl: VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR,
|
||||
videoChannel: null,
|
||||
existingVideoStrategy: null
|
||||
})
|
||||
|
||||
listUserChannelsForSelect(this.authService)
|
||||
.subscribe(channels => this.userVideoChannels = channels)
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return $localize`Create`
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoChannelSyncCreate: VideoChannelSyncCreate = {
|
||||
externalChannelUrl: body.externalChannelUrl,
|
||||
videoChannelId: body.videoChannel
|
||||
}
|
||||
|
||||
const importExistingVideos = body['existingVideoStrategy'] === 'import'
|
||||
|
||||
this.videoChannelSyncService.createSync(videoChannelSyncCreate)
|
||||
.pipe(mergeMap(({ videoChannelSync }) => {
|
||||
return importExistingVideos
|
||||
? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
|
||||
: Promise.resolve(null)
|
||||
}))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Synchronization created successfully.`)
|
||||
this.router.navigate([ '/my-library', 'video-channel-syncs' ])
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,22 @@
|
|||
<h1>
|
||||
<h1 class="d-flex justify-content-between">
|
||||
<span>
|
||||
<my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My imports</ng-container>
|
||||
</span>
|
||||
|
||||
<a routerLink="/my-library/video-channel-syncs" class="button-link">
|
||||
<my-global-icon iconName="repeat" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My synchronizations</ng-container>
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<div class="mb-4 d-flex justify-content-between">
|
||||
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
[value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" dataKey="id"
|
||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
|
|
|
@ -8,3 +8,9 @@ pre {
|
|||
.video-import-error {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
@include peertube-button-link;
|
||||
@include grey-button;
|
||||
@include button-with-icon(18px, 3px, -1px);
|
||||
}
|
||||
|
|
|
@ -33,12 +33,16 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
|||
switch (state) {
|
||||
case VideoImportState.FAILED:
|
||||
return 'badge-red'
|
||||
|
||||
case VideoImportState.REJECTED:
|
||||
return 'badge-banned'
|
||||
|
||||
case VideoImportState.PENDING:
|
||||
return 'badge-yellow'
|
||||
|
||||
case VideoImportState.PROCESSING:
|
||||
return 'badge-blue'
|
||||
|
||||
default:
|
||||
return 'badge-green'
|
||||
}
|
||||
|
@ -87,7 +91,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
protected reloadData () {
|
||||
this.videoImportService.getMyVideoImports(this.pagination, this.sort)
|
||||
this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search)
|
||||
.subscribe({
|
||||
next: resultList => {
|
||||
this.videoImports = resultList.data
|
||||
|
|
|
@ -43,7 +43,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
privacyLabel: false,
|
||||
privacyText: true,
|
||||
state: true,
|
||||
blacklistInfo: true
|
||||
blacklistInfo: true,
|
||||
forceChannelInBy: true
|
||||
}
|
||||
videoDropdownDisplayOptions: VideoActionsDisplayType = {
|
||||
playlist: false,
|
||||
|
|
|
@ -98,7 +98,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
|||
this.search()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.text)
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
|
||||
this.userService.getAnonymousOrLoggedUser()
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
</cdk-step>
|
||||
|
||||
<cdk-step [stepControl]="formStepUser" label="My account" [editable]="!signupSuccess">
|
||||
<cdk-step [stepControl]="formStepUser" i18n-label label="My account" [editable]="!signupSuccess">
|
||||
<my-signup-step-title mascotImageName="account" i18n>
|
||||
<strong>Setup</strong>
|
||||
<div>your account</div>
|
||||
|
|
|
@ -528,7 +528,7 @@ export class VideoStatsComponent implements OnInit {
|
|||
const date = new Date(label)
|
||||
|
||||
if (data.groupInterval.match(/ month?$/)) {
|
||||
return date.toLocaleDateString([], { month: 'numeric' })
|
||||
return date.toLocaleDateString([], { year: '2-digit', month: 'numeric' })
|
||||
}
|
||||
|
||||
if (data.groupInterval.match(/ days?$/)) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<ng-container [formGroup]="form">
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Edit caption</h4>
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<label i18n for="captionFileContent">Caption</label>
|
||||
<textarea
|
||||
id="captionFileContent"
|
||||
formControlName="captionFileContent"
|
||||
class="form-control caption-textarea"
|
||||
[ngClass]="{ 'input-error': formErrors['captionFileContent'] }"
|
||||
#textarea
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<div *ngIf="formErrors.captionFileContent" class="form-error">
|
||||
{{ formErrors.captionFileContent }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
|
||||
(click)="cancel()" (key.enter)="cancel()"
|
||||
>
|
||||
|
||||
<input
|
||||
type="submit" i18n-value value="Edit this caption" class="peertube-button orange-button"
|
||||
[disabled]="!form.valid" (click)="updateCaption()"
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
|
@ -2,28 +2,33 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild }
|
|||
import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
|
||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main'
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HTMLServerConfig, VideoConstant } from '@shared/models'
|
||||
import { ServerService } from '../../../../core'
|
||||
|
||||
/**
|
||||
* https://github.com/valor-software/ngx-bootstrap/issues/3825
|
||||
* https://stackblitz.com/edit/angular-t5dfp7
|
||||
* https://medium.com/@izzatnadiri/how-to-pass-data-to-and-receive-from-ng-bootstrap-modals-916f2ad5d66e
|
||||
*/
|
||||
@Component({
|
||||
selector: 'my-video-caption-edit-modal',
|
||||
styleUrls: [ './video-caption-edit-modal.component.scss' ],
|
||||
templateUrl: './video-caption-edit-modal.component.html'
|
||||
selector: 'my-video-caption-edit-modal-content',
|
||||
styleUrls: [ './video-caption-edit-modal-content.component.scss' ],
|
||||
templateUrl: './video-caption-edit-modal-content.component.html'
|
||||
})
|
||||
|
||||
export class VideoCaptionEditModalComponent extends FormReactive implements OnInit {
|
||||
export class VideoCaptionEditModalContentComponent extends FormReactive implements OnInit {
|
||||
@Input() videoCaption: VideoCaptionWithPathEdit
|
||||
@Input() serverConfig: HTMLServerConfig
|
||||
|
||||
@Output() captionEdited = new EventEmitter<VideoCaptionEdit>()
|
||||
|
||||
@ViewChild('modal', { static: true }) modal: ElementRef
|
||||
@ViewChild('textarea', { static: true }) textarea!: ElementRef
|
||||
|
||||
videoCaptionLanguages: VideoConstant<string>[] = []
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
constructor (
|
||||
protected openedModal: NgbActiveModal,
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private modalService: NgbModal,
|
||||
private videoCaptionService: VideoCaptionService,
|
||||
|
@ -49,11 +54,14 @@ export class VideoCaptionEditModalComponent extends FormReactive implements OnIn
|
|||
this.form.patchValue({
|
||||
captionFileContent: res
|
||||
})
|
||||
this.resetTextarea()
|
||||
})
|
||||
}
|
||||
|
||||
show () {
|
||||
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
|
||||
resetTextarea () {
|
||||
this.textarea.nativeElement.scrollTop = 0
|
||||
this.textarea.nativeElement.selectionStart = 0
|
||||
this.textarea.nativeElement.selectionEnd = 0
|
||||
}
|
||||
|
||||
hide () {
|
|
@ -1,36 +0,0 @@
|
|||
<ng-template #modal>
|
||||
<ng-container [formGroup]="form">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Edit caption</h4>
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<label i18n for="captionFileContent">Caption</label>
|
||||
<textarea
|
||||
id="captionFileContent"
|
||||
formControlName="captionFileContent"
|
||||
class="form-control caption-textarea"
|
||||
[ngClass]="{ 'input-error': formErrors['captionFileContent'] }"
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<div *ngIf="formErrors.captionFileContent" class="form-error">
|
||||
{{ formErrors.captionFileContent }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
|
||||
(click)="cancel()" (key.enter)="cancel()"
|
||||
>
|
||||
|
||||
<input
|
||||
type="submit" i18n-value value="Edit this caption" class="peertube-button orange-button"
|
||||
[disabled]="!form.valid" (click)="updateCaption()"
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
|
@ -147,7 +147,7 @@
|
|||
</ng-template>
|
||||
|
||||
<ng-template ptTemplate="help">
|
||||
<ng-container i18n>If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends.</ng-container>
|
||||
<ng-container i18n>The video may be unplayable during the transcoding process. It's the reason why we prefer to publish publicly the video after transcoding.</ng-container>
|
||||
</ng-template>
|
||||
</my-peertube-checkbox>
|
||||
|
||||
|
@ -185,7 +185,7 @@
|
|||
|
||||
<div i18n class="caption-entry-state">Already uploaded on {{ videoCaption.updatedAt | date }} ✔</div>
|
||||
|
||||
<span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span>
|
||||
<span i18n class="caption-entry-edit" (click)="openEditCaptionModal(videoCaption)">Edit</span>
|
||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
|
||||
</ng-container>
|
||||
|
||||
|
@ -212,13 +212,6 @@
|
|||
|
||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
|
||||
</ng-container>
|
||||
|
||||
<my-video-caption-edit-modal
|
||||
#videoCaptionEditModal
|
||||
[videoCaption]="videoCaption"
|
||||
[serverConfig]="serverConfig"
|
||||
(captionEdited)="onCaptionEdited($event)"
|
||||
></my-video-caption-edit-modal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -314,7 +307,7 @@
|
|||
<div class="col-md-12 col-xl-8">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="previewfile">Video preview</label>
|
||||
<label i18n for="previewfile">Video thumbnail</label>
|
||||
|
||||
<my-preview-upload
|
||||
i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
|
||||
|
|
|
@ -35,10 +35,11 @@ import {
|
|||
} from '@shared/models'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
|
||||
import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
|
||||
import { VideoEditType } from './video-edit.type'
|
||||
import { VideoSource } from '@shared/models/videos/video-source'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
type VideoLanguages = VideoConstant<string> & { group?: string }
|
||||
type PluginField = {
|
||||
|
@ -70,7 +71,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
@Input() liveVideo: LiveVideo
|
||||
|
||||
@ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
|
||||
@ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent
|
||||
|
||||
@Output() formBuilt = new EventEmitter<void>()
|
||||
@Output() pluginFieldsAdded = new EventEmitter<void>()
|
||||
|
@ -128,7 +128,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
private i18nPrimengCalendarService: I18nPrimengCalendarService,
|
||||
private ngZone: NgZone,
|
||||
private hooks: HooksService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
|
||||
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
|
||||
|
@ -286,6 +287,13 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
this.videoCaptionAddModal.show()
|
||||
}
|
||||
|
||||
openEditCaptionModal (videoCaption: VideoCaptionWithPathEdit) {
|
||||
const modalRef = this.modalService.open(VideoCaptionEditModalContentComponent, { centered: true, keyboard: false })
|
||||
modalRef.componentInstance.videoCaption = videoCaption
|
||||
modalRef.componentInstance.serverConfig = this.serverConfig
|
||||
modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this))
|
||||
}
|
||||
|
||||
isSaveReplayEnabled () {
|
||||
return this.serverConfig.live.allowReplay
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
|||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
|
||||
import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
|
||||
import { VideoEditComponent } from './video-edit.component'
|
||||
|
||||
@NgModule({
|
||||
|
@ -22,7 +22,7 @@ import { VideoEditComponent } from './video-edit.component'
|
|||
declarations: [
|
||||
VideoEditComponent,
|
||||
VideoCaptionAddModalComponent,
|
||||
VideoCaptionEditModalComponent
|
||||
VideoCaptionEditModalContentComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<div class="submit-container">
|
||||
<my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
|
||||
(click)="updateSecondStep()"
|
||||
[disabled]="!form.valid"
|
||||
[disabled]="!form.valid || isUpdatingVideo === true"
|
||||
></my-button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -2,11 +2,3 @@
|
|||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.peertube-radio-container {
|
||||
width: 250px;
|
||||
|
||||
.form-group-description {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
firstStepPermanentLive: boolean
|
||||
|
||||
isInUpdateForm = false
|
||||
isUpdatingVideo = false
|
||||
isOrHasGoingLive = false
|
||||
|
||||
liveVideo: LiveVideo
|
||||
|
||||
|
@ -64,6 +66,9 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
}
|
||||
|
||||
goLive () {
|
||||
if (this.isOrHasGoingLive) return
|
||||
this.isOrHasGoingLive = true
|
||||
|
||||
const name = 'Live'
|
||||
|
||||
const video: LiveVideoCreate = {
|
||||
|
@ -115,6 +120,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
async updateSecondStep () {
|
||||
if (!await this.isFormValid()) return
|
||||
|
||||
this.isUpdatingVideo = true
|
||||
|
||||
const video = new VideoEdit()
|
||||
video.patch(this.form.value)
|
||||
video.id = this.videoId
|
||||
|
@ -134,6 +141,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
this.liveVideoService.updateLive(this.videoId, liveVideoUpdate)
|
||||
]).subscribe({
|
||||
next: () => {
|
||||
this.isUpdatingVideo = false
|
||||
|
||||
this.notifier.success($localize`Live published.`)
|
||||
|
||||
this.router.navigateByUrl(Video.buildWatchUrl(video))
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
</my-help>
|
||||
|
||||
<input type="text" id="targetUrl" [(ngModel)]="targetUrl" class="form-control" />
|
||||
|
||||
<div *ngIf="isChannelSyncEnabled()" class="form-group-description" i18n>
|
||||
You can also synchronize a remote channel in <a routerLink="/my-library/video-channel-syncs">your library</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -64,6 +64,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
|
|||
return this.targetUrl?.match(/https?:\/\//)
|
||||
}
|
||||
|
||||
isChannelSyncEnabled () {
|
||||
return this.serverConfig.import.videoChannelSynchronization.enabled
|
||||
}
|
||||
|
||||
importVideo () {
|
||||
this.isImportingVideo = true
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
$width-size: 250px;
|
||||
$width-size: 275px;
|
||||
|
||||
.alert.alert-danger {
|
||||
text-align: center;
|
||||
|
@ -27,7 +27,9 @@ $width-size: 250px;
|
|||
@include peertube-select-container($width-size);
|
||||
}
|
||||
my-select-options ::ng-deep ng-select,
|
||||
my-select-channel ::ng-deep ng-select {
|
||||
my-select-channel ::ng-deep ng-select,
|
||||
.peertube-radio-container,
|
||||
.form-group-description {
|
||||
width: $width-size;
|
||||
|
||||
@media screen and (max-width: $width-size) {
|
||||
|
@ -35,6 +37,10 @@ $width-size: 250px;
|
|||
}
|
||||
}
|
||||
|
||||
.form-group-description {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text($width-size);
|
||||
display: block;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { isIOS } from '@root-helpers/web-browser'
|
|||
import { HttpStatusCode, VideoCreateResult } from '@shared/models'
|
||||
import { UploaderXFormData } from './uploaderx-form-data'
|
||||
import { VideoSend } from './video-send'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-upload',
|
||||
|
@ -56,6 +57,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
|
||||
private alreadyRefreshedToken = false
|
||||
|
||||
private uploadServiceSubscription: Subscription
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
protected loadingBar: LoadingBarService,
|
||||
|
@ -87,7 +90,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
||||
})
|
||||
|
||||
this.resumableUploadService.events
|
||||
this.uploadServiceSubscription = this.resumableUploadService.events
|
||||
.subscribe(state => this.onUploadVideoOngoing(state))
|
||||
}
|
||||
|
||||
|
@ -96,7 +99,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.cancelUpload()
|
||||
this.resumableUploadService.disconnect()
|
||||
|
||||
if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
|
|
|
@ -148,7 +148,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
|
|||
error: err => {
|
||||
this.addingComment = false
|
||||
|
||||
this.notifier.error(err.text)
|
||||
this.notifier.error(err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -628,6 +628,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
: null,
|
||||
authorizationHeader: this.authService.getRequestHeaderValue(),
|
||||
|
||||
metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
|
||||
|
||||
embedUrl: video.embedUrl,
|
||||
embedTitle: video.name,
|
||||
instanceName: this.serverConfig.instance.name,
|
||||
|
|
|
@ -97,7 +97,7 @@ export class AuthService {
|
|||
let errorMessage = err.message
|
||||
|
||||
if (err.status === HttpStatusCode.FORBIDDEN_403) {
|
||||
errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.text}.
|
||||
errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.message}.
|
||||
Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.`
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ScreenService } from '../wrappers'
|
|||
|
||||
export type MenuLink = {
|
||||
icon: GlobalIconName
|
||||
iconClass?: string
|
||||
|
||||
label: string
|
||||
// Used by the left menu for example
|
||||
|
@ -70,6 +71,14 @@ export class MenuService {
|
|||
let links: MenuLink[] = []
|
||||
|
||||
if (userCanSeeVideosLink) {
|
||||
links.push({
|
||||
path: '/my-library/video-channels',
|
||||
icon: 'channel' as GlobalIconName,
|
||||
iconClass: 'channel-icon',
|
||||
shortLabel: $localize`Channels`,
|
||||
label: $localize`My channels`
|
||||
})
|
||||
|
||||
links.push({
|
||||
path: '/my-library/videos',
|
||||
icon: 'videos' as GlobalIconName,
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { throwError as observableThrowError } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { dateToHuman } from '@app/helpers'
|
||||
import { HttpStatusCode, ResultList } from '@shared/models'
|
||||
import { DateFormat, dateToHuman } from '@app/helpers'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { HttpStatusCode, ResultList } from '@shared/models'
|
||||
|
||||
@Injectable()
|
||||
export class RestExtractor {
|
||||
|
||||
constructor (private router: Router) { }
|
||||
constructor (
|
||||
@Inject(LOCALE_ID) private localeId: string,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
applyToResultListData <T, A, U> (
|
||||
result: ResultList<T>,
|
||||
|
@ -23,13 +26,17 @@ export class RestExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
convertResultListDateToHuman <T> (result: ResultList<T>, fieldsToConvert: string[] = [ 'createdAt' ]): ResultList<T> {
|
||||
return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ])
|
||||
convertResultListDateToHuman <T> (
|
||||
result: ResultList<T>,
|
||||
fieldsToConvert: string[] = [ 'createdAt' ],
|
||||
format?: DateFormat
|
||||
): ResultList<T> {
|
||||
return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert, format ])
|
||||
}
|
||||
|
||||
convertDateToHuman (target: any, fieldsToConvert: string[]) {
|
||||
convertDateToHuman (target: any, fieldsToConvert: string[], format?: DateFormat) {
|
||||
fieldsToConvert.forEach(field => {
|
||||
target[field] = dateToHuman(target[field])
|
||||
target[field] = dateToHuman(this.localeId, new Date(target[field]), format)
|
||||
})
|
||||
|
||||
return target
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
|
||||
<my-search-typeahead class="w-100 d-flex justify-content-center"></my-search-typeahead>
|
||||
|
||||
<a class="publish-button" routerLink="/videos/upload">
|
||||
<my-global-icon iconName="upload" aria-hidden="true"></my-global-icon>
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
my-search-typeahead {
|
||||
@include margin-right(80px);
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
@include margin-right(15px);
|
||||
}
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include peertube-button-link;
|
||||
@include orange-button;
|
||||
|
|
|
@ -75,6 +75,7 @@ li.suggestion {
|
|||
|
||||
#typeahead-container {
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
|
||||
input {
|
||||
border: 1px solid pvar(--mainBackgroundColor) !important;
|
||||
|
@ -83,15 +84,15 @@ li.suggestion {
|
|||
transition: box-shadow .3s ease, width .2s ease;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $mobile-view) {
|
||||
@include margin-left(10px);
|
||||
@media screen and (max-width: $small-view) {
|
||||
input {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
flex: 1;
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
input {
|
||||
width: 70px;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
import { DatePipe } from '@angular/common'
|
||||
|
||||
const datePipe = new DatePipe('en')
|
||||
function dateToHuman (date: string) {
|
||||
return datePipe.transform(date, 'medium')
|
||||
let datePipe: DatePipe
|
||||
let intl: Intl.DateTimeFormat
|
||||
|
||||
type DateFormat = 'medium' | 'precise'
|
||||
|
||||
function dateToHuman (localeId: string, date: Date, format: 'medium' | 'precise' = 'medium') {
|
||||
if (!datePipe) {
|
||||
datePipe = new DatePipe(localeId)
|
||||
}
|
||||
|
||||
if (!intl) {
|
||||
intl = new Intl.DateTimeFormat(localeId, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
year: '2-digit',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
fractionalSecondDigits: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (format === 'medium') return datePipe.transform(date, format)
|
||||
if (format === 'precise') return intl.format(date)
|
||||
}
|
||||
|
||||
function durationToString (duration: number) {
|
||||
|
@ -20,6 +41,8 @@ function durationToString (duration: number) {
|
|||
}
|
||||
|
||||
export {
|
||||
DateFormat,
|
||||
|
||||
durationToString,
|
||||
dateToHuman
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ function sortBy (obj: any[], key1: string, key2?: string) {
|
|||
})
|
||||
}
|
||||
|
||||
function intoArray (value: any) {
|
||||
function splitIntoArray (value: any) {
|
||||
if (!value) return undefined
|
||||
if (Array.isArray(value)) return value
|
||||
|
||||
|
@ -42,6 +42,6 @@ export {
|
|||
sortBy,
|
||||
immutableAssign,
|
||||
removeElementFromArray,
|
||||
intoArray,
|
||||
splitIntoArray,
|
||||
toBoolean
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
</a>
|
||||
|
||||
<a class="menu-link" routerLink="/my-library" routerLinkActive="active" #libraryLink (click)="onActiveLinkScrollToAnchor(libraryLink)">
|
||||
<my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
|
||||
<my-global-icon class="channel-icon" iconName="channel" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>My library</ng-container>
|
||||
</a>
|
||||
|
||||
|
@ -111,7 +111,7 @@
|
|||
<div i18n class="block-title">{{ menuSection.title }}</div>
|
||||
|
||||
<a class="menu-link" *ngFor="let link of menuSection.links" [routerLink]="link.path" routerLinkActive="active">
|
||||
<my-global-icon *ngIf="link.icon" [iconName]="link.icon" aria-hidden="true"></my-global-icon>
|
||||
<my-global-icon *ngIf="link.icon" [iconName]="link.icon" [ngClass]="link.iconClass" aria-hidden="true"></my-global-icon>
|
||||
<ng-container>{{ link.shortLabel }}</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -391,26 +391,17 @@ my-actor-avatar {
|
|||
}
|
||||
|
||||
my-global-icon {
|
||||
&[iconName=playlists] {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
||||
.playlist-icon {
|
||||
@include margin-right(16px);
|
||||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
&[iconName=videos] {
|
||||
position: relative;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
&[iconName=channel] {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
&[iconName='sign-out'] {
|
||||
position: relative;
|
||||
right: -2px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
&.channel-icon {
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ export const INSTANCE_NAME_VALIDATOR: BuildFormValidator = {
|
|||
}
|
||||
|
||||
export const INSTANCE_SHORT_DESCRIPTION_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.max(250) ],
|
||||
VALIDATORS: [ Validators.maxLength(250) ],
|
||||
MESSAGES: {
|
||||
max: $localize`Short description should not be longer than 250 characters.`
|
||||
maxlength: $localize`Short description should not be longer than 250 characters.`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,3 +48,16 @@ export const VIDEO_CHANNEL_SUPPORT_VALIDATOR: BuildFormValidator = {
|
|||
maxlength: $localize`Support text cannot be more than 1000 characters long.`
|
||||
}
|
||||
}
|
||||
|
||||
export const VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [
|
||||
Validators.required,
|
||||
Validators.pattern(/^https?:\/\//),
|
||||
Validators.maxLength(1000)
|
||||
],
|
||||
MESSAGES: {
|
||||
required: $localize`Remote channel url is required.`,
|
||||
pattern: $localize`External channel URL must begin with "https://" or "http://"`,
|
||||
maxlength: $localize`External channel URL cannot be more than 1000 characters long`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,6 @@ export class EmbedMarkupComponent implements CustomMarkupComponent, OnInit {
|
|||
? buildVideoEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
|
||||
: buildPlaylistEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
|
||||
|
||||
this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
|
||||
this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed({ embedUrl: link, embedTitle: this.uuid })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { AbstractControl, FormGroup } from '@angular/forms'
|
||||
import { wait } from '@root-helpers/utils'
|
||||
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
||||
import { FormValidatorService } from './form-validator.service'
|
||||
|
@ -44,6 +44,21 @@ export abstract class FormReactive {
|
|||
} while (this.form.status === 'PENDING')
|
||||
}
|
||||
|
||||
protected markAllAsDirty (controlsArg?: { [ key: string ]: AbstractControl }) {
|
||||
const controls = controlsArg || this.form.controls
|
||||
|
||||
for (const key of Object.keys(controls)) {
|
||||
const control = controls[key]
|
||||
|
||||
if (control instanceof FormGroup) {
|
||||
this.markAllAsDirty(control.controls)
|
||||
continue
|
||||
}
|
||||
|
||||
control.markAsDirty()
|
||||
}
|
||||
}
|
||||
|
||||
protected forceCheck () {
|
||||
this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
|
||||
}
|
||||
|
@ -59,7 +74,8 @@ export abstract class FormReactive {
|
|||
this.onStatusChanged(
|
||||
form.controls[field] as FormGroup,
|
||||
formErrors[field] as FormReactiveErrors,
|
||||
validationMessages[field] as FormReactiveValidationMessages
|
||||
validationMessages[field] as FormReactiveValidationMessages,
|
||||
onlyDirty
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -106,6 +106,13 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th i18n class="sub-label" scope="row">Channel synchronization with other platforms (YouTube, Vimeo, ...)</th>
|
||||
<td>
|
||||
<my-feature-boolean [value]="serverConfig.import.videoChannelSynchronization.enabled"></my-feature-boolean>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th i18n class="label" colspan="2">Search</th>
|
||||
</tr>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { from, Observable } from 'rxjs'
|
||||
import { catchError, concatMap, map, toArray } from 'rxjs/operators'
|
||||
import { catchError, concatMap, toArray } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
|
@ -40,10 +40,7 @@ export class InstanceFollowService {
|
|||
if (actorType) params = params.append('actorType', actorType)
|
||||
|
||||
return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/following', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
getFollowers (options: {
|
||||
|
@ -66,10 +63,7 @@ export class InstanceFollowService {
|
|||
if (actorType) params = params.append('actorType', actorType)
|
||||
|
||||
return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/followers', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
follow (hostsOrHandles: string[]) {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<ng-template #templateActionLabel let-action>
|
||||
<my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="item-label">
|
||||
<span i18n>{{ action.label }}</span>
|
||||
<small class="muted" *ngIf="action.description">{{ action.description }}</small>
|
||||
</div>
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
}
|
||||
|
||||
.dropdown-menu {
|
||||
max-width: 75vw;
|
||||
|
||||
.dropdown-header {
|
||||
padding: 0.2rem 1rem;
|
||||
}
|
||||
|
@ -72,3 +74,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 1px;
|
||||
|
||||
> * {
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,18 @@ import { Component, Input, OnInit } from '@angular/core'
|
|||
@Component({
|
||||
selector: 'my-delete-button',
|
||||
template: `
|
||||
<my-button icon="delete" className="grey-button" [label]="label" [title]="title" [responsiveLabel]="responsiveLabel"></my-button>
|
||||
<my-button
|
||||
icon="delete" className="grey-button"
|
||||
[disabled]="disabled" [label]="label" [title]="title"
|
||||
[responsiveLabel]="responsiveLabel"
|
||||
></my-button>
|
||||
`
|
||||
})
|
||||
export class DeleteButtonComponent implements OnInit {
|
||||
@Input() label: string
|
||||
@Input() title: string
|
||||
@Input() responsiveLabel = false
|
||||
@Input() disabled: boolean
|
||||
|
||||
ngOnInit () {
|
||||
if (this.label === undefined && !this.title) {
|
||||
|
|
|
@ -13,3 +13,4 @@ export * from './video'
|
|||
export * from './video-caption'
|
||||
export * from './video-channel'
|
||||
export * from './shared-main.module'
|
||||
export * from './video-channel-sync'
|
||||
|
|
|
@ -6,7 +6,6 @@ import { NgModule } from '@angular/core'
|
|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbButtonsModule,
|
||||
NgbCollapseModule,
|
||||
NgbDropdownModule,
|
||||
NgbModalModule,
|
||||
|
@ -66,7 +65,6 @@ import { VideoChannelService } from './video-channel'
|
|||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
NgbCollapseModule,
|
||||
NgbButtonsModule,
|
||||
|
||||
ClipboardModule,
|
||||
|
||||
|
@ -129,7 +127,6 @@ import { VideoChannelService } from './video-channel'
|
|||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
NgbCollapseModule,
|
||||
NgbButtonsModule,
|
||||
|
||||
ClipboardModule,
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ export class UserNotificationService {
|
|||
|
||||
return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, context })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './video-channel-sync.service'
|
|
@ -0,0 +1,50 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { catchError, Observable } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
import { ResultList } from '@shared/models/common'
|
||||
import { VideoChannelSync, VideoChannelSyncCreate } from '@shared/models/videos'
|
||||
import { Account, AccountService } from '../account'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class VideoChannelSyncService {
|
||||
static BASE_VIDEO_CHANNEL_URL = environment.apiUrl + '/api/v1/video-channel-syncs'
|
||||
|
||||
constructor (
|
||||
private authHttp: HttpClient,
|
||||
private restExtractor: RestExtractor,
|
||||
private restService: RestService
|
||||
) { }
|
||||
|
||||
listAccountVideoChannelsSyncs (parameters: {
|
||||
sort: SortMeta
|
||||
pagination: RestPagination
|
||||
account: Account
|
||||
}): Observable<ResultList<VideoChannelSync>> {
|
||||
const { pagination, sort, account } = parameters
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channel-syncs'
|
||||
|
||||
return this.authHttp.get<ResultList<VideoChannelSync>>(url, { params })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
createSync (body: VideoChannelSyncCreate) {
|
||||
return this.authHttp.post<{ videoChannelSync: VideoChannelSync }>(VideoChannelSyncService.BASE_VIDEO_CHANNEL_URL, body)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
deleteSync (videoChannelsSyncId: number) {
|
||||
const url = `${VideoChannelSyncService.BASE_VIDEO_CHANNEL_URL}/${videoChannelsSyncId}`
|
||||
|
||||
return this.authHttp.delete(url)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
|
@ -3,7 +3,14 @@ import { catchError, map, tap } from 'rxjs/operators'
|
|||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
|
||||
import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
|
||||
import {
|
||||
ActorImage,
|
||||
ResultList,
|
||||
VideoChannel as VideoChannelServer,
|
||||
VideoChannelCreate,
|
||||
VideoChannelUpdate,
|
||||
VideosImportInChannelCreate
|
||||
} from '@shared/models'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
import { Account } from '../account'
|
||||
import { AccountService } from '../account/account.service'
|
||||
|
@ -95,4 +102,16 @@ export class VideoChannelService {
|
|||
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
importVideos (videoChannelName: string, externalChannelUrl: string, syncId?: number) {
|
||||
const path = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/import-videos'
|
||||
|
||||
const body: VideosImportInChannelCreate = {
|
||||
externalChannelUrl,
|
||||
videoChannelSyncId: syncId
|
||||
}
|
||||
|
||||
return this.authHttp.post(path, body)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,15 +20,15 @@ export class EmbedComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
const html = buildVideoOrPlaylistEmbed(
|
||||
decorateVideoLink({
|
||||
const html = buildVideoOrPlaylistEmbed({
|
||||
embedUrl: decorateVideoLink({
|
||||
url: buildVideoEmbedLink(this.video, environment.originServerUrl),
|
||||
|
||||
title: false,
|
||||
warningTitle: false
|
||||
}),
|
||||
this.video.name
|
||||
)
|
||||
embedTitle: this.video.name
|
||||
})
|
||||
|
||||
this.embedHTML = this.sanitizer.bypassSecurityTrustHtml(html)
|
||||
}
|
||||
|
|
|
@ -43,15 +43,27 @@ export class VideoImportService {
|
|||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> {
|
||||
getMyVideoImports (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<VideoImport>> {
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (search) {
|
||||
const filters = this.restService.parseQueryStringFilter(search, {
|
||||
videoChannelSyncId: {
|
||||
prefix: 'videoChannelSyncId:'
|
||||
},
|
||||
targetUrl: {
|
||||
prefix: 'targetUrl:'
|
||||
}
|
||||
})
|
||||
|
||||
params = this.restService.addObjectParams(params, filters)
|
||||
}
|
||||
|
||||
return this.authHttp
|
||||
.get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
|
||||
.pipe(
|
||||
switchMap(res => this.extractVideoImports(res)),
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { Observable } from 'rxjs'
|
||||
import { catchError, map } from 'rxjs/operators'
|
||||
import { catchError } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
|
@ -35,10 +35,7 @@ export class VideoOwnershipService {
|
|||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
return this.authHttp.get<ResultList<VideoChangeOwnership>>(url, { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
acceptOwnership (id: number, input: VideoChangeOwnershipAccept) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { AuthUser } from '@app/core'
|
||||
import { User } from '@app/core/users/user.model'
|
||||
import { durationToString, prepareIcu, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
|
||||
import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl, prepareIcu } from '@app/helpers'
|
||||
import { Actor } from '@app/shared/shared-main/account/actor.model'
|
||||
import { buildVideoWatchPath } from '@shared/core-utils'
|
||||
import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils'
|
||||
import { peertubeTranslate } from '@shared/core-utils/i18n'
|
||||
import {
|
||||
ActorImage,
|
||||
|
@ -240,6 +240,13 @@ export class Video implements VideoServerModel {
|
|||
return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.SEE_ALL_VIDEOS))
|
||||
}
|
||||
|
||||
canRemoveOneFile (user: AuthUser) {
|
||||
return this.isLocal &&
|
||||
user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) &&
|
||||
this.state.id !== VideoState.TO_TRANSCODE &&
|
||||
getAllFiles(this).length > 1
|
||||
}
|
||||
|
||||
canRemoveFiles (user: AuthUser) {
|
||||
return this.isLocal &&
|
||||
user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) &&
|
||||
|
|
|
@ -53,7 +53,6 @@ export class BlocklistService {
|
|||
|
||||
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
|
@ -84,10 +83,7 @@ export class BlocklistService {
|
|||
if (search) params = params.append('search', search)
|
||||
|
||||
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
blockServerByUser (host: string) {
|
||||
|
@ -116,7 +112,6 @@ export class BlocklistService {
|
|||
|
||||
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
|
@ -151,10 +146,7 @@ export class BlocklistService {
|
|||
if (search) params = params.append('search', search)
|
||||
|
||||
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
blockServerByInstance (host: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { from as observableFrom, Observable } from 'rxjs'
|
||||
import { catchError, concatMap, map, toArray } from 'rxjs/operators'
|
||||
import { catchError, concatMap, toArray } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
|
@ -47,10 +47,7 @@ export class VideoBlockService {
|
|||
if (type) params = params.append('type', type.toString())
|
||||
|
||||
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlockService.BASE_VIDEOS_URL + 'blacklist', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
unblockVideo (videoIdArgs: number | number[]) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user