diff --git a/.eslintrc.json b/.eslintrc.json index fa6fb1b6f..042254c95 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -88,6 +88,7 @@ "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-use-before-define": "off", // bugged but useful "@typescript-eslint/restrict-plus-operands": "off" }, diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 42096433b..c8e00bf68 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - - name: 💬 IRC - url: https://kiwiirc.com/client/irc.freenode.net/#peertube - about: Chat with us via IRC for quick Q/A here - name: 💬 Matrix url: https://matrix.to/#/#peertube:matrix.org about: Chat with us via Matrix for quick Q/A here + - name: 💬 IRC + url: https://kiwiirc.com/client/irc.freenode.net/#peertube + about: Chat with us via IRC for quick Q/A here - name: 🤷💻🤦 Forum url: https://framacolibri.org/c/peertube about: You can ask and answer other questions here diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml index a2f0945b3..968eb9612 100644 --- a/.github/workflows/stats.yml +++ b/.github/workflows/stats.yml @@ -45,11 +45,6 @@ jobs: branch-base: develop bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} - - name: PeerTube client stats - if: github.event_name != 'pull_request' - run: | - node dist/scripts/client-build-stats.js > client-build-stats.json - - name: PeerTube code stats if: github.event_name != 'pull_request' run: | @@ -57,11 +52,24 @@ jobs: unzip "scc-3.0.0-x86_64-unknown-linux.zip" ./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,yarn.lock,client/yarn.lock,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,server/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json + - name: PeerTube client stats + if: github.event_name != 'pull_request' + run: | + node dist/scripts/client-build-stats.js > client-build-stats.json + + - name: PeerTube client lighthouse report + if: github.event_name != 'pull_request' + run: | + sudo apt-get install chromium-browser + sudo npm install -g lighthouse + lighthouse --chrome-flags="--headless" https://peertube2.cpy.re --output=json --output-path=./lighthouse.json + - name: Display stats if: github.event_name != 'pull_request' run: | cat client-build-stats.json cat scc.json + cat lighthouse.json - name: Upload stats if: github.event_name != 'pull_request' @@ -87,5 +95,5 @@ jobs: if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then echo "Uploading files" - scp client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats; + scp lighthouse.json client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats; fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 442317ce2..a1edde1ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: env: PGUSER: peertube PGHOST: localhost - NODE_PENDING_JOB_WAIT: 500 + NODE_PENDING_JOB_WAIT: 250 steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 98f337490..3027b6058 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ yarn-error.log /server/tests/fixtures/video_59fps.mp4 # Production -/storage/ +/storage /config/production.yaml /config/local* /ffmpeg/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d8fa4069f..d0398825c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,150 @@ # Changelog +## v3.2.0 + +### IMPORTANT NOTES + + * **Important:** You must update your nginx configuration to add the `upload-resumable` endpoint: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube#L81 + * **Important:** Due to a bug in ffmpeg, PeerTube is not compatible with ffmpeg 4.4. See https://github.com/Chocobozzz/PeerTube/issues/3990 + * **Important:** Drop NodeJS 10 support + * PeerTube is not compatible with NodeJS 16 yet + * By default, HLS transcoding is now enabled and webtorrent is disabled. We suggest you to reflect this change. + See [the documentation](https://docs.joinpeertube.org/admin-configuration?id=webtorrent-transcoding-or-hls-transcoding) for more information + * PeerTube client now displays bigger video thumbnails. + To fix old thumbnails quality, run `regenerate-thumbnails` script after your PeerTube upgrade: https://docs.joinpeertube.org/maintain-tools?id=regenerate-thumbnailsjs + +### Docker + + * Support SSL database env parameter [#4114](https://github.com/Chocobozzz/PeerTube/pull/4114) + +### Maintenance + + * Support `X-Frame-Options` header, enabled by default in the configuration + * Directly use `node` in [systemd template](https://github.com/Chocobozzz/PeerTube/blob/develop/support/systemd/peertube.service) + * Check ffmpeg version at PeerTube startup + * Add `upload-resumable` nginx endpoint: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube#L81 + +### CLI tools + + * Add `regenerate-thumbnails` script to regenerate thumbnails of local videos + +### Plugins/Themes/Embed API + + * Theme: + * `--submenuColor` becomes `--submenuBackgroundColor` + * Support HTML placeholders for plugins. See [the documentation](https://docs.joinpeertube.org/contribute-plugins?id=html-placeholder-elements) for more information + * `player-next` next to the PeerTube player + * Support storing files for plugins in a dedicated directory. See [the documentation](https://docs.joinpeertube.org/contribute-plugins?id=storage) for more information + * Transcoding: + * Add `inputOptions` option support for transcoding profile [#3917](https://github.com/Chocobozzz/PeerTube/pull/3917) + * Add `scaleFilter.name` option support for transcoding profile [#3917](https://github.com/Chocobozzz/PeerTube/pull/3917) + * Plugin settings: + * Add ability to register `html` and `select` setting + * Add ability to hide a plugin setting depending on the form state + * Plugin form fields (to add inputs to video form...): + * Add ability to hide a plugin field depending on the form state using `.hidden` property + * Add client helpers: + * `getServerConfig()` + * `getAuthHeader()` + * Add server helpers: + * `config.getServerConfig()` + * `plugin.getBaseStaticRoute()` + * `plugin.getBaseRouterRoute()` + * `plugin.getDataDirectoryPath()` + * `user.getAuthUser()` + * Add client plugin hooks (https://docs.joinpeertube.org/api-plugins): + * `action:modal.video-download.shown` + * `action:video-upload.init` + * `action:video-url-import.init` + * `action:video-torrent-import.init` + * `action:go-live.init` + * `action:auth-user.logged-in` & `action:auth-user.logged-out` + * `action:auth-user.information-loaded` + * `action:admin-plugin-settings.init` + * Add server plugin hooks (https://docs.joinpeertube.org/api-plugins): + * `filter:api.download.video.allowed.result` & `filter:api.download.torrent.allowed.result` to forbid download + * `filter:html.embed.video-playlist.allowed.result` & `filter:html.embed.video.allowed.result` to forbid embed + * `filter:api.search.videos.local.list.params` & `filter:api.search.videos.local.list.result` + * `filter:api.search.videos.index.list.params` & `filter:api.search.videos.index.list.result` + * `filter:api.search.video-channels.local.list.params` & `filter:api.search.video-channels.local.list.result` + * `filter:api.search.video-channels.index.list.params` & `filter:api.search.video-channels.index.list.result` + +### Features + + * :tada: More robust uploads using a resumable upload endpoint [#3933](https://github.com/Chocobozzz/PeerTube/pull/3933) + * Accessibility/UI: + * :tada: Redesign channel and account page + * :tada: Increase video miniature size + * :tada: Add channel banner support + * Use a square avatar for channels and a round avatar for accounts + * Use account initial as default account avatar [#4002](https://github.com/Chocobozzz/PeerTube/pull/4002) + * Prefer channel display in video miniature + * Add *support* button in channel page + * Set direct download as default in video download modal [#3880](https://github.com/Chocobozzz/PeerTube/pull/3880) + * Show less information in video download modal by default [#3890](https://github.com/Chocobozzz/PeerTube/pull/3890) + * Autofocus admin plugin search input + * Add `1.75` playback rate to player [#3888](https://github.com/Chocobozzz/PeerTube/pull/3888) + * Add `title` attribute to embed code [#3901](https://github.com/Chocobozzz/PeerTube/pull/3901) + * Don't pause player when opening a modal [#3909](https://github.com/Chocobozzz/PeerTube/pull/3909) + * Add link below the player to open the video on origin instance [#3624](https://github.com/Chocobozzz/PeerTube/issues/3624) + * Notify admins on new available PeerTube version + * Notify admins on new available plugin version + * Sort channels by last uploaded videos + * Video player: + * Add loop toggle to context menu [#3949](https://github.com/Chocobozzz/PeerTube/pull/3949) + * Add icons to context menu [#3955](https://github.com/Chocobozzz/PeerTube/pull/3955) + * Add a *Previous* button in playlist watch page [#3485](https://github.com/Chocobozzz/PeerTube/pull/3485) + * Automatically close the settings menu when clicking outside the player + * Add "stats for nerds" panel in context menu [#3958](https://github.com/Chocobozzz/PeerTube/pull/3958) + * Add channel and playlist stats to stats endpoint [#3747](https://github.com/Chocobozzz/PeerTube/pull/3747) + * Support `playlistPosition=last` and negative index (`playlistPosition=-2`) URL query parameters for playlists [#3974](https://github.com/Chocobozzz/PeerTube/pull/3974) + * My videos: + * Add ability to sort videos (publication date, most viewed...) + * Add ability to only display live videos + * Automatically resume videos for non logged-in users [#3885](https://github.com/Chocobozzz/PeerTube/pull/3885) + * Admin plugins: + * Show a modal when upgrading a plugin to a major version + * Display a setting button after plugin installation + * Add ability to search live videos + * Use bigger thumbnails for feeds + * Parse video description markdown for Opengraph/Twitter/HTML elements + * Open the remote interaction modal when replying to a comment if we are logged-out + * Handle `.srt` captions with broken durations + * Performance: + * Player now lazy loads video captions + * Faster admin table filters + * Optimize feed endpoint + +### Bug fixes + + * More robust comments fetcher of remote video + * Fix database ssl connection + * Remove unnecessary black border above and below video in player [#3920](https://github.com/Chocobozzz/PeerTube/pull/3920) + * Reduce tag input excessive padding [#3927](https://github.com/Chocobozzz/PeerTube/pull/3927) + * Fix disappearing hamburger menu for narrow screens [#3929](https://github.com/Chocobozzz/PeerTube/pull/3929) + * Fix Youtube subtitle import with some languages + * Fix transcoding profile update in admin config + * Fix outbox fetch with subtitled videos + * Correctly unload a plugin on update/uninstall [#3940](https://github.com/Chocobozzz/PeerTube/pull/3940) + * Ensure to install plugins that are supported by PeerTube + * Fix welcome/warning modal displaying twice + * Fix h265 video import using CLI + * Fix context menu when watching a playlist + * Fix transcoding job priority preventing video publication when there are many videos to transcode + * Fix remote account/channel "joined at" + * Fix CLI plugins list command options [#4055](https://github.com/Chocobozzz/PeerTube/pull/4055) + * Fix HTTP player defaulting to audio resolution + * Logger warning level is "warn" + * Fix default boolean plugin setting [#4107](https://github.com/Chocobozzz/PeerTube/pull/4107) + * Fix duplicate ffmpeg preset option for live + * Avoid federation error when file has no torrent file + * Fix local user auth select + * Fix live ending banner display + * Fix redundancy max size + * Fix broken lives handling + + + ## v3.1.0 ### IMPORTANT NOTES diff --git a/CREDITS.md b/CREDITS.md index c721970b3..a54cfe5f3 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -3,25 +3,27 @@ * Chocobozzz * Rigel Kent * Filip Bengtsson - * kimsible * josé m + * kimsible * Simon Brosdetzko * Александр - * Clemens Schielicke * Berto Te + * Clemens Schielicke * Jeff Huang * kontrollanten - * Phongpanot * Laurent Ettouati * Racida S - * Kim + * Phongpanot * Marcin Mikołajczak + * Kim * Tirifto - * Felix Ableitner * Vodoyo Kamal + * Felix Ableitner * Gérald Niel - * Zet + * Duy * GunChleoc + * Slimane Selyan AMIRI + * Zet * x * Frank Sträter * Julien Maulny @@ -29,12 +31,11 @@ * Jorropo * Josh Morel * BO41 - * Slimane Selyan AMIRI - * Francesc - * mando laress * Balázs Meskó - * Duy + * Francesc * John Livingston + * mando laress + * Eivind Ødegård * Quentin PAGÈS * Besnik Bleta * Ihor Hordiichuk @@ -53,7 +54,6 @@ * Thomas Citharel * Agron Selimaj * Benjamin Bouvier - * Eivind Ødegård * Joe Bill * Kemal Oktay Aktoğan * Luc Didry @@ -66,6 +66,7 @@ * David Libeau * Ewald Arnold * Florent F + * Florian CUNY * Nassim Bounouas * NorbiPeti * Rafael Fontenelle @@ -81,7 +82,6 @@ * David Soh * Dimitri Gilbert * Florent Poinsaut - * Florian CUNY * Frank Chang * Green-Star * Micah Elizabeth Scott @@ -94,6 +94,7 @@ * test2a * 路过是好事 * Ajeje Brazorf + * Andrey * Angristan * Ch * Chris Sakura 佐倉くりす on Youtube @@ -103,6 +104,7 @@ * Mildred * Okhin * Pierre-Alain TORET + * Poslovitch * Serge Victor * Théo Le Calvar * Ugaitz @@ -115,7 +117,6 @@ * Ahsan Haris Ahmed * Alberto Teira * Aliaksandr Hrankin - * Andrey * Andréas Livet * Andrés Maldonado * Arco @@ -133,6 +134,7 @@ * Kiro * LecygneNoir * Leopere + * Loukas Stamellos * Lukas Winkler * Manuel Viens * Manuela Silva @@ -250,6 +252,7 @@ * Fabio Agreles Bezerra * Fernandez, ReK2 * Florent + * Gabriel Scherer * Glandos * Guillaume Pérution-Kihli * Gérald CHATAGNON @@ -265,6 +268,7 @@ * Jacob * Jacques Foucry * Jagannath Bhat + * Jan Prunk * Janey Muñoz * Jarosław Maciejewski * Jeena @@ -315,6 +319,7 @@ * PhieF * Philip Durbin * Philipp Fischbeck + * Philo van Kemenade * Pierre-Jean * Predatorix Phoenix * Quentin Dupont @@ -361,6 +366,7 @@ * bikepunk * bsky * ctlaltdefeat + * decentral1se * dingycle * eduard pintilie * gillux diff --git a/README.md b/README.md index f5fb6acea..bd7be2610 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Website - | Join an instance + | Join an instance | Create an instance | Chat with us | Donate @@ -67,23 +67,24 @@ Introduction PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper: -But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers, -all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse -(federated video network) by talking our implementation of ActivityPub. -Video load is reduced thanks to P2P in the web browser using WebTorrent or p2p-media-loader. - -To learn more, see: +To learn more: * This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works * PeerTube's project homepage, [joinpeertube.org](https://joinpeertube.org) * Demonstration instances: - * [peertube.cpy.re](https://peertube.cpy.re) - * [peertube2.cpy.re](https://peertube2.cpy.re) - * [peertube3.cpy.re](https://peertube3.cpy.re) + * [peertube.cpy.re](https://peertube.cpy.re) (stable) + * [peertube2.cpy.re](https://peertube2.cpy.re) (Nightly) + * [peertube3.cpy.re](https://peertube3.cpy.re) (RC) * This [video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) demonstrating the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) (a decentralized Twitter alternative) :sparkles: Features ---------------------------------------------------------------- +

