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
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|
||||||
[*.yml]
|
[*.{yml,html}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[{client,server,shared}/**.{ts,json,js}]
|
[{client,server,shared,scripts}/**.{ts,json,js}]
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = space
|
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
|
$ 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**.
|
In dev mode, administrator username is **root** and password is **test**.
|
||||||
|
|
||||||
### Online development
|
### Online development
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -75,6 +75,8 @@ jobs:
|
||||||
- name: Run Test
|
- name: Run Test
|
||||||
# external-plugins tests only run on schedule
|
# external-plugins tests only run on schedule
|
||||||
if: github.event_name == 'schedule' || matrix.test_suite != 'external-plugins'
|
if: github.event_name == 'schedule' || matrix.test_suite != 'external-plugins'
|
||||||
|
env:
|
||||||
|
AKISMET_KEY: ${{ secrets.AKISMET_KEY }}
|
||||||
run: npm run ci -- ${{ matrix.test_suite }}
|
run: npm run ci -- ${{ matrix.test_suite }}
|
||||||
|
|
||||||
- name: Display errors
|
- name: Display errors
|
||||||
|
|
132
CHANGELOG.md
132
CHANGELOG.md
|
@ -1,5 +1,137 @@
|
||||||
# Changelog
|
# 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
|
## v4.2.2
|
||||||
|
|
||||||
### IMPORTANT NOTES
|
### IMPORTANT NOTES
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"root": true,
|
"root": true,
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"projects/**/*",
|
"projects/**/*",
|
||||||
"node_modules/"
|
"node_modules/",
|
||||||
|
"src/standalone/player/dist"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -82,7 +82,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
browserName: 'Chrome',
|
browserName: 'Chrome',
|
||||||
|
|
||||||
...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S6', '5.0')
|
...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S8', '7.0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Safari',
|
browserName: 'Safari',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "peertube-client",
|
"name": "peertube-client",
|
||||||
"version": "4.2.2",
|
"version": "4.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -27,11 +27,11 @@
|
||||||
"typings": "*.d.ts",
|
"typings": "*.d.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^14.0.1",
|
"@angular-devkit/build-angular": "^14.0.1",
|
||||||
"@angular-eslint/builder": "14.0.2",
|
"@angular-eslint/builder": "^14.0.2",
|
||||||
"@angular-eslint/eslint-plugin": "14.0.2",
|
"@angular-eslint/eslint-plugin": "^14.0.2",
|
||||||
"@angular-eslint/eslint-plugin-template": "14.0.2",
|
"@angular-eslint/eslint-plugin-template": "^14.0.2",
|
||||||
"@angular-eslint/schematics": "14.0.2",
|
"@angular-eslint/schematics": "^14.0.2",
|
||||||
"@angular-eslint/template-parser": "14.0.2",
|
"@angular-eslint/template-parser": "^14.0.2",
|
||||||
"@angular/animations": "^14.0.1",
|
"@angular/animations": "^14.0.1",
|
||||||
"@angular/cdk": "^14.0.1",
|
"@angular/cdk": "^14.0.1",
|
||||||
"@angular/cli": "^14.0.1",
|
"@angular/cli": "^14.0.1",
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
"@angular/service-worker": "^14.0.1",
|
"@angular/service-worker": "^14.0.1",
|
||||||
"@babel/core": "^7.18.5",
|
"@babel/core": "^7.18.5",
|
||||||
"@babel/preset-env": "^7.18.2",
|
"@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",
|
"@ng-select/ng-select": "^9.0.1",
|
||||||
"@ngx-loading-bar/core": "^6.0.0",
|
"@ngx-loading-bar/core": "^6.0.0",
|
||||||
"@ngx-loading-bar/http-client": "^6.0.0",
|
"@ngx-loading-bar/http-client": "^6.0.0",
|
||||||
|
@ -69,8 +69,8 @@
|
||||||
"@types/sha.js": "^2.4.0",
|
"@types/sha.js": "^2.4.0",
|
||||||
"@types/video.js": "^7.3.40",
|
"@types/video.js": "^7.3.40",
|
||||||
"@types/webtorrent": "^0.109.0",
|
"@types/webtorrent": "^0.109.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.31.0",
|
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||||
"@typescript-eslint/parser": "5.31.0",
|
"@typescript-eslint/parser": "^5.31.0",
|
||||||
"@wdio/browserstack-service": "^7.20.2",
|
"@wdio/browserstack-service": "^7.20.2",
|
||||||
"@wdio/cli": "^7.20.2",
|
"@wdio/cli": "^7.20.2",
|
||||||
"@wdio/local-runner": "^7.20.2",
|
"@wdio/local-runner": "^7.20.2",
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
"cache-chunk-store": "^3.0.0",
|
"cache-chunk-store": "^3.0.0",
|
||||||
"chart.js": "^3.8.0",
|
"chart.js": "^3.8.0",
|
||||||
"chartjs-plugin-zoom": "^1.2.1",
|
"chartjs-plugin-zoom": "^1.2.1",
|
||||||
"chromedriver": "^103.0.0",
|
"chromedriver": "^105.0.0",
|
||||||
"core-js": "^3.22.8",
|
"core-js": "^3.22.8",
|
||||||
"css-loader": "^6.2.0",
|
"css-loader": "^6.2.0",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
"expect-webdriverio": "^3.4.0",
|
"expect-webdriverio": "^3.4.0",
|
||||||
"focus-visible": "^5.0.2",
|
"focus-visible": "^5.0.2",
|
||||||
"geckodriver": "^3.0.1",
|
"geckodriver": "^3.0.1",
|
||||||
"hls.js": "1.2.0",
|
"hls.js": "1.2.2",
|
||||||
"html-loader": "^4.1.0",
|
"html-loader": "^4.1.0",
|
||||||
"html-webpack-plugin": "^5.3.1",
|
"html-webpack-plugin": "^5.3.1",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
"stylelint-config-sass-guidelines": "^9.0.1",
|
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.0",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "~4.7.3",
|
"typescript": "~4.8.3",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"video.js": "^7.19.2",
|
"video.js": "^7.19.2",
|
||||||
"videostream": "~3.2.1",
|
"videostream": "~3.2.1",
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.73.0",
|
||||||
"webpack-bundle-analyzer": "^4.4.2",
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
"webpack-cli": "^4.10.0",
|
"webpack-cli": "^4.10.0",
|
||||||
"webtorrent": "^1.8.22",
|
"webtorrent": "1.8.26",
|
||||||
"whatwg-fetch": "^3.0.0",
|
"whatwg-fetch": "^3.0.0",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {
|
||||||
PluginSearchComponent,
|
PluginSearchComponent,
|
||||||
PluginShowInstalledComponent
|
PluginShowInstalledComponent
|
||||||
} from './plugins'
|
} from './plugins'
|
||||||
|
import { SharedAdminModule } from './shared'
|
||||||
import { JobService, LogsComponent, LogsService } from './system'
|
import { JobService, LogsComponent, LogsService } from './system'
|
||||||
import { DebugComponent, DebugService } from './system/debug'
|
import { DebugComponent, DebugService } from './system/debug'
|
||||||
import { JobsComponent } from './system/jobs/jobs.component'
|
import { JobsComponent } from './system/jobs/jobs.component'
|
||||||
|
@ -69,6 +70,7 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
||||||
SharedVideoMiniatureModule,
|
SharedVideoMiniatureModule,
|
||||||
SharedTablesModule,
|
SharedTablesModule,
|
||||||
SharedUsersModule,
|
SharedUsersModule,
|
||||||
|
SharedAdminModule,
|
||||||
|
|
||||||
TableModule,
|
TableModule,
|
||||||
ChartModule
|
ChartModule
|
||||||
|
|
|
@ -218,6 +218,8 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></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 *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -267,10 +269,10 @@
|
||||||
inputName="importVideosHttpEnabled" formControlName="enabled"
|
inputName="importVideosHttpEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n>⚠️ If enabled, we recommend to use <a class="link-orange" href="https://docs.joinpeertube.org/maintain-configuration?id=security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
<span i18n>⚠️ If enabled, we recommend to use <a class="link-orange" href="https://docs.joinpeertube.org/maintain-configuration?id=security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="torrent">
|
<div class="form-group" formGroupName="torrent">
|
||||||
|
@ -285,6 +287,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</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>
|
||||||
|
|
||||||
<ng-container formGroupName="autoBlacklist">
|
<ng-container formGroupName="autoBlacklist">
|
||||||
|
|
|
@ -25,11 +25,12 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private themeService: ThemeService
|
private themeService: ThemeService
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildLandingPageOptions()
|
this.buildLandingPageOptions()
|
||||||
this.checkSignupField()
|
this.checkSignupField()
|
||||||
|
this.checkImportSyncField()
|
||||||
|
|
||||||
this.availableThemes = this.themeService.buildAvailableThemes()
|
this.availableThemes = this.themeService.buildAvailableThemes()
|
||||||
}
|
}
|
||||||
|
@ -59,6 +60,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||||
return !!enabled.find((e: string) => e === algorithm)
|
return !!enabled.find((e: string) => e === algorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserVideoQuota () {
|
||||||
|
return this.form.value['user']['videoQuota']
|
||||||
|
}
|
||||||
|
|
||||||
isSignupEnabled () {
|
isSignupEnabled () {
|
||||||
return this.form.value['signup']['enabled'] === true
|
return this.form.value['signup']['enabled'] === true
|
||||||
}
|
}
|
||||||
|
@ -67,6 +72,14 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||||
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
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 () {
|
hasUnlimitedSignup () {
|
||||||
return this.form.value['signup']['limit'] === -1
|
return this.form.value['signup']['limit'] === -1
|
||||||
}
|
}
|
||||||
|
@ -97,6 +110,21 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||||
return this.themeService.getDefaultThemeLabel()
|
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 () {
|
private checkSignupField () {
|
||||||
const signupControl = this.form.get('signup.enabled')
|
const signupControl = this.form.get('signup.enabled')
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,10 @@ input[type=number] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.2em;
|
top: 0.2em;
|
||||||
right: 2.5rem;
|
right: 2.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile-view) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[disabled] {
|
input[disabled] {
|
||||||
|
@ -146,3 +150,9 @@ ngb-tabset:not(.previews) ::ng-deep {
|
||||||
padding: 0 .3em;
|
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: {
|
torrent: {
|
||||||
enabled: null
|
enabled: null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
videoChannelSynchronization: {
|
||||||
|
enabled: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trending: {
|
trending: {
|
||||||
|
@ -289,6 +292,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
formValidated () {
|
formValidated () {
|
||||||
|
this.forceCheck()
|
||||||
|
if (!this.form.valid) return
|
||||||
|
|
||||||
const value: ComponentCustomConfig = this.form.getRawValue()
|
const value: ComponentCustomConfig = this.form.getRawValue()
|
||||||
|
|
||||||
forkJoin([
|
forkJoin([
|
||||||
|
@ -378,8 +384,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
||||||
|
|
||||||
this.updateForm()
|
this.updateForm()
|
||||||
// Force form validation
|
this.markAllAsDirty()
|
||||||
this.forceCheck()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
error: err => this.notifier.error(err.message)
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
<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">
|
<div class="ms-2 d-flex flex-column">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<h1>
|
<h1>
|
||||||
<my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
|
<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>
|
</h1>
|
||||||
|
|
||||||
<p-table
|
<p-table
|
||||||
|
@ -45,8 +45,8 @@
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template pSelectableRow="follow" pTemplate="body" let-follow>
|
<ng-template pTemplate="body" let-follow>
|
||||||
<tr>
|
<tr pSelectableRow="follow">
|
||||||
<td class="checkbox-cell">
|
<td class="checkbox-cell">
|
||||||
<p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
<p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -145,15 +145,15 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoEmbed (entry: VideoBlacklist) {
|
getVideoEmbed (entry: VideoBlacklist) {
|
||||||
return buildVideoOrPlaylistEmbed(
|
return buildVideoOrPlaylistEmbed({
|
||||||
decorateVideoLink({
|
embedUrl: decorateVideoLink({
|
||||||
url: buildVideoEmbedLink(entry.video, environment.originServerUrl),
|
url: buildVideoEmbedLink(entry.video, environment.originServerUrl),
|
||||||
|
|
||||||
title: false,
|
title: false,
|
||||||
warningTitle: false
|
warningTitle: false
|
||||||
}),
|
}),
|
||||||
entry.video.name
|
embedTitle: entry.video.name
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reloadData () {
|
protected reloadData () {
|
||||||
|
|
|
@ -54,6 +54,10 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
|
||||||
{
|
{
|
||||||
value: 'local:false',
|
value: 'local:false',
|
||||||
label: $localize`Remote comments`
|
label: $localize`Remote comments`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'localVideo:true',
|
||||||
|
label: $localize`Comments on local videos`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,10 +152,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
|
<my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||||
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>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors.videoQuota" class="form-error">
|
<div *ngIf="formErrors.videoQuota" class="form-error">
|
||||||
{{ formErrors.videoQuota }}
|
{{ formErrors.videoQuota }}
|
||||||
|
|
|
@ -41,7 +41,8 @@ button {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcoding-information {
|
my-user-real-quota-info {
|
||||||
|
display: block;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 11px;
|
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 { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||||
import { FormReactive } from '@app/shared/shared-forms'
|
import { FormReactive } from '@app/shared/shared-forms'
|
||||||
import { USER_ROLE_LABELS } from '@shared/core-utils/users'
|
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'
|
import { SelectOptionsItem } from '../../../../../types/select-options-item.model'
|
||||||
|
|
||||||
@Directive()
|
@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 () {
|
resetPassword () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserVideoQuota () {
|
||||||
|
return this.form.value['videoQuota']
|
||||||
|
}
|
||||||
|
|
||||||
protected buildAdminFlags (formValue: any) {
|
protected buildAdminFlags (formValue: any) {
|
||||||
return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE
|
return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
||||||
|
|
||||||
<my-global-icon
|
<my-global-icon
|
||||||
|
*ngIf="canRemoveOneFile(video)"
|
||||||
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
||||||
(click)="removeVideoFile(video, file, 'webtorrent')"
|
(click)="removeVideoFile(video, file, 'webtorrent')"
|
||||||
></my-global-icon>
|
></my-global-icon>
|
||||||
|
@ -124,6 +125,7 @@
|
||||||
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
|
||||||
|
|
||||||
<my-global-icon
|
<my-global-icon
|
||||||
|
*ngIf="canRemoveOneFile(video)"
|
||||||
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button"
|
||||||
(click)="removeVideoFile(video, file, 'hls')"
|
(click)="removeVideoFile(video, file, 'hls')"
|
||||||
></my-global-icon>
|
></my-global-icon>
|
||||||
|
|
|
@ -166,6 +166,10 @@ export class VideoListComponent extends RestTable implements OnInit {
|
||||||
return video.files.length !== 0
|
return video.files.length !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canRemoveOneFile (video: Video) {
|
||||||
|
return video.canRemoveOneFile(this.authUser)
|
||||||
|
}
|
||||||
|
|
||||||
getFilesSize (video: Video) {
|
getFilesSize (video: Video) {
|
||||||
let files = video.files
|
let files = video.files
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,15 @@
|
||||||
|
|
||||||
<my-button
|
<my-button
|
||||||
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
|
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-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>
|
</div>
|
||||||
</my-plugin-card>
|
</my-plugin-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -24,6 +24,7 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
|
|
||||||
plugins: PeerTubePlugin[] = []
|
plugins: PeerTubePlugin[] = []
|
||||||
updating: { [name: string]: boolean } = {}
|
updating: { [name: string]: boolean } = {}
|
||||||
|
uninstalling: { [name: string]: boolean } = {}
|
||||||
|
|
||||||
onDataSubject = new Subject<any[]>()
|
onDataSubject = new Subject<any[]>()
|
||||||
|
|
||||||
|
@ -99,7 +100,11 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
isUpdating (plugin: PeerTubePlugin) {
|
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) {
|
isTheme (plugin: PeerTubePlugin) {
|
||||||
|
@ -107,12 +112,17 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall (plugin: PeerTubePlugin) {
|
async uninstall (plugin: PeerTubePlugin) {
|
||||||
|
const pluginKey = this.getPluginKey(plugin)
|
||||||
|
if (this.uninstalling[pluginKey]) return
|
||||||
|
|
||||||
const res = await this.confirmService.confirm(
|
const res = await this.confirmService.confirm(
|
||||||
$localize`Do you really want to uninstall ${plugin.name}?`,
|
$localize`Do you really want to uninstall ${plugin.name}?`,
|
||||||
$localize`Uninstall`
|
$localize`Uninstall`
|
||||||
)
|
)
|
||||||
if (res === false) return
|
if (res === false) return
|
||||||
|
|
||||||
|
this.uninstalling[pluginKey] = true
|
||||||
|
|
||||||
this.pluginApiService.uninstall(plugin.name, plugin.type)
|
this.pluginApiService.uninstall(plugin.name, plugin.type)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
@ -120,15 +130,20 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
|
|
||||||
this.plugins = this.plugins.filter(p => p.name !== plugin.name)
|
this.plugins = this.plugins.filter(p => p.name !== plugin.name)
|
||||||
this.pagination.totalItems--
|
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) {
|
async update (plugin: PeerTubePlugin) {
|
||||||
const updatingKey = this.getUpdatingKey(plugin)
|
const pluginKey = this.getPluginKey(plugin)
|
||||||
if (this.updating[updatingKey]) return
|
if (this.updating[pluginKey]) return
|
||||||
|
|
||||||
if (this.isMajorUpgrade(plugin)) {
|
if (this.isMajorUpgrade(plugin)) {
|
||||||
const res = await this.confirmService.confirm(
|
const res = await this.confirmService.confirm(
|
||||||
|
@ -140,20 +155,23 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
if (res === false) return
|
if (res === false) return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updating[updatingKey] = true
|
this.updating[pluginKey] = true
|
||||||
|
|
||||||
this.pluginApiService.update(plugin.name, plugin.type)
|
this.pluginApiService.update(plugin.name, plugin.type)
|
||||||
.pipe()
|
.pipe()
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: res => {
|
next: res => {
|
||||||
this.updating[updatingKey] = false
|
this.updating[pluginKey] = false
|
||||||
|
|
||||||
this.notifier.success($localize`${plugin.name} updated.`)
|
this.notifier.success($localize`${plugin.name} updated.`)
|
||||||
|
|
||||||
Object.assign(plugin, res)
|
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)
|
return this.pluginApiService.getPluginOrThemeHref(this.pluginType, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUpdatingKey (plugin: PeerTubePlugin) {
|
private getPluginKey (plugin: PeerTubePlugin) {
|
||||||
return plugin.name + plugin.type
|
return plugin.name + plugin.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
></my-edit-button>
|
></my-edit-button>
|
||||||
|
|
||||||
<my-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"
|
[loading]="isInstalling(plugin)" label="Install" [responsiveLabel]="true"
|
||||||
icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
|
icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
|
||||||
></my-button>
|
></my-button>
|
||||||
|
|
|
@ -145,7 +145,11 @@ export class PluginSearchComponent implements OnInit {
|
||||||
plugin.installed = true
|
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 })
|
return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL + `/${jobState || ''}`, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => {
|
map(res => this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ], 'precise')),
|
||||||
return this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ])
|
|
||||||
}),
|
|
||||||
map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData)),
|
map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData)),
|
||||||
map(res => this.restExtractor.applyToResultListData(res, this.buildUniqId)),
|
map(res => this.restExtractor.applyToResultListData(res, this.buildUniqId)),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
<ng-container *ngIf="hasProgress(job)">{{ getProgress(job) }}</ng-container>
|
<ng-container *ngIf="hasProgress(job)">{{ getProgress(job) }}</ng-container>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
|
@ -23,22 +23,28 @@ export class JobsComponent extends RestTable implements OnInit {
|
||||||
jobTypes: JobTypeClient[] = [
|
jobTypes: JobTypeClient[] = [
|
||||||
'all',
|
'all',
|
||||||
|
|
||||||
|
'activitypub-cleaner',
|
||||||
'activitypub-follow',
|
'activitypub-follow',
|
||||||
'activitypub-http-broadcast',
|
|
||||||
'activitypub-http-broadcast-parallel',
|
'activitypub-http-broadcast-parallel',
|
||||||
|
'activitypub-http-broadcast',
|
||||||
'activitypub-http-fetcher',
|
'activitypub-http-fetcher',
|
||||||
'activitypub-http-unicast',
|
'activitypub-http-unicast',
|
||||||
'activitypub-refresher',
|
'activitypub-refresher',
|
||||||
'activitypub-cleaner',
|
|
||||||
'actor-keys',
|
'actor-keys',
|
||||||
|
'after-video-channel-import',
|
||||||
'email',
|
'email',
|
||||||
|
'federate-video',
|
||||||
|
'manage-video-torrent',
|
||||||
|
'move-to-object-storage',
|
||||||
|
'notify',
|
||||||
|
'video-channel-import',
|
||||||
'video-file-import',
|
'video-file-import',
|
||||||
'video-import',
|
'video-import',
|
||||||
'video-live-ending',
|
'video-live-ending',
|
||||||
'video-redundancy',
|
'video-redundancy',
|
||||||
|
'video-studio-edition',
|
||||||
'video-transcoding',
|
'video-transcoding',
|
||||||
'videos-views-stats',
|
'videos-views-stats'
|
||||||
'move-to-object-storage'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
jobs: Job[] = []
|
jobs: Job[] = []
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="support">Support</label>
|
<label i18n for="support">Support</label>
|
||||||
<my-help
|
<my-help
|
||||||
helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
|
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."
|
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
|
<strong>{{ user.pendingEmail }}</strong> is awaiting email verification
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label i18n for="new-email">Change your email</label>
|
<label i18n for="new-email">Change your email</label>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="col-12 col-lg-4 col-xl-3">
|
||||||
<h2 i18n class="account-title">EMAIL</h2>
|
<h2 i18n class="account-title">EMAIL</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
<h1>
|
<h1>
|
||||||
<my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
|
<span>
|
||||||
<ng-container i18n>My channels</ng-container>
|
<my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
|
||||||
<span *ngIf="totalItems" class="pt-badge badge-secondary">{{ totalItems }}</span>
|
<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>
|
</h1>
|
||||||
|
|
||||||
<my-channels-setup-message [hideLink]="true"></my-channels-setup-message>
|
<my-channels-setup-message [hideLink]="true"></my-channels-setup-message>
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
@use '_variables' as *;
|
@use '_variables' as *;
|
||||||
@use '_mixins' as *;
|
@use '_mixins' as *;
|
||||||
|
|
||||||
h1 my-global-icon {
|
h1 {
|
||||||
position: relative;
|
display: flex;
|
||||||
top: -2px;
|
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 {
|
.create-button {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { MySubscriptionsComponent } from './my-follows/my-subscriptions.componen
|
||||||
import { MyHistoryComponent } from './my-history/my-history.component'
|
import { MyHistoryComponent } from './my-history/my-history.component'
|
||||||
import { MyLibraryComponent } from './my-library.component'
|
import { MyLibraryComponent } from './my-library.component'
|
||||||
import { MyOwnershipComponent } from './my-ownership/my-ownership.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 { MyVideoImportsComponent } from './my-video-imports/my-video-imports.component'
|
||||||
import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
|
import { MyVideoPlaylistCreateComponent } from './my-video-playlists/my-video-playlist-create.component'
|
||||||
import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component'
|
import { MyVideoPlaylistElementsComponent } from './my-video-playlists/my-video-playlist-elements.component'
|
||||||
|
@ -131,6 +133,26 @@ const myLibraryRoutes: Routes = [
|
||||||
key: 'my-videos-history-list'
|
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 { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component'
|
||||||
import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
|
import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
|
||||||
import { MyVideosComponent } from './my-videos/my-videos.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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -63,6 +65,8 @@ import { MyVideosComponent } from './my-videos/my-videos.component'
|
||||||
MyOwnershipComponent,
|
MyOwnershipComponent,
|
||||||
MyAcceptOwnershipComponent,
|
MyAcceptOwnershipComponent,
|
||||||
MyVideoImportsComponent,
|
MyVideoImportsComponent,
|
||||||
|
MyVideoChannelSyncsComponent,
|
||||||
|
VideoChannelSyncEditComponent,
|
||||||
MySubscriptionsComponent,
|
MySubscriptionsComponent,
|
||||||
MyFollowersComponent,
|
MyFollowersComponent,
|
||||||
MyHistoryComponent,
|
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">
|
||||||
<my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
|
<span>
|
||||||
<ng-container i18n>My imports</ng-container>
|
<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>
|
</h1>
|
||||||
|
|
||||||
|
<div class="mb-4 d-flex justify-content-between">
|
||||||
|
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p-table
|
<p-table
|
||||||
[value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
[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
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
|
||||||
[expandedRowKeys]="expandedRows"
|
[expandedRowKeys]="expandedRows"
|
||||||
|
|
|
@ -8,3 +8,9 @@ pre {
|
||||||
.video-import-error {
|
.video-import-error {
|
||||||
color: #ff0000;
|
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) {
|
switch (state) {
|
||||||
case VideoImportState.FAILED:
|
case VideoImportState.FAILED:
|
||||||
return 'badge-red'
|
return 'badge-red'
|
||||||
|
|
||||||
case VideoImportState.REJECTED:
|
case VideoImportState.REJECTED:
|
||||||
return 'badge-banned'
|
return 'badge-banned'
|
||||||
|
|
||||||
case VideoImportState.PENDING:
|
case VideoImportState.PENDING:
|
||||||
return 'badge-yellow'
|
return 'badge-yellow'
|
||||||
|
|
||||||
case VideoImportState.PROCESSING:
|
case VideoImportState.PROCESSING:
|
||||||
return 'badge-blue'
|
return 'badge-blue'
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 'badge-green'
|
return 'badge-green'
|
||||||
}
|
}
|
||||||
|
@ -87,7 +91,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reloadData () {
|
protected reloadData () {
|
||||||
this.videoImportService.getMyVideoImports(this.pagination, this.sort)
|
this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: resultList => {
|
next: resultList => {
|
||||||
this.videoImports = resultList.data
|
this.videoImports = resultList.data
|
||||||
|
|
|
@ -43,7 +43,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
||||||
privacyLabel: false,
|
privacyLabel: false,
|
||||||
privacyText: true,
|
privacyText: true,
|
||||||
state: true,
|
state: true,
|
||||||
blacklistInfo: true
|
blacklistInfo: true,
|
||||||
|
forceChannelInBy: true
|
||||||
}
|
}
|
||||||
videoDropdownDisplayOptions: VideoActionsDisplayType = {
|
videoDropdownDisplayOptions: VideoActionsDisplayType = {
|
||||||
playlist: false,
|
playlist: false,
|
||||||
|
|
|
@ -98,7 +98,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||||
this.search()
|
this.search()
|
||||||
},
|
},
|
||||||
|
|
||||||
error: err => this.notifier.error(err.text)
|
error: err => this.notifier.error(err.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.userService.getAnonymousOrLoggedUser()
|
this.userService.getAnonymousOrLoggedUser()
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
</cdk-step>
|
</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>
|
<my-signup-step-title mascotImageName="account" i18n>
|
||||||
<strong>Setup</strong>
|
<strong>Setup</strong>
|
||||||
<div>your account</div>
|
<div>your account</div>
|
||||||
|
|
|
@ -528,7 +528,7 @@ export class VideoStatsComponent implements OnInit {
|
||||||
const date = new Date(label)
|
const date = new Date(label)
|
||||||
|
|
||||||
if (data.groupInterval.match(/ month?$/)) {
|
if (data.groupInterval.match(/ month?$/)) {
|
||||||
return date.toLocaleDateString([], { month: 'numeric' })
|
return date.toLocaleDateString([], { year: '2-digit', month: 'numeric' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.groupInterval.match(/ days?$/)) {
|
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 { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
|
||||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main'
|
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 { HTMLServerConfig, VideoConstant } from '@shared/models'
|
||||||
import { ServerService } from '../../../../core'
|
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({
|
@Component({
|
||||||
selector: 'my-video-caption-edit-modal',
|
selector: 'my-video-caption-edit-modal-content',
|
||||||
styleUrls: [ './video-caption-edit-modal.component.scss' ],
|
styleUrls: [ './video-caption-edit-modal-content.component.scss' ],
|
||||||
templateUrl: './video-caption-edit-modal.component.html'
|
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() videoCaption: VideoCaptionWithPathEdit
|
||||||
@Input() serverConfig: HTMLServerConfig
|
@Input() serverConfig: HTMLServerConfig
|
||||||
|
|
||||||
@Output() captionEdited = new EventEmitter<VideoCaptionEdit>()
|
@Output() captionEdited = new EventEmitter<VideoCaptionEdit>()
|
||||||
|
|
||||||
@ViewChild('modal', { static: true }) modal: ElementRef
|
@ViewChild('textarea', { static: true }) textarea!: ElementRef
|
||||||
|
|
||||||
videoCaptionLanguages: VideoConstant<string>[] = []
|
videoCaptionLanguages: VideoConstant<string>[] = []
|
||||||
private openedModal: NgbModalRef
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
protected openedModal: NgbActiveModal,
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private videoCaptionService: VideoCaptionService,
|
private videoCaptionService: VideoCaptionService,
|
||||||
|
@ -49,11 +54,14 @@ export class VideoCaptionEditModalComponent extends FormReactive implements OnIn
|
||||||
this.form.patchValue({
|
this.form.patchValue({
|
||||||
captionFileContent: res
|
captionFileContent: res
|
||||||
})
|
})
|
||||||
|
this.resetTextarea()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
show () {
|
resetTextarea () {
|
||||||
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
|
this.textarea.nativeElement.scrollTop = 0
|
||||||
|
this.textarea.nativeElement.selectionStart = 0
|
||||||
|
this.textarea.nativeElement.selectionEnd = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
hide () {
|
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>
|
||||||
|
|
||||||
<ng-template ptTemplate="help">
|
<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>
|
</ng-template>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
|
|
||||||
<div i18n class="caption-entry-state">Already uploaded on {{ videoCaption.updatedAt | date }} ✔</div>
|
<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>
|
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -212,13 +212,6 @@
|
||||||
|
|
||||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
|
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<my-video-caption-edit-modal
|
|
||||||
#videoCaptionEditModal
|
|
||||||
[videoCaption]="videoCaption"
|
|
||||||
[serverConfig]="serverConfig"
|
|
||||||
(captionEdited)="onCaptionEdited($event)"
|
|
||||||
></my-video-caption-edit-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -314,7 +307,7 @@
|
||||||
<div class="col-md-12 col-xl-8">
|
<div class="col-md-12 col-xl-8">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="previewfile">Video preview</label>
|
<label i18n for="previewfile">Video thumbnail</label>
|
||||||
|
|
||||||
<my-preview-upload
|
<my-preview-upload
|
||||||
i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
|
i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
|
||||||
|
|
|
@ -35,10 +35,11 @@ import {
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
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 { VideoEditType } from './video-edit.type'
|
||||||
import { VideoSource } from '@shared/models/videos/video-source'
|
import { VideoSource } from '@shared/models/videos/video-source'
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
type VideoLanguages = VideoConstant<string> & { group?: string }
|
type VideoLanguages = VideoConstant<string> & { group?: string }
|
||||||
type PluginField = {
|
type PluginField = {
|
||||||
|
@ -70,7 +71,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
@Input() liveVideo: LiveVideo
|
@Input() liveVideo: LiveVideo
|
||||||
|
|
||||||
@ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
|
@ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
|
||||||
@ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent
|
|
||||||
|
|
||||||
@Output() formBuilt = new EventEmitter<void>()
|
@Output() formBuilt = new EventEmitter<void>()
|
||||||
@Output() pluginFieldsAdded = new EventEmitter<void>()
|
@Output() pluginFieldsAdded = new EventEmitter<void>()
|
||||||
|
@ -128,7 +128,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
private i18nPrimengCalendarService: I18nPrimengCalendarService,
|
private i18nPrimengCalendarService: I18nPrimengCalendarService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private hooks: HooksService,
|
private hooks: HooksService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef,
|
||||||
|
private modalService: NgbModal
|
||||||
) {
|
) {
|
||||||
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
|
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
|
||||||
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
|
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
|
||||||
|
@ -286,6 +287,13 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
this.videoCaptionAddModal.show()
|
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 () {
|
isSaveReplayEnabled () {
|
||||||
return this.serverConfig.live.allowReplay
|
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 { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
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'
|
import { VideoEditComponent } from './video-edit.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -22,7 +22,7 @@ import { VideoEditComponent } from './video-edit.component'
|
||||||
declarations: [
|
declarations: [
|
||||||
VideoEditComponent,
|
VideoEditComponent,
|
||||||
VideoCaptionAddModalComponent,
|
VideoCaptionAddModalComponent,
|
||||||
VideoCaptionEditModalComponent
|
VideoCaptionEditModalContentComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
<div class="submit-container">
|
<div class="submit-container">
|
||||||
<my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
|
<my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
|
||||||
(click)="updateSecondStep()"
|
(click)="updateSecondStep()"
|
||||||
[disabled]="!form.valid"
|
[disabled]="!form.valid || isUpdatingVideo === true"
|
||||||
></my-button>
|
></my-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -2,11 +2,3 @@
|
||||||
margin-top: 15px;
|
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
|
firstStepPermanentLive: boolean
|
||||||
|
|
||||||
isInUpdateForm = false
|
isInUpdateForm = false
|
||||||
|
isUpdatingVideo = false
|
||||||
|
isOrHasGoingLive = false
|
||||||
|
|
||||||
liveVideo: LiveVideo
|
liveVideo: LiveVideo
|
||||||
|
|
||||||
|
@ -64,6 +66,9 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
||||||
}
|
}
|
||||||
|
|
||||||
goLive () {
|
goLive () {
|
||||||
|
if (this.isOrHasGoingLive) return
|
||||||
|
this.isOrHasGoingLive = true
|
||||||
|
|
||||||
const name = 'Live'
|
const name = 'Live'
|
||||||
|
|
||||||
const video: LiveVideoCreate = {
|
const video: LiveVideoCreate = {
|
||||||
|
@ -115,6 +120,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
||||||
async updateSecondStep () {
|
async updateSecondStep () {
|
||||||
if (!await this.isFormValid()) return
|
if (!await this.isFormValid()) return
|
||||||
|
|
||||||
|
this.isUpdatingVideo = true
|
||||||
|
|
||||||
const video = new VideoEdit()
|
const video = new VideoEdit()
|
||||||
video.patch(this.form.value)
|
video.patch(this.form.value)
|
||||||
video.id = this.videoId
|
video.id = this.videoId
|
||||||
|
@ -134,6 +141,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
||||||
this.liveVideoService.updateLive(this.videoId, liveVideoUpdate)
|
this.liveVideoService.updateLive(this.videoId, liveVideoUpdate)
|
||||||
]).subscribe({
|
]).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.isUpdatingVideo = false
|
||||||
|
|
||||||
this.notifier.success($localize`Live published.`)
|
this.notifier.success($localize`Live published.`)
|
||||||
|
|
||||||
this.router.navigateByUrl(Video.buildWatchUrl(video))
|
this.router.navigateByUrl(Video.buildWatchUrl(video))
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
</my-help>
|
</my-help>
|
||||||
|
|
||||||
<input type="text" id="targetUrl" [(ngModel)]="targetUrl" class="form-control" />
|
<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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -64,6 +64,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
|
||||||
return this.targetUrl?.match(/https?:\/\//)
|
return this.targetUrl?.match(/https?:\/\//)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isChannelSyncEnabled () {
|
||||||
|
return this.serverConfig.import.videoChannelSynchronization.enabled
|
||||||
|
}
|
||||||
|
|
||||||
importVideo () {
|
importVideo () {
|
||||||
this.isImportingVideo = true
|
this.isImportingVideo = true
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use '_variables' as *;
|
@use '_variables' as *;
|
||||||
@use '_mixins' as *;
|
@use '_mixins' as *;
|
||||||
|
|
||||||
$width-size: 250px;
|
$width-size: 275px;
|
||||||
|
|
||||||
.alert.alert-danger {
|
.alert.alert-danger {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -27,7 +27,9 @@ $width-size: 250px;
|
||||||
@include peertube-select-container($width-size);
|
@include peertube-select-container($width-size);
|
||||||
}
|
}
|
||||||
my-select-options ::ng-deep ng-select,
|
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;
|
width: $width-size;
|
||||||
|
|
||||||
@media screen and (max-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] {
|
input[type=text] {
|
||||||
@include peertube-input-text($width-size);
|
@include peertube-input-text($width-size);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { isIOS } from '@root-helpers/web-browser'
|
||||||
import { HttpStatusCode, VideoCreateResult } from '@shared/models'
|
import { HttpStatusCode, VideoCreateResult } from '@shared/models'
|
||||||
import { UploaderXFormData } from './uploaderx-form-data'
|
import { UploaderXFormData } from './uploaderx-form-data'
|
||||||
import { VideoSend } from './video-send'
|
import { VideoSend } from './video-send'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-upload',
|
selector: 'my-video-upload',
|
||||||
|
@ -56,6 +57,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
|
|
||||||
private alreadyRefreshedToken = false
|
private alreadyRefreshedToken = false
|
||||||
|
|
||||||
|
private uploadServiceSubscription: Subscription
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
protected loadingBar: LoadingBarService,
|
protected loadingBar: LoadingBarService,
|
||||||
|
@ -87,7 +90,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
||||||
})
|
})
|
||||||
|
|
||||||
this.resumableUploadService.events
|
this.uploadServiceSubscription = this.resumableUploadService.events
|
||||||
.subscribe(state => this.onUploadVideoOngoing(state))
|
.subscribe(state => this.onUploadVideoOngoing(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +99,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
this.cancelUpload()
|
this.resumableUploadService.disconnect()
|
||||||
|
|
||||||
|
if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeactivate () {
|
canDeactivate () {
|
||||||
|
|
|
@ -148,7 +148,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
|
||||||
error: err => {
|
error: err => {
|
||||||
this.addingComment = false
|
this.addingComment = false
|
||||||
|
|
||||||
this.notifier.error(err.text)
|
this.notifier.error(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -628,6 +628,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
: null,
|
: null,
|
||||||
authorizationHeader: this.authService.getRequestHeaderValue(),
|
authorizationHeader: this.authService.getRequestHeaderValue(),
|
||||||
|
|
||||||
|
metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
|
||||||
|
|
||||||
embedUrl: video.embedUrl,
|
embedUrl: video.embedUrl,
|
||||||
embedTitle: video.name,
|
embedTitle: video.name,
|
||||||
instanceName: this.serverConfig.instance.name,
|
instanceName: this.serverConfig.instance.name,
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class AuthService {
|
||||||
let errorMessage = err.message
|
let errorMessage = err.message
|
||||||
|
|
||||||
if (err.status === HttpStatusCode.FORBIDDEN_403) {
|
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.`
|
Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { ScreenService } from '../wrappers'
|
||||||
|
|
||||||
export type MenuLink = {
|
export type MenuLink = {
|
||||||
icon: GlobalIconName
|
icon: GlobalIconName
|
||||||
|
iconClass?: string
|
||||||
|
|
||||||
label: string
|
label: string
|
||||||
// Used by the left menu for example
|
// Used by the left menu for example
|
||||||
|
@ -70,6 +71,14 @@ export class MenuService {
|
||||||
let links: MenuLink[] = []
|
let links: MenuLink[] = []
|
||||||
|
|
||||||
if (userCanSeeVideosLink) {
|
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({
|
links.push({
|
||||||
path: '/my-library/videos',
|
path: '/my-library/videos',
|
||||||
icon: 'videos' as GlobalIconName,
|
icon: 'videos' as GlobalIconName,
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { throwError as observableThrowError } from 'rxjs'
|
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 { Router } from '@angular/router'
|
||||||
import { dateToHuman } from '@app/helpers'
|
import { DateFormat, dateToHuman } from '@app/helpers'
|
||||||
import { HttpStatusCode, ResultList } from '@shared/models'
|
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
|
import { HttpStatusCode, ResultList } from '@shared/models'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RestExtractor {
|
export class RestExtractor {
|
||||||
|
|
||||||
constructor (private router: Router) { }
|
constructor (
|
||||||
|
@Inject(LOCALE_ID) private localeId: string,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
applyToResultListData <T, A, U> (
|
applyToResultListData <T, A, U> (
|
||||||
result: ResultList<T>,
|
result: ResultList<T>,
|
||||||
|
@ -23,13 +26,17 @@ export class RestExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
convertResultListDateToHuman <T> (result: ResultList<T>, fieldsToConvert: string[] = [ 'createdAt' ]): ResultList<T> {
|
convertResultListDateToHuman <T> (
|
||||||
return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ])
|
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 => {
|
fieldsToConvert.forEach(field => {
|
||||||
target[field] = dateToHuman(target[field])
|
target[field] = dateToHuman(this.localeId, new Date(target[field]), format)
|
||||||
})
|
})
|
||||||
|
|
||||||
return target
|
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">
|
<a class="publish-button" routerLink="/videos/upload">
|
||||||
<my-global-icon iconName="upload" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="upload" aria-hidden="true"></my-global-icon>
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
@use '_variables' as *;
|
@use '_variables' as *;
|
||||||
@use '_mixins' as *;
|
@use '_mixins' as *;
|
||||||
|
|
||||||
my-search-typeahead {
|
|
||||||
@include margin-right(80px);
|
|
||||||
|
|
||||||
@media screen and (max-width: $small-view) {
|
|
||||||
@include margin-right(15px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-button {
|
.publish-button {
|
||||||
@include peertube-button-link;
|
@include peertube-button-link;
|
||||||
@include orange-button;
|
@include orange-button;
|
||||||
|
|
|
@ -75,6 +75,7 @@ li.suggestion {
|
||||||
|
|
||||||
#typeahead-container {
|
#typeahead-container {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
margin: 0 10px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
border: 1px solid pvar(--mainBackgroundColor) !important;
|
border: 1px solid pvar(--mainBackgroundColor) !important;
|
||||||
|
@ -83,15 +84,15 @@ li.suggestion {
|
||||||
transition: box-shadow .3s ease, width .2s ease;
|
transition: box-shadow .3s ease, width .2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $mobile-view) {
|
@media screen and (max-width: $small-view) {
|
||||||
@include margin-left(10px);
|
input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $small-view) {
|
@media screen and (max-width: $mobile-view) {
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 70px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,29 @@
|
||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
|
|
||||||
const datePipe = new DatePipe('en')
|
let datePipe: DatePipe
|
||||||
function dateToHuman (date: string) {
|
let intl: Intl.DateTimeFormat
|
||||||
return datePipe.transform(date, 'medium')
|
|
||||||
|
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) {
|
function durationToString (duration: number) {
|
||||||
|
@ -20,6 +41,8 @@ function durationToString (duration: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
DateFormat,
|
||||||
|
|
||||||
durationToString,
|
durationToString,
|
||||||
dateToHuman
|
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 (!value) return undefined
|
||||||
if (Array.isArray(value)) return value
|
if (Array.isArray(value)) return value
|
||||||
|
|
||||||
|
@ -42,6 +42,6 @@ export {
|
||||||
sortBy,
|
sortBy,
|
||||||
immutableAssign,
|
immutableAssign,
|
||||||
removeElementFromArray,
|
removeElementFromArray,
|
||||||
intoArray,
|
splitIntoArray,
|
||||||
toBoolean
|
toBoolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="menu-link" routerLink="/my-library" routerLinkActive="active" #libraryLink (click)="onActiveLinkScrollToAnchor(libraryLink)">
|
<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>
|
<ng-container i18n>My library</ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
<div i18n class="block-title">{{ menuSection.title }}</div>
|
<div i18n class="block-title">{{ menuSection.title }}</div>
|
||||||
|
|
||||||
<a class="menu-link" *ngFor="let link of menuSection.links" [routerLink]="link.path" routerLinkActive="active">
|
<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>
|
<ng-container>{{ link.shortLabel }}</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -391,26 +391,17 @@ my-actor-avatar {
|
||||||
}
|
}
|
||||||
|
|
||||||
my-global-icon {
|
my-global-icon {
|
||||||
&[iconName=playlists] {
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
|
||||||
|
.playlist-icon {
|
||||||
@include margin-right(16px);
|
@include margin-right(16px);
|
||||||
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[iconName=videos] {
|
&.channel-icon {
|
||||||
position: relative;
|
top: -2px;
|
||||||
right: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[iconName=channel] {
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[iconName='sign-out'] {
|
|
||||||
position: relative;
|
|
||||||
right: -2px;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ export const INSTANCE_NAME_VALIDATOR: BuildFormValidator = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INSTANCE_SHORT_DESCRIPTION_VALIDATOR: BuildFormValidator = {
|
export const INSTANCE_SHORT_DESCRIPTION_VALIDATOR: BuildFormValidator = {
|
||||||
VALIDATORS: [ Validators.max(250) ],
|
VALIDATORS: [ Validators.maxLength(250) ],
|
||||||
MESSAGES: {
|
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.`
|
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)
|
? buildVideoEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
|
||||||
: buildPlaylistEmbedLink({ 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 { wait } from '@root-helpers/utils'
|
||||||
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
||||||
import { FormValidatorService } from './form-validator.service'
|
import { FormValidatorService } from './form-validator.service'
|
||||||
|
@ -44,6 +44,21 @@ export abstract class FormReactive {
|
||||||
} while (this.form.status === 'PENDING')
|
} 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 () {
|
protected forceCheck () {
|
||||||
this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
|
this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +74,8 @@ export abstract class FormReactive {
|
||||||
this.onStatusChanged(
|
this.onStatusChanged(
|
||||||
form.controls[field] as FormGroup,
|
form.controls[field] as FormGroup,
|
||||||
formErrors[field] as FormReactiveErrors,
|
formErrors[field] as FormReactiveErrors,
|
||||||
validationMessages[field] as FormReactiveValidationMessages
|
validationMessages[field] as FormReactiveValidationMessages,
|
||||||
|
onlyDirty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,13 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<th i18n class="label" colspan="2">Search</th>
|
<th i18n class="label" colspan="2">Search</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { from, Observable } from 'rxjs'
|
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 { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||||
|
@ -40,10 +40,7 @@ export class InstanceFollowService {
|
||||||
if (actorType) params = params.append('actorType', actorType)
|
if (actorType) params = params.append('actorType', actorType)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/following', { params })
|
return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/following', { params })
|
||||||
.pipe(
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFollowers (options: {
|
getFollowers (options: {
|
||||||
|
@ -66,10 +63,7 @@ export class InstanceFollowService {
|
||||||
if (actorType) params = params.append('actorType', actorType)
|
if (actorType) params = params.append('actorType', actorType)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/followers', { params })
|
return this.authHttp.get<ResultList<ActorFollow>>(InstanceFollowService.BASE_APPLICATION_URL + '/followers', { params })
|
||||||
.pipe(
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
follow (hostsOrHandles: string[]) {
|
follow (hostsOrHandles: string[]) {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<ng-template #templateActionLabel let-action>
|
<ng-template #templateActionLabel let-action>
|
||||||
<my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName" aria-hidden="true"></my-global-icon>
|
<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>
|
<span i18n>{{ action.label }}</span>
|
||||||
<small class="muted" *ngIf="action.description">{{ action.description }}</small>
|
<small class="muted" *ngIf="action.description">{{ action.description }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -53,6 +53,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
|
max-width: 75vw;
|
||||||
|
|
||||||
.dropdown-header {
|
.dropdown-header {
|
||||||
padding: 0.2rem 1rem;
|
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({
|
@Component({
|
||||||
selector: 'my-delete-button',
|
selector: 'my-delete-button',
|
||||||
template: `
|
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 {
|
export class DeleteButtonComponent implements OnInit {
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() title: string
|
@Input() title: string
|
||||||
@Input() responsiveLabel = false
|
@Input() responsiveLabel = false
|
||||||
|
@Input() disabled: boolean
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
if (this.label === undefined && !this.title) {
|
if (this.label === undefined && !this.title) {
|
||||||
|
|
|
@ -13,3 +13,4 @@ export * from './video'
|
||||||
export * from './video-caption'
|
export * from './video-caption'
|
||||||
export * from './video-channel'
|
export * from './video-channel'
|
||||||
export * from './shared-main.module'
|
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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
NgbButtonsModule,
|
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
@ -66,7 +65,6 @@ import { VideoChannelService } from './video-channel'
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbButtonsModule,
|
|
||||||
|
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
|
|
||||||
|
@ -129,7 +127,6 @@ import { VideoChannelService } from './video-channel'
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
NgbButtonsModule,
|
|
||||||
|
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,6 @@ export class UserNotificationService {
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, context })
|
return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, context })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))),
|
map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
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 { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/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 { environment } from '../../../../environments/environment'
|
||||||
import { Account } from '../account'
|
import { Account } from '../account'
|
||||||
import { AccountService } from '../account/account.service'
|
import { AccountService } from '../account/account.service'
|
||||||
|
@ -95,4 +102,16 @@ export class VideoChannelService {
|
||||||
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
|
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.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 () {
|
ngOnInit () {
|
||||||
const html = buildVideoOrPlaylistEmbed(
|
const html = buildVideoOrPlaylistEmbed({
|
||||||
decorateVideoLink({
|
embedUrl: decorateVideoLink({
|
||||||
url: buildVideoEmbedLink(this.video, environment.originServerUrl),
|
url: buildVideoEmbedLink(this.video, environment.originServerUrl),
|
||||||
|
|
||||||
title: false,
|
title: false,
|
||||||
warningTitle: false
|
warningTitle: false
|
||||||
}),
|
}),
|
||||||
this.video.name
|
embedTitle: this.video.name
|
||||||
)
|
})
|
||||||
|
|
||||||
this.embedHTML = this.sanitizer.bypassSecurityTrustHtml(html)
|
this.embedHTML = this.sanitizer.bypassSecurityTrustHtml(html)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,15 +43,27 @@ export class VideoImportService {
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
.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()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
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
|
return this.authHttp
|
||||||
.get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
|
.get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(res => this.extractVideoImports(res)),
|
switchMap(res => this.extractVideoImports(res)),
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { catchError, map } from 'rxjs/operators'
|
import { catchError } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||||
|
@ -35,10 +35,7 @@ export class VideoOwnershipService {
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<VideoChangeOwnership>>(url, { params })
|
return this.authHttp.get<ResultList<VideoChangeOwnership>>(url, { params })
|
||||||
.pipe(
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptOwnership (id: number, input: VideoChangeOwnershipAccept) {
|
acceptOwnership (id: number, input: VideoChangeOwnershipAccept) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { AuthUser } from '@app/core'
|
import { AuthUser } from '@app/core'
|
||||||
import { User } from '@app/core/users/user.model'
|
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 { 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 { peertubeTranslate } from '@shared/core-utils/i18n'
|
||||||
import {
|
import {
|
||||||
ActorImage,
|
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))
|
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) {
|
canRemoveFiles (user: AuthUser) {
|
||||||
return this.isLocal &&
|
return this.isLocal &&
|
||||||
user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) &&
|
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 })
|
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
|
map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
)
|
)
|
||||||
|
@ -84,10 +83,7 @@ export class BlocklistService {
|
||||||
if (search) params = params.append('search', search)
|
if (search) params = params.append('search', search)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
|
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
|
||||||
.pipe(
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockServerByUser (host: string) {
|
blockServerByUser (host: string) {
|
||||||
|
@ -116,7 +112,6 @@ export class BlocklistService {
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params })
|
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
|
map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
)
|
)
|
||||||
|
@ -151,10 +146,7 @@ export class BlocklistService {
|
||||||
if (search) params = params.append('search', search)
|
if (search) params = params.append('search', search)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params })
|
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params })
|
||||||
.pipe(
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockServerByInstance (host: string) {
|
blockServerByInstance (host: string) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { from as observableFrom, Observable } from 'rxjs'
|
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 { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||||
|
@ -47,10 +47,7 @@ export class VideoBlockService {
|
||||||
if (type) params = params.append('type', type.toString())
|
if (type) params = params.append('type', type.toString())
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlockService.BASE_VIDEOS_URL + 'blacklist', { params })
|
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlockService.BASE_VIDEOS_URL + 'blacklist', { params })
|
||||||
.pipe(
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unblockVideo (videoIdArgs: number | number[]) {
|
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