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}}
- Show full list
+ Show full list
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 @@
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 @@
+
+ Homepage
+
+
+
+
+
+
- Instance information
+ Information
@@ -13,7 +21,7 @@
- Basic configuration
+ Basic
@@ -40,7 +48,7 @@
- Advanced configuration
+ Advanced
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 4b35d65fc..dc8334dd0 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -1,4 +1,5 @@
+import omit from 'lodash-es/omit'
import { forkJoin } from 'rxjs'
import { SelectOptionsItem } from 'src/types/select-options-item.model'
import { Component, OnInit } from '@angular/core'
@@ -24,9 +25,14 @@ import {
} from '@app/shared/form-validators/custom-config-validators'
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
-import { CustomConfig, ServerConfig } from '@shared/models'
+import { CustomPageService } from '@app/shared/shared-main/custom-page'
+import { CustomConfig, CustomPage, ServerConfig } from '@shared/models'
import { EditConfigurationService } from './edit-configuration.service'
+type ComponentCustomConfig = CustomConfig & {
+ instanceCustomHomepage: CustomPage
+}
+
@Component({
selector: 'my-edit-custom-config',
templateUrl: './edit-custom-config.component.html',
@@ -35,9 +41,11 @@ import { EditConfigurationService } from './edit-configuration.service'
export class EditCustomConfigComponent extends FormReactive implements OnInit {
activeNav: string
- customConfig: CustomConfig
+ customConfig: ComponentCustomConfig
serverConfig: ServerConfig
+ homepage: CustomPage
+
languageItems: SelectOptionsItem[] = []
categoryItems: SelectOptionsItem[] = []
@@ -47,6 +55,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
protected formValidatorService: FormValidatorService,
private notifier: Notifier,
private configService: ConfigService,
+ private customPage: CustomPageService,
private serverService: ServerService,
private editConfigurationService: EditConfigurationService
) {
@@ -56,11 +65,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
ngOnInit () {
this.serverConfig = this.serverService.getTmpConfig()
this.serverService.getConfig()
- .subscribe(config => {
- this.serverConfig = config
- })
+ .subscribe(config => this.serverConfig = config)
- const formGroupData: { [key in keyof CustomConfig ]: any } = {
+ const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = {
instance: {
name: INSTANCE_NAME_VALIDATOR,
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
@@ -215,6 +222,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
disableLocalSearch: null,
isDefaultSearch: null
}
+ },
+
+ instanceCustomHomepage: {
+ content: null
}
}
@@ -250,15 +261,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
}
async formValidated () {
- const value: CustomConfig = this.form.getRawValue()
+ const value: ComponentCustomConfig = this.form.getRawValue()
- this.configService.updateCustomConfig(value)
+ forkJoin([
+ this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
+ this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
+ ])
.subscribe(
- res => {
- this.customConfig = res
+ ([ resConfig ]) => {
+ const instanceCustomHomepage = {
+ content: value.instanceCustomHomepage.content
+ }
+
+ this.customConfig = { ...resConfig, instanceCustomHomepage }
// Reload general configuration
this.serverService.resetConfig()
+ .subscribe(config => this.serverConfig = config)
this.updateForm()
@@ -317,9 +336,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
}
private loadConfigAndUpdateForm () {
- this.configService.getCustomConfig()
- .subscribe(config => {
- this.customConfig = config
+ forkJoin([
+ this.configService.getCustomConfig(),
+ this.customPage.getInstanceHomepage()
+ ])
+ .subscribe(([ config, homepage ]) => {
+ this.customConfig = { ...config, instanceCustomHomepage: homepage }
this.updateForm()
// Force form validation
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
new file mode 100644
index 000000000..c48fa5bf8
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts
new file mode 100644
index 000000000..7decf8f75
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+import { CustomMarkupService } from '@app/shared/shared-custom-markup'
+
+@Component({
+ selector: 'my-edit-homepage',
+ templateUrl: './edit-homepage.component.html',
+ styleUrls: [ './edit-custom-config.component.scss' ]
+})
+export class EditHomepageComponent implements OnInit {
+ @Input() form: FormGroup
+ @Input() formErrors: any
+
+ customMarkdownRenderer: (text: string) => Promise
+
+ constructor (private customMarkup: CustomMarkupService) {
+
+ }
+
+ ngOnInit () {
+ this.customMarkdownRenderer = async (text: string) => {
+ return this.customMarkup.buildElement(text)
+ }
+ }
+}
diff --git a/client/src/app/+admin/config/edit-custom-config/index.ts b/client/src/app/+admin/config/edit-custom-config/index.ts
index 95fcc8f52..4281ad09b 100644
--- a/client/src/app/+admin/config/edit-custom-config/index.ts
+++ b/client/src/app/+admin/config/edit-custom-config/index.ts
@@ -2,6 +2,7 @@ export * from './edit-advanced-configuration.component'
export * from './edit-basic-configuration.component'
export * from './edit-configuration.service'
export * from './edit-custom-config.component'
+export * from './edit-homepage.component'
export * from './edit-instance-information.component'
export * from './edit-live-configuration.component'
export * from './edit-vod-transcoding.component'
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
index 1a95980ae..6af224920 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
@@ -5,8 +5,7 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
import { PluginService } from '@app/core/plugins/plugin.service'
import { compareSemVer } from '@shared/core-utils/miscs/miscs'
-import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
-import { PluginType } from '@shared/models/plugins/plugin.type'
+import { PeerTubePlugin, PluginType } from '@shared/models'
@Component({
selector: 'my-plugin-list-installed',
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
index 6900e8717..8d8f12c48 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
@@ -20,7 +20,7 @@
- {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}"
+ {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}"
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 @@
+
+
@@ -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
-
+
+
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 }}
-
-
+
+
Upload a file
@@ -31,7 +31,7 @@
-
+
Import with URL
@@ -41,7 +41,7 @@
-
+
Import with torrent
@@ -51,7 +51,7 @@
-
+
Go live
diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts
index 441d5a3db..d735c936c 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add.component.ts
@@ -1,4 +1,5 @@
import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
import { ServerConfig } from '@shared/models'
import { VideoEditType } from './shared/video-edit.type'
@@ -22,11 +23,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
secondStepType: VideoEditType
videoName: string
- serverConfig: ServerConfig
+
+ activeNav: string
+
+ private serverConfig: ServerConfig
constructor (
private auth: AuthService,
- private serverService: ServerService
+ private serverService: ServerService,
+ private route: ActivatedRoute,
+ private router: Router
) {}
get userInformationLoaded () {
@@ -42,6 +48,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
.subscribe(config => this.serverConfig = config)
this.user = this.auth.getUser()
+
+ if (this.route.snapshot.fragment) {
+ this.onNavChange(this.route.snapshot.fragment)
+ }
+ }
+
+ onNavChange (newActiveNav: string) {
+ this.activeNav = newActiveNav
+
+ this.router.navigate([], { fragment: this.activeNav })
}
onFirstStepDone (type: VideoEditType, videoName: string) {
diff --git a/client/src/app/+videos/+video-edit/video-add.module.ts b/client/src/app/+videos/+video-edit/video-add.module.ts
index da651119b..e836cf81e 100644
--- a/client/src/app/+videos/+video-edit/video-add.module.ts
+++ b/client/src/app/+videos/+video-edit/video-add.module.ts
@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core'
import { CanDeactivateGuard } from '@app/core'
+import { UploadxModule } from 'ngx-uploadx'
import { VideoEditModule } from './shared/video-edit.module'
import { DragDropDirective } from './video-add-components/drag-drop.directive'
import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
@@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component'
imports: [
VideoAddRoutingModule,
- VideoEditModule
+ VideoEditModule,
+
+ UploadxModule
],
declarations: [
diff --git a/client/src/app/+videos/+video-edit/video-update-routing.module.ts b/client/src/app/+videos/+video-edit/video-update-routing.module.ts
index a04351b05..ba9167dd0 100644
--- a/client/src/app/+videos/+video-edit/video-update-routing.module.ts
+++ b/client/src/app/+videos/+video-edit/video-update-routing.module.ts
@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { CanDeactivateGuard, LoginGuard } from '@app/core'
-import { MetaGuard } from '@ngx-meta/core'
import { VideoUpdateComponent } from './video-update.component'
import { VideoUpdateResolver } from './video-update.resolver'
@@ -9,7 +8,7 @@ const videoUpdateRoutes: Routes = [
{
path: '',
component: VideoUpdateComponent,
- canActivate: [ MetaGuard, LoginGuard ],
+ canActivate: [ LoginGuard ],
canDeactivate: [ CanDeactivateGuard ],
resolve: {
videoData: VideoUpdateResolver
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts
index 276548b79..9172b78a8 100644
--- a/client/src/app/+videos/+video-edit/video-update.resolver.ts
+++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts
@@ -2,7 +2,9 @@ import { forkJoin, of } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
-import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main'
+import { AuthService } from '@app/core'
+import { listUserChannels } from '@app/helpers'
+import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
import { LiveVideoService } from '@app/shared/shared-video-live'
@Injectable()
@@ -10,7 +12,7 @@ export class VideoUpdateResolver implements Resolve {
constructor (
private videoService: VideoService,
private liveVideoService: LiveVideoService,
- private videoChannelService: VideoChannelService,
+ private authService: AuthService,
private videoCaptionService: VideoCaptionService
) {
}
@@ -31,17 +33,7 @@ export class VideoUpdateResolver implements Resolve {
.loadCompleteDescription(video.descriptionPath)
.pipe(map(description => Object.assign(video, { description }))),
- this.videoChannelService
- .listAccountVideoChannels(video.account)
- .pipe(
- map(result => result.data),
- map(videoChannels => videoChannels.map(c => ({
- id: c.id,
- label: c.displayName,
- support: c.support,
- avatarPath: c.avatar?.path
- })))
- ),
+ listUserChannels(this.authService),
this.videoCaptionService
.listCaptions(video.id)
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
index fd379e80e..04f8f0d58 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
@@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
// Before HTML rendering restore line feed for markdown list compatibility
const commentText = this.comment.text.replace(//g, '\r\n')
const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
- this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html)
+ this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html)
this.newParentComments = this.parentComments.concat([ this.comment ])
if (this.comment.account) {
diff --git a/client/src/app/+videos/+video-watch/video-watch-routing.module.ts b/client/src/app/+videos/+video-watch/video-watch-routing.module.ts
index d8fecb87d..cb77685c0 100644
--- a/client/src/app/+videos/+video-watch/video-watch-routing.module.ts
+++ b/client/src/app/+videos/+video-watch/video-watch-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 { VideoWatchComponent } from './video-watch.component'
const videoWatchRoutes: Routes = [
{
path: 'playlist/:playlistId',
- component: VideoWatchComponent,
- canActivate: [ MetaGuard ]
+ component: VideoWatchComponent
},
{
path: ':videoId/comments/:commentId',
@@ -15,8 +13,7 @@ const videoWatchRoutes: Routes = [
},
{
path: ':videoId',
- component: VideoWatchComponent,
- canActivate: [ MetaGuard ]
+ component: VideoWatchComponent
}
]
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss
index 301762695..6124090c9 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/+videos/+video-watch/video-watch.component.scss
@@ -146,6 +146,8 @@ $video-info-margin-left: 44px;
}
.video-info-name {
+ @include peertube-word-wrap;
+
margin-right: 30px;
min-height: 40px; // Align with the action buttons
font-size: 27px;
@@ -173,6 +175,7 @@ $video-info-margin-left: 44px;
a {
@include disable-default-a-behaviour;
+ @include peertube-word-wrap;
color: pvar(--mainForegroundColor);
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 1c510d6b0..88c5cef52 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -9,6 +9,7 @@ import {
AuthUser,
ConfirmService,
MarkdownService,
+ MetaService,
Notifier,
PeerTubeSocket,
RestExtractor,
@@ -25,7 +26,6 @@ import { SupportModalComponent } from '@app/shared/shared-support-modal'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
-import { MetaService } from '@ngx-meta/core'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
@@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private async setVideoDescriptionHTML () {
const html = await this.markdownService.textMarkdownToHTML(this.video.description)
- this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html)
+ this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
}
private setVideoLikesBarTooltipText () {
@@ -674,7 +674,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.player.one('ended', () => {
if (this.video.isLive) {
- this.video.state.id = VideoState.LIVE_ENDED
+ this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED)
}
})
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
index 55040f3c9..bbb02a236 100644
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
@@ -31,7 +31,8 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
private route: ActivatedRoute,
private router: Router,
private auth: AuthService,
- private serverService: ServerService
+ private serverService: ServerService,
+ private redirectService: RedirectService
) {
super(data)
@@ -84,12 +85,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
this.algorithmChangeSub = this.route.queryParams.subscribe(
queryParams => {
- const algorithm = queryParams['alg']
- if (algorithm) {
- this.data.model = algorithm
- } else {
- this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
- }
+ this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
}
)
}
@@ -99,7 +95,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
}
setSort () {
- const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
+ const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
? this.data.model
: undefined
diff --git a/client/src/app/+videos/video-list/trending/video-trending.component.ts b/client/src/app/+videos/video-list/trending/video-trending.component.ts
index e50d6ec3a..ebec672f3 100644
--- a/client/src/app/+videos/video-list/trending/video-trending.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending.component.ts
@@ -35,11 +35,12 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
protected storageService: LocalStorageService,
protected cfr: ComponentFactoryResolver,
private videoService: VideoService,
+ private redirectService: RedirectService,
private hooks: HooksService
) {
super()
- this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
+ this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
this.headerComponentInjector = this.getInjector()
}
@@ -106,7 +107,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
}
protected loadPageRouteParams (queryParams: Params) {
- const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
+ const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
this.sort = this.parseAlgorithm(algorithm)
}
@@ -115,8 +116,10 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
switch (algorithm) {
case 'most-viewed':
return '-trending'
+
case 'most-liked':
return '-likes'
+
default:
return '-' + algorithm as VideoSortField
}
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts
index 16e3b9bb2..f9f476b18 100644
--- a/client/src/app/+videos/videos-routing.module.ts
+++ b/client/src/app/+videos/videos-routing.module.ts
@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { LoginGuard } from '@app/core'
-import { MetaGuard } from '@ngx-meta/core'
import { VideoTrendingComponent } from './video-list'
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
import { VideoLocalComponent } from './video-list/video-local.component'
@@ -13,7 +12,6 @@ const videosRoutes: Routes = [
{
path: '',
component: VideosComponent,
- canActivateChild: [ MetaGuard ],
children: [
{
path: 'overview',
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 0a43ab0ad..4619c4046 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -3,7 +3,7 @@ import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment }
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
import { MenuGuards } from '@app/core/routing/menu-guard.service'
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
-import { PreloadSelectedModulesList } from './core'
+import { MetaGuard, PreloadSelectedModulesList } from './core'
import { EmptyComponent } from './empty.component'
import { RootComponent } from './root.component'
@@ -12,55 +12,72 @@ const routes: Routes = [
path: 'admin',
canActivate: [ MenuGuards.close() ],
canDeactivate: [ MenuGuards.open() ],
- loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule)
+ loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule),
+ canActivateChild: [ MetaGuard ]
+ },
+ {
+ path: 'home',
+ loadChildren: () => import('./+home/home.module').then(m => m.HomeModule)
},
{
path: 'my-account',
- loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule)
+ loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'my-library',
- loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule)
+ loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'verify-account',
- loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule)
+ loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'a',
- loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule)
+ loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'c',
- loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule)
+ loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'about',
- loadChildren: () => import('./+about/about.module').then(m => m.AboutModule)
+ loadChildren: () => import('./+about/about.module').then(m => m.AboutModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'signup',
- loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule)
+ loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'reset-password',
- loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule)
+ loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'login',
- loadChildren: () => import('./+login/login.module').then(m => m.LoginModule)
+ loadChildren: () => import('./+login/login.module').then(m => m.LoginModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'search',
- loadChildren: () => import('./+search/search.module').then(m => m.SearchModule)
+ loadChildren: () => import('./+search/search.module').then(m => m.SearchModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'videos',
- loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule)
+ loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'remote-interaction',
- loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule)
+ loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule),
+ canActivateChild: [ MetaGuard ]
},
{
path: 'video-playlists/watch',
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index e21ada0f1..0543564b4 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -40,8 +40,10 @@
}
.icon-menu {
- background-color: pvar(--mainForegroundColor);
mask-image: url('../assets/images/misc/menu.svg');
+ -webkit-mask-image: url('../assets/images/misc/menu.svg');
+
+ background-color: pvar(--mainForegroundColor);
margin: 0 18px 0 20px;
@media screen and (max-width: $mobile-view) {
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 66d871b4a..863c3f3b5 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}
goToDefaultRoute () {
- return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
+ return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
}
ngOnInit () {
@@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}
this.broadcastMessage = {
- message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
+ message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
dismissable: messageConfig.dismissable,
class: classes[messageConfig.level]
}
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 3cec6d739..9f46d49a2 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -4,9 +4,7 @@ import { APP_BASE_HREF, registerLocaleData } from '@angular/common'
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { ServiceWorkerModule } from '@angular/service-worker'
-import { ServerService } from '@app/core'
import localeOc from '@app/helpers/locales/oc'
-import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { CoreModule } from './core'
@@ -19,12 +17,12 @@ import { CustomModalComponent } from './modal/custom-modal.component'
import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
import { WelcomeModalComponent } from './modal/welcome-modal.component'
+import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
import { SharedFormModule } from './shared/shared-forms'
import { SharedGlobalIconModule } from './shared/shared-icons'
import { SharedInstanceModule } from './shared/shared-instance'
import { SharedMainModule } from './shared/shared-main'
import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
-import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
registerLocaleData(localeOc, 'oc')
@@ -62,22 +60,6 @@ registerLocaleData(localeOc, 'oc')
SharedInstanceModule,
SharedActorImageModule,
- MetaModule.forRoot({
- provide: MetaLoader,
- useFactory: (serverService: ServerService) => {
- return new MetaStaticLoader({
- pageTitlePositioning: PageTitlePositioning.PrependPageTitle,
- pageTitleSeparator: ' - ',
- get applicationName () { return serverService.getTmpConfig().instance.name },
- defaults: {
- get title () { return serverService.getTmpConfig().instance.name },
- get description () { return serverService.getTmpConfig().instance.shortDescription }
- }
- })
- },
- deps: [ ServerService ]
- }),
-
AppRoutingModule // Put it after all the module because it has the 404 route
],
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 3152a7003..de3274544 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -14,7 +14,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
import { Notifier } from './notification'
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
import { RestExtractor, RestService } from './rest'
-import { LoginGuard, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
+import { LoginGuard, MetaGuard, MetaService, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
import { ServerConfigResolver } from './routing/server-config-resolver.service'
import { ScopedTokensService } from './scoped-tokens'
@@ -77,7 +77,10 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
MessageService,
PeerTubeSocket,
ServerConfigResolver,
- CanDeactivateGuard
+ CanDeactivateGuard,
+
+ MetaService,
+ MetaGuard
]
})
export class CoreModule {
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index 502d3bb2f..77592cbb6 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -1,8 +1,19 @@
import { fromEvent } from 'rxjs'
import { debounceTime } from 'rxjs/operators'
import { Injectable } from '@angular/core'
+import { GlobalIconName } from '@app/shared/shared-icons'
+import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
+import { ServerConfig } from '@shared/models/server'
import { ScreenService } from '../wrappers'
+export type MenuLink = {
+ icon: GlobalIconName
+ label: string
+ menuLabel: string
+ path: string
+ priority: number
+}
+
@Injectable()
export class MenuService {
isMenuDisplayed = true
@@ -48,6 +59,53 @@ export class MenuService {
this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
}
+ buildCommonLinks (config: ServerConfig) {
+ let entries: MenuLink[] = [
+ {
+ icon: 'globe' as 'globe',
+ label: $localize`Discover videos`,
+ menuLabel: $localize`Discover`,
+ path: '/videos/overview',
+ priority: 150
+ },
+ {
+ icon: 'trending' as 'trending',
+ label: $localize`Trending videos`,
+ menuLabel: $localize`Trending`,
+ path: '/videos/trending',
+ priority: 140
+ },
+ {
+ icon: 'recently-added' as 'recently-added',
+ label: $localize`Recently added videos`,
+ menuLabel: $localize`Recently added`,
+ path: '/videos/recently-added',
+ priority: 130
+ },
+ {
+ icon: 'octagon' as 'octagon',
+ label: $localize`Local videos`,
+ menuLabel: $localize`Local videos`,
+ path: '/videos/local',
+ priority: 120
+ }
+ ]
+
+ if (config.homepage.enabled) {
+ entries.push({
+ icon: 'home' as 'home',
+ label: $localize`Home`,
+ menuLabel: $localize`Home`,
+ path: '/home',
+ priority: 160
+ })
+ }
+
+ entries = entries.sort(sortObjectComparator('priority', 'desc'))
+
+ return entries
+ }
+
private handleWindowResize () {
// On touch screens, do not handle window resize event since opened menu is handled with a content overlay
if (this.screenService.isInTouchScreen()) return
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts
index 3176cf6a4..418d8603e 100644
--- a/client/src/app/core/renderer/html-renderer.service.ts
+++ b/client/src/app/core/renderer/html-renderer.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { LinkifierService } from './linkifier.service'
-import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html'
+import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
@Injectable()
export class HtmlRendererService {
@@ -20,7 +20,7 @@ export class HtmlRendererService {
})
}
- async toSafeHtml (text: string) {
+ async toSafeHtml (text: string, additionalAllowedTags: string[] = []) {
const [ html ] = await Promise.all([
// Convert possible markdown to html
this.linkifier.linkify(text),
@@ -28,7 +28,11 @@ export class HtmlRendererService {
this.loadSanitizeHtml()
])
- return this.sanitizeHtml(html, SANITIZE_OPTIONS)
+ const options = additionalAllowedTags.length !== 0
+ ? getCustomMarkupSanitizeOptions(additionalAllowedTags)
+ : getSanitizeOptions()
+
+ return this.sanitizeHtml(html, options)
}
private async loadSanitizeHtml () {
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts
index edddb0a66..ca1bf4eb9 100644
--- a/client/src/app/core/renderer/markdown.service.ts
+++ b/client/src/app/core/renderer/markdown.service.ts
@@ -17,12 +17,15 @@ type MarkdownParsers = {
enhancedMarkdownIt: MarkdownIt
enhancedWithHTMLMarkdownIt: MarkdownIt
- completeMarkdownIt: MarkdownIt
+ unsafeMarkdownIt: MarkdownIt
+
+ customPageMarkdownIt: MarkdownIt
}
type MarkdownConfig = {
rules: string[]
html: boolean
+ breaks: boolean
escape?: boolean
}
@@ -35,18 +38,24 @@ export class MarkdownService {
private markdownParsers: MarkdownParsers = {
textMarkdownIt: null,
textWithHTMLMarkdownIt: null,
+
enhancedMarkdownIt: null,
enhancedWithHTMLMarkdownIt: null,
- completeMarkdownIt: null
+
+ unsafeMarkdownIt: null,
+
+ customPageMarkdownIt: null
}
private parsersConfig: MarkdownParserConfigs = {
- textMarkdownIt: { rules: TEXT_RULES, html: false },
- textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, html: true, escape: true },
+ textMarkdownIt: { rules: TEXT_RULES, breaks: true, html: false },
+ textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, breaks: true, html: true, escape: true },
- enhancedMarkdownIt: { rules: ENHANCED_RULES, html: false },
- enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, html: true, escape: true },
+ enhancedMarkdownIt: { rules: ENHANCED_RULES, breaks: true, html: false },
+ enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, breaks: true, html: true, escape: true },
- completeMarkdownIt: { rules: COMPLETE_RULES, html: true }
+ unsafeMarkdownIt: { rules: COMPLETE_RULES, breaks: true, html: true, escape: false },
+
+ customPageMarkdownIt: { rules: COMPLETE_RULES, breaks: false, html: true, escape: true }
}
private emojiModule: any
@@ -54,22 +63,26 @@ export class MarkdownService {
constructor (private htmlRenderer: HtmlRendererService) {}
textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
- if (withHtml) return this.render('textWithHTMLMarkdownIt', markdown, withEmoji)
+ if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji })
- return this.render('textMarkdownIt', markdown, withEmoji)
+ return this.render({ name: 'textMarkdownIt', markdown, withEmoji })
}
enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
- if (withHtml) return this.render('enhancedWithHTMLMarkdownIt', markdown, withEmoji)
+ if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji })
- return this.render('enhancedMarkdownIt', markdown, withEmoji)
+ return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji })
}
- completeMarkdownToHTML (markdown: string) {
- return this.render('completeMarkdownIt', markdown, true)
+ unsafeMarkdownToHTML (markdown: string, _trustedInput: true) {
+ return this.render({ name: 'unsafeMarkdownIt', markdown, withEmoji: true })
}
- async processVideoTimestamps (html: string) {
+ customPageMarkdownToHTML (markdown: string, additionalAllowedTags: string[]) {
+ return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
+ }
+
+ processVideoTimestamps (html: string) {
return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
const url = buildVideoLink({ startTime: t })
@@ -77,7 +90,13 @@ export class MarkdownService {
})
}
- private async render (name: keyof MarkdownParsers, markdown: string, withEmoji = false) {
+ private async render (options: {
+ name: keyof MarkdownParsers
+ markdown: string
+ withEmoji: boolean
+ additionalAllowedTags?: string[]
+ }) {
+ const { name, markdown, withEmoji, additionalAllowedTags } = options
if (!markdown) return ''
const config = this.parsersConfig[ name ]
@@ -96,7 +115,7 @@ export class MarkdownService {
let html = this.markdownParsers[ name ].render(markdown)
html = this.avoidTruncatedTags(html)
- if (config.escape) return this.htmlRenderer.toSafeHtml(html)
+ if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
return html
}
@@ -105,7 +124,7 @@ export class MarkdownService {
// FIXME: import('...') returns a struct module, containing a "default" field
const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
- const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
+ const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: config.breaks, html: config.html })
for (const rule of config.rules) {
markdownIt.enable(rule)
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
index 239c27caf..4314ea475 100644
--- a/client/src/app/core/routing/index.ts
+++ b/client/src/app/core/routing/index.ts
@@ -3,6 +3,8 @@ export * from './custom-reuse-strategy'
export * from './disable-for-reuse-hook'
export * from './login-guard.service'
export * from './menu-guard.service'
+export * from './meta-guard.service'
+export * from './meta.service'
export * from './preload-selected-modules-list'
export * from './redirect.service'
export * from './server-config-resolver.service'
diff --git a/client/src/app/core/routing/meta-guard.service.ts b/client/src/app/core/routing/meta-guard.service.ts
new file mode 100644
index 000000000..bedb3450e
--- /dev/null
+++ b/client/src/app/core/routing/meta-guard.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core'
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'
+import { MetaService } from './meta.service'
+
+@Injectable()
+export class MetaGuard implements CanActivate, CanActivateChild {
+
+ constructor (private meta: MetaService) { }
+
+ canActivate (route: ActivatedRouteSnapshot): boolean {
+ const metaSettings = route.data?.meta
+
+ if (metaSettings) {
+ this.meta.update(metaSettings)
+ }
+
+ return true
+ }
+
+ canActivateChild (route: ActivatedRouteSnapshot): boolean {
+ return this.canActivate(route)
+ }
+}
diff --git a/client/src/app/core/routing/meta.service.ts b/client/src/app/core/routing/meta.service.ts
new file mode 100644
index 000000000..a5ac778dc
--- /dev/null
+++ b/client/src/app/core/routing/meta.service.ts
@@ -0,0 +1,40 @@
+import { Injectable } from '@angular/core'
+import { Meta, Title } from '@angular/platform-browser'
+import { HTMLServerConfig } from '@shared/models/server'
+import { ServerService } from '../server'
+
+export interface MetaSettings {
+ title?: string
+}
+
+@Injectable()
+export class MetaService {
+ private config: HTMLServerConfig
+
+ constructor (
+ private titleService: Title,
+ private meta: Meta,
+ private server: ServerService
+ ) {
+ this.config = this.server.getTmpConfig()
+ this.server.getConfig()
+ .subscribe(config => this.config = config)
+ }
+
+ setTitle (subTitle?: string) {
+ let title = ''
+ if (subTitle) title += `${subTitle} - `
+
+ title += this.config.instance.name
+
+ this.titleService.setTitle(title)
+ }
+
+ setTag (name: string, value: string) {
+ this.meta.addTag({ name, content: value })
+ }
+
+ update (meta: MetaSettings) {
+ this.setTitle(meta.title)
+ }
+}
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 6d26fb504..cf690a4d0 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -6,14 +6,14 @@ import { ServerService } from '../server'
export class RedirectService {
// Default route could change according to the instance configuration
static INIT_DEFAULT_ROUTE = '/videos/trending'
- static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
- static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
private previousUrl: string
private currentUrl: string
private redirectingToHomepage = false
+ private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
+ private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
constructor (
private router: Router,
@@ -22,10 +22,10 @@ export class RedirectService {
// The config is first loaded from the cache so try to get the default route
const tmpConfig = this.serverService.getTmpConfig()
if (tmpConfig?.instance?.defaultClientRoute) {
- RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
+ this.defaultRoute = tmpConfig.instance.defaultClientRoute
}
if (tmpConfig?.trending?.videos?.algorithms?.default) {
- RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default
+ this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default
}
// Load default route
@@ -34,13 +34,8 @@ export class RedirectService {
const defaultRouteConfig = config.instance.defaultClientRoute
const defaultTrendingConfig = config.trending.videos.algorithms.default
- if (defaultRouteConfig) {
- RedirectService.DEFAULT_ROUTE = defaultRouteConfig
- }
-
- if (defaultTrendingConfig) {
- RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
- }
+ if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
+ if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
})
// Track previous url
@@ -53,6 +48,14 @@ export class RedirectService {
})
}
+ getDefaultRoute () {
+ return this.defaultRoute
+ }
+
+ getDefaultTrendingAlgorithm () {
+ return this.defaultTrendingAlgorithm
+ }
+
redirectToPreviousRoute () {
const exceptions = [
'/verify-account',
@@ -72,21 +75,21 @@ export class RedirectService {
this.redirectingToHomepage = true
- console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
+ console.log('Redirecting to %s...', this.defaultRoute)
- this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+ this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
.then(() => this.redirectingToHomepage = false)
.catch(() => {
this.redirectingToHomepage = false
console.error(
'Cannot navigate to %s, resetting default route to %s.',
- RedirectService.DEFAULT_ROUTE,
+ this.defaultRoute,
RedirectService.INIT_DEFAULT_ROUTE
)
- RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
- return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+ this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
+ return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
})
}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 906191ae1..5b1b7603f 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -3,7 +3,6 @@ import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
import { HttpClient } from '@angular/common/http'
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
-import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
import { environment } from '../../../environments/environment'
@@ -16,8 +15,6 @@ export class ServerService {
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
- private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
-
configReloaded = new Subject()
private localeObservable: Observable
@@ -176,6 +173,9 @@ export class ServerService {
disableLocalSearch: false,
isDefaultSearch: false
}
+ },
+ homepage: {
+ enabled: false
}
}
@@ -201,9 +201,7 @@ export class ServerService {
this.configReset = true
// Notify config update
- this.getConfig().subscribe(() => {
- // empty, to fire a reset config event
- })
+ return this.getConfig()
}
getConfig () {
@@ -212,7 +210,6 @@ export class ServerService {
if (!this.configObservable) {
this.configObservable = this.http.get(ServerService.BASE_CONFIG_URL)
.pipe(
- tap(config => this.saveConfigLocally(config)),
tap(config => {
this.config = config
this.configLoaded = true
@@ -343,20 +340,15 @@ export class ServerService {
)
}
- private saveConfigLocally (config: ServerConfig) {
- peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
- }
-
private loadConfigLocally () {
- const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
+ const configString = window['PeerTubeServerConfig']
+ if (!configString) return
- if (configString) {
- try {
- const parsed = JSON.parse(configString)
- Object.assign(this.config, parsed)
- } catch (err) {
- console.error('Cannot parse config saved in local storage.', err)
- }
+ try {
+ const parsed = JSON.parse(configString)
+ Object.assign(this.config, parsed)
+ } catch (err) {
+ console.error('Cannot parse config saved in from index.html.', err)
}
}
}
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts
index 4c4611d01..e7a5ae17a 100644
--- a/client/src/app/core/theme/theme.service.ts
+++ b/client/src/app/core/theme/theme.service.ts
@@ -82,7 +82,19 @@ export class ThemeService {
: this.userService.getAnonymousUser().theme
if (theme !== 'instance-default') return theme
- return this.serverConfig.theme.default
+
+ const instanceTheme = this.serverConfig.theme.default
+ if (instanceTheme !== 'default') return instanceTheme
+
+ // Default to dark theme if available and wanted by the user
+ if (
+ this.themes.find(t => t.name === 'dark') &&
+ window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
+ ) {
+ return 'dark'
+ }
+
+ return instanceTheme
}
private loadTheme (name: string) {
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index a1747af3c..94f6def26 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -1,4 +1,4 @@
-import { map } from 'rxjs/operators'
+import { first, map } from 'rxjs/operators'
import { SelectChannelItem } from 'src/types/select-options-item.model'
import { DatePipe } from '@angular/common'
import { HttpErrorResponse } from '@angular/common/http'
@@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) {
function listUserChannels (authService: AuthService) {
return authService.userInformationLoaded
- .pipe(map(() => {
- const user = authService.getUser()
- if (!user) return undefined
+ .pipe(
+ first(),
+ map(() => {
+ const user = authService.getUser()
+ if (!user) return undefined
- const videoChannels = user.videoChannels
- if (Array.isArray(videoChannels) === false) return undefined
+ const videoChannels = user.videoChannels
+ if (Array.isArray(videoChannels) === false) return undefined
- return videoChannels.map(c => ({
- id: c.id,
- label: c.displayName,
- support: c.support,
- avatarPath: c.avatar?.path
- }) as SelectChannelItem)
- }))
+ return videoChannels
+ .sort((a, b) => {
+ if (a.updatedAt < b.updatedAt) return 1
+ if (a.updatedAt > b.updatedAt) return -1
+ return 0
+ })
+ .map(c => ({
+ id: c.id,
+ label: c.displayName,
+ support: c.support,
+ avatarPath: c.avatar?.path
+ }) as SelectChannelItem)
+ })
+ )
}
function getAbsoluteAPIUrl () {
@@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
)
}
-function uploadErrorHandler (parameters: {
- err: HttpErrorResponse
+function genericUploadErrorHandler (parameters: {
+ err: Pick
name: string
notifier: Notifier
sticky?: boolean
@@ -180,6 +189,9 @@ function uploadErrorHandler (parameters: {
if (err instanceof ErrorEvent) { // network error
message = $localize`The connection was interrupted`
notifier.error(message, title, null, sticky)
+ } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
+ message = $localize`The server encountered an error`
+ notifier.error(message, title, null, sticky)
} else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
notifier.error(message, title, null, sticky)
@@ -210,5 +222,5 @@ export {
isInViewport,
isXPercentInViewport,
listUserChannels,
- uploadErrorHandler
+ genericUploadErrorHandler
}
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 3a7ffcbb6..2c2c4f260 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -123,24 +123,9 @@
ON {{instanceName}}
-
-
-
-
-
-
-
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 8fa1de326..2f7e0cf07 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators'
import { ViewportScroller } from '@angular/common'
import { Component, OnInit, ViewChild } from '@angular/core'
import { Router } from '@angular/router'
-import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core'
+import {
+ AuthService,
+ AuthStatus,
+ AuthUser,
+ MenuLink,
+ MenuService,
+ RedirectService,
+ ScreenService,
+ ServerService,
+ UserService
+} from '@app/core'
import { scrollToTop } from '@app/helpers'
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
@@ -35,6 +45,8 @@ export class MenuComponent implements OnInit {
currentInterfaceLanguage: string
+ commonMenuLinks: MenuLink[] = []
+
private languages: VideoConstant[] = []
private serverConfig: ServerConfig
private routesPerRight: { [role in UserRight]?: string } = {
@@ -80,7 +92,10 @@ export class MenuComponent implements OnInit {
ngOnInit () {
this.serverConfig = this.serverService.getTmpConfig()
this.serverService.getConfig()
- .subscribe(config => this.serverConfig = config)
+ .subscribe(config => {
+ this.serverConfig = config
+ this.buildMenuLinks()
+ })
this.isLoggedIn = this.authService.isLoggedIn()
if (this.isLoggedIn === true) {
@@ -241,6 +256,10 @@ export class MenuComponent implements OnInit {
}
}
+ private buildMenuLinks () {
+ this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig)
+ }
+
private buildUserLanguages () {
if (!this.user) {
this.videoLanguages = []
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts
index 8c12d3c4c..08372d8ad 100644
--- a/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts
+++ b/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts
@@ -42,7 +42,7 @@ export class ActorBannerEditComponent implements OnInit {
this.bannerExtensions = config.banner.file.extensions.join(', ')
// tslint:disable:max-line-length
- this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
+ this.bannerFormat = $localize`ratio 6/1, recommended size: 1920x317, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
})
}
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html
new file mode 100644
index 000000000..da81006b9
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html
@@ -0,0 +1,8 @@
+
+
+
+
{{ channel.displayName }}
+
{{ channel.name }}
+
+
{{ channel.description }}
+
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss
new file mode 100644
index 000000000..85018afe2
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss
@@ -0,0 +1,9 @@
+@import '_variables';
+@import '_mixins';
+
+.channel {
+ border-radius: 15px;
+ padding: 10px;
+ width: min-content;
+ border: 1px solid pvar(--mainColor);
+}
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts
new file mode 100644
index 000000000..97bb5567e
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts
@@ -0,0 +1,26 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { VideoChannel, VideoChannelService } from '../shared-main'
+
+/*
+ * Markup component that creates a channel miniature only
+*/
+
+@Component({
+ selector: 'my-channel-miniature-markup',
+ templateUrl: 'channel-miniature-markup.component.html',
+ styleUrls: [ 'channel-miniature-markup.component.scss' ]
+})
+export class ChannelMiniatureMarkupComponent implements OnInit {
+ @Input() name: string
+
+ channel: VideoChannel
+
+ constructor (
+ private channelService: VideoChannelService
+ ) { }
+
+ ngOnInit () {
+ this.channelService.getVideoChannel(this.name)
+ .subscribe(channel => this.channel = channel)
+ }
+}
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
new file mode 100644
index 000000000..ffaf15710
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -0,0 +1,136 @@
+import { ComponentRef, Injectable } from '@angular/core'
+import { MarkdownService } from '@app/core'
+import {
+ ChannelMiniatureMarkupData,
+ EmbedMarkupData,
+ PlaylistMiniatureMarkupData,
+ VideoMiniatureMarkupData,
+ VideosListMarkupData
+} from '@shared/models'
+import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
+import { DynamicElementService } from './dynamic-element.service'
+import { EmbedMarkupComponent } from './embed-markup.component'
+import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
+import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
+import { VideosListMarkupComponent } from './videos-list-markup.component'
+
+type BuilderFunction = (el: HTMLElement) => ComponentRef
+
+@Injectable()
+export class CustomMarkupService {
+ private builders: { [ selector: string ]: BuilderFunction } = {
+ 'peertube-video-embed': el => this.embedBuilder(el, 'video'),
+ 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
+ 'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
+ 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
+ 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
+ 'peertube-videos-list': el => this.videosListBuilder(el)
+ }
+
+ constructor (
+ private dynamicElementService: DynamicElementService,
+ private markdown: MarkdownService
+ ) { }
+
+ async buildElement (text: string) {
+ const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
+
+ const rootElement = document.createElement('div')
+ rootElement.innerHTML = html
+
+ for (const selector of this.getSupportedTags()) {
+ rootElement.querySelectorAll(selector)
+ .forEach((e: HTMLElement) => {
+ try {
+ const component = this.execBuilder(selector, e)
+
+ this.dynamicElementService.injectElement(e, component)
+ } catch (err) {
+ console.error('Cannot inject component %s.', selector, err)
+ }
+ })
+ }
+
+ return rootElement
+ }
+
+ private getSupportedTags () {
+ return Object.keys(this.builders)
+ }
+
+ private execBuilder (selector: string, el: HTMLElement) {
+ return this.builders[selector](el)
+ }
+
+ private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
+ const data = el.dataset as EmbedMarkupData
+ const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
+
+ this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
+
+ return component
+ }
+
+ private videoMiniatureBuilder (el: HTMLElement) {
+ const data = el.dataset as VideoMiniatureMarkupData
+ const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
+
+ this.dynamicElementService.setModel(component, { uuid: data.uuid })
+
+ return component
+ }
+
+ private playlistMiniatureBuilder (el: HTMLElement) {
+ const data = el.dataset as PlaylistMiniatureMarkupData
+ const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
+
+ this.dynamicElementService.setModel(component, { uuid: data.uuid })
+
+ return component
+ }
+
+ private channelMiniatureBuilder (el: HTMLElement) {
+ const data = el.dataset as ChannelMiniatureMarkupData
+ const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
+
+ this.dynamicElementService.setModel(component, { name: data.name })
+
+ return component
+ }
+
+ private videosListBuilder (el: HTMLElement) {
+ const data = el.dataset as VideosListMarkupData
+ const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
+
+ const model = {
+ title: data.title,
+ description: data.description,
+ sort: data.sort,
+ categoryOneOf: this.buildArrayNumber(data.categoryOneOf),
+ languageOneOf: this.buildArrayString(data.languageOneOf),
+ count: this.buildNumber(data.count) || 10
+ }
+
+ this.dynamicElementService.setModel(component, model)
+
+ return component
+ }
+
+ private buildNumber (value: string) {
+ if (!value) return undefined
+
+ return parseInt(value, 10)
+ }
+
+ private buildArrayNumber (value: string) {
+ if (!value) return undefined
+
+ return value.split(',').map(v => parseInt(v, 10))
+ }
+
+ private buildArrayString (value: string) {
+ if (!value) return undefined
+
+ return value.split(',')
+ }
+}
diff --git a/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts
new file mode 100644
index 000000000..e967e30ac
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts
@@ -0,0 +1,57 @@
+import {
+ ApplicationRef,
+ ComponentFactoryResolver,
+ ComponentRef,
+ EmbeddedViewRef,
+ Injectable,
+ Injector,
+ OnChanges,
+ SimpleChange,
+ SimpleChanges,
+ Type
+} from '@angular/core'
+
+@Injectable()
+export class DynamicElementService {
+
+ constructor (
+ private injector: Injector,
+ private applicationRef: ApplicationRef,
+ private componentFactoryResolver: ComponentFactoryResolver
+ ) { }
+
+ createElement (ofComponent: Type) {
+ const div = document.createElement('div')
+
+ const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent)
+ .create(this.injector, [], div)
+
+ return component
+ }
+
+ injectElement (wrapper: HTMLElement, componentRef: ComponentRef) {
+ const hostView = componentRef.hostView as EmbeddedViewRef
+
+ this.applicationRef.attachView(hostView)
+ wrapper.appendChild(hostView.rootNodes[0])
+ }
+
+ setModel (componentRef: ComponentRef, attributes: Partial) {
+ const changes: SimpleChanges = {}
+
+ for (const key of Object.keys(attributes)) {
+ const previousValue = componentRef.instance[key]
+ const newValue = attributes[key]
+
+ componentRef.instance[key] = newValue
+ changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined)
+ }
+
+ const component = componentRef.instance
+ if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') {
+ (component as unknown as OnChanges).ngOnChanges(changes)
+ }
+
+ componentRef.changeDetectorRef.detectChanges()
+ }
+}
diff --git a/client/src/app/shared/shared-custom-markup/embed-markup.component.ts b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts
new file mode 100644
index 000000000..a854d89f6
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts
@@ -0,0 +1,22 @@
+import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
+import { environment } from 'src/environments/environment'
+import { Component, ElementRef, Input, OnInit } from '@angular/core'
+
+@Component({
+ selector: 'my-embed-markup',
+ template: ''
+})
+export class EmbedMarkupComponent implements OnInit {
+ @Input() uuid: string
+ @Input() type: 'video' | 'playlist' = 'video'
+
+ constructor (private el: ElementRef) { }
+
+ ngOnInit () {
+ const link = this.type === 'video'
+ ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` })
+ : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` })
+
+ this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
+ }
+}
diff --git a/client/src/app/shared/shared-custom-markup/index.ts b/client/src/app/shared/shared-custom-markup/index.ts
new file mode 100644
index 000000000..14bde3ea9
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/index.ts
@@ -0,0 +1,3 @@
+export * from './custom-markup.service'
+export * from './dynamic-element.service'
+export * from './shared-custom-markup.module'
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html
new file mode 100644
index 000000000..4e1d1a13f
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html
@@ -0,0 +1,2 @@
+
+
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss
new file mode 100644
index 000000000..281cef726
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss
@@ -0,0 +1,7 @@
+@import '_variables';
+@import '_mixins';
+
+my-video-playlist-miniature {
+ display: inline-block;
+ width: $video-thumbnail-width;
+}
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts
new file mode 100644
index 000000000..7aee450f1
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts
@@ -0,0 +1,38 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { MiniatureDisplayOptions } from '../shared-video-miniature'
+import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist'
+
+/*
+ * Markup component that creates a playlist miniature only
+*/
+
+@Component({
+ selector: 'my-playlist-miniature-markup',
+ templateUrl: 'playlist-miniature-markup.component.html',
+ styleUrls: [ 'playlist-miniature-markup.component.scss' ]
+})
+export class PlaylistMiniatureMarkupComponent implements OnInit {
+ @Input() uuid: string
+
+ playlist: VideoPlaylist
+
+ displayOptions: MiniatureDisplayOptions = {
+ date: true,
+ views: true,
+ by: true,
+ avatar: false,
+ privacyLabel: false,
+ privacyText: false,
+ state: false,
+ blacklistInfo: false
+ }
+
+ constructor (
+ private playlistService: VideoPlaylistService
+ ) { }
+
+ ngOnInit () {
+ this.playlistService.getVideoPlaylist(this.uuid)
+ .subscribe(playlist => this.playlist = playlist)
+ }
+}
diff --git a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
new file mode 100644
index 000000000..4bbb71588
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
@@ -0,0 +1,49 @@
+
+import { CommonModule } from '@angular/common'
+import { NgModule } from '@angular/core'
+import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
+import { SharedGlobalIconModule } from '../shared-icons'
+import { SharedMainModule } from '../shared-main'
+import { SharedVideoMiniatureModule } from '../shared-video-miniature'
+import { SharedVideoPlaylistModule } from '../shared-video-playlist'
+import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
+import { CustomMarkupService } from './custom-markup.service'
+import { DynamicElementService } from './dynamic-element.service'
+import { EmbedMarkupComponent } from './embed-markup.component'
+import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
+import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
+import { VideosListMarkupComponent } from './videos-list-markup.component'
+
+@NgModule({
+ imports: [
+ CommonModule,
+
+ SharedMainModule,
+ SharedGlobalIconModule,
+ SharedVideoMiniatureModule,
+ SharedVideoPlaylistModule,
+ SharedActorImageModule
+ ],
+
+ declarations: [
+ VideoMiniatureMarkupComponent,
+ PlaylistMiniatureMarkupComponent,
+ ChannelMiniatureMarkupComponent,
+ EmbedMarkupComponent,
+ VideosListMarkupComponent
+ ],
+
+ exports: [
+ VideoMiniatureMarkupComponent,
+ PlaylistMiniatureMarkupComponent,
+ ChannelMiniatureMarkupComponent,
+ VideosListMarkupComponent,
+ EmbedMarkupComponent
+ ],
+
+ providers: [
+ CustomMarkupService,
+ DynamicElementService
+ ]
+})
+export class SharedCustomMarkupModule { }
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html
new file mode 100644
index 000000000..9b4930b6d
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html
@@ -0,0 +1,6 @@
+
+
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss
new file mode 100644
index 000000000..81e265f29
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss
@@ -0,0 +1,7 @@
+@import '_variables';
+@import '_mixins';
+
+my-video-miniature {
+ display: inline-block;
+ width: $video-thumbnail-width;
+}
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts
new file mode 100644
index 000000000..79add0c3b
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts
@@ -0,0 +1,44 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { AuthService } from '@app/core'
+import { Video, VideoService } from '../shared-main'
+import { MiniatureDisplayOptions } from '../shared-video-miniature'
+
+/*
+ * Markup component that creates a video miniature only
+*/
+
+@Component({
+ selector: 'my-video-miniature-markup',
+ templateUrl: 'video-miniature-markup.component.html',
+ styleUrls: [ 'video-miniature-markup.component.scss' ]
+})
+export class VideoMiniatureMarkupComponent implements OnInit {
+ @Input() uuid: string
+
+ video: Video
+
+ displayOptions: MiniatureDisplayOptions = {
+ date: true,
+ views: true,
+ by: true,
+ avatar: false,
+ privacyLabel: false,
+ privacyText: false,
+ state: false,
+ blacklistInfo: false
+ }
+
+ constructor (
+ private auth: AuthService,
+ private videoService: VideoService
+ ) { }
+
+ getUser () {
+ return this.auth.getUser()
+ }
+
+ ngOnInit () {
+ this.videoService.getVideo({ videoId: this.uuid })
+ .subscribe(video => this.video = video)
+ }
+}
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html
new file mode 100644
index 000000000..501f35e04
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html
@@ -0,0 +1,13 @@
+
+
{{ title }}
+
{{ description }}
+
+
+
+
+
+
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss
new file mode 100644
index 000000000..dcd931090
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss
@@ -0,0 +1,9 @@
+@import '_variables';
+@import '_mixins';
+
+my-video-miniature {
+ margin-right: 15px;
+ display: inline-block;
+ min-width: $video-thumbnail-width;
+ max-width: $video-thumbnail-width;
+}
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts
new file mode 100644
index 000000000..cc25d0a51
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts
@@ -0,0 +1,60 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { AuthService } from '@app/core'
+import { VideoSortField } from '@shared/models'
+import { Video, VideoService } from '../shared-main'
+import { MiniatureDisplayOptions } from '../shared-video-miniature'
+
+/*
+ * Markup component list videos depending on criterias
+*/
+
+@Component({
+ selector: 'my-videos-list-markup',
+ templateUrl: 'videos-list-markup.component.html',
+ styleUrls: [ 'videos-list-markup.component.scss' ]
+})
+export class VideosListMarkupComponent implements OnInit {
+ @Input() title: string
+ @Input() description: string
+ @Input() sort = '-publishedAt'
+ @Input() categoryOneOf: number[]
+ @Input() languageOneOf: string[]
+ @Input() count = 10
+
+ videos: Video[]
+
+ displayOptions: MiniatureDisplayOptions = {
+ date: true,
+ views: true,
+ by: true,
+ avatar: false,
+ privacyLabel: false,
+ privacyText: false,
+ state: false,
+ blacklistInfo: false
+ }
+
+ constructor (
+ private auth: AuthService,
+ private videoService: VideoService
+ ) { }
+
+ getUser () {
+ return this.auth.getUser()
+ }
+
+ ngOnInit () {
+ const options = {
+ videoPagination: {
+ currentPage: 1,
+ itemsPerPage: this.count
+ },
+ categoryOneOf: this.categoryOneOf,
+ languageOneOf: this.languageOneOf,
+ sort: this.sort as VideoSortField
+ }
+
+ this.videoService.getVideos(options)
+ .subscribe(({ data }) => this.videos = data)
+ }
+}
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html
index 513b543cd..6e70e2f37 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.html
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html
@@ -19,6 +19,7 @@
Complete preview
+
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
index 9b3ab9cf3..a233a4205 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -1,9 +1,10 @@
-import { ViewportScroller } from '@angular/common'
import truncate from 'lodash-es/truncate'
import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
+import { ViewportScroller } from '@angular/common'
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
+import { SafeHtml } from '@angular/platform-browser'
import { MarkdownService, ScreenService } from '@app/core'
@Component({
@@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core'
export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
@Input() content = ''
+
@Input() classes: string[] | { [klass: string]: any[] | any } = []
+
@Input() textareaMaxWidth = '100%'
@Input() textareaHeight = '150px'
+
@Input() truncate: number
+
@Input() markdownType: 'text' | 'enhanced' = 'text'
+ @Input() customMarkdownRenderer?: (text: string) => Promise
+
@Input() markdownVideo = false
+
@Input() name = 'description'
@ViewChild('textarea') textareaElement: ElementRef
+ @ViewChild('previewElement') previewElement: ElementRef
+
+ truncatedPreviewHTML: SafeHtml | string = ''
+ previewHTML: SafeHtml | string = ''
- truncatedPreviewHTML = ''
- previewHTML = ''
isMaximized = false
maximizeInText = $localize`Maximize editor`
@@ -115,10 +125,31 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
}
private async markdownRender (text: string) {
- const html = this.markdownType === 'text' ?
- await this.markdownService.textMarkdownToHTML(text) :
- await this.markdownService.enhancedMarkdownToHTML(text)
+ let html: string
- return this.markdownVideo ? this.markdownService.processVideoTimestamps(html) : html
+ if (this.customMarkdownRenderer) {
+ const result = await this.customMarkdownRenderer(text)
+
+ if (result instanceof HTMLElement) {
+ html = ''
+
+ const wrapperElement = this.previewElement.nativeElement as HTMLElement
+ wrapperElement.innerHTML = ''
+ wrapperElement.appendChild(result)
+ return
+ }
+
+ html = result
+ } else if (this.markdownType === 'text') {
+ html = await this.markdownService.textMarkdownToHTML(text)
+ } else {
+ html = await this.markdownService.enhancedMarkdownToHTML(text)
+ }
+
+ if (this.markdownVideo) {
+ html = this.markdownService.processVideoTimestamps(html)
+ }
+
+ return html
}
}
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index 3af517927..a4dd72db6 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -72,6 +72,7 @@ const icons = {
'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default,
'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default,
+ 'octagon': require('!!raw-loader?!../../../assets/images/feather/octagon.svg').default,
'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default
}
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index 6d9f0ee65..7b5611f35 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -4,8 +4,12 @@ import { Actor } from './actor.model'
export class Account extends Actor implements ServerAccount {
displayName: string
description: string
+
+ updatedAt: Date | string
+
nameWithHost: string
nameWithHostForced: string
+
mutedByUser: boolean
mutedByInstance: boolean
mutedServerByUser: boolean
@@ -30,6 +34,8 @@ export class Account extends Actor implements ServerAccount {
this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
+ if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString())
+
this.mutedByUser = false
this.mutedByInstance = false
this.mutedServerByUser = false
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts
index 6ba0bb09e..2fccc472a 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -12,7 +12,6 @@ export abstract class Actor implements ServerActor {
followersCount: number
createdAt: Date | string
- updatedAt: Date | string
avatar: ActorImage
@@ -55,7 +54,6 @@ export abstract class Actor implements ServerActor {
this.followersCount = hash.followersCount
if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString())
- if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString())
this.avatar = hash.avatar
this.isLocal = Actor.IS_LOCAL(this.host)
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts
index 5e7832807..d62c1f88e 100644
--- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts
+++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts
@@ -3,32 +3,37 @@ import { Pipe, PipeTransform } from '@angular/core'
// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
@Pipe({ name: 'myFromNow' })
export class FromNowPipe implements PipeTransform {
-
transform (arg: number | Date | string) {
const argDate = new Date(arg)
const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
- let interval = Math.round(seconds / 31536000)
+ let interval = Math.floor(seconds / 31536000)
if (interval > 1) return $localize`${interval} years ago`
- if (interval === 1) return $localize`${interval} year ago`
+ if (interval === 1) return $localize`1 year ago`
- interval = Math.round(seconds / 2592000)
+ interval = Math.floor(seconds / 2419200)
+ // 12 months = 360 days, but a year ~ 365 days
+ // Display "1 year ago" rather than "12 months ago"
+ if (interval >= 12) return $localize`1 year ago`
if (interval > 1) return $localize`${interval} months ago`
- if (interval === 1) return $localize`${interval} month ago`
+ if (interval === 1) return $localize`1 month ago`
- interval = Math.round(seconds / 604800)
+ interval = Math.floor(seconds / 604800)
+ // 4 weeks ~ 28 days, but our month is 30 days
+ // Display "1 month ago" rather than "4 weeks ago"
+ if (interval >= 4) return $localize`1 month ago`
if (interval > 1) return $localize`${interval} weeks ago`
- if (interval === 1) return $localize`${interval} week ago`
+ if (interval === 1) return $localize`1 week ago`
- interval = Math.round(seconds / 86400)
+ interval = Math.floor(seconds / 86400)
if (interval > 1) return $localize`${interval} days ago`
- if (interval === 1) return $localize`${interval} day ago`
+ if (interval === 1) return $localize`1 day ago`
- interval = Math.round(seconds / 3600)
+ interval = Math.floor(seconds / 3600)
if (interval > 1) return $localize`${interval} hours ago`
- if (interval === 1) return $localize`${interval} hour ago`
+ if (interval === 1) return $localize`1 hour ago`
- interval = Math.round(seconds / 60)
+ interval = Math.floor(seconds / 60)
if (interval >= 1) return $localize`${interval} min ago`
return $localize`just now`
diff --git a/client/src/app/shared/shared-main/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss
index 09b5f95d7..22b24c853 100644
--- a/client/src/app/shared/shared-main/buttons/button.component.scss
+++ b/client/src/app/shared/shared-main/buttons/button.component.scss
@@ -30,7 +30,7 @@ span[class$=-button] {
.action-button {
@include peertube-button-link;
- @include button-with-icon(21px, 0, -1px);
+ @include button-with-icon(21px);
}
.orange-button {
diff --git a/client/src/app/shared/shared-main/custom-page/custom-page.service.ts b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts
new file mode 100644
index 000000000..e5c2b3cd4
--- /dev/null
+++ b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts
@@ -0,0 +1,38 @@
+import { of } from 'rxjs'
+import { catchError, map } from 'rxjs/operators'
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { RestExtractor } from '@app/core'
+import { CustomPage } from '@shared/models'
+import { environment } from '../../../../environments/environment'
+
+@Injectable()
+export class CustomPageService {
+ static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance'
+
+ constructor (
+ private authHttp: HttpClient,
+ private restExtractor: RestExtractor
+ ) { }
+
+ getInstanceHomepage () {
+ return this.authHttp.get(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL)
+ .pipe(
+ catchError(err => {
+ if (err.status === 404) {
+ return of({ content: '' })
+ }
+
+ this.restExtractor.handleError(err)
+ })
+ )
+ }
+
+ updateInstanceHomepage (content: string) {
+ return this.authHttp.put(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL, { content })
+ .pipe(
+ map(this.restExtractor.extractDataBool),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
+}
diff --git a/client/src/app/shared/shared-main/custom-page/index.ts b/client/src/app/shared/shared-main/custom-page/index.ts
new file mode 100644
index 000000000..7269ece95
--- /dev/null
+++ b/client/src/app/shared/shared-main/custom-page/index.ts
@@ -0,0 +1 @@
+export * from './custom-page.service'
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
index 93ba9fb9b..4d5381e8d 100644
--- a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
+++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
@@ -3,7 +3,8 @@ import { PluginElementPlaceholder } from '@shared/models'
@Component({
selector: 'my-plugin-placeholder',
- template: '
'
+ template: '
',
+ styles: [ 'div { height: 100%; }' ]
})
export class PluginPlaceholderComponent {
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 05a5d77c7..f06f25ca5 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -29,6 +29,7 @@ import {
} from './angular'
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
+import { CustomPageService } from './custom-page'
import { DateToggleComponent } from './date'
import { FeedComponent } from './feeds'
import { LoaderComponent, SmallLoaderComponent } from './loaders'
@@ -172,7 +173,9 @@ import { VideoChannelService } from './video-channel'
VideoCaptionService,
- VideoChannelService
+ VideoChannelService,
+
+ CustomPageService
]
})
export class SharedMainModule { }
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
index c40dd5311..a9dcf2fa2 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
@@ -16,6 +16,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
banner: ActorImage
bannerUrl: string
+ updatedAt: Date | string
+
ownerAccount?: ServerAccount
ownerBy?: string
@@ -59,6 +61,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
this.videosCount = hash.videosCount
+ if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString())
+
if (hash.viewsPerDay) {
this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) }))
}
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index e65261763..a89f1065a 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -40,23 +40,24 @@ export class VideoChannelService {
)
}
- listAccountVideoChannels (
- account: Account,
- componentPagination?: ComponentPaginationLight,
- withStats = false,
+ listAccountVideoChannels (options: {
+ account: Account
+ componentPagination?: ComponentPaginationLight
+ withStats?: boolean
+ sort?: string
search?: string
- ): Observable> {
+ }): Observable> {
+ const { account, componentPagination, withStats = false, sort, search } = options
+
const pagination = componentPagination
? this.restService.componentPaginationToRestPagination(componentPagination)
: { start: 0, count: 20 }
let params = new HttpParams()
- params = this.restService.addRestGetParams(params, pagination)
+ params = this.restService.addRestGetParams(params, pagination, sort)
params = params.set('withStats', withStats + '')
- if (search) {
- params = params.set('search', search)
- }
+ if (search) params = params.set('search', search)
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels'
return this.authHttp.get>(url, { params })
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index 0e3924841..2c83f53b6 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -1,4 +1,4 @@
-import { BooleanBothQuery, SearchTargetType } from '@shared/models'
+import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models'
export class AdvancedSearch {
startDate: string // ISO 8601
@@ -21,6 +21,8 @@ export class AdvancedSearch {
durationMin: number // seconds
durationMax: number // seconds
+ isLive: BooleanQuery
+
sort: string
searchTarget: SearchTargetType
@@ -41,6 +43,8 @@ export class AdvancedSearch {
tagsOneOf?: any
tagsAllOf?: any
+ isLive?: BooleanQuery
+
durationMin?: string
durationMax?: string
sort?: string
@@ -54,6 +58,8 @@ export class AdvancedSearch {
this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined
this.nsfw = options.nsfw || undefined
+ this.isLive = options.isLive || undefined
+
this.categoryOneOf = options.categoryOneOf || undefined
this.licenceOneOf = options.licenceOneOf || undefined
this.languageOneOf = options.languageOneOf || undefined
@@ -94,6 +100,7 @@ export class AdvancedSearch {
this.tagsAllOf = undefined
this.durationMin = undefined
this.durationMax = undefined
+ this.isLive = undefined
this.sort = '-match'
}
@@ -112,12 +119,16 @@ export class AdvancedSearch {
tagsAllOf: this.tagsAllOf,
durationMin: this.durationMin,
durationMax: this.durationMax,
+ isLive: this.isLive,
sort: this.sort,
searchTarget: this.searchTarget
}
}
- toAPIObject () {
+ toAPIObject (): VideosSearchQuery {
+ let isLive: boolean
+ if (this.isLive) isLive = this.isLive === 'true'
+
return {
startDate: this.startDate,
endDate: this.endDate,
@@ -131,6 +142,7 @@ export class AdvancedSearch {
tagsAllOf: this.tagsAllOf,
durationMin: this.durationMin,
durationMax: this.durationMax,
+ isLive,
sort: this.sort,
searchTarget: this.searchTarget
}
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
index 75cfc918b..d8699ff69 100644
--- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
+++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
@@ -40,7 +40,7 @@
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index 5df89d019..0bbdff1e6 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -95,6 +95,7 @@ my-actor-avatar {
.video-bottom {
display: flex;
width: 100%;
+ min-width: 1px;
}
.video-miniature-name {
@@ -145,6 +146,7 @@ my-actor-avatar {
.video-bottom {
display: flex;
+ min-width: 1px;
}
// We don't display avatar in row mode
diff --git a/client/src/assets/images/feather/octagon.svg b/client/src/assets/images/feather/octagon.svg
new file mode 100644
index 000000000..1ed9bacbf
--- /dev/null
+++ b/client/src/assets/images/feather/octagon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts
index d9f0d2fe9..a93f59506 100644
--- a/client/src/assets/player/stats/stats-card.ts
+++ b/client/src/assets/player/stats/stats-card.ts
@@ -89,9 +89,9 @@ class StatsCard extends Component {
this.container.style.display = 'block'
this.updateInterval = setInterval(async () => {
try {
- const options = this.mode === 'webtorrent'
- ? await this.buildWebTorrentOptions()
- : await this.buildHLSOptions()
+ const options = this.mode === 'p2p-media-loader'
+ ? await this.buildHLSOptions()
+ : await this.buildWebTorrentOptions() // Default
this.list.innerHTML = this.getListTemplate(options)
} catch (err) {
@@ -212,7 +212,7 @@ class StatsCard extends Component {
: undefined
return `
- ${this.buildElement(player.localize('Player mode'), this.options_.mode)}
+ ${this.buildElement(player.localize('Player mode'), this.mode || 'HTTP')}
${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)}
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index d7451fa1d..1243526d2 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -95,7 +95,7 @@ function buildVideoLink (options: {
function buildPlaylistLink (options: {
baseUrl?: string
- playlistPosition: number
+ playlistPosition?: number
}) {
const { baseUrl } = options
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 6f5fbe4c9..e27a16390 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -557,7 +557,8 @@ class WebTorrentPlugin extends Plugin {
private pickAverageVideoFile () {
if (this.videoFiles.length === 1) return this.videoFiles[0]
- return this.videoFiles[Math.floor(this.videoFiles.length / 2)]
+ const files = this.videoFiles.filter(f => f.resolution.id !== 0)
+ return files[Math.floor(files.length / 2)]
}
private stopTorrent (torrent: WebTorrent.Torrent) {
diff --git a/client/src/index.html b/client/src/index.html
index 72c184dc1..28667cdd0 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -29,6 +29,7 @@
+
diff --git a/client/src/locale/angular.ar.xlf b/client/src/locale/angular.ar.xlf
index 906b00dd9..19e27dba6 100644
--- a/client/src/locale/angular.ar.xlf
+++ b/client/src/locale/angular.ar.xlf
@@ -20,7 +20,7 @@
-
+
@@ -32,7 +32,7 @@
سجل مشاهداتي
src/app/+my-library/my-history/my-history.component.ts 67
-
+
Delete
احذف
@@ -44,120 +44,120 @@
53
-
+
Edit
حرر
- src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 85
- src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 11
- src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 85
- src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 11
- src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html 11
- src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html 11
- src/app/+videos/+video-edit/video-add-components/video-upload.component.html 38
- src/app/+videos/+video-edit/shared/video-edit.component.html 270
- src/app/+admin/users/user-edit/user-edit.component.html 11
- src/app/+admin/users/user-edit/user-edit.component.html 11
-
+
+
+
+
+
+
+
+
+
+
+ src/app/+admin/users/user-edit/user-edit.component.html 11 src/app/+admin/users/user-edit/user-edit.component.html 11 src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html 11 src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html 11 src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 11 src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 11 src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 85 src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html 85 src/app/+videos/+video-edit/shared/video-edit.component.html 270 src/app/+videos/+video-edit/video-add-components/video-upload.component.html 43
<a href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noopener noreferrer">Markdown</a> compatible that supports:
<a href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noopener noreferrer">تكامل ماركداون</a> المدعوم:
src/app/shared/shared-main/misc/help.component.ts 75
-
+
You don't have notifications.
ليس لديك إشعارات.
src/app/shared/shared-main/users/user-notifications.component.html 1
-
+
published a new video:
نشر مقطع فيديو جديد:
src/app/shared/shared-main/users/user-notifications.component.html 15
-
+
The notification concerns a video now unavailable
الإشعار يتعلق بفيديو غير متوفر الآن
src/app/shared/shared-main/users/user-notifications.component.html 24
-
+
Your video has been unblocked
الفيديو الخاص بك أُزيل الحجب عنه
src/app/shared/shared-main/users/user-notifications.component.html 33
-
+
Your video has been blocked
الفيديو الخاص بك حُجب
src/app/shared/shared-main/users/user-notifications.component.html 41
-
+
A new video abuse has been created on video
إساءة استخدام جديدة لفيديو
src/app/shared/shared-main/users/user-notifications.component.html 49
-
+
A new comment abuse has been created on video
تعليق مسيئ جديد على فيديو
src/app/shared/shared-main/users/user-notifications.component.html 53
-
+
A new account abuse has been created on account
إساءة استخدام حساب جديدة للحساب
src/app/shared/shared-main/users/user-notifications.component.html 57
-
+
A new abuse has been created
إساءة جديدة
src/app/shared/shared-main/users/user-notifications.component.html 62
-
+
Your abuse has been accepted rejected
إساءة استخدامك قُبلت رُفضت
src/app/shared/shared-main/users/user-notifications.component.html 70
-
+
Abuse has a new message
رسالة جديدة حول الإساءة
src/app/shared/shared-main/users/user-notifications.component.html 80
-
+
The recently added video has been automatically blocked
الفيديو المضاف مؤخرا حُجب تلقائيا
src/app/shared/shared-main/users/user-notifications.component.html 88
-
+
commented your video
علق على فيديو لك
src/app/shared/shared-main/users/user-notifications.component.html 99
-
+
The notification concerns a comment now unavailable
الإشعار يتعلق بتعليق غير متوفر الآن
src/app/shared/shared-main/users/user-notifications.component.html 107 src/app/shared/shared-main/users/user-notifications.component.html 172
-
+
Your video has been published
نُشر الفيديو الخاص بك
src/app/shared/shared-main/users/user-notifications.component.html 116
-
+
Your video import succeeded
عملية استيراد الفيديو تمت بنجاح
src/app/shared/shared-main/users/user-notifications.component.html 124
-
+
Your video import failed
عملية أستيراد الفيديو فشلت
src/app/shared/shared-main/users/user-notifications.component.html 132
-
+
User registered on your instance
سجَل المستخدم في مثيل الخادم الخاص بك.
src/app/shared/shared-main/users/user-notifications.component.html 140
-
+
is following your channel your account
يتابع قناتك حسابك
- src/app/shared/shared-main/users/user-notifications.component.html 150
+ src/app/shared/shared-main/users/user-notifications.component.html 150
mentioned you on video mentioned you on video
src/app/shared/shared-main/users/user-notifications.component.html
@@ -165,55 +165,55 @@
-
+
Your instance has a new follower ( ) awaiting your approval
لدى مثيل الخادم الخاص بك متابع جديد ( ) ينتظر موافقتك
src/app/shared/shared-main/users/user-notifications.component.html 181
-
+
Your instance automatically followed
قام مثيل الخادم الخاص بك بالتتبع التقائي لـ
- src/app/shared/shared-main/users/user-notifications.component.html 190
+ src/app/shared/shared-main/users/user-notifications.component.html 190
A new version of the plugin/theme is available: A new version of the plugin/theme is available:
src/app/shared/shared-main/users/user-notifications.component.html
198,199
-
+
A new version of PeerTube is available: A new version of PeerTube is available:
src/app/shared/shared-main/users/user-notifications.component.html
206,207
-
+
The notification points to content now unavailable
يشير الإشعار إلى محتوى غير متوفر الآن
src/app/shared/shared-main/users/user-notifications.component.html 214
-
+
Change your avatar
غيّر صورتك الرمزية
src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html 18
-
+
Remove avatar
احذف صورتك الرمزية
src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html 40
-
+
subscribers
مشترك
src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html 27
-
+
Upload a new avatar
ارفع صورة رمزية جديدة
src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html 9 src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html 34
-
+
Truncated preview
معاينة مقتطعة
src/app/shared/shared-forms/markdown-textarea.component.html 11
@@ -228,33 +228,33 @@
اعرض
src/app/shared/shared-forms/input-toggle-hidden.component.ts 39
-
+
Complete preview
معاينة كاملة
src/app/shared/shared-forms/markdown-textarea.component.html 19
-
+
Recommended
مقترح
src/app/shared/shared-forms/peertube-checkbox.component.html 33
-
+
Theme
السمة
src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html 14
src/app/shared/shared-user-settings/user-interface-settings.component.html 4
-
+
instance default
مثيل الخادم الإفتراضي
src/app/shared/shared-user-settings/user-interface-settings.component.html 8
-
+
peertube default
افتراضي بيرتيوب
src/app/shared/shared-user-settings/user-interface-settings.component.html 9
-
+
Save
احفظ
src/app/shared/shared-user-settings/user-interface-settings.component.html 16
@@ -264,11 +264,11 @@
src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html 82
src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html 38
-
+
Default policy on videos containing sensitive content
السياسة الفيديوهات ذات المحتوى الحساس
src/app/shared/shared-user-settings/user-video-settings.component.html 4
-
+
With Hide or Blur thumbnails , a confirmation will be requested to watch the video. With Hide or Blur thumbnails , a confirmation will be requested to watch the video.
src/app/shared/shared-user-settings/user-video-settings.component.html
@@ -276,251 +276,251 @@
-
+
Policy for sensitive videos
سياسة الفيديوهات الحساسة
src/app/+admin/config/edit-custom-config/edit-instance-information.component.html 114
src/app/shared/shared-user-settings/user-video-settings.component.html 15
-
+
Blur thumbnails
صور مصغرة مطموسة
src/app/+admin/config/edit-custom-config/edit-instance-information.component.html 116
src/app/shared/shared-user-settings/user-video-settings.component.html 17
-
+
Display
أظهرها
src/app/+admin/config/edit-custom-config/edit-instance-information.component.html 117
src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html 8
src/app/shared/shared-user-settings/user-video-settings.component.html 18
-
+
Only display videos in the following languages/subtitles
عرض مقاطع الفيديو باللغات / الترجمات التالية فقط
src/app/shared/shared-user-settings/user-video-settings.component.html 25
-
+
In Recently added, Trending, Local, Most liked and Search pages
في صفحات: المضافة حديثًا ، الشائعة ، المحلية ، الأكثر إعجابًا البحث
src/app/shared/shared-user-settings/user-video-settings.component.html 28
-
+
Add a new language
أضف لغة جديدة
src/app/+admin/config/edit-custom-config/edit-instance-information.component.html 67
src/app/shared/shared-user-settings/user-video-settings.component.html 36
-
+
The sharing system implies that some technical information about your system (such as a public IP address) can be sent to other peers, but greatly helps to reduce server load.
يشير نظام المشاركة إلى أنه يمكن إرسال بعض المعلومات الفنية حول نظامك (مثل عنوان IP) إلى الأقران الآخرين ، ولكنه يساعد بشكل كبير في تقليل حمل الخادم.
src/app/shared/shared-user-settings/user-video-settings.component.html 50
-
+
Help share videos being played
المساهمة في نشر الفيديوهات
src/app/shared/shared-user-settings/user-video-settings.component.html 47
-
+
When on a video page, directly start playing the video.
عندما تكون في صفحة فيديو ، شغِل الفيديو.
src/app/shared/shared-user-settings/user-video-settings.component.html 61
-
+
Automatically play videos
شغّل الفيديوهات تلقائيا
src/app/shared/shared-user-settings/user-video-settings.component.html 58
-
+
When a video ends, follow up with the next suggested video.
عندما ينتهي الفيديو ، شغّل الفيديو المقترح التالي.
src/app/shared/shared-user-settings/user-video-settings.component.html 72
-
+
Automatically start playing the next video
شغّل الفيديو التالي تلقائيا
src/app/shared/shared-user-settings/user-video-settings.component.html 69
-
+
yes
نعم
src/app/shared/shared-instance/feature-boolean.component.html 1
src/app/shared/shared-instance/feature-boolean.component.html 1
-
+
no
لا
src/app/shared/shared-instance/feature-boolean.component.html 2
src/app/shared/shared-instance/feature-boolean.component.html 2
-
+
Features found on this instance
مميزات مثيل الخادم
src/app/shared/shared-instance/instance-features-table.component.html 4
src/app/shared/shared-instance/instance-about-accordion.component.html 6
src/app/shared/shared-instance/instance-about-accordion.component.html 6
-
+
PeerTube version
إصدار بيرتيوب
src/app/shared/shared-instance/instance-features-table.component.html 6
-
+
Default NSFW/sensitive videos policy can be redefined by the users
سياسة مقاطع الفيديو الحساسة / NSFW الافتراضية يمكن إعادة تعريفها من قبل المستخدمين
src/app/shared/shared-instance/instance-features-table.component.html 13
-
+
User registration allowed
التسجيل مسموح
src/app/shared/shared-instance/instance-features-table.component.html 21
-
+
Video uploads
رفع فيديو
src/app/shared/shared-instance/instance-features-table.component.html 28
src/app/shared/shared-instance/instance-features-table.component.html 39
-
+
Transcoding in multiple resolutions
تحويل الترميز لإنشاء عدة دِقات
src/app/shared/shared-instance/instance-features-table.component.html 32
-
+
Live streaming enabled
البث المباشر مفعل
src/app/shared/shared-instance/instance-features-table.component.html 71
-
+
Transcode live video in multiple resolutions
تحويل ترميز فيديو البث المباشر لإنشاء عدة دِقات
src/app/shared/shared-instance/instance-features-table.component.html 78
-
+
Max parallel lives
أقصى عدد للبثوث المباشرة
src/app/shared/shared-instance/instance-features-table.component.html 85
-
+
per user / per instance
للمستخدم / لمثيل الخادم
src/app/shared/shared-instance/instance-features-table.component.html 86
-
+
Requires manual validation by moderators
يتطلب تأكيدًا يدويًا من قبل المشرفين
src/app/shared/shared-instance/instance-features-table.component.html 41
-
+
Automatically published
نُشر تلقائيًا
src/app/shared/shared-instance/instance-features-table.component.html 42
-
+
Video quota
حصة الفيديو
src/app/shared/shared-instance/instance-features-table.component.html 47
src/app/+admin/users/user-edit/user-edit.component.html 151
src/app/+admin/users/user-edit/user-edit.component.html 151
-
+
Unlimited ( per day)
غير محدود ( في اليوم الواحد)
src/app/shared/shared-instance/instance-features-table.component.html 61
-
+
Import
استورد
src/app/shared/shared-instance/instance-features-table.component.html 92
src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html 44
src/app/+videos/+video-edit/video-add-components/video-import-url.component.html 36
-
+
You can import any torrent file that points to a media file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
يمكنك استيراد أي ملف تورنت يشير لملف وسائط ، تأكد من أنك تمتلك حقوق توزيع هذا الملف وإلا ستسبب لنفسك ولمثيل الخادم مشاكل قانونية.
src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html 20
-
+
HTTP import (YouTube, Vimeo, direct URL...)
استيراد عبرHTTP (YouTube ، Vimeo ،رابط مباشر ...)
src/app/shared/shared-instance/instance-features-table.component.html 96
-
+
Torrent import
استيراد تورنت
src/app/shared/shared-instance/instance-features-table.component.html 103
-
+
Player
المشغل
src/app/shared/shared-instance/instance-features-table.component.html 111
-
+
P2P enabled
تم تمكين الند لند
src/app/shared/shared-instance/instance-features-table.component.html 115
-
+
Search
ابحث
src/app/header/search-typeahead.component.html 8
src/app/shared/shared-instance/instance-features-table.component.html 122
src/app/+admin/plugins/plugins.component.html 5
-
+
Users can resolve distant content
يمكن للمستخدمين من تحليل المحتوى البعيد
src/app/shared/shared-instance/instance-features-table.component.html 126
-
+
Loading instance statistics...
يحمل إحصائيات مثيل الخادم ...
src/app/shared/shared-instance/instance-statistics.component.html 1
-
+
Local
المحلية
src/app/shared/shared-instance/instance-statistics.component.html 4
-
+
users
مستخدمون
src/app/shared/shared-instance/instance-statistics.component.html 11
-
+
videos
فيديوهات
src/app/shared/shared-instance/instance-statistics.component.html 21
src/app/shared/shared-instance/instance-statistics.component.html 65
-
+
video views
مرات مشاهدة الفيديو
src/app/shared/shared-instance/instance-statistics.component.html 31
-
+
video comments
تعليقات الفيديو
src/app/shared/shared-instance/instance-statistics.component.html 41
src/app/shared/shared-instance/instance-statistics.component.html 75
-
+
of hosted video
الفيديو المستضاف
src/app/shared/shared-instance/instance-statistics.component.html 51
-
+
Federation
الفديرالية
src/app/shared/shared-instance/instance-statistics.component.html 58
-
+
followers
متابِعون
src/app/shared/shared-instance/instance-statistics.component.html 85
-
+
following
يتابِع
src/app/shared/shared-instance/instance-statistics.component.html 95
@@ -528,58 +528,64 @@
The upload failed
فشل الرفع
- src/app/helpers/utils.ts 177
-
+
+ src/app/helpers/utils.ts 186
The connection was interrupted
قُطع الاتصال
- src/app/helpers/utils.ts 181
+
+ src/app/helpers/utils.ts 190
+ The server encountered an error The server encountered an error
+
+ src/app/helpers/utils.ts
+ 193
+
Your file couldn't be transferred before the set timeout (usually 10min)
Your file couldn't be transferred before the set timeout (usually 10min)
- src/app/helpers/utils.ts 184
-
+
+ src/app/helpers/utils.ts 196
Your file was too large (max. size: )
حجم ملف كبير ( حد الحجم: )
- src/app/helpers/utils.ts 188
-
+
+ src/app/helpers/utils.ts 200
User
مستخدم
src/app/core/users/user.service.ts 405
-
+
Configuration warning!
تحذير التكوين!
src/app/modal/instance-config-warning-modal.component.html 3
-
+
You enabled user registration on your instance but did not configure the following fields:
مكنت تسجيل المستخدمين الجدد على مثيل الخادم الخاص بك ولكنك لم تقم بتكوين الحقول التالية:
src/app/modal/instance-config-warning-modal.component.html 10
-
+
Instance name
اسم مثيل الخادم
src/app/modal/instance-config-warning-modal.component.html 13
-
+
Instance short description
وصف مختصر لمثيل الخادم
src/app/modal/instance-config-warning-modal.component.html 14
-
+
Who you are
من أنت
src/app/modal/instance-config-warning-modal.component.html 16
-
+
How long you plan to maintain your instance
إلى متى تخطط على إبقاء مثيل الخادم شغلًا
src/app/modal/instance-config-warning-modal.component.html 17
-
+
How you plan to pay for keeping your instance running How you plan to pay for keeping your instance running
src/app/modal/instance-config-warning-modal.component.html
@@ -587,23 +593,23 @@
-
+
How you will moderate your instance
كيف ستشرف على مثيل الخادم
src/app/modal/instance-config-warning-modal.component.html 20
-
+
Instance terms
شروط استخدام مثيل الخادم
src/app/modal/instance-config-warning-modal.component.html 21
-
+
My settings
إعداداتي
src/app/menu/menu.component.html 152 src/app/modal/quick-settings-modal.component.html 3
-
+
These settings apply only to your session on this instance.
تطبق هذه الإعدادات على جلساتك في هذا المثيل فقط.
@@ -611,244 +617,244 @@
8
-
+
Please consider configuring these fields to help people to choose the appropriate instance . Without them, your instance may not be referenced on the JoinPeerTube website .
يرجى تكوين هذه الحقول لمساعدة الناس على اختيار مثيل الخادم المناسب لهم . بدونها ، قد لا تتم الإشارة إلى مثيل الخادم الخاص بك على موقع JoinPeerTube .
src/app/modal/instance-config-warning-modal.component.html 25
-
+
Don't show me this warning anymore
لا تريني هذا التحذير مجددًا
src/app/modal/instance-config-warning-modal.component.html 33
-
+
Close
اغلق
src/app/modal/instance-config-warning-modal.component.html 38 src/app/shared/shared-video-live/live-stream-information.component.html 33
-
+
Update live settings
حدّث إعدادات البث المباشر
src/app/shared/shared-video-live/live-stream-information.component.html 36
-
+
Configure
اضبط
src/app/modal/instance-config-warning-modal.component.html 44
-
+
Welcome to PeerTube, dear administrator!
مرحبًا بك في بيرتيوب ، مديرنا العزيز!
src/app/modal/welcome-modal.component.html 3
-
+
CLI documentation
وثائق CLI
src/app/modal/welcome-modal.component.html 12
-
+
Upload or import videos, parse logs, prune storage directories, reset user password...
ارفع أو استورد ملفات الفيديو ،راجع السجلات، نظف أدلة التخزين، أو أعد تعيين كلمة مرور المستخدم ...
src/app/modal/welcome-modal.component.html 15
-
+
Administer documentation
توثيق الإدارة
src/app/modal/welcome-modal.component.html 19
-
+
Managing users, following other instances, dealing with spammers...
إدارة المستخدمين ، متابعة مثيلات الخادم الأخرى ، التعامل مع مرسلي الرسائل غير المرغوب فيها ...
src/app/modal/welcome-modal.component.html 22
-
+
Use documentation
وثائق الاستخدام
src/app/modal/welcome-modal.component.html 26
-
+
Setup your account, managing video playlists, discover third-party applications...
قم بإعداد حسابك ، وإدارة قوائم تشغيل الفيديو ، واكتشاف تطبيقات الطرف الثالث ...
src/app/modal/welcome-modal.component.html 29
-
+
Useful links
روابط مفيدة
src/app/modal/welcome-modal.component.html 39
-
+
Official PeerTube website (news, support, contribute...): https://joinpeertube.org
موقع PeerTube الرسمي (الأخبار ، الدعم ، المساهمة ...): https://joinpeertube.org
src/app/modal/welcome-modal.component.html 42
-
+
Put your instance on the public PeerTube index: https://instances.joinpeertube.org/instances
أضف مثيل الخادم الخاص بك الى فهرس PeerTube العام: https://instances.joinpeertube.org/instances
src/app/modal/welcome-modal.component.html 45
-
+
It's time to configure your instance!
حان الوقت تكوين مثيل الخادم الخاص بك!
src/app/modal/welcome-modal.component.html 55
-
+
Choosing your instance name , setting up a description , specifying who you are , why you created your instance and how long you plan to maintain your it is very important for visitors to understand on what type of instance they are.
أختيارك اسما لمثيل الخادم واعدادك لوصف ، تحدد فيه من أنت ، لماذا قمت بأنشاء هذا المثيل ، وكم من الوقت تخطط لإبقائه شغالا . أمر مهم ليفهم الزوار نوع مثيل الخادم الذي يستخدمونه.
src/app/modal/welcome-modal.component.html 58
-
+
If you want to open registrations, please decide what your moderation rules and instance terms of service are, as well as specify the categories and languages and your moderators speak. This way, you will help users to register on the appropriate PeerTube instance.
إذا أردت فتح التسجيل، رجاء حدد قواعد الاشراف ، واكتب شروط استخدام مثيل الخادم ،وحدد الفئات واللغات التي تستخدم. بهذا ستسمح للمتسخدمين بتحديد مثيل الخادم الذي يناسبهم .
src/app/modal/welcome-modal.component.html 64
-
+
Remind me later
ذكرني لاحقا
src/app/modal/welcome-modal.component.html 74
-
+
Configure my instance
كوّن مثيل الخادم الخاص بي
src/app/modal/welcome-modal.component.html 81
-
+
Close this message
أغلق هذه الرسالة
src/app/app.component.html 34
src/app/app.component.html 34
-
+
Change the language
غيّر اللغة
src/app/menu/language-chooser.component.html 3
-
+
Help to translate PeerTube!
ساهموا في ترجمة PeerTube!
src/app/menu/language-chooser.component.html 9
-
+
Settings
الإعدادات
src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html 27 src/app/+admin/plugins/plugin-search/plugin-search.component.html 53 src/app/+my-account/my-account-settings/my-account-settings.component.html 1
-
+
Display settings
اعدادات العرض
src/app/modal/quick-settings-modal.component.html 10
-
+
Video settings
إعدادات الفيديو
src/app/modal/quick-settings-modal.component.html 18
-
+
Interface settings
إعدادات الواجهة
src/app/modal/quick-settings-modal.component.html 22
-
+
Public profile
الملف الشخصي العام
src/app/menu/menu.component.html 23
-
+
Interface:
الواجهة:
src/app/menu/menu.component.html 30
-
+
Videos:
الفيديوهات:
src/app/menu/menu.component.html 37
-
+
Sensitive:
حساس:
src/app/menu/menu.component.html 47
-
+
Help share videos
ساهِم في مشاركة الفيديوهات
src/app/menu/menu.component.html 53
-
+
Keyboard shortcuts
اختصارات لوحة المفاتيح
src/app/menu/menu.component.html 62 src/app/menu/menu.component.html 174
-
+
powered by PeerTube - CopyLeft 2015-2021
يعمل بـ بيرتيوب - جميع الحقوق متروكة 2015-2021
src/app/menu/menu.component.html 179
-
+
Log out
خروج
src/app/menu/menu.component.html 67
-
+
My account
حسابي
src/app/menu/menu.component.html 78
-
+
My library
مكتبتي
src/app/menu/menu.component.html 83
-
+
Login
لِج
src/app/+login/login.component.html 44 src/app/menu/menu.component.html 94
-
+
Create an account
أنشئ حسابًا
src/app/+login/login.component.html 50 src/app/menu/menu.component.html 95
-
+
IN MY LIBRARY
في مكتبتي
src/app/menu/menu.component.html 99
-
+
Videos
الفيديوهات
src/app/menu/menu.component.html 103
-
+
Interface:
الواجهة:
src/app/menu/menu.component.html 165
-
+
Playlists
قوائم التشغيل
src/app/menu/menu.component.html 108
-
+
Subscriptions
الإشتراكات
src/app/menu/menu.component.html 113
-
+
History
السجل
src/app/menu/menu.component.html 118
-
+
VIDEOS
الفيديوهات
src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html 210
-
+
Import jobs concurrency
Import jobs concurrency
@@ -856,7 +862,7 @@
220
-
+
allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.
السماح باستيراد متوازٍ لعدة فيديوهات. يحتاج الى إعادة تشغيل بيرتيوب ⚠️.
@@ -864,7 +870,7 @@
221
-
+
jobs in parallel
العمليات التي تعمل بالتوازي
@@ -876,109 +882,109 @@
171
-
+
Allow import with HTTP URL (e.g. YouTube)
اسمح بالاستيراد عبر الروابط (مثل يوتيوب)
src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html 234
-
+
Discover
اكتشف
src/app/+videos/video-list/overview/video-overview.component.html 1 src/app/menu/menu.component.html 128
-
+
Trending
الشائعة
src/app/menu/menu.component.html 133
-
+
Recently added
أُضيفت حديثًا
src/app/menu/menu.component.html 138
-
+
Administration
الإدارة
src/app/menu/menu.component.html 88
-
+
About
عن
src/app/menu/menu.component.html 157
-
+
Contact
اتصل
src/app/menu/menu.component.html 169
-
+
Help
مساعدة
src/app/menu/menu.component.html 170
-
+
Get help using PeerTube
احصل على تعليمات استخدام بيرتيوب
src/app/menu/menu.component.html 170
-
+
FAQ
الأسئلة الشائعة
src/app/menu/menu.component.html 171
-
+
Frequently asked questions about PeerTube
أسئلة متكررة حول بيرتيوب
src/app/menu/menu.component.html 171
-
+
Stats
الإحصائيات
src/app/menu/menu.component.html 172
-
+
API
API
src/app/menu/menu.component.html 173
-
+
API documentation
وثائق API
src/app/menu/menu.component.html 173
-
+
powered by PeerTube
يعمل بـ بيرتيوب
src/app/menu/menu.component.html 180
-
+
View your notifications
اعرض اشعاراتك
src/app/menu/notification.component.html 3
src/app/menu/notification.component.html 11
src/app/menu/notification.component.html 11
-
+
Notifications
الإشعارات
src/app/menu/notification.component.html 22
src/app/+my-account/my-account-notifications/my-account-notifications.component.html 1
-
+
Mark all as read
علّم الكل كمقروء
src/app/menu/notification.component.html 27
src/app/menu/notification.component.html 27
src/app/+my-account/my-account-notifications/my-account-notifications.component.html 20
-
+
Update your notification preferences
حدّث تفضيلات اشعاراتك
src/app/menu/notification.component.html 31
-
+
See all your notifications
اعرض كل اشعاراتك
src/app/menu/notification.component.html 49
@@ -988,20 +994,20 @@
I'm a teapot
src/app/+page-not-found/page-not-found.component.ts 27
-
+
That's an error.
هذا خطأ.
src/app/+page-not-found/page-not-found.component.html
4
-
+
We couldn't find any video tied to the URL you were looking for. We couldn't find any video tied to the URL you were looking for.
src/app/+page-not-found/page-not-found.component.html
7
-
+
We couldn't find any resource tied to the URL you were looking for. We couldn't find any resource tied to the URL you were looking for.
src/app/+page-not-found/page-not-found.component.html
@@ -1009,23 +1015,23 @@
-
+
Possible reasons:
الأسباب المحتملة:
Possible reasons preceding a list of reasons a `Not Found` error page may occur
src/app/+page-not-found/page-not-found.component.html 12
-
+
You may have used an outdated or broken link
استخدمت رابط معطل أو منتهي الصلاحية
- src/app/+page-not-found/page-not-found.component.html 15
+ src/app/+page-not-found/page-not-found.component.html 15
The video may have been moved or deleted The video may have been moved or deleted
src/app/+page-not-found/page-not-found.component.html
17
-
+
The resource may have been moved or deleted The resource may have been moved or deleted
src/app/+page-not-found/page-not-found.component.html
@@ -1033,22 +1039,22 @@
-
+
You may have typed the address or URL incorrectly
ربما أخطأت كتابة الرابط
src/app/+page-not-found/page-not-found.component.html 20
-
+
You are not authorized here.
You are not authorized here.
- src/app/+page-not-found/page-not-found.component.html 27
+ src/app/+page-not-found/page-not-found.component.html 27
You might need to check your account is allowed by the video or instance owner. You might need to check your account is allowed by the video or instance owner.
src/app/+page-not-found/page-not-found.component.html
30
-
+
You might need to check your account is allowed by the resource or instance owner. You might need to check your account is allowed by the resource or instance owner.
src/app/+page-not-found/page-not-found.component.html
@@ -1056,13 +1062,13 @@
-
+
The requested entity body blends sweet bits with a mellow earthiness.
The requested entity body blends sweet bits with a mellow earthiness.
Description of a tea flavour, keeping the 'requested entity body' as a technical expression referring to a web request
src/app/+page-not-found/page-not-found.component.html 40
-
+
Sepia seems to like it.
يبدو أنّه يعجب سبيا.
This is about Sepia's tea
@@ -1073,75 +1079,75 @@
هذا الملف كبير. اتصل بالمدير حتى يزيد حد الرفع.
src/app/core/rest/rest-extractor.service.ts 62
-
+
In this instance's network
في شبكة هذ المثيل
src/app/header/suggestion.component.html 14
-
+
In the vidiverse
في الكون
src/app/header/suggestion.component.html 15
-
+
Search videos, channels…
ابحث عن فيديوهات ، قنوات…
src/app/header/search-typeahead.component.html 3
-
+
GLOBAL SEARCH
بحث عالمي
src/app/header/search-typeahead.component.html 26
-
+
using
باستخدام
src/app/header/search-typeahead.component.html 28
-
+
Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.
سيزداد عدد النتائج باستخدام فهرس الطرف الثالث. سترسل البيانات اللازمة لإجراء الاستعلام فقط.
src/app/header/search-typeahead.component.html 32
-
+
Your query will be matched against video names or descriptions, channel names. Your query will be matched against video names or descriptions, channel names.
src/app/header/search-typeahead.component.html
37
-
+
ADVANCED SEARCH
البحث المتقدم
src/app/header/search-typeahead.component.html 39
-
+
any instance
أي مثيل خادم
src/app/header/search-typeahead.component.html 42
-
+
only followed instances
مثلاء الخادم المتّبعون فقط
src/app/header/search-typeahead.component.html 43
-
+
Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.
يحدد ما إذا كان يمكنك حل أي محتوى بعيد ، أو إذا كان هذا المثيل يسمح فقط بالقيام بذلك المثلاء المتبعين.
src/app/header/search-typeahead.component.html 41
-
+
will list the matching channel
يسرد القناة المطابقة
src/app/header/search-typeahead.component.html 50 src/app/header/search-typeahead.component.html 53
-
+
will list the matching video
يسرد الفيديو المطابق
src/app/header/search-typeahead.component.html 56
-
+
Cancel
ألغِ
@@ -1162,58 +1168,58 @@
- src/app/+about/about-instance/contact-admin-modal.component.html 48 src/app/+login/login.component.html 117 src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html 25 src/app/+my-library/my-videos/modals/video-change-ownership.component.html 22 src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html 37 src/app/+videos/+video-edit/video-add-components/video-upload.component.html 58 src/app/+videos/+video-edit/video-add-components/video-upload.component.html 69 src/app/+videos/+video-watch/comment/video-comment-add.component.html 70 src/app/modal/confirm.component.html 20 src/app/shared/shared-abuse-list/moderation-comment-modal.component.html 26 src/app/shared/shared-moderation/batch-domains-modal.component.html 31 src/app/shared/shared-moderation/report-modals/report.component.html 54 src/app/shared/shared-moderation/report-modals/report.component.html 54 src/app/shared/shared-moderation/report-modals/video-report.component.html 92 src/app/shared/shared-moderation/user-ban-modal.component.html 26 src/app/shared/shared-moderation/video-block.component.html 38 src/app/shared/shared-video-miniature/video-download.component.html 134
-
+ src/app/+about/about-instance/contact-admin-modal.component.html 48 src/app/+login/login.component.html 117 src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html 20 src/app/+my-library/my-videos/modals/video-change-ownership.component.html 22 src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html 37 src/app/+videos/+video-edit/video-add-components/video-upload.component.html 69 src/app/+videos/+video-edit/video-add-components/video-upload.component.html 81 src/app/+videos/+video-watch/comment/video-comment-add.component.html 70 src/app/modal/confirm.component.html 20 src/app/shared/shared-abuse-list/moderation-comment-modal.component.html 26 src/app/shared/shared-moderation/batch-domains-modal.component.html 31 src/app/shared/shared-moderation/report-modals/report.component.html 54 src/app/shared/shared-moderation/report-modals/report.component.html 54 src/app/shared/shared-moderation/report-modals/video-report.component.html 92 src/app/shared/shared-moderation/user-ban-modal.component.html 26 src/app/shared/shared-moderation/video-block.component.html 38 src/app/shared/shared-video-miniature/video-download.component.html 134
+
Ban
احظر
src/app/shared/shared-moderation/user-ban-modal.component.html 3
-
+
Reason...
السبب…
src/app/shared/shared-moderation/user-ban-modal.component.html 12
-
+
A banned user will no longer be able to login.
لن يتمكن المستخدم المحظور من الولوج.
src/app/shared/shared-moderation/user-ban-modal.component.html 21
-
+
Ban this user
احظر هذا المستخدم
src/app/shared/shared-moderation/user-ban-modal.component.html 30
-
+
Block video " "
احجب فيديو " "
src/app/shared/shared-moderation/video-block.component.html 3
-
+
Block live " "
احجب البث المباشر
src/app/shared/shared-moderation/video-block.component.html 4
-
+
Please describe the reason...
يرجى وصف السبب ...
src/app/shared/shared-moderation/video-block.component.html 13
-
+
This will ask remote instances to delete it
سيطلب من المثلاء البعدين حذفها
src/app/shared/shared-moderation/video-block.component.html 27
-
+
Blocking this live will automatically terminate the live stream.
حجب هذا البث المباشر سينهيه تلقائيا.
src/app/shared/shared-moderation/video-block.component.html 33
-
+
Unfederate the video
إعزل هذا الفيديو عن الاتحاد
src/app/shared/shared-moderation/video-block.component.html 24
-
+
Submit
أرسل
@@ -1223,19 +1229,19 @@
src/app/+about/about-instance/contact-admin-modal.component.html 52 src/app/+my-library/my-videos/modals/video-change-ownership.component.html 27 src/app/shared/shared-moderation/report-modals/report.component.html 58 src/app/shared/shared-moderation/report-modals/report.component.html 58 src/app/shared/shared-moderation/report-modals/video-report.component.html 96 src/app/shared/shared-moderation/video-block.component.html 42
-
+
Report video " "
بلِّغ عن فيديو " "
src/app/shared/shared-moderation/report-modals/video-report.component.html 3
-
+
What is the issue?
ما هي المشكلة؟
src/app/shared/shared-moderation/report-modals/report.component.html 13
src/app/shared/shared-moderation/report-modals/video-report.component.html 13
src/app/shared/shared-moderation/report-modals/report.component.html 13
-
+
Start at
ابدأ من
src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html 57
@@ -1243,7 +1249,7 @@
src/app/shared/shared-video-playlist/video-add-to-playlist.component.html 34
src/app/shared/shared-moderation/report-modals/video-report.component.html 47
-
+
Stop at
توقف عند
src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html 71
@@ -1251,31 +1257,31 @@
src/app/shared/shared-video-playlist/video-add-to-playlist.component.html 35
src/app/shared/shared-moderation/report-modals/video-report.component.html 62
-
+
Your report will be sent to moderators of and will be forwarded to the video origin ( ) too .
سيرسل الإبلاغ الى مشرفي وسيوجه الى الفيديو الأصلي ( ) كذلك .
src/app/shared/shared-moderation/report-modals/video-report.component.html 74
-
+
Please describe the issue...
يرجى وصف المشكلة ...
src/app/shared/shared-moderation/report-modals/report.component.html 42
src/app/shared/shared-moderation/report-modals/video-report.component.html 80
src/app/shared/shared-moderation/report-modals/report.component.html 42
-
+
src/app/shared/shared-moderation/batch-domains-modal.component.html 3
-
+
1 host (without "http://") per line
مضيف واحد (بدون "http: //") لكل سطر
src/app/shared/shared-moderation/batch-domains-modal.component.html 11
-
+
Your report will be sent to moderators of and will be forwarded to the comment origin ( ) too .
سيرسل الإبلاغ الى مشرفي وسيوجه الى التعليق الأصلي ( ) كذلك .
src/app/shared/shared-moderation/report-modals/report.component.html 36
@@ -1305,7 +1311,7 @@
49
-
+
Applications
Applications
@@ -1313,7 +1319,7 @@
3
-
+
SUBSCRIPTION FEED
SUBSCRIPTION FEED
@@ -1321,7 +1327,7 @@
8
-
+
Use third-party feed aggregators to retrieve the list of videos from channels you subscribed to.
Use third-party feed aggregators to retrieve the list of videos from channels you subscribed to.
@@ -1329,7 +1335,7 @@
10,12
-
+
Feed URL
رابط التلقيم
@@ -1337,7 +1343,7 @@
18
-
+
Feed Token
Feed Token
@@ -1345,7 +1351,7 @@
23
-
+
⚠️ Never share your feed token with anyone.
⚠️ Never share your feed token with anyone.
@@ -1353,7 +1359,7 @@
26
-
+
Renew token
Renew token
src/app/+my-account/my-account-applications/my-account-applications.component.html 35
@@ -1363,7 +1369,7 @@
تكرار الفيديو
src/app/+admin/admin.component.ts 39
-
+
Filter...
مرشح...
@@ -1377,7 +1383,7 @@
src/app/shared/shared-forms/advanced-input-filter.component.html 17
-
+
Clear filters
امسح المرشحات
@@ -1396,39 +1402,39 @@
src/app/shared/shared-forms/advanced-input-filter.component.html 23
-
+
Video/Comment/Account
فيديو\تعليق\حساب
src/app/shared/shared-abuse-list/abuse-list-table.component.html 22
-
+
Follower handle
مقبض تابع
src/app/+admin/follows/followers-list/followers-list.component.html 24
-
+
State
حالة
src/app/+admin/follows/followers-list/followers-list.component.html 25 src/app/+admin/follows/following-list/following-list.component.html 32 src/app/shared/shared-abuse-list/abuse-list-table.component.html 24
-
+
Messages
رسائل
src/app/shared/shared-abuse-list/abuse-list-table.component.html 25
-
+
Internal note
ملاحظة داخلية
src/app/shared/shared-abuse-list/abuse-list-table.component.html 26
-
+
Score
تقييم
src/app/+admin/follows/followers-list/followers-list.component.html 26
-
+
Created
أُنشئ
@@ -1437,108 +1443,108 @@
src/app/+admin/follows/followers-list/followers-list.component.html 27 src/app/+admin/follows/following-list/following-list.component.html 33 src/app/+admin/system/jobs/jobs.component.html 50 src/app/+my-library/my-video-imports/my-video-imports.component.html 20 src/app/shared/shared-abuse-list/abuse-list-table.component.html 23
-
+
Open actor page in a new tab
افتح صفحة الممثل في علامة تبويب جديدة
src/app/+admin/follows/followers-list/followers-list.component.html 42
-
+
Accepted
تم القبول
src/app/+admin/follows/followers-list/followers-list.component.html 49 src/app/+admin/follows/following-list/following-list.component.html 51
-
+
Pending
معلق
src/app/+admin/follows/followers-list/followers-list.component.html 52 src/app/+admin/follows/following-list/following-list.component.html 54
-
+
Accept
موافقة
- src/app/+admin/follows/followers-list/followers-list.component.html 35 src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html 30 src/app/+my-library/my-ownership/my-ownership.component.html 33
-
+ src/app/+admin/follows/followers-list/followers-list.component.html 35 src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html 25 src/app/+my-library/my-ownership/my-ownership.component.html 33
+
Refuse
رفض
src/app/+admin/follows/followers-list/followers-list.component.html 36 src/app/+my-library/my-ownership/my-ownership.component.html 34
-
+
No follower found matching current filters.
لم يتم العثور على متابع مطابق للمرشحات الحالية.
src/app/+admin/follows/followers-list/followers-list.component.html 64
-
+
Your instance doesn't have any follower.
مثيل الخادم الخاص بك ليس لديه أي متابع.
src/app/+admin/follows/followers-list/followers-list.component.html 65
-
+
Showing to of followers
يعرض ل من متابع
src/app/+admin/follows/followers-list/followers-list.component.html 11
-
+
Instances you follow
المثلاء المتابَعون
src/app/+admin/follows/following-list/following-list.component.html 3
-
+
Host
المضيف
src/app/+admin/follows/following-list/following-list.component.html 31
-
+
Redundancy allowed
Redundancy allowed
src/app/+admin/follows/following-list/following-list.component.html 34
-
+
Unfollow
ألغ المتابعة
src/app/+admin/follows/following-list/following-list.component.html 41
-
+
Open instance in a new tab
افتح مثيل الخادم في لسان جديد
src/app/+admin/follows/following-list/following-list.component.html 44 src/app/shared/shared-moderation/server-blocklist.component.html 42 src/app/shared/shared-moderation/server-blocklist.component.html 42
-
+
No host found matching current filters.
لم يُعثر على مضيف مطابق للمرشحات الحالية.
src/app/+admin/follows/following-list/following-list.component.html 70
-
+
Your instance is not following anyone.
مثيل الخادم الخاص بك لا يتابع أي شخص.
src/app/+admin/follows/following-list/following-list.component.html 71
-
+
Showing to of hosts
يعرض ل من مضيفا
src/app/+admin/follows/following-list/following-list.component.html 11
-
+
It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
يبدو أنك لست على خادم HTTPS. يحتاج خادم الويب الخاص بك إلى تنشيط TLS لمتابعة الخوادم.
src/app/+admin/follows/following-list/following-list.component.html 81
-
+
Follow domains
تابع النطاق
src/app/+admin/follows/following-list/following-list.component.html 78
-
+
Follow instances
Follow instances
- src/app/+admin/follows/following-list/following-list.component.html 18
+ src/app/+admin/follows/following-list/following-list.component.html 18
Action Action
@@ -1547,74 +1553,74 @@
src/app/+admin/follows/following-list/following-list.component.html 30 src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html 27 src/app/shared/shared-moderation/account-blocklist.component.html 22 src/app/shared/shared-moderation/account-blocklist.component.html 22 src/app/shared/shared-moderation/server-blocklist.component.html 30 src/app/shared/shared-moderation/server-blocklist.component.html 30
-
+
Videos redundancies
Videos redundancies
src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html 3
-
+
My videos duplicated by remote instances
كررت مقاطع الفيديو الخاصة بي بواسطة مثلاء بعيدين
src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html 12
-
+
Remote videos duplicated by my instance
كرر مثيل الخادم خاصتي مقاطع الفيديو البعيدة