+ All features for viewers + | All features for content creators + | All features for administrators +

+

Video streaming, even in live!

@@ -121,6 +122,8 @@ In addition to visitors using WebTorrent to share the load among them, instances Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and incentivize alter creativity (more about that in our FAQ).

+ + :raised_hands: Contributing ---------------------------------------------------------------- @@ -132,8 +135,8 @@ guide](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING. You can also join the cheerful bunch that makes our community: * Chat: - * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)** * Matrix (bridged on IRC and [Discord](https://discord.gg/wj8DDUT)) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)** + * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)** * Forum: * Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube) diff --git a/client/.stylelintrc.json b/client/.stylelintrc.json index 25f0b1002..6a322da62 100644 --- a/client/.stylelintrc.json +++ b/client/.stylelintrc.json @@ -24,6 +24,12 @@ "rule-empty-line-before": null, "selector-max-id": null, "scss/at-function-pattern": null, - "function-parentheses-space-inside": "never-single-line" + "function-parentheses-space-inside": "never-single-line", + "property-no-vendor-prefix": [ + true, + { + "ignoreProperties": [ "mask-image" ] + } + ] } } diff --git a/client/angular.json b/client/angular.json index 0cd827218..c9d90d5ec 100644 --- a/client/angular.json +++ b/client/angular.json @@ -131,13 +131,14 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { - "aot": true, "localize": true, "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "tsConfig": "tsconfig.app.json", "polyfills": "src/polyfills.ts", + "baseHref": "/", + "deployUrl": "client/", "stylePreprocessorOptions": { "includePaths": [ "src/sass/include" @@ -151,39 +152,24 @@ "src/sass/application.scss" ], "allowedCommonJsDependencies": [ - "@angularclass/hmr", - "debug", - "mousetrap", + "qrcode", "chart.js", - "linkifyjs/html", - "linkifyjs", - "markdown-it", + "htmlparser2", "markdown-it-emoji/light", "sanitize-html", - "socket.io-client", - "socket.io-parser", - "@app/+about/about-peertube/about-peertube-contributors.component", - "path", - "video.js", + "debug", "p2p-media-loader-hlsjs", - "videojs-hotkeys/videojs.hotkeys", - "p2p-media-loader-core", - "qrcode", - "webtorrent", - "cache-chunk-store", - "global/document", - "videojs-vtt.js", - "videojs-vtt.js", - "@babel/runtime/helpers/possibleConstructorReturn", - "@babel/runtime/helpers/inherits", - "@babel/runtime/helpers/construct", - "@videojs/xhr", - "htmlparser2", - "url", - "parse-srcset", + "video.js", + "sha1", "postcss" ], - "scripts": [] + "scripts": [], + "vendorChunk": true, + "extractLicenses": false, + "buildOptimizer": false, + "sourceMap": true, + "optimization": false, + "namedChunks": true }, "configurations": { "production": { @@ -191,7 +177,6 @@ "outputHashing": "all", "sourceMap": false, "namedChunks": false, - "aot": true, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, @@ -251,8 +236,6 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "baseHref": "/", - "deployUrl": "client/", "browserTarget": "PeerTube:build", "proxyConfig": "proxy.config.json" }, diff --git a/client/e2e/src/po/video-upload.po.ts b/client/e2e/src/po/video-upload.po.ts index 942025b6b..ad2acee7f 100644 --- a/client/e2e/src/po/video-upload.po.ts +++ b/client/e2e/src/po/video-upload.po.ts @@ -26,7 +26,12 @@ export class VideoUploadPage { await elem.sendKeys(fileToUpload) // Wait for the upload to finish - await browser.wait(browser.ExpectedConditions.elementToBeClickable(this.getSecondStepSubmitButton())) + await browser.wait(async () => { + const actionButton = this.getSecondStepSubmitButton().element(by.css('.action-button')) + + const klass = await actionButton.getAttribute('class') + return !klass.includes('disabled') + }) } async validSecondUploadStep (videoName: string) { diff --git a/client/package.json b/client/package.json index 140fc3095..7a461a5ca 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "peertube-client", - "version": "3.1.0", + "version": "3.2.0", "private": true, "license": "AGPL-3.0", "author": { @@ -29,20 +29,20 @@ "@types/mousetrap": "1.6.3" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.1102.2", - "@angular/animations": "^11.1.1", - "@angular/cdk": "^11.0.0", - "@angular/cli": "^11.1.2", - "@angular/common": "^11.1.1", - "@angular/compiler": "^11.1.1", - "@angular/compiler-cli": "^11.1.1", - "@angular/core": "^11.1.1", - "@angular/forms": "^11.1.1", - "@angular/localize": "^11.1.1", - "@angular/platform-browser": "^11.1.1", - "@angular/platform-browser-dynamic": "^11.1.1", - "@angular/router": "^11.1.1", - "@angular/service-worker": "^11.1.1", + "@angular-devkit/build-angular": "^12.0.0", + "@angular/animations": "^12.0.0", + "@angular/cdk": "^12.0.0", + "@angular/cli": "^12.0.0", + "@angular/common": "^12.0.0", + "@angular/compiler": "^12.0.0", + "@angular/compiler-cli": "^12.0.0", + "@angular/core": "^12.0.0", + "@angular/forms": "^12.0.0", + "@angular/localize": "^12.0.0", + "@angular/platform-browser": "^12.0.0", + "@angular/platform-browser-dynamic": "^12.0.0", + "@angular/router": "^12.0.0", + "@angular/service-worker": "^12.0.0", "@neos21/bootstrap3-glyphicons": "^1.0.1", "@ng-bootstrap/ng-bootstrap": "^9.0.2", "@ng-select/ng-select": "^6.0.0", @@ -51,7 +51,6 @@ "@ngx-loading-bar/core": "^5.0.0", "@ngx-loading-bar/http-client": "^5.0.0", "@ngx-loading-bar/router": "^5.0.0", - "@ngx-meta/core": "^9.0.0", "@types/chart.js": "^2.9.16", "@types/core-js": "^2.5.2", "@types/debug": "^4.1.5", @@ -70,19 +69,19 @@ "angular2-hotkeys": "^2.1.2", "angularx-qrcode": "11.0.0", "bootstrap": "^4.1.3", - "buffer": "^6.0.2", + "buffer": "^6.0.3", "cache-chunk-store": "^3.0.0", "chart.js": "^2.9.3", "codelyzer": "^6.0.0", "core-js": "^3.1.4", - "css-loader": "^5.0.1", + "css-loader": "^5.2.6", "debug": "^4.3.1", "dexie": "^3.0.0", "file-loader": "^6.0.0", "focus-visible": "^5.0.2", "hls.js": "^0.14.16", - "html-loader": "^1.0.0", - "html-webpack-plugin": "^4.0.3", + "html-loader": "^2.1.2", + "html-webpack-plugin": "^5.3.1", "https-browserify": "^1.0.0", "jasmine-core": "~3.7.1", "jasmine-spec-reporter": "~7.0.0", @@ -95,42 +94,42 @@ "linkifyjs": "^2.1.5", "lodash-es": "^4.17.4", "markdown-it": "12.0.4", - "mini-css-extract-plugin": "^1.3.1", + "mini-css-extract-plugin": "^1.6.0", + "ngx-uploadx": "^4.1.0", "p2p-media-loader-hlsjs": "^0.6.2", "path-browserify": "^1.0.0", - "primeng": "^11.0.0-rc.1", + "primeng": "^12.0.0-rc.1", "process": "^0.11.10", "protractor": "~7.0.0", "purify-css": "^1.2.5", "raw-loader": "^4.0.0", "rxjs": "^6.5.2", "sanitize-html": "^2.1.2", - "sass": "^1.29.0", - "sass-loader": "^10", - "sass-resources-loader": "^2.0.0", + "sass": "^1.34.0", + "sass-loader": "^11.1.1", "sha.js": "^2.4.11", "socket.io-client": "^4.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.0.0", "stylelint": "^13.13.0", "stylelint-config-sass-guidelines": "^8.0.0", - "terser-webpack-plugin": "^4", - "ts-loader": "^8.0.14", + "terser-webpack-plugin": "^5.1.2", + "ts-loader": "^9.2.2", "tslib": "^2.0.0", "tslint": "~6.1.0", "tslint-angular": "^3.0.2", "tslint-config-standard": "^9.0.0", - "typescript": "~4.1", + "typescript": "~4.2.4", "video.js": "^7", "videojs-contextmenu-pt": "^5.4.1", "videojs-contrib-quality-levels": "^2.0.9", "videojs-dock": "^2.0.2", "videojs-hotkeys": "^0.2.27", "videostream": "~3.2.1", - "webpack-bundle-analyzer": "^4.1.0", - "webpack-cli": "^4.2.0", + "webpack-bundle-analyzer": "^4.4.2", + "webpack-cli": "^4.7.0", "webtorrent": "^0.116.1", "whatwg-fetch": "^3.0.0", - "zone.js": "~0.11.3" + "zone.js": "~0.11.4" } } diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html index f81465f88..6bc1d0448 100644 --- a/client/src/app/+about/about-follows/about-follows.component.html +++ b/client/src/app/+about/about-follows/about-follows.component.html @@ -9,7 +9,7 @@ {{ follower}} - +
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts index c45269be4..dd774a4ef 100644 --- a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts +++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts @@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit { constructor (private markdownService: MarkdownService) { } async ngOnInit () { - this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown) + this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true) } } diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts index 96a737555..880bf4a39 100644 --- a/client/src/app/+about/about-routing.module.ts +++ b/client/src/app/+about/about-routing.module.ts @@ -1,17 +1,15 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' -import { AboutComponent } from './about.component' -import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' -import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' +import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' +import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' +import { AboutComponent } from './about.component' const aboutRoutes: Routes = [ { path: '', component: AboutComponent, - canActivateChild: [ MetaGuard ], children: [ { path: '', diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index f9d097644..2dfb057e7 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss @@ -36,6 +36,8 @@ } a { + @include peertube-word-wrap; + color: pvar(--mainForegroundColor); } diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index 96d1e0f85..e146a5cd2 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts @@ -79,7 +79,13 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { } loadMoreChannels () { - this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination) + const options = { + account: this.account, + componentPagination: this.channelPagination, + sort: '-updatedAt' + } + + this.videoChannelService.listAccountVideoChannels(options) .pipe( tap(res => this.channelPagination.totalItems = res.total), switchMap(res => from(res.data)), diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index 3bf0f7185..2f3792a8d 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { AccountSearchComponent } from './account-search/account-search.component' import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' import { AccountVideosComponent } from './account-videos/account-videos.component' @@ -14,7 +13,6 @@ const accountsRoutes: Routes = [ { path: ':accountId', component: AccountsComponent, - canActivateChild: [ MetaGuard ], children: [ { path: '', diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index fbd7380a9..c69b04a01 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts @@ -66,7 +66,7 @@ export class AccountsComponent implements OnInit, OnDestroy { distinctUntilChanged(), switchMap(accountId => this.accountService.getAccount(accountId)), tap(account => this.onAccount(account)), - switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), + switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })), catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [ HttpStatusCode.BAD_REQUEST_400, HttpStatusCode.NOT_FOUND_404 diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts index 986dae8eb..d029661d3 100644 --- a/client/src/app/+admin/admin-routing.module.ts +++ b/client/src/app/+admin/admin-routing.module.ts @@ -4,7 +4,6 @@ import { ConfigRoutes } from '@app/+admin/config' import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' import { SystemRoutes } from '@app/+admin/system' -import { MetaGuard } from '@ngx-meta/core' import { AdminComponent } from './admin.component' import { FollowsRoutes } from './follows' import { UsersRoutes } from './users' @@ -13,8 +12,6 @@ const adminRoutes: Routes = [ { path: '', component: AdminComponent, - canActivate: [ MetaGuard ], - canActivateChild: [ MetaGuard ], children: [ { path: '', diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 45366f9ec..a7fe20b07 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table' import { NgModule } from '@angular/core' import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' +import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module' +import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup' import { SharedFormModule } from '@app/shared/shared-forms' import { SharedGlobalIconModule } from '@app/shared/shared-icons' import { SharedMainModule } from '@app/shared/shared-main' import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' -import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' import { AdminRoutingModule } from './admin-routing.module' import { AdminComponent } from './admin.component' import { @@ -18,6 +19,7 @@ import { EditBasicConfigurationComponent, EditConfigurationService, EditCustomConfigComponent, + EditHomepageComponent, EditInstanceInformationComponent, EditLiveConfigurationComponent, EditVODTranscodingComponent @@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom SharedVideoCommentModule, SharedActorImageModule, SharedActorImageEditModule, + SharedCustomMarkupModule, TableModule, SelectButtonModule, @@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom EditVODTranscodingComponent, EditLiveConfigurationComponent, EditAdvancedConfigurationComponent, - EditInstanceInformationComponent + EditInstanceInformationComponent, + EditHomepageComponent ], exports: [ diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 84a793ae4..451e6a34a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html @@ -26,22 +26,13 @@
-
- -
+
{{ formErrors.instance.defaultClientRoute }}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts index 34d05f9f3..d50148e7a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts @@ -1,7 +1,9 @@ import { pairwise } from 'rxjs/operators' -import { Component, Input, OnInit } from '@angular/core' +import { SelectOptionsItem } from 'src/types/select-options-item.model' +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' import { FormGroup } from '@angular/forms' +import { MenuService } from '@app/core' import { ServerConfig } from '@shared/models' import { ConfigService } from '../shared/config.service' @@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service' templateUrl: './edit-basic-configuration.component.html', styleUrls: [ './edit-custom-config.component.scss' ] }) -export class EditBasicConfigurationComponent implements OnInit { +export class EditBasicConfigurationComponent implements OnInit, OnChanges { @Input() form: FormGroup @Input() formErrors: any @Input() serverConfig: ServerConfig signupAlertMessage: string + defaultLandingPageOptions: SelectOptionsItem[] = [] constructor ( - private configService: ConfigService + private configService: ConfigService, + private menuService: MenuService ) { } ngOnInit () { + this.buildLandingPageOptions() this.checkSignupField() } + ngOnChanges (changes: SimpleChanges) { + if (changes['serverConfig']) { + this.buildLandingPageOptions() + } + } + getVideoQuotaOptions () { return this.configService.videoQuotaOptions } @@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit { return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true } + buildLandingPageOptions () { + this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) + .map(o => ({ + id: o.path, + label: o.label, + description: o.path + })) + } + private checkSignupField () { const signupControl = this.form.get('signup.enabled') diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index b6365614d..3ceea02ca 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -3,8 +3,16 @@ diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts index d2c179aba..0a6e57904 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts @@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core' -import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' -import { PluginType } from '@shared/models/plugins/plugin.type' +import { PeerTubePluginIndex, PluginType } from '@shared/models' @Component({ selector: 'my-plugin-search', diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index 281c3dcef..1527508f7 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts @@ -81,6 +81,8 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) + if (userUpdate.pluginAuth === 'null') userUpdate.pluginAuth = null + this.userService.updateUser(this.user.id, userUpdate).subscribe( () => { this.notifier.success($localize`User ${this.user.username} updated.`) diff --git a/client/src/app/+home/home-routing.module.ts b/client/src/app/+home/home-routing.module.ts new file mode 100644 index 000000000..1eaee4449 --- /dev/null +++ b/client/src/app/+home/home-routing.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { MetaGuard } from '@ngx-meta/core' +import { HomeComponent } from './home.component' + +const homeRoutes: Routes = [ + { + path: '', + component: HomeComponent, + canActivateChild: [ MetaGuard ] + } +] + +@NgModule({ + imports: [ RouterModule.forChild(homeRoutes) ], + exports: [ RouterModule ] +}) +export class HomeRoutingModule {} diff --git a/client/src/app/+home/home.component.html b/client/src/app/+home/home.component.html new file mode 100644 index 000000000..645b9dc69 --- /dev/null +++ b/client/src/app/+home/home.component.html @@ -0,0 +1,4 @@ +
+
+
+ diff --git a/client/src/app/+home/home.component.scss b/client/src/app/+home/home.component.scss new file mode 100644 index 000000000..6c73e9248 --- /dev/null +++ b/client/src/app/+home/home.component.scss @@ -0,0 +1,3 @@ +.root { + padding-top: 20px; +} diff --git a/client/src/app/+home/home.component.ts b/client/src/app/+home/home.component.ts new file mode 100644 index 000000000..16d3a6df7 --- /dev/null +++ b/client/src/app/+home/home.component.ts @@ -0,0 +1,26 @@ + +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' +import { CustomMarkupService } from '@app/shared/shared-custom-markup' +import { CustomPageService } from '@app/shared/shared-main/custom-page' + +@Component({ + templateUrl: './home.component.html', + styleUrls: [ './home.component.scss' ] +}) + +export class HomeComponent implements OnInit { + @ViewChild('contentWrapper') contentWrapper: ElementRef + + constructor ( + private customMarkupService: CustomMarkupService, + private customPageService: CustomPageService + ) { } + + async ngOnInit () { + this.customPageService.getInstanceHomepage() + .subscribe(async ({ content }) => { + const element = await this.customMarkupService.buildElement(content) + this.contentWrapper.nativeElement.appendChild(element) + }) + } +} diff --git a/client/src/app/+home/home.module.ts b/client/src/app/+home/home.module.ts new file mode 100644 index 000000000..102cdc296 --- /dev/null +++ b/client/src/app/+home/home.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core' +import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup' +import { SharedMainModule } from '@app/shared/shared-main' +import { HomeRoutingModule } from './home-routing.module' +import { HomeComponent } from './home.component' + +@NgModule({ + imports: [ + HomeRoutingModule, + + SharedMainModule, + SharedCustomMarkupModule + ], + + declarations: [ + HomeComponent + ], + + exports: [ + HomeComponent + ], + + providers: [ ] +}) +export class HomeModule { } diff --git a/client/src/app/+home/index.ts b/client/src/app/+home/index.ts new file mode 100644 index 000000000..7c77cf9fd --- /dev/null +++ b/client/src/app/+home/index.ts @@ -0,0 +1,3 @@ +export * from './home-routing.module' +export * from './home.component' +export * from './home.module' diff --git a/client/src/app/+login/login-routing.module.ts b/client/src/app/+login/login-routing.module.ts index 258ddc5c1..c5f0f23c2 100644 --- a/client/src/app/+login/login-routing.module.ts +++ b/client/src/app/+login/login-routing.module.ts @@ -1,14 +1,12 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' -import { LoginComponent } from './login.component' import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' +import { LoginComponent } from './login.component' const loginRoutes: Routes = [ { path: '', component: LoginComponent, - canActivate: [ MetaGuard ], data: { meta: { title: $localize`Login` diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index e2f8660fb..ef39c1a36 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -1,20 +1,19 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { LoginGuard } from '../core' import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' +import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' import { MyAccountComponent } from './my-account.component' -import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' const myAccountRoutes: Routes = [ { path: '', component: MyAccountComponent, - canActivateChild: [ MetaGuard, LoginGuard ], + canActivateChild: [ LoginGuard ], children: [ { path: '', diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index c16368952..a0f2f28f8 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts @@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' import { HttpErrorResponse } from '@angular/common/http' import { AfterViewChecked, Component, OnInit } from '@angular/core' import { AuthService, Notifier, User, UserService } from '@app/core' -import { uploadErrorHandler } from '@app/helpers' +import { genericUploadErrorHandler } from '@app/helpers' @Component({ selector: 'my-account-settings', @@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { this.user.updateAccountAvatar(data.avatar) }, - (err: HttpErrorResponse) => uploadErrorHandler({ + (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index a29af176c..c9173039a 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts @@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { AuthService, Notifier, ServerService } from '@app/core' -import { uploadErrorHandler } from '@app/helpers' +import { genericUploadErrorHandler } from '@app/helpers' import { VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, @@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements this.videoChannel.updateAvatar(data.avatar) }, - (err: HttpErrorResponse) => uploadErrorHandler({ + (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier @@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements this.videoChannel.updateBanner(data.banner) }, - (err: HttpErrorResponse) => uploadErrorHandler({ + (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts index 9e3bf35b4..67b3ee496 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts @@ -68,8 +68,14 @@ channel with the same name (${videoChannel.name})!`, this.authService.userInformationLoaded .pipe(mergeMap(() => { const user = this.authService.getUser() + const options = { + account: user.account, + withStats: true, + search: this.search, + sort: '-updatedAt' + } - return this.videoChannelService.listAccountVideoChannels(user.account, null, true, this.search) + return this.videoChannelService.listAccountVideoChannels(options) })).subscribe(res => { this.videoChannels = res.data this.totalItems = res.total diff --git a/client/src/app/+my-library/my-library-routing.module.ts b/client/src/app/+my-library/my-library-routing.module.ts index d8e5aa562..76894bed8 100644 --- a/client/src/app/+my-library/my-library-routing.module.ts +++ b/client/src/app/+my-library/my-library-routing.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { LoginGuard } from '../core' import { MyHistoryComponent } from './my-history/my-history.component' import { MyLibraryComponent } from './my-library.component' @@ -17,7 +16,7 @@ const myLibraryRoutes: Routes = [ { path: '', component: MyLibraryComponent, - canActivateChild: [ MetaGuard, LoginGuard ], + canActivateChild: [ LoginGuard ], children: [ { path: '', diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html index 088765b20..d0393a2a4 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html @@ -8,13 +8,8 @@ diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts index 0e2395754..7889d0985 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts @@ -1,11 +1,12 @@ -import { switchMap } from 'rxjs/operators' +import { SelectChannelItem } from 'src/types/select-options-item.model' import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' import { AuthService, Notifier } from '@app/core' +import { listUserChannels } from '@app/helpers' import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' +import { VideoOwnershipService } from '@app/shared/shared-main' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { VideoChangeOwnership, VideoChannel } from '@shared/models' +import { VideoChangeOwnership } from '@shared/models' @Component({ selector: 'my-accept-ownership', @@ -18,8 +19,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { @ViewChild('modal', { static: true }) modal: ElementRef videoChangeOwnership: VideoChangeOwnership | undefined = undefined - - videoChannels: VideoChannel[] + videoChannels: SelectChannelItem[] error: string = null @@ -28,7 +28,6 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { private videoOwnershipService: VideoOwnershipService, private notifier: Notifier, private authService: AuthService, - private videoChannelService: VideoChannelService, private modalService: NgbModal ) { super() @@ -37,9 +36,8 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { ngOnInit () { this.videoChannels = [] - this.authService.userInformationLoaded - .pipe(switchMap(() => this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account))) - .subscribe(videoChannels => this.videoChannels = videoChannels.data) + listUserChannels(this.authService) + .subscribe(channels => this.videoChannels = channels) this.buildForm({ channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR diff --git a/client/src/app/+reset-password/reset-password-routing.module.ts b/client/src/app/+reset-password/reset-password-routing.module.ts index 7f1ba2f68..3532cdbc1 100644 --- a/client/src/app/+reset-password/reset-password-routing.module.ts +++ b/client/src/app/+reset-password/reset-password-routing.module.ts @@ -1,16 +1,14 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { ResetPasswordComponent } from './reset-password.component' const resetPasswordRoutes: Routes = [ { path: '', component: ResetPasswordComponent, - canActivate: [ MetaGuard ], data: { meta: { - title: `Reset password` + title: $localize`Reset password` } } } diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html index 1d1e7b868..421bc7f6f 100644 --- a/client/src/app/+search/search-filters.component.html +++ b/client/src/app/+search/search-filters.component.html @@ -16,6 +16,25 @@
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
@@ -44,7 +63,7 @@
- +
@@ -60,7 +79,7 @@
- +
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts index a2af9a942..59aba22ff 100644 --- a/client/src/app/+search/search-filters.component.ts +++ b/client/src/app/+search/search-filters.component.ts @@ -3,6 +3,8 @@ import { ServerService } from '@app/core' import { AdvancedSearch } from '@app/shared/shared-search' import { ServerConfig, VideoConstant } from '@shared/models' +type FormOption = { id: string, label: string } + @Component({ selector: 'my-search-filters', styleUrls: [ './search-filters.component.scss' ], @@ -17,9 +19,10 @@ export class SearchFiltersComponent implements OnInit { videoLicences: VideoConstant[] = [] videoLanguages: VideoConstant[] = [] - publishedDateRanges: { id: string, label: string }[] = [] - sorts: { id: string, label: string }[] = [] - durationRanges: { id: string, label: string }[] = [] + publishedDateRanges: FormOption[] = [] + sorts: FormOption[] = [] + durationRanges: FormOption[] = [] + videoType: FormOption[] = [] publishedDateRange: string durationRange: string @@ -33,10 +36,6 @@ export class SearchFiltersComponent implements OnInit { private serverService: ServerService ) { this.publishedDateRanges = [ - { - id: 'any_published_date', - label: $localize`Any` - }, { id: 'today', label: $localize`Today` @@ -55,11 +54,18 @@ export class SearchFiltersComponent implements OnInit { } ] - this.durationRanges = [ + this.videoType = [ { - id: 'any_duration', - label: $localize`Any` + id: 'vod', + label: $localize`VOD videos` }, + { + id: 'live', + label: $localize`Live videos` + } + ] + + this.durationRanges = [ { id: 'short', label: $localize`Short (< 4 min)` @@ -104,24 +110,26 @@ export class SearchFiltersComponent implements OnInit { this.loadOriginallyPublishedAtYears() } - inputUpdated () { + onInputUpdated () { this.updateModelFromDurationRange() this.updateModelFromPublishedRange() this.updateModelFromOriginallyPublishedAtYears() } formUpdated () { - this.inputUpdated() + this.onInputUpdated() this.filtered.emit(this.advancedSearch) } reset () { this.advancedSearch.reset() + + this.resetOriginalPublicationYears() + this.durationRange = undefined this.publishedDateRange = undefined - this.originallyPublishedStartYear = undefined - this.originallyPublishedEndYear = undefined - this.inputUpdated() + + this.onInputUpdated() } resetField (fieldName: string, value?: any) { @@ -130,7 +138,7 @@ export class SearchFiltersComponent implements OnInit { resetLocalField (fieldName: string, value?: any) { this[fieldName] = value - this.inputUpdated() + this.onInputUpdated() } resetOriginalPublicationYears () { diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts index e5d7d1ede..0d778af0d 100644 --- a/client/src/app/+search/search-routing.module.ts +++ b/client/src/app/+search/search-routing.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' import { SearchComponent } from './search.component' import { VideoLazyLoadResolver } from './video-lazy-load.resolver' @@ -9,7 +8,6 @@ const searchRoutes: Routes = [ { path: '', component: SearchComponent, - canActivate: [ MetaGuard ], data: { meta: { title: $localize`Search` @@ -19,7 +17,6 @@ const searchRoutes: Routes = [ { path: 'lazy-load-video', component: SearchComponent, - canActivate: [ MetaGuard ], resolve: { data: VideoLazyLoadResolver } @@ -27,7 +24,6 @@ const searchRoutes: Routes = [ { path: 'lazy-load-channel', component: SearchComponent, - canActivate: [ MetaGuard ], resolve: { data: ChannelLazyLoadResolver } diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index 8a781cbe4..4381659e1 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts @@ -1,12 +1,11 @@ import { forkJoin, of, Subscription } from 'rxjs' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core' +import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' import { immutableAssign } from '@app/helpers' import { Video, VideoChannel } from '@app/shared/shared-main' import { AdvancedSearch, SearchService } from '@app/shared/shared-search' import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' -import { MetaService } from '@ngx-meta/core' import { SearchTargetType, ServerConfig } from '@shared/models' @Component({ @@ -238,7 +237,10 @@ export class SearchComponent implements OnInit, OnDestroy { } private updateTitle () { - const suffix = this.currentSearch ? ' ' + this.currentSearch : '' + const suffix = this.currentSearch + ? ' ' + this.currentSearch + : '' + this.metaService.setTitle($localize`Search` + suffix) } diff --git a/client/src/app/+signup/+register/register-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts index 61a2fa42d..dabe79fa5 100644 --- a/client/src/app/+signup/+register/register-routing.module.ts +++ b/client/src/app/+signup/+register/register-routing.module.ts @@ -1,14 +1,13 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' import { ServerConfigResolver, UnloggedGuard } from '@app/core' -import { MetaGuard } from '@ngx-meta/core' import { RegisterComponent } from './register.component' const registerRoutes: Routes = [ { path: '', component: RegisterComponent, - canActivate: [ MetaGuard, UnloggedGuard ], + canActivate: [ UnloggedGuard ], data: { meta: { title: $localize`Register` diff --git a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts index 67c80ae93..1bc636345 100644 --- a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts +++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts @@ -1,13 +1,11 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' -import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' +import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' const verifyAccountRoutes: Routes = [ { path: '', - canActivateChild: [ MetaGuard ], children: [ { path: 'email', diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index fcaad8934..4ee052873 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' import { VideoChannelsComponent } from './video-channels.component' @@ -9,7 +8,6 @@ const videoChannelsRoutes: Routes = [ { path: ':videoChannelName', component: VideoChannelsComponent, - canActivateChild: [ MetaGuard ], children: [ { path: '', diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 094b4d3b3..50d030ac9 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html @@ -5,7 +5,7 @@ Basic info -
+
@@ -76,7 +76,7 @@ - Choose the appropriate license for your work. + Choose the appropriate licence for your work. diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss index bc32d7964..c1c7c686d 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss @@ -1,9 +1,3 @@ -// Bootstrap grid utilities require functions, variables and mixins -@import 'node_modules/bootstrap/scss/functions'; -@import 'node_modules/bootstrap/scss/variables'; -@import 'node_modules/bootstrap/scss/mixins'; -@import 'node_modules/bootstrap/scss/grid'; - @import 'variables'; @import 'mixins'; @@ -57,63 +51,60 @@ my-peertube-checkbox { } } -.captions { +.captions-header { + text-align: right; + margin-bottom: 1rem; +} - .captions-header { - text-align: right; - margin-bottom: 1rem; +.create-caption { + @include create-button; +} - .create-caption { - @include create-button; +.caption-entry { + display: flex; + height: 40px; + align-items: center; + + a.caption-entry-label { + @include disable-default-a-behaviour; + + flex-grow: 1; + color: #000; + + &:hover { + opacity: 0.8; } } - .caption-entry { - display: flex; - height: 40px; - align-items: center; - - a.caption-entry-label { - @include disable-default-a-behaviour; - - flex-grow: 1; - color: #000; - - &:hover { - opacity: 0.8; - } - } - - .caption-entry-label { - font-size: 15px; - font-weight: bold; - - margin-right: 20px; - width: 150px; - } - - .caption-entry-state { - width: 200px; - - &.caption-entry-state-create { - color: #39CC0B; - } - - &.caption-entry-state-delete { - color: #FF0000; - } - } - - .caption-entry-delete { - @include peertube-button; - @include grey-button; - } - } - - .no-caption { - text-align: center; + .caption-entry-label { font-size: 15px; + font-weight: bold; + + margin-right: 20px; + width: 150px; } + + .caption-entry-state { + width: 200px; + + &.caption-entry-state-create { + color: #39CC0B; + } + + &.caption-entry-state-delete { + color: #FF0000; + } + } + + .caption-entry-delete { + @include peertube-button; + @include grey-button; + } +} + +.no-caption { + text-align: center; + font-size: 15px; } .submit-container { @@ -143,35 +134,15 @@ p-calendar { } } -// columns for the video -.col-video-edit { - @include make-col-ready(); +.form-columns { + display: grid; - @include media-breakpoint-up(md) { - @include make-col(7); - - + .col-video-edit { - @include make-col(5); - } - } - - @include media-breakpoint-up(xl) { - @include make-col(8); - - + .col-video-edit { - @include make-col(4); - } - } + grid-template-columns: 66% 1fr; + grid-gap: 30px; } -:host-context(.expanded) { - .col-video-edit { - @include media-breakpoint-up(md) { - @include make-col(8); - - + .col-video-edit { - @include make-col(4); - } - } +@include on-small-main-col { + .form-columns { + grid-template-columns: 1fr; } } diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 34119f7ab..3d916dbce 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts @@ -21,8 +21,15 @@ import { import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' import { InstanceService } from '@app/shared/shared-instance' import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' -import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models' -import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' +import { + LiveVideo, + RegisterClientFormFieldOptions, + RegisterClientVideoFieldOptions, + ServerConfig, + VideoConstant, + VideoDetails, + VideoPrivacy +} from '@shared/models' import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' import { VideoEditType } from './video-edit.type' diff --git a/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts new file mode 100644 index 000000000..3392a0d8a --- /dev/null +++ b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts @@ -0,0 +1,48 @@ +import { objectToFormData } from '@app/helpers' +import { resolveUrl, UploaderX } from 'ngx-uploadx' + +/** + * multipart/form-data uploader extending the UploaderX implementation of Google Resumable + * for use with multer + * + * @see https://github.com/kukhariev/ngx-uploadx/blob/637e258fe366b8095203f387a6101a230ee4f8e6/src/uploadx/lib/uploaderx.ts + * @example + * + * options: UploadxOptions = { + * uploaderClass: UploaderXFormData + * }; + */ +export class UploaderXFormData extends UploaderX { + + async getFileUrl (): Promise { + const headers = { + 'X-Upload-Content-Length': this.size.toString(), + 'X-Upload-Content-Type': this.file.type || 'application/octet-stream' + } + + const previewfile = this.metadata.previewfile as any as File + delete this.metadata.previewfile + + const data = objectToFormData(this.metadata) + if (previewfile !== undefined) { + data.append('previewfile', previewfile, previewfile.name) + data.append('thumbnailfile', previewfile, previewfile.name) + } + + await this.request({ + method: 'POST', + body: data, + url: this.endpoint, + headers + }) + + const location = this.getValueFromResponse('location') + if (!location) { + throw new Error('Invalid or missing Location header') + } + + this.offset = this.responseStatus === 201 ? 0 : undefined + + return resolveUrl(location, this.endpoint) + } +} diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts index 3aae24732..23bd5ef76 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts @@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' -import { VideoPrivacy, VideoUpdate } from '@shared/models' +import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models' import { hydrateFormFromVideo } from '../shared/video-edit-utils' import { VideoSend } from './video-send' @@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af this.loadingBar.useRef().complete() this.isImportingVideo = false this.firstStepError.emit() - this.notifier.error(err.message) + + let message = err.message + if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) { + message = $localize`Torrents with only 1 file are supported.` + } + + this.notifier.error(message) } ) } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 4c0b09894..86a779f8a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html @@ -1,12 +1,17 @@ -
+
Select the file to upload
@@ -41,7 +46,13 @@
- + +
@@ -64,6 +75,7 @@ {{ error }}
+
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss index 9549257f6..d9f348a70 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss @@ -47,8 +47,4 @@ margin-left: 10px; } - - .btn-group > input:not(:first-child) { - margin-left: 0; - } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index effb37077..bca1b6eb6 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -1,15 +1,16 @@ -import { Subscription } from 'rxjs' -import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { Router } from '@angular/router' +import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx' +import { UploaderXFormData } from './uploaderx-form-data' import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' -import { scrollToTop, uploadErrorHandler } from '@app/helpers' +import { scrollToTop, genericUploadErrorHandler } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' import { VideoPrivacy } from '@shared/models' import { VideoSend } from './video-send' +import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' @Component({ selector: 'my-video-upload', @@ -20,23 +21,18 @@ import { VideoSend } from './video-send' './video-send.scss' ] }) -export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { +export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate { @Output() firstStepDone = new EventEmitter() @Output() firstStepError = new EventEmitter() @ViewChild('videofileInput') videofileInput: ElementRef - // So that it can be accessed in the template - readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY - userVideoQuotaUsed = 0 userVideoQuotaUsedDaily = 0 isUploadingAudioFile = false isUploadingVideo = false - isUpdatingVideo = false videoUploaded = false - videoUploadObservable: Subscription = null videoUploadPercents = 0 videoUploadedIds = { id: 0, @@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView error: string enableRetryAfterError: boolean + // So that it can be accessed in the template protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC + protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable' + + private uploadxOptions: UploadxOptions + private isUpdatingVideo = false + private fileToUpload: File constructor ( protected formValidatorService: FormValidatorService, @@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView protected videoCaptionService: VideoCaptionService, private userService: UserService, private router: Router, - private hooks: HooksService - ) { + private hooks: HooksService, + private resumableUploadService: UploadxService + ) { super() + + this.uploadxOptions = { + endpoint: this.BASE_VIDEO_UPLOAD_URL, + multiple: false, + token: this.authService.getAccessToken(), + uploaderClass: UploaderXFormData, + retryConfig: { + maxAttempts: 6, + shouldRetry: (code: number) => { + return code < 400 || code >= 501 + } + } + } } get videoExtensions () { return this.serverConfig.video.file.extensions.join(', ') } + onUploadVideoOngoing (state: UploadState) { + switch (state.status) { + case 'error': + const error = state.response?.error || 'Unknow error' + + this.handleUploadError({ + error: new Error(error), + name: 'HttpErrorResponse', + message: error, + ok: false, + headers: new HttpHeaders(state.responseHeaders), + status: +state.responseStatus, + statusText: error, + type: HttpEventType.Response, + url: state.url + }) + break + + case 'cancelled': + this.isUploadingVideo = false + this.videoUploadPercents = 0 + + this.firstStepError.emit() + this.enableRetryAfterError = false + this.error = '' + break + + case 'queue': + this.closeFirstStep(state.name) + break + + case 'uploading': + this.videoUploadPercents = state.progress + break + + case 'paused': + this.notifier.info($localize`Upload on hold`) + break + + case 'complete': + this.videoUploaded = true + this.videoUploadPercents = 100 + + this.videoUploadedIds = state?.response.video + break + } + } + ngOnInit () { super.ngOnInit() @@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView this.userVideoQuotaUsed = data.videoQuotaUsed this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily }) + + this.resumableUploadService.events + .subscribe(state => this.onUploadVideoOngoing(state)) } ngAfterViewInit () { @@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView } ngOnDestroy () { - if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() + this.cancelUpload() } canDeactivate () { @@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView } } - getVideoFile () { - return this.videofileInput.nativeElement.files[0] - } - - setVideoFile (files: FileList) { + onFileDropped (files: FileList) { this.videofileInput.nativeElement.files = files - this.fileChange() + + this.onFileChange({ target: this.videofileInput.nativeElement }) } - getAudioUploadLabel () { - const videofile = this.getVideoFile() - if (!videofile) return $localize`Upload` + onFileChange (event: Event | { target: HTMLInputElement }) { + const file = (event.target as HTMLInputElement).files[0] - return $localize`Upload ${videofile.name}` + if (!file) return + + if (!this.checkGlobalUserQuota(file)) return + if (!this.checkDailyUserQuota(file)) return + + if (this.isAudioFile(file.name)) { + this.isUploadingAudioFile = true + return + } + + this.isUploadingVideo = true + this.fileToUpload = file + + this.uploadFile(file) } - fileChange () { - this.uploadFirstStep() + uploadAudio () { + this.uploadFile(this.getInputVideoFile(), this.previewfileUpload) } retryUpload () { this.enableRetryAfterError = false this.error = '' - this.uploadVideo() + this.uploadFile(this.fileToUpload) } cancelUpload () { - if (this.videoUploadObservable !== null) { - this.videoUploadObservable.unsubscribe() - } - - this.isUploadingVideo = false - this.videoUploadPercents = 0 - this.videoUploadObservable = null - - this.firstStepError.emit() - this.enableRetryAfterError = false - this.error = '' - - this.notifier.info($localize`Upload cancelled`) - } - - uploadFirstStep (clickedOnButton = false) { - const videofile = this.getVideoFile() - if (!videofile) return - - if (!this.checkGlobalUserQuota(videofile)) return - if (!this.checkDailyUserQuota(videofile)) return - - if (clickedOnButton === false && this.isAudioFile(videofile.name)) { - this.isUploadingAudioFile = true - return - } - - // Build name field - const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') - let name: string - - // If the name of the file is very small, keep the extension - if (nameWithoutExtension.length < 3) name = videofile.name - else name = nameWithoutExtension - - const nsfw = this.serverConfig.instance.isNSFW - const waitTranscoding = true - const commentsEnabled = true - const downloadEnabled = true - const channelId = this.firstStepChannelId.toString() - - this.formData = new FormData() - this.formData.append('name', name) - // Put the video "private" -> we are waiting the user validation of the second step - this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) - this.formData.append('nsfw', '' + nsfw) - this.formData.append('commentsEnabled', '' + commentsEnabled) - this.formData.append('downloadEnabled', '' + downloadEnabled) - this.formData.append('waitTranscoding', '' + waitTranscoding) - this.formData.append('channelId', '' + channelId) - this.formData.append('videofile', videofile) - - if (this.previewfileUpload) { - this.formData.append('previewfile', this.previewfileUpload) - this.formData.append('thumbnailfile', this.previewfileUpload) - } - - this.isUploadingVideo = true - this.firstStepDone.emit(name) - - this.form.patchValue({ - name, - privacy: this.firstStepPrivacyId, - nsfw, - channelId: this.firstStepChannelId, - previewfile: this.previewfileUpload - }) - - this.uploadVideo() - } - - uploadVideo () { - this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe( - event => { - if (event.type === HttpEventType.UploadProgress) { - this.videoUploadPercents = Math.round(100 * event.loaded / event.total) - } else if (event instanceof HttpResponse) { - this.videoUploaded = true - - this.videoUploadedIds = event.body.video - - this.videoUploadObservable = null - } - }, - - (err: HttpErrorResponse) => { - // Reset progress (but keep isUploadingVideo true) - this.videoUploadPercents = 0 - this.videoUploadObservable = null - this.enableRetryAfterError = true - - this.error = uploadErrorHandler({ - err, - name: $localize`video`, - notifier: this.notifier, - sticky: false - }) - - if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 || - err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { - this.cancelUpload() - } - } - ) + this.resumableUploadService.control({ action: 'cancel' }) } isPublishingButtonDisabled () { @@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView !this.videoUploadedIds.id } + getAudioUploadLabel () { + const videofile = this.getInputVideoFile() + if (!videofile) return $localize`Upload` + + return $localize`Upload ${videofile.name}` + } + updateSecondStep () { if (this.isPublishingButtonDisabled() || !this.checkForm()) { return @@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView ) } + private getInputVideoFile () { + return this.videofileInput.nativeElement.files[0] + } + + private uploadFile (file: File, previewfile?: File) { + const metadata = { + waitTranscoding: true, + commentsEnabled: true, + downloadEnabled: true, + channelId: this.firstStepChannelId, + nsfw: this.serverConfig.instance.isNSFW, + privacy: VideoPrivacy.PRIVATE.toString(), + filename: file.name, + previewfile: previewfile as any + } + + this.resumableUploadService.handleFiles(file, { + ...this.uploadxOptions, + metadata + }) + + this.isUploadingVideo = true + } + + private handleUploadError (err: HttpErrorResponse) { + // Reset progress (but keep isUploadingVideo true) + this.videoUploadPercents = 0 + this.enableRetryAfterError = true + + this.error = genericUploadErrorHandler({ + err, + name: $localize`video`, + notifier: this.notifier, + sticky: false + }) + + if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { + this.cancelUpload() + } + } + + private closeFirstStep (filename: string) { + const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '') + const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension + + this.form.patchValue({ + name, + privacy: this.firstStepPrivacyId, + nsfw: this.serverConfig.instance.isNSFW, + channelId: this.firstStepChannelId, + previewfile: this.previewfileUpload + }) + + this.firstStepDone.emit(name) + } + private checkGlobalUserQuota (videofile: File) { const bytePipes = new BytesPipe() @@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) const videoQuotaBytes = bytePipes.transform(videoQuota, 0) - const msg = $localize`Your video quota is exceeded with this video ( -video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` + const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` this.notifier.error(msg) return false @@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota const videoSizeBytes = bytePipes.transform(videofile.size, 0) const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) - - const msg = $localize`Your daily video quota is exceeded with this video ( -video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` + const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` this.notifier.error(msg) return false diff --git a/client/src/app/+videos/+video-edit/video-add-routing.module.ts b/client/src/app/+videos/+video-edit/video-add-routing.module.ts index 9ff66bea0..3b9a5ab3a 100644 --- a/client/src/app/+videos/+video-edit/video-add-routing.module.ts +++ b/client/src/app/+videos/+video-edit/video-add-routing.module.ts @@ -1,14 +1,13 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' import { CanDeactivateGuard, LoginGuard } from '@app/core' -import { MetaGuard } from '@ngx-meta/core' import { VideoAddComponent } from './video-add.component' const videoAddRoutes: Routes = [ { path: '', component: VideoAddComponent, - canActivate: [ MetaGuard, LoginGuard ], + canActivate: [ LoginGuard ], canDeactivate: [ CanDeactivateGuard ] } ] diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html index dc8c2f21d..ac75d9ff8 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html @@ -20,8 +20,8 @@ Upload {{ videoName }}
-