]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Merge remote-tracking branch 'weblate/develop' into develop
authorChocobozzz <me@florianbigard.com>
Wed, 5 May 2021 08:53:21 +0000 (10:53 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 5 May 2021 08:53:21 +0000 (10:53 +0200)
285 files changed:
README.md
client/.sass-lint.yml [deleted file]
client/.stylelintrc.json [new file with mode: 0644]
client/e2e/src/po/video-watch.po.ts
client/package.json
client/src/app/+about/about-follows/about-follows.component.html
client/src/app/+about/about-follows/about-follows.component.scss
client/src/app/+about/about-instance/about-instance.component.scss
client/src/app/+about/about-peertube/about-peertube.component.scss
client/src/app/+accounts/account-video-channels/account-video-channels.component.html
client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
client/src/app/+accounts/accounts.component.html
client/src/app/+accounts/accounts.component.scss
client/src/app/+accounts/accounts.module.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/followers-list/followers-list.component.scss
client/src/app/+admin/follows/followers-list/followers-list.component.ts
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.scss
client/src/app/+admin/follows/following-list/following-list.component.ts
client/src/app/+admin/follows/follows.component.scss
client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss
client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss
client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss
client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss
client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
client/src/app/+admin/plugins/plugins.component.scss [deleted file]
client/src/app/+admin/plugins/plugins.component.ts
client/src/app/+admin/plugins/shared/plugin-list.component.scss
client/src/app/+admin/system/jobs/jobs.component.scss
client/src/app/+admin/system/jobs/jobs.component.ts
client/src/app/+admin/system/logs/logs.component.scss
client/src/app/+admin/users/user-edit/user-edit.component.scss
client/src/app/+admin/users/user-edit/user-password.component.scss
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/user-list/user-list.component.scss
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/+login/login.component.scss
client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html
client/src/app/+my-account/my-account-applications/my-account-applications.component.scss
client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.scss
client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss
client/src/app/+my-account/my-account.component.scss
client/src/app/+my-account/my-account.module.ts
client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
client/src/app/+my-library/my-history/my-history.component.html
client/src/app/+my-library/my-history/my-history.component.scss
client/src/app/+my-library/my-history/my-history.component.ts
client/src/app/+my-library/my-library.component.scss
client/src/app/+my-library/my-library.module.ts
client/src/app/+my-library/my-ownership/my-ownership.component.html
client/src/app/+my-library/my-ownership/my-ownership.component.scss
client/src/app/+my-library/my-ownership/my-ownership.component.ts
client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
client/src/app/+my-library/my-subscriptions/my-subscriptions.component.ts
client/src/app/+my-library/my-video-imports/my-video-imports.component.scss
client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts
client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
client/src/app/+my-library/my-videos/my-videos.component.html
client/src/app/+my-library/my-videos/my-videos.component.scss
client/src/app/+my-library/my-videos/my-videos.component.ts
client/src/app/+search/search-filters.component.scss
client/src/app/+search/search.component.html
client/src/app/+search/search.component.scss
client/src/app/+search/search.component.ts
client/src/app/+search/search.module.ts
client/src/app/+signup/+register/register.component.scss
client/src/app/+signup/shared/signup-success.component.scss
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.scss
client/src/app/+video-channels/video-channels.module.ts
client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
client/src/app/+videos/+video-edit/shared/video-edit.component.html
client/src/app/+videos/+video-edit/shared/video-edit.component.scss
client/src/app/+videos/+video-edit/video-add-components/video-send.scss
client/src/app/+videos/+video-edit/video-add.component.scss
client/src/app/+videos/+video-watch/comment/video-comment-add.component.html
client/src/app/+videos/+video-watch/comment/video-comment-add.component.scss
client/src/app/+videos/+video-watch/comment/video-comment.component.html
client/src/app/+videos/+video-watch/comment/video-comment.component.scss
client/src/app/+videos/+video-watch/comment/video-comment.component.ts
client/src/app/+videos/+video-watch/comment/video-comments.component.scss
client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
client/src/app/+videos/+video-watch/video-avatar-channel.component.html
client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
client/src/app/+videos/+video-watch/video-watch-playlist.component.scss
client/src/app/+videos/+video-watch/video-watch.component.html
client/src/app/+videos/+video-watch/video-watch.component.scss
client/src/app/+videos/+video-watch/video-watch.module.ts
client/src/app/+videos/video-list/overview/video-overview.component.html
client/src/app/+videos/video-list/overview/video-overview.component.scss
client/src/app/+videos/video-list/overview/video-overview.component.ts
client/src/app/+videos/video-list/trending/video-trending-header.component.scss
client/src/app/+videos/videos.module.ts
client/src/app/app.component.scss
client/src/app/app.module.ts
client/src/app/core/hotkeys/hotkeys.component.scss
client/src/app/core/rest/rest-table.ts
client/src/app/core/rest/rest.service.ts
client/src/app/core/users/user.service.ts
client/src/app/core/wrappers/screen.service.ts
client/src/app/header/search-typeahead.component.scss
client/src/app/header/suggestion.component.scss
client/src/app/menu/language-chooser.component.scss
client/src/app/menu/menu.component.html
client/src/app/menu/menu.component.scss
client/src/app/menu/notification.component.scss
client/src/app/modal/welcome-modal.component.scss
client/src/app/shared/shared-abuse-list/abuse-details.component.html
client/src/app/shared/shared-abuse-list/abuse-details.component.ts
client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
client/src/app/shared/shared-account-avatar/account-avatar.component.html [deleted file]
client/src/app/shared/shared-account-avatar/account-avatar.component.scss [deleted file]
client/src/app/shared/shared-account-avatar/account-avatar.component.ts [deleted file]
client/src/app/shared/shared-account-avatar/index.ts [deleted file]
client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts [deleted file]
client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html [moved from client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html with 93% similarity]
client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss [moved from client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss with 84% similarity]
client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts [moved from client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts with 91% similarity]
client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.html [moved from client/src/app/shared/shared-actor-image/actor-banner-edit.component.html with 100% similarity]
client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.scss [moved from client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss with 100% similarity]
client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts [moved from client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts with 100% similarity]
client/src/app/shared/shared-actor-image-edit/actor-image-edit.scss [moved from client/src/app/shared/shared-actor-image/actor-image-edit.scss with 100% similarity]
client/src/app/shared/shared-actor-image-edit/index.ts [new file with mode: 0644]
client/src/app/shared/shared-actor-image-edit/shared-actor-image-edit.module.ts [new file with mode: 0644]
client/src/app/shared/shared-actor-image/actor-avatar.component.html [new file with mode: 0644]
client/src/app/shared/shared-actor-image/actor-avatar.component.scss [new file with mode: 0644]
client/src/app/shared/shared-actor-image/actor-avatar.component.ts [new file with mode: 0644]
client/src/app/shared/shared-actor-image/shared-actor-image.module.ts
client/src/app/shared/shared-forms/advanced-input-filter.component.html [new file with mode: 0644]
client/src/app/shared/shared-forms/advanced-input-filter.component.scss [new file with mode: 0644]
client/src/app/shared/shared-forms/advanced-input-filter.component.ts [new file with mode: 0644]
client/src/app/shared/shared-forms/index.ts
client/src/app/shared/shared-forms/input-switch.component.scss
client/src/app/shared/shared-forms/markdown-textarea.component.scss
client/src/app/shared/shared-forms/peertube-checkbox.component.scss
client/src/app/shared/shared-forms/preview-upload.component.scss
client/src/app/shared/shared-forms/select/select-shared.component.scss
client/src/app/shared/shared-forms/shared-form.module.ts
client/src/app/shared/shared-forms/timestamp-input.component.scss
client/src/app/shared/shared-instance/instance-about-accordion.component.scss
client/src/app/shared/shared-instance/instance-features-table.component.scss
client/src/app/shared/shared-main/account/account.model.ts
client/src/app/shared/shared-main/account/actor.model.ts
client/src/app/shared/shared-main/buttons/action-dropdown.component.scss
client/src/app/shared/shared-main/buttons/button.component.scss
client/src/app/shared/shared-main/buttons/button.component.ts
client/src/app/shared/shared-main/buttons/delete-button.component.html
client/src/app/shared/shared-main/buttons/delete-button.component.ts
client/src/app/shared/shared-main/buttons/edit-button.component.html
client/src/app/shared/shared-main/buttons/edit-button.component.ts
client/src/app/shared/shared-main/date/date-toggle.component.scss
client/src/app/shared/shared-main/feeds/feed.component.scss
client/src/app/shared/shared-main/loaders/loader.component.scss
client/src/app/shared/shared-main/misc/help.component.scss
client/src/app/shared/shared-main/misc/list-overflow.component.html
client/src/app/shared/shared-main/misc/list-overflow.component.scss
client/src/app/shared/shared-main/misc/top-menu-dropdown.component.scss
client/src/app/shared/shared-main/users/user-notification.model.ts
client/src/app/shared/shared-main/users/user-notifications.component.scss
client/src/app/shared/shared-main/users/user-quota.component.scss
client/src/app/shared/shared-main/video-channel/video-channel.model.ts
client/src/app/shared/shared-main/video/video.model.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/app/shared/shared-moderation/account-blocklist.component.html
client/src/app/shared/shared-moderation/account-blocklist.component.scss
client/src/app/shared/shared-moderation/account-blocklist.component.ts
client/src/app/shared/shared-moderation/moderation.scss
client/src/app/shared/shared-moderation/server-blocklist.component.html
client/src/app/shared/shared-moderation/server-blocklist.component.scss
client/src/app/shared/shared-moderation/server-blocklist.component.ts
client/src/app/shared/shared-moderation/shared-moderation.module.ts
client/src/app/shared/shared-moderation/video-block.component.scss
client/src/app/shared/shared-search/advanced-search.model.ts
client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
client/src/app/shared/shared-user-settings/user-video-settings.component.html
client/src/app/shared/shared-user-settings/user-video-settings.component.ts
client/src/app/shared/shared-user-subscription/remote-subscribe.component.scss
client/src/app/shared/shared-user-subscription/subscribe-button.component.scss
client/src/app/shared/shared-video-comment/video-comment.service.ts
client/src/app/shared/shared-video-miniature/abstract-video-list.scss
client/src/app/shared/shared-video-miniature/shared-video-miniature.module.ts
client/src/app/shared/shared-video-miniature/video-download.component.scss
client/src/app/shared/shared-video-miniature/video-miniature.component.html
client/src/app/shared/shared-video-miniature/video-miniature.component.scss
client/src/app/shared/shared-video-miniature/video-miniature.component.ts
client/src/app/shared/shared-video-miniature/videos-selection.component.html
client/src/app/shared/shared-video-miniature/videos-selection.component.ts
client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
client/src/app/shared/shared-video-playlist/video-playlist.model.ts
client/src/assets/player/images/info.svg [new file with mode: 0644]
client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/peertube-player-local-storage.ts
client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/peertube-videojs-typings.ts
client/src/assets/player/stats/stats-card.ts [new file with mode: 0644]
client/src/assets/player/stats/stats-plugin.ts [new file with mode: 0644]
client/src/assets/player/videojs-components/settings-menu-button.ts
client/src/assets/player/webtorrent/webtorrent-plugin.ts
client/src/sass/application.scss
client/src/sass/bootstrap.scss
client/src/sass/include/_actor.scss
client/src/sass/include/_bootstrap.scss
client/src/sass/include/_fonts.scss
client/src/sass/include/_miniature.scss
client/src/sass/include/_mixins.scss
client/src/sass/include/_variables.scss
client/src/sass/ng-select.scss
client/src/sass/player/context-menu.scss
client/src/sass/player/index.scss
client/src/sass/player/mobile.scss
client/src/sass/player/peertube-skin.scss
client/src/sass/player/playlist.scss
client/src/sass/player/settings-menu.scss
client/src/sass/player/spinner.scss
client/src/sass/player/stats.scss [new file with mode: 0644]
client/src/sass/player/upnext.scss
client/src/sass/primeng-custom.scss
client/src/standalone/videos/embed.scss
client/src/standalone/videos/test-embed.scss
client/yarn.lock
package.json
scripts/clean/client/index.sh [moved from scripts/clean/client/dist.sh with 100% similarity]
scripts/clean/server/dist.sh [deleted file]
scripts/danger/clean/cleaner.ts [deleted file]
scripts/danger/clean/dev.sh [deleted file]
scripts/danger/clean/modules.sh [deleted file]
scripts/danger/clean/prod.sh [deleted file]
scripts/help.sh
scripts/i18n/create-custom-files.ts
scripts/play.sh [deleted file]
scripts/prune-storage.ts
server/controllers/api/accounts.ts
server/controllers/api/users/me.ts
server/controllers/api/users/my-subscriptions.ts
server/controllers/api/video-channel.ts
server/controllers/api/videos/index.ts
server/helpers/custom-validators/search.ts
server/initializers/constants.ts
server/lib/job-queue/handlers/video-transcoding.ts
server/lib/video.ts
server/middlewares/validators/videos/videos.ts
server/models/video/video-query-builder.ts
server/models/video/video.ts
server/tests/api/live/live.ts
server/tests/api/search/search-videos.ts
server/tests/api/videos/single-server.ts
server/tests/api/videos/video-transcoder.ts
shared/extra-utils/videos/videos.ts
shared/models/search/boolean-both-query.model.ts [new file with mode: 0644]
shared/models/search/index.ts
shared/models/search/nsfw-query.model.ts [deleted file]
shared/models/search/videos-common-query.model.ts [new file with mode: 0644]
shared/models/search/videos-search-query.model.ts
support/doc/api/openapi.yaml
support/doc/production.md
yarn.lock

index 377794a0aeeaeebf6c5fc5c4a81a6e9c5144712d..f5fb6aceae4a4d4db00949994764e12694d5d970 100644 (file)
--- a/README.md
+++ b/README.md
@@ -142,14 +142,14 @@ Feel free to reach out if you have any questions or ideas! :speech_balloon:
 :package: Create your own instance
 ----------------------------------------------------------------
 
-See the [production guide](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md), which is the recommended way to install or upgrade PeerTube. For hardware requirements, see [Should I have a big server to run PeerTube?](https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md#should-i-have-a-big-server-to-run-peertube) in the FAQ.
+See the [production guide](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md), which is the recommended way to install or upgrade PeerTube. For hardware requirements, see [Should I have a big server to run PeerTube?](https://joinpeertube.org/faq#should-i-have-a-big-server-to-run-peertube) in the FAQ.
 
 See the [community packages](https://docs.joinpeertube.org/install-unofficial), which cover various platforms (including [YunoHost](https://install-app.yunohost.org/?app=peertube) and [Docker](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/docker.md)).
 
 :book: Documentation
 ----------------------------------------------------------------
 
-If you have a question, please try to find the answer in the [FAQ](https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md) first.
+If you have a question, please try to find the answer in the [FAQ](https://joinpeertube.org/faq) first.
 
 ### User documentation
 
diff --git a/client/.sass-lint.yml b/client/.sass-lint.yml
deleted file mode 100644 (file)
index a6e949c..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-files:
-  include:
-    - "src/app/**/*.scss"
-    - "src/assets/**/*.scss"
-    - "src/sass/**/*.scss"
-    - "src/standalone/**/*.scss"
-syntax:
-    include:
-      - scss
-      - sass
-rules:
-  property-sort-order: 0
-  attribute-quotes: 0
-  border-zero: 0
-  no-color-keywords: 0
-  no-color-literals: 0
-  no-css-comments: 0
-  no-important: 0
-  no-trailing-zero: 1
-  space-after-bang: 1
-  space-before-bang: 1
-  space-after-colon: 1
-  space-before-colon: 1
-  clean-import-paths: 0
-  hex-length: 1
-  hex-notation: 0
-  nesting-depth:
-    - 1
-    - max-depth: 4
-  indentation: 2
diff --git a/client/.stylelintrc.json b/client/.stylelintrc.json
new file mode 100644 (file)
index 0000000..25f0b10
--- /dev/null
@@ -0,0 +1,29 @@
+{
+  "extends": "stylelint-config-sass-guidelines",
+  "rules": {
+    "scss/at-import-no-partial-leading-underscore": null,
+    "color-hex-case": null,
+    "color-hex-length": null,
+    "order/properties-alphabetical-order": null,
+    "selector-pseudo-element-no-unknown": [
+      true,
+      {
+        "ignorePseudoElements": [ "ng-deep" ]
+      }
+    ],
+    "max-nesting-depth": [
+      8,
+      {
+        "ignore": [ "blockless-at-rules", "pseudo-classes" ]
+      }
+    ],
+    "selector-max-compound-selectors": 9,
+    "selector-no-qualifying-type": null,
+    "scss/at-extend-no-missing-placeholder": null,
+    "number-leading-zero": null,
+    "rule-empty-line-before": null,
+    "selector-max-id": null,
+    "scss/at-function-pattern": null,
+    "function-parentheses-space-inside": "never-single-line"
+  }
+}
index fb9c3a000661e8628f42c3023c3a3a1043ff3378..b41fe0882343e2ee9715e5ef26aa836ff638af1d 100644 (file)
@@ -86,7 +86,7 @@ export class VideoWatchPage {
     const dropdown = element(by.css('my-video-actions-dropdown .action-button'))
     await dropdown.click()
 
-    const items: ElementFinder[] = await element.all(by.css('my-video-actions-dropdown .dropdown-menu .dropdown-item'))
+    const items: ElementFinder[] = await element.all(by.css('.dropdown-menu.show .dropdown-item'))
 
     for (const item of items) {
       const href = await item.getAttribute('href')
index 8a344c1af577a49cb81bf27f002be6715ff8e621..140fc30959b01ccc361a08dbf6dacc7200009d1c 100644 (file)
   "scripts": {
     "lint": "npm run lint-ts && npm run lint-scss",
     "lint-ts": "tslint --project ./tsconfig.app.json -c ./tslint.json 'src/app/**/*.ts' 'src/standalone/**/*.ts'",
-    "lint-scss": "sass-lint -c .sass-lint.yml",
+    "lint-scss": "stylelint 'src/**/*.scss'",
     "webpack": "webpack",
     "tslint": "tslint",
     "ng": "ng",
     "webpack-bundle-analyzer": "webpack-bundle-analyzer",
     "webdriver-manager": "webdriver-manager",
     "ngx-extractor": "ngx-extractor",
-    "sass-lint": "sass-lint"
+    "stylelint": "stylelint"
   },
   "typings": "*.d.ts",
   "resolutions": {
     "rxjs": "^6.5.2",
     "sanitize-html": "^2.1.2",
     "sass": "^1.29.0",
-    "sass-lint": "^1.13.1",
     "sass-loader": "^10",
     "sass-resources-loader": "^2.0.0",
     "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",
     "tslib": "^2.0.0",
index e9139b503678183fbb75574c2fde49d30776fe8a..f81465f88c2ed2a224c99ac0166f91419371ba30 100644 (file)
@@ -21,7 +21,7 @@
       {{ following }}
     </a>
 
-    <button i18n class="showMore" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button>
+    <button i18n class="show-more" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button>
   </div>
 
 </div>
index a393c9d9207d89f45d4411fdf121a8176a4253ec..83241e7273f82678976275f71fe5cfc6875cd075 100644 (file)
@@ -17,9 +17,9 @@ a {
   justify-content: flex-start;
 }
 
-.showMore {
+.show-more {
   @include peertube-button-link;
   @include grey-button;
-  
+
   margin-top: 1%;
 }
index 7158a3a792fd17a4d0301cf8acafa3f4c2400026..9886bdfef21833220d84f5a2caf9eaf4a853f5d2 100644 (file)
@@ -63,7 +63,8 @@
 
   position: relative;
 
-  &:hover, &:active {
+  &:hover,
+  &:active {
     &::after {
       content: '#';
       display: inline-block;
@@ -71,7 +72,8 @@
     }
   }
 
-  .middle-title, .section-title {
+  .middle-title,
+  .section-title {
     display: inline-block;
   }
 
index e6725241065e8a7fb9c6e781d802d51d6ac17edc..e5d2bc5b859ef9b062e4b06b5765f682e8f7208e 100644 (file)
@@ -45,7 +45,8 @@
 .p2p-privacy,
 my-about-peertube-contributors {
   ::ng-deep {
-    p, li {
+    p,
+    li {
       font-size: 15px;
     }
   }
index 19a4b3c9c4888fe0c7b38e5c1f546603b6acfe97..922608127ac871218a0e71c0ff0cb18c87b0a93a 100644 (file)
@@ -8,9 +8,10 @@
     <div class="channel" *ngFor="let videoChannel of videoChannels">
 
       <div class="channel-avatar-row">
-        <a class="avatar-link" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
-          <img [src]="videoChannel.avatarUrl" alt="Avatar" />
-        </a>
+        <my-actor-avatar
+          [channel]="videoChannel" [internalHref]="getVideoChannelLink(videoChannel)"
+          i18n-title title="See this video channel"
+        ></my-actor-avatar>
 
         <h2>
           <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
index 7e88802f323027e5e1e712c57652e49d50b23063..f9d09764462354ec5bdcf734f8fe766083b2b62b 100644 (file)
   grid-template-columns: auto auto 1fr;
   grid-template-rows: auto 1fr;
 
-  .avatar-link {
+  my-actor-avatar {
+    @include actor-avatar-size(75px);
+
     grid-column: 1;
     grid-row: 1 / 3;
-    margin-right: 30px;
-  }
-
-  img {
-    @include channel-avatar(75px);
+    margin-right: 15px;
   }
 
   a {
   }
 
   .description-html {
+    @include fade-text(30px, pvar(--channelBackgroundColor));
+
     grid-column: 2 / 4;
     grid-row: 2;
 
     max-height: 80px;
     font-size: 16px;
-
-    @include fade-text(30px, pvar(--channelBackgroundColor));
   }
 }
 
index ea7a317eb63780f64cb08c6bbe8a352ecad7eb79..350c77f1e697d275d8cdabc4d335fb3bc04c2c0f 100644 (file)
@@ -2,7 +2,7 @@
   <div class="account-info">
 
     <div class="account-avatar-row">
-      <my-account-avatar [account]="account" size="120"></my-account-avatar>
+      <my-actor-avatar class="main-avatar" [account]="account"></my-actor-avatar>
 
       <div>
         <div class="section-label" i18n>PEERTUBE ACCOUNT</div>
index 56927dea6917a6afa464c0d5d476661ab0f0577e..2e99fe8a5ce8a51f52ca795ae8fdcaf538623752 100644 (file)
@@ -40,7 +40,7 @@ my-user-moderation-dropdown,
 }
 
 .copy-button {
-  border: none;
+  border: 0;
 }
 
 .account-info {
@@ -104,9 +104,9 @@ my-user-moderation-dropdown,
   }
 
   .description:not(.expanded) {
-    max-height: 70px;
-
     @include fade-text(30px, pvar(--submenuBackgroundColor));
+
+    max-height: 70px;
   }
 
   .show-more {
index 22cdd0642550d8fe99b9e35d1e4bcc81bd7306cb..1bafc5141b45cb56204da3a12bd59254fd339de6 100644 (file)
@@ -10,7 +10,7 @@ import { AccountVideoChannelsComponent } from './account-video-channels/account-
 import { AccountVideosComponent } from './account-videos/account-videos.component'
 import { AccountsRoutingModule } from './accounts-routing.module'
 import { AccountsComponent } from './accounts.component'
-import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -22,7 +22,7 @@ import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/share
     SharedModerationModule,
     SharedVideoMiniatureModule,
     SharedGlobalIconModule,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
index 8d1c3eadbd1483748bafb0d74434eda2f7f9f65c..45366f9ec41c1770d2a22548a250c61bc3530b74 100644 (file)
@@ -3,12 +3,13 @@ import { SelectButtonModule } from 'primeng/selectbutton'
 import { TableModule } from 'primeng/table'
 import { NgModule } from '@angular/core'
 import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
-import { SharedActorImageModule } from '@app/shared/shared-actor-image'
+import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
 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 {
@@ -39,7 +40,6 @@ import { JobService, LogsComponent, LogsService, SystemComponent } from './syste
 import { DebugComponent, DebugService } from './system/debug'
 import { JobsComponent } from './system/jobs/jobs.component'
 import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
-import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
 
 @NgModule({
   imports: [
@@ -51,8 +51,8 @@ import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/share
     SharedGlobalIconModule,
     SharedAbuseListModule,
     SharedVideoCommentModule,
-    SharedAccountAvatarModule,
     SharedActorImageModule,
+    SharedActorImageEditModule,
 
     TableModule,
     SelectButtonModule,
index c12465d458a6e89da5eea73e9ec06ef734c27276..cc2a98a178b56c68b142164ce4bb3232feb1d914 100644 (file)
@@ -57,7 +57,7 @@ input[type=submit] {
   display: flex;
   margin-left: auto;
 
-  + .form-error {
+  + .form-error {
     display: inline;
     margin-left: 5px;
   }
@@ -84,7 +84,8 @@ textarea {
 }
 
 .disabled-checkbox-extra {
-  &, ::ng-deep label {
+  &,
+  ::ng-deep label {
     opacity: .5;
     pointer-events: none;
   }
index 35b42e742097557030ee0675a21e0b4d372c5569..cbff26e5af4c147a0d951de8dfefb78c6a061540 100644 (file)
           <my-help>
             <ng-template ptTemplate="customHtml">
               <ng-container i18n>
-                With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
+                With <strong>Hide</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
               </ng-container>
             </ng-template>
           </my-help>
           <div class="peertube-select-container">
             <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy" class="form-control">
               <option i18n value="undefined" disabled>Policy for sensitive videos</option>
-              <option i18n value="do_not_list">Do not list</option>
+              <option i18n value="do_not_list">Hide</option>
               <option i18n value="blur">Blur thumbnails</option>
               <option i18n value="display">Display</option>
             </select>
index 633de96775bb38815dbcca28c0df8497b6c8e0bc..c2e9a4df69fa61d6537697a2ccc17a8def72fd09 100644 (file)
@@ -4,20 +4,16 @@
 </h1>
 
 <p-table
-  [value]="followers" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
+  [value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onPage)="onPage($event)"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto has-feedback has-clear">
-        <input
-          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-          (keyup)="onSearch($event)"
-        >
-        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-        <span class="sr-only" i18n>Clear filters</span>
+      <div class="ml-auto">
+        <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
index f2d752eb581fe0297a6095c24b582575d4964b4f..35f38aae0664aa2f72c3a8da8568991cb2526d43 100644 (file)
@@ -1,19 +1,12 @@
 @import '_variables';
 @import '_mixins';
 
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-  }
-}
-
 a {
   @include disable-default-a-behaviour;
   display: inline-block;
 
-  &, &:hover {
+  &,
+  &:hover {
     color: pvar(--mainForegroundColor);
   }
 
index 904e3c338a90d06aab05a810c851daf8b0e525fd..4a312f6aa891bbdec94ed53bebc6236bf7871198 100644 (file)
@@ -59,7 +59,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
             const handle = follow.follower.name + '@' + follow.follower.host
             this.notifier.success($localize`${handle} rejected from instance followers`)
 
-            this.loadData()
+            this.reloadData()
           },
 
           err => {
@@ -80,14 +80,14 @@ export class FollowersListComponent extends RestTable implements OnInit {
             const handle = follow.follower.name + '@' + follow.follower.host
             this.notifier.success($localize`${handle} removed from instance followers`)
 
-            this.loadData()
+            this.reloadData()
           },
 
           err => this.notifier.error(err.message)
         )
   }
 
-  protected loadData () {
+  protected reloadData () {
     this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search })
                       .subscribe(
                         resultList => {
index f4e6a60fe134044d99778a63eedb95f3b0248569..e7c0c908823bc1ec4d6f9531a31ae7bf2e36d358 100644 (file)
@@ -4,8 +4,9 @@
 </h1>
 
 <p-table
-  [value]="following" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
 >
         </a>
       </div>
 
-      <div class="ml-auto has-feedback has-clear">
-        <input
-          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-          (keyup)="onSearch($event)"
-        >
-        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-        <span class="sr-only" i18n>Clear filters</span>
+      <div class="ml-auto">
+        <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
index b108218b80fc8ffc01a983928c49ba14305e6719..9474b0a234f9b26c7bce2e31739c40be57b0222e 100644 (file)
@@ -5,7 +5,8 @@ a {
   @include disable-default-a-behaviour;
   display: inline-block;
 
-  &, &:hover {
+  &,
+  &:hover {
     color: pvar(--mainForegroundColor);
   }
 
@@ -15,14 +16,6 @@ a {
   }
 }
 
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-  }
-}
-
 .follow-button {
   @include create-button;
 }
index f34490cc87c9888e7afc326e1f6b43ddea310729..b63fe08c0b4b36f0854f9da306836f02338f8492 100644 (file)
@@ -45,7 +45,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
     this.followService.follow(hosts).subscribe(
       () => {
         this.notifier.success($localize`Follow request(s) sent!`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message)
@@ -62,14 +62,14 @@ export class FollowingListComponent extends RestTable implements OnInit {
     this.followService.unfollow(follow).subscribe(
       () => {
         this.notifier.success($localize`You are not following ${follow.following.host} anymore.`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message)
     )
   }
 
-  protected loadData () {
+  protected reloadData () {
     this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search })
                       .subscribe(
                         resultList => {
index 33ff1753917b72e01e71de36ca7e75a59825f2f4..1ed0d925fe4fdaa82195f16d5fef166df3b0ca9c 100644 (file)
@@ -1,4 +1,4 @@
-@import "mixins";
+@import 'mixins';
 
 .form-sub-title {
   flex-grow: 0;
index adcf2037ee5f629d25bf9118515213c1791aaed3..30b9f2147805c833b251b7ce0ef3a9316a26d1ca 100644 (file)
@@ -5,7 +5,8 @@ a {
   @include disable-default-a-behaviour;
   display: inline-block;
 
-  &, &:hover {
+  &,
+  &:hover {
     color: pvar(--mainForegroundColor);
   }
 
index d6fd1a1ab62e020237832ccc18848e6a5a65a67f..3cd65dd6e3ed144798dd4c751ec5bc7f2d626d82 100644 (file)
@@ -78,7 +78,7 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
     this.pagination.start = 0
     this.saveSelectLocalStorage()
 
-    this.loadData()
+    this.reloadData()
   }
 
   getRedundancyStrategy (redundancy: VideoRedundancy) {
@@ -145,7 +145,7 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
       .subscribe(
         () => {
           this.notifier.success($localize`Video redundancies removed!`)
-          this.loadData()
+          this.reloadData()
         },
 
         err => this.notifier.error(err.message)
@@ -153,7 +153,7 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
 
   }
 
-  protected loadData () {
+  protected reloadData () {
     const options = {
       pagination: this.pagination,
       sort: this.sort,
index 9a6c124e12d28334cc44ea6303d6b27b8b7a8267..a9e0931f828b11fdc39601a4abdbbdfcbf3852c8 100644 (file)
@@ -3,4 +3,4 @@
   <ng-container i18n>Reports</ng-container>
 </h1>
 
-<my-abuse-list-table viewType="admin" baseRoute="/admin/moderation/abuses/list"></my-abuse-list-table>
+<my-abuse-list-table viewType="admin"></my-abuse-list-table>
index f5cf93adb8ab48f57e992f77b463c30c6d16a88f..e3a3a83207b49744cb10860630794f8de2278e79 100644 (file)
@@ -1,18 +1,14 @@
 <p-table
-  [value]="blockedAccounts" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
+  [value]="blockedAccounts" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onPage)="onPage($event)"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted accounts"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto has-feedback has-clear">
-        <input
-          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-          (keyup)="onSearch($event)"
-        >
-        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-        <span class="sr-only" i18n>Clear filters</span>
+      <div class="ml-auto">
+        <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
@@ -34,7 +30,7 @@
       <td>
         <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="chip two-lines">
-            <my-account-avatar [account]="accountBlock.blockedAccount"></my-account-avatar>
+            <my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar>
             <div>
               {{ accountBlock.blockedAccount.displayName }}
               <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
index 6028b75eaa641b53cb7511293211ab9e1f7782bc..1d98e44d92f3ff70ddd1a32dbe08e5c481f8e38a 100644 (file)
@@ -4,4 +4,4 @@
 .unblock-button {
   @include peertube-button;
   @include grey-button;
-}
\ No newline at end of file
+}
index c7275de1b5817bd72e911912e939ae4d508bdf61..d89c8f24445c2754eb36617e90584b57c16bc715 100644 (file)
@@ -4,8 +4,9 @@
 </h1>
 
 <p-table
-  [value]="blocklist" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
+  [value]="blocklist" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} blocked videos"
   (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
   <ng-template pTemplate="caption">
     <div class="caption">
       <div class="ml-auto">
-        <div class="input-group has-feedback has-clear">
-          <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
-            <div class="input-group-text" ngbDropdownToggle>
-              <span class="caret" aria-haspopup="menu" role="button"></span>
-            </div>
-
-            <div role="menu" ngbDropdownMenu>
-              <h6 class="dropdown-header" i18n>Advanced block filters</h6>
-              <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:auto' }" class="dropdown-item" i18n>Automatic blocks</a>
-              <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:manual' }" class="dropdown-item" i18n>Manual blocks</a>
-            </div>
-          </div>
-          <input
-            type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-            (keyup)="onSearch($event)"
-          >
-          <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
-          <span class="sr-only" i18n>Clear filters</span>
-        </div>
+        <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
index b67e33cc1a0ec68b863390d15796f09594f3cb4d..068aa2aee76bd8df041c572b1f2066440d773a1d 100644 (file)
@@ -5,23 +5,6 @@ my-global-icon {
   height: 24px;
 }
 
-.input-group {
-  @include peertube-input-group(300px);
-
-  .dropdown-toggle::after {
-    margin-left: 0;
-  }
-}
-
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-    flex-grow: 1;
-  }
-}
-
 .badge {
   @include table-badge;
 }
index d6aca10e7dfb8f3ee686e4558c537dfdb21d7b61..498d8321aff7b1f538584117df97d152b8ce3741 100644 (file)
@@ -2,10 +2,11 @@ import { SortMeta } from 'primeng/api'
 import { switchMap } from 'rxjs/operators'
 import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 import { environment } from 'src/environments/environment'
-import { AfterViewInit, Component, OnInit } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
 import { DomSanitizer } from '@angular/platform-browser'
-import { ActivatedRoute, Params, Router } from '@angular/router'
+import { ActivatedRoute, Router } from '@angular/router'
 import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
+import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
 import { VideoBlockService } from '@app/shared/shared-moderation'
 import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
@@ -15,7 +16,7 @@ import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
   templateUrl: './video-block-list.component.html',
   styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ]
 })
-export class VideoBlockListComponent extends RestTable implements OnInit, AfterViewInit {
+export class VideoBlockListComponent extends RestTable implements OnInit {
   blocklist: (VideoBlacklist & { reasonHtml?: string, embedHtml?: string })[] = []
   totalRecords = 0
   sort: SortMeta = { field: 'createdAt', order: -1 }
@@ -24,6 +25,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
 
   videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = []
 
+  inputFilters: AdvancedInputFilter[] = [
+    {
+      queryParams: { 'search': 'type:auto' },
+      label: $localize`Automatic blocks`
+    },
+    {
+      queryParams: { 'search': 'type:manual' },
+      label: $localize`Manual blocks`
+    }
+  ]
+
   constructor (
     protected route: ActivatedRoute,
     protected router: Router,
@@ -52,7 +64,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
             ).subscribe(
               () => {
                 this.notifier.success($localize`Video ${videoBlock.video.name} switched to manual block.`)
-                this.loadData()
+                this.reloadData()
               },
 
               err => this.notifier.error(err.message)
@@ -104,31 +116,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
         })
 
     this.initialize()
-    this.listenToSearchChange()
-  }
-
-  ngAfterViewInit () {
-    if (this.search) this.setTableFilter(this.search, false)
-  }
-
-  /* Table filter functions */
-  onBlockSearch (event: Event) {
-    this.onSearch(event)
-    this.setQueryParams((event.target as HTMLInputElement).value)
-  }
-
-  setQueryParams (search: string) {
-    const queryParams: Params = {}
-    if (search) Object.assign(queryParams, { search })
-    this.router.navigate([ '/admin/moderation/video-blocks/list' ], { queryParams })
-  }
-
-  resetTableFilter () {
-    this.setTableFilter('')
-    this.setQueryParams('')
-    this.resetSearch()
   }
-  /* END Table filter functions */
 
   getIdentifier () {
     return 'VideoBlockListComponent'
@@ -151,7 +139,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
     this.videoBlocklistService.unblockVideo(entry.video.id).subscribe(
       () => {
         this.notifier.success($localize`Video ${entry.video.name} unblocked.`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message)
@@ -169,7 +157,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
     )
   }
 
-  protected loadData () {
+  protected reloadData () {
     this.videoBlocklistService.listBlocks({
       pagination: this.pagination,
       sort: this.sort,
index d360c3c51043559cb6c91be006fb50bc42799a67..9d9283536dccdfdc5c94191c674dd0abca8371d9 100644 (file)
@@ -8,8 +8,9 @@
 <em i18n>This view also shows comments from muted accounts.</em>
 
 <p-table
-  [value]="comments" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
+  [value]="comments" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments"
   (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
       </div>
 
       <div class="ml-auto">
-        <div class="input-group has-feedback has-clear">
-          <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
-            <div class="input-group-text" ngbDropdownToggle>
-              <span class="caret" aria-haspopup="menu" role="button"></span>
-            </div>
-
-            <div role="menu" ngbDropdownMenu>
-              <h6 class="dropdown-header" i18n>Advanced comments filters</h6>
-              <a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:true' }" class="dropdown-item" i18n>Local comments</a>
-              <a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:false' }" class="dropdown-item" i18n>Remote comments</a>
-            </div>
-          </div>
-          <input
-            type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-            (keyup)="onSearch($event)"
-          >
-          <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
-          <span class="sr-only" i18n>Clear filters</span>
-        </div>
+        <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
@@ -86,7 +69,7 @@
       <td>
         <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="chip two-lines">
-            <my-account-avatar [account]="videoComment.account"></my-account-avatar>
+            <my-actor-avatar [account]="videoComment.account"></my-actor-avatar>
           <div>
               {{ videoComment.account.displayName }}
               <span>{{ videoComment.by }}</span>
index c9262da098f136ff9715c1deef0e7818c080e61a..a6f931e3b5bce9dd0c151e506fd94812d16b529f 100644 (file)
@@ -11,23 +11,6 @@ my-global-icon {
   height: 24px;
 }
 
-.input-group {
-  @include peertube-input-group(300px);
-
-  .dropdown-toggle::after {
-    margin-left: 0;
-  }
-}
-
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-    flex-grow: 1;
-  }
-}
-
 .video {
   display: flex;
   flex-direction: column;
@@ -49,7 +32,8 @@ my-global-icon {
       max-height: 22px;
     }
 
-    div, p {
+    div,
+    p {
       @include ellipsis;
     }
 
index 63493d00d59ccd6600dc81ddb74ba239014829b6..e2ae993b0598fa02e8c7a804acf811b5a9d8a0d4 100644 (file)
@@ -2,6 +2,7 @@ import { SortMeta } from 'primeng/api'
 import { AfterViewInit, Component, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
+import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { DropdownAction } from '@app/shared/shared-main'
 import { BulkService } from '@app/shared/shared-moderation'
 import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
@@ -12,9 +13,7 @@ import { FeedFormat, UserRight } from '@shared/models'
   templateUrl: './video-comment-list.component.html',
   styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ]
 })
-export class VideoCommentListComponent extends RestTable implements OnInit, AfterViewInit {
-  baseRoute = '/admin/moderation/video-comments/list'
-
+export class VideoCommentListComponent extends RestTable implements OnInit {
   comments: VideoCommentAdmin[]
   totalRecords = 0
   sort: SortMeta = { field: 'createdAt', order: -1 }
@@ -43,6 +42,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
   selectedComments: VideoCommentAdmin[] = []
   bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = []
 
+  inputFilters: AdvancedInputFilter[] = [
+    {
+      queryParams: { 'search': 'local:true' },
+      label: $localize`Local comments`
+    },
+    {
+      queryParams: { 'search': 'local:false' },
+      label: $localize`Remote comments`
+    }
+  ]
+
   get authUser () {
     return this.auth.getUser()
   }
@@ -79,7 +89,6 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
 
   ngOnInit () {
     this.initialize()
-    this.listenToSearchChange()
 
     this.bulkCommentActions = [
       {
@@ -91,10 +100,6 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
     ]
   }
 
-  ngAfterViewInit () {
-    if (this.search) this.setTableFilter(this.search, false)
-  }
-
   getIdentifier () {
     return 'VideoCommentListComponent'
   }
@@ -107,7 +112,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
     return this.selectedComments.length !== 0
   }
 
-  protected loadData () {
+  protected reloadData () {
     this.videoCommentService.getAdminVideoComments({
       pagination: this.pagination,
       sort: this.sort,
@@ -135,7 +140,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
     this.videoCommentService.deleteVideoComments(commentArgs).subscribe(
       () => {
         this.notifier.success($localize`${commentArgs.length} comments deleted.`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message),
@@ -147,7 +152,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
   private deleteComment (comment: VideoCommentAdmin) {
     this.videoCommentService.deleteVideoComment(comment.video.id, comment.id)
       .subscribe(
-        () => this.loadData(),
+        () => this.reloadData(),
 
         err => this.notifier.error(err.message)
       )
index 9cbec03a11b6729422ad42392dd7f9e0cce4c732..bc4c2ef88bbd6c0ecf5eceb4c6efb7c52a0fc9eb 100644 (file)
         </a>
 
         <div class="buttons">
-          <my-edit-button *ngIf="!isTheme(plugin)" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
-
-          <my-button class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
-                     [label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)"
+          <my-edit-button
+            *ngIf="!isTheme(plugin)" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label
+            [responsiveLabel]="true"
+          ></my-edit-button>
+
+          <my-button
+            class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
+            [label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)" [responsiveLabel]="true"
           ></my-button>
 
-          <my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label></my-delete-button>
+          <my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label [responsiveLabel]="true"></my-delete-button>
         </div>
       </div>
 
index 9376e38b04828d7e7d535f0374011cde09dbe3cc..22d4a59ab2acbcf4e216d13f3bdd1126c3990574 100644 (file)
@@ -1,6 +1,6 @@
 @import '_variables';
 @import '_mixins';
 
-.update-button[disabled="true"] ::ng-deep .action-button {
+.update-button[disabled=true] ::ng-deep .action-button {
   cursor: default !important;
 }
index 727633399655002369123d1d3aa8e7043d0e4397..6900e87174140f039ad96091ca20a3d848226c7c 100644 (file)
         <span *ngIf="plugin.installed" class="badge badge-success">Installed</span>
 
         <div class="buttons">
-          <my-edit-button *ngIf="plugin.installed === true && !isThemeSearch()" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
+          <my-edit-button
+            *ngIf="plugin.installed === true && !isThemeSearch()" [routerLink]="getShowRouterLink(plugin)"
+            label="Settings" i18n-label [responsiveLabel]="true"
+          ></my-edit-button>
 
-          <my-button class="update-button" *ngIf="plugin.installed === false" (click)="install(plugin)" [loading]="isInstalling(plugin)"
-                     label="Install" icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
+          <my-button
+            class="update-button" *ngIf="plugin.installed === false" (click)="install(plugin)"
+            [loading]="isInstalling(plugin)" label="Install" [responsiveLabel]="true"
+            icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
           ></my-button>
         </div>
       </div>
index 5ab6e5f1b4ce18a07ad67a11f60722d4572d0712..6b7b84e299d776f247a9f04d33fd77b4825daadf 100644 (file)
@@ -5,7 +5,8 @@ h2 {
   margin-bottom: 20px;
 }
 
-input[type=submit], button {
+input[type=submit],
+button {
   @include peertube-button;
   @include orange-button;
 
diff --git a/client/src/app/+admin/plugins/plugins.component.scss b/client/src/app/+admin/plugins/plugins.component.scss
deleted file mode 100644 (file)
index ce9825f..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-@media screen and (max-width: $small-view) {
-  ::ng-deep .plugins .plugin .first-row {
-    flex-wrap: wrap;
-
-    .plugin-name,
-    .plugin-version,
-    .plugin-icon {
-      margin-bottom: 10px;
-    }
-
-    .buttons {
-      my-edit-button,
-      my-delete-button,
-      my-button {
-        .action-button {
-          padding: 0 13px;
-        }
-
-        .button-label {
-          display: none;
-        }
-      }
-    }
-  }
-}
index 6ec6fa4a1ebedc352bc8672b020a9c854ce5e8b4..de06c0671c0c268779545c876a8a8437a1d3f27a 100644 (file)
@@ -1,8 +1,7 @@
 import { Component } from '@angular/core'
 
 @Component({
-  templateUrl: './plugins.component.html',
-  styleUrls: [ './plugins.component.scss' ]
+  templateUrl: './plugins.component.html'
 })
 export class PluginsComponent {
 }
index f59a01b74759ca21437ec6c5f4fe3ca9e13307c9..47cb1e6e5caa72877bc3f478142cf335bf5d2b71 100644 (file)
@@ -27,7 +27,7 @@
     my-global-icon {
       @include apply-svg-color(pvar(--greyForegroundColor));
 
-      &[iconName="npm"] {
+      &[iconName=npm] {
         @include fill-svg-color(pvar(--greyForegroundColor));
       }
     }
@@ -36,6 +36,7 @@
   .buttons {
     margin-left: auto;
     width: max-content;
+
     > *:not(:last-child) {
       margin-right: 10px;
     }
@@ -49,7 +50,7 @@
   justify-content: space-between;
 
   .description {
-    opacity: 0.8
+    opacity: 0.8;
   }
 }
 
   @include peertube-button-link;
   @include button-with-icon(21px, 0, -2px);
 }
+
+@media screen and (max-width: $small-view) {
+  .first-row {
+    flex-wrap: wrap;
+
+    .buttons {
+      flex-basis: 100%;
+      margin-top: 10px;
+    }
+  }
+}
index 7c61594205fdea1699358f9897c1df12b236ea95..65ee6ec5fce31fb511b7262869299cb24bc9adf3 100644 (file)
@@ -51,7 +51,7 @@ pre {
 }
 
 .job-error {
-  color: red;
+  color: #ff0000;
 }
 
 .badge {
index 43578eedd434a4f9c92cd50754b7c21ec73e5a27..29ba95c5c2ad9c4f428294194e6ff86b725f5f66 100644 (file)
@@ -86,7 +86,7 @@ export class JobsComponent extends RestTable implements OnInit {
   onJobStateOrTypeChanged () {
     this.pagination.start = 0
 
-    this.loadData()
+    this.reloadData()
     this.saveJobStateAndType()
   }
 
@@ -104,10 +104,10 @@ export class JobsComponent extends RestTable implements OnInit {
     this.jobs = []
     this.totalRecords = 0
 
-    this.loadData()
+    this.reloadData()
   }
 
-  protected loadData () {
+  protected reloadData () {
     let jobState = this.jobState as JobState
     if (this.jobState === 'all') jobState = null
 
index 587a9795c11c59f7b1c16d3376318a83c4d571bd..1a7c3be752e2c60da3bd027cc81ea6b4263acc31 100644 (file)
@@ -66,7 +66,7 @@
     ng-select,
     my-button {
       width: 100% !important;
-      margin-left: 0px !important;
+      margin-left: 0 !important;
       margin-bottom: 10px !important;
     }
 
@@ -85,7 +85,7 @@
       ng-select,
       my-button {
         width: 100% !important;
-        margin-left: 0px !important;
+        margin-left: 0 !important;
         margin-bottom: 10px !important;
       }
 
index 8b0ac87837e77c6ed4ac2d4d1749ad0c8f46bedc..145177fb91cc87b1c86e0e9e24f64dd6d9c35a4a 100644 (file)
@@ -37,7 +37,8 @@ my-select-custom-value {
   display: block;
 }
 
-input[type=submit], button {
+input[type=submit],
+button {
   @include peertube-button;
   @include orange-button;
 
index 1f0d49227e9c568844b88a75c4cd1d20a87f7c97..66d15ee9cc01de3c90193c452bf6697c077131b3 100644 (file)
@@ -7,7 +7,7 @@ input:not([type=submit]):not([type=checkbox]) {
   display: block;
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
-  border-right: none;
+  border-right: 0;
 }
 
 input[type=submit] {
index e114f3425e800f724205cf38a5c245756d84984b..44d8a7e87c7f16f618967578d543c512da9a819a 100644 (file)
@@ -1,7 +1,7 @@
 <p-table
-  [value]="users" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
-  [(selection)]="selectedUsers"
+  [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order"  dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
   (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
       </div>
 
       <div class="ml-auto">
-        <div class="input-group has-feedback has-clear">
-          <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
-            <div class="input-group-text" ngbDropdownToggle>
-              <span class="caret" aria-haspopup="menu" role="button"></span>
-            </div>
-
-            <div role="menu" ngbDropdownMenu>
-              <h6 class="dropdown-header" i18n>Advanced user filters</h6>
-              <a [routerLink]="[ '/admin/users/list' ]" [queryParams]="{ 'search': 'banned:true' }" class="dropdown-item" i18n>Banned users</a>
-            </div>
-          </div>
-          <input
-            type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-            (keyup)="onSearch($event)"
-          >
-          <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
-          <span class="sr-only" i18n>Clear filters</span>
-        </div>
+        <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
 
     </div>
       <td *ngIf="isSelected('username')">
         <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
           <div class="chip two-lines">
-            <my-account-avatar [account]="user?.account"></my-account-avatar>
+            <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar>
            <div>
               <span class="user-table-primary-text">{{ user.account.displayName }}</span>
               <span class="text-muted">{{ user.username }}</span>
index 50080bad61ec8d9c04d9e3a930658662e02a6006..db4979a51eec72537e60b3cf78deb7e900629c8e 100644 (file)
@@ -11,6 +11,7 @@ tr.banned > td {
 
 .table-email {
   @include disable-default-a-behaviour;
+
   color: pvar(--mainForegroundColor);
 }
 
@@ -24,18 +25,10 @@ tr.banned > td {
 
 .user-table-primary-text .glyphicon {
   font-size: 80%;
-  color: gray;
+  color: #808080;
   margin-left: 0.1rem;
 }
 
-.caption {
-  justify-content: space-between;
-
-  input {
-    @include peertube-input-text(250px);
-  }
-}
-
 p-tableCheckbox {
   position: relative;
   top: -2.5px;
@@ -55,18 +48,7 @@ my-global-icon {
 
 .progress {
   @include progressbar($small: true);
+
   width: auto;
   max-width: 100%;
 }
-
-.input-group {
-  @include peertube-input-group(300px);
-
-  input {
-    flex: 1;
-  }
-
-  .dropdown-toggle::after {
-    margin-left: 0;
-  }
-}
index 339e182067c4610febaaddc509fe3570e4167308..1c60adf89dc26ec3e4fe3cdf1c002b12aa951243 100644 (file)
@@ -1,8 +1,9 @@
 import { SortMeta } from 'primeng/api'
 import { Component, OnInit, ViewChild } from '@angular/core'
-import { ActivatedRoute, Params, Router } from '@angular/router'
+import { ActivatedRoute, Router } from '@angular/router'
 import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core'
-import { Account, DropdownAction } from '@app/shared/shared-main'
+import { AdvancedInputFilter } from '@app/shared/shared-forms'
+import { DropdownAction } from '@app/shared/shared-main'
 import { UserBanModalComponent } from '@app/shared/shared-moderation'
 import { ServerConfig, User, UserRole } from '@shared/models'
 
@@ -22,15 +23,24 @@ export class UserListComponent extends RestTable implements OnInit {
   @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
 
   users: User[] = []
+
   totalRecords = 0
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
   highlightBannedUsers = false
 
   selectedUsers: User[] = []
   bulkUserActions: DropdownAction<User[]>[][] = []
   columns: { id: string, label: string }[]
 
+  inputFilters: AdvancedInputFilter[] = [
+    {
+      queryParams: { 'search': 'banned:true' },
+      label: $localize`Banned users`
+    }
+  ]
+
   private _selectedColumns: string[]
   private serverConfig: ServerConfig
 
@@ -68,7 +78,6 @@ export class UserListComponent extends RestTable implements OnInit {
         .subscribe(config => this.serverConfig = config)
 
     this.initialize()
-    this.listenToSearchChange()
 
     this.bulkUserActions = [
       [
@@ -160,7 +169,7 @@ export class UserListComponent extends RestTable implements OnInit {
   }
 
   onUserChanged () {
-    this.loadData()
+    this.reloadData()
   }
 
   async unbanUsers (users: User[]) {
@@ -171,7 +180,7 @@ export class UserListComponent extends RestTable implements OnInit {
         .subscribe(
           () => {
             this.notifier.success($localize`${users.length} users unbanned.`)
-            this.loadData()
+            this.reloadData()
           },
 
           err => this.notifier.error(err.message)
@@ -193,7 +202,7 @@ export class UserListComponent extends RestTable implements OnInit {
     this.userService.removeUser(users).subscribe(
       () => {
         this.notifier.success($localize`${users.length} users deleted.`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message)
@@ -204,7 +213,7 @@ export class UserListComponent extends RestTable implements OnInit {
     this.userService.updateUsers(users, { emailVerified: true }).subscribe(
       () => {
         this.notifier.success($localize`${users.length} users email set as verified.`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message)
@@ -215,7 +224,7 @@ export class UserListComponent extends RestTable implements OnInit {
     return this.selectedUsers.length !== 0
   }
 
-  protected loadData () {
+  protected reloadData () {
     this.selectedUsers = []
 
     this.userService.getUsers({
index eddaff542364fdaa3ab4358c9ac08353c813b1e4..62ae722c3983019c324198e0f8a255efebacf4b9 100644 (file)
@@ -33,7 +33,8 @@ input[type=email] {
   }
 }
 
-.create-an-account, .forgot-password-button {
+.create-an-account,
+.forgot-password-button {
   color: pvar(--mainForegroundColor);
   cursor: pointer;
   transition: opacity cubic-bezier(0.39, 0.575, 0.565, 1);
@@ -49,7 +50,7 @@ input[type=email] {
   justify-content: space-around;
   flex-wrap: wrap;
 
-  > div {
+  > div {
     flex: 1 1;
   }
 
@@ -65,7 +66,8 @@ input[type=email] {
     form {
       margin: 0;
 
-      &, input {
+      &,
+      input {
         width: 100%;
       }
 
@@ -82,7 +84,8 @@ input[type=email] {
 
           color: var(--mainColor);
 
-          &:hover, &:active {
+          &:hover,
+          &:active {
             color: var(--mainHoverColor);
           }
         }
@@ -111,7 +114,7 @@ input[type=email] {
         min-width: 100px;
 
         &:hover {
-          background-color: rgba(209, 215, 224, 0.5)
+          background-color: rgba(209, 215, 224, 0.5);
         }
       }
     }
@@ -138,7 +141,7 @@ input[type=email] {
   }
 }
 
-@mixin columnReverseDisplay {
+@mixin column-reverse-display {
   flex-direction: column-reverse;
 
   .login-form-and-externals,
@@ -168,14 +171,14 @@ input[type=email] {
 
 @media screen and (max-width: breakpoint(md)) {
   .wrapper {
-    @include columnReverseDisplay();
+    @include column-reverse-display();
   }
 }
 
 @media screen and (max-width: breakpoint(md) + $menu-width) {
   :host-context(.main-col:not(.expanded)) {
     .wrapper {
-      @include columnReverseDisplay();
+      @include column-reverse-display();
     }
   }
 }
index 59ca61be66807a38f11e6945962048aa9c2e3e9b..e83b59019a4706bbb2e75f9fb850dc4bfca0dade 100644 (file)
@@ -3,4 +3,4 @@
   <ng-container i18n>Reports</ng-container>
 </h1>
 
-<my-abuse-list-table viewType="user" baseRoute="/my-account/abuses"></my-abuse-list-table>
+<my-abuse-list-table viewType="user"></my-abuse-list-table>
index 704132c03ddec76aabf9af9f8d67658b655d7a8b..c1e1f2432973f6c7e72cd116aba76fc423598c7a 100644 (file)
@@ -21,7 +21,7 @@ input[type=submit] {
   display: flex;
   margin-left: auto;
 
-  + .form-error {
+  + .form-error {
     display: inline;
     margin-left: 5px;
   }
index 035fa2b27ecb5d654e54656f704862553e3ce8a9..abf52504a44d78fcab91150ec90b155a300b2ef4 100644 (file)
@@ -32,7 +32,8 @@ my-user-notifications {
   .header {
     flex-direction: column;
 
-    & >:first-child, .peertube-select-container {
+    > :first-child,
+    .peertube-select-container {
       margin-bottom: 15px;
     }
 
index a5bb499b42cb52fbe1d034ee1a3aabcce10ba3c1..b32bc84e7b795b4962e6ea2b78efd888a8f85b82 100644 (file)
@@ -2,12 +2,12 @@
 @import '_mixins';
 
 .row {
+  @include sub-menu-h1;
+
   flex-direction: column;
   width: 100%;
 
-  > my-top-menu-dropdown:nth-child(1) {
+  > my-top-menu-dropdown:nth-child(1) {
     flex-grow: 1;
   }
-
-  @include sub-menu-h1;
 }
index 050cd4b34fee2f3cbce265303c469b4436be2777..4081e4f01376096c9a98c73703ffb9abe3341317 100644 (file)
@@ -3,13 +3,14 @@ import { TableModule } from 'primeng/table'
 import { DragDropModule } from '@angular/cdk/drag-drop'
 import { NgModule } from '@angular/core'
 import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
-import { SharedActorImageModule } from '@app/shared/shared-actor-image'
+import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
 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 { SharedShareModal } from '@app/shared/shared-share-modal'
 import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
+import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 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'
@@ -23,7 +24,6 @@ import { MyAccountNotificationPreferencesComponent } from './my-account-settings
 import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
 import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
 import { MyAccountComponent } from './my-account.component'
-import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
 
 @NgModule({
   imports: [
@@ -40,8 +40,8 @@ import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/share
     SharedGlobalIconModule,
     SharedAbuseListModule,
     SharedShareModal,
-    SharedAccountAvatarModule,
-    SharedActorImageModule
+    SharedActorImageModule,
+    SharedActorImageEditModule
   ],
 
   declarations: [
index 22de103d17f8fa344445f138d2b632b7db85a0ee..667726c2257f65e6a189216b68f9b34ea78a5823 100644 (file)
@@ -66,7 +66,8 @@ textarea {
     width: auto !important;
   }
 
-  label[for=name] + div, textarea {
+  label[for=name] + div,
+  textarea {
     width: 100%;
   }
 }
index b704a1cc6835ecc7af295e48204193bd1cc3ef90..e41cbe921f3720eade830521f999bc591546f7ef 100644 (file)
@@ -1,18 +1,11 @@
 <h1>
-  <span>
-    <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
-    <ng-container i18n>My channels</ng-container>
-    <span class="badge badge-secondary">{{ totalItems }}</span>
-  </span>
+  <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
+  <ng-container i18n>My channels</ng-container>
+  <span class="badge badge-secondary">{{ totalItems }}</span>
 </h1>
 
 <div class="video-channels-header d-flex justify-content-between">
-  <div class="has-feedback has-clear">
-    <input type="text" placeholder="Search your channels" i18n-placeholder [(ngModel)]="channelsSearch"
-      (ngModelChange)="onChannelsSearchChanged()" />
-    <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-    <span class="sr-only" i18n>Clear filters</span>
-  </div>
+  <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
 
   <a class="create-button" routerLink="create">
     <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
   </a>
 </div>
 
+<div class="no-results" i18n *ngIf="totalItems === 0">No channel found.</div>
+
 <div class="video-channels">
   <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
-    <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
-      <img [src]="videoChannel.avatarUrl" alt="Avatar" />
-    </a>
+    <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar>
 
     <div class="video-channel-info">
       <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
index 8804fa95c533c2fb42fbf332027d1e4b42f1b04a..191c5169d998cb76c7e42da15e79287bb0670380 100644 (file)
@@ -1,6 +1,11 @@
 @import '_variables';
 @import '_mixins';
 
+h1 my-global-icon {
+  position: relative;
+  top: -2px;
+}
+
 .create-button {
   @include create-button;
 }
@@ -9,10 +14,8 @@ input[type=text] {
   @include peertube-input-text(300px);
 }
 
-::ng-deep .action-button {
-  &.action-button-edit {
-    margin-right: 10px;
-  }
+my-edit-button {
+  margin-right: 10px;
 }
 
 .video-channel {
@@ -20,40 +23,40 @@ input[type=text] {
 
   padding-bottom: 0;
 
-  img {
-    @include channel-avatar(80px);
+  my-actor-avatar {
+    @include actor-avatar-size(80px);
 
     margin-right: 10px;
   }
+}
 
-  .video-channel-info {
-    flex-grow: 1;
-
-    a.video-channel-names {
-      @include disable-default-a-behaviour;
-
-      width: fit-content;
-      display: flex;
-      align-items: baseline;
-      color: pvar(--mainForegroundColor);
-
-      .video-channel-display-name {
-        font-weight: $font-semibold;
-        font-size: 18px;
-      }
-
-      .video-channel-name {
-        font-size: 14px;
-        color: $grey-actor-name;
-        margin-left: 5px;
-      }
-    }
-  }
+.video-channel-info {
+  flex-grow: 1;
+}
 
-  .video-channel-buttons {
-    margin-top: 10px;
-    min-width: 190px;
-  }
+.video-channel-names {
+  @include disable-default-a-behaviour;
+
+  width: fit-content;
+  display: flex;
+  align-items: baseline;
+  color: pvar(--mainForegroundColor);
+}
+
+.video-channel-display-name {
+  font-weight: $font-semibold;
+  font-size: 18px;
+}
+
+.video-channel-name {
+  font-size: 14px;
+  color: $grey-actor-name;
+  margin-left: 5px;
+}
+
+.video-channel-buttons {
+  margin-top: 10px;
+  min-width: 190px;
 }
 
 ::ng-deep .chartjs-render-monitor {
@@ -73,21 +76,6 @@ input[type=text] {
   .video-channel {
     padding-bottom: 10px;
 
-    .video-channel-info {
-      padding-bottom: 10px;
-      text-align: center;
-
-      .video-channel-names {
-        flex-direction: column;
-        align-items: center !important;
-        margin: auto;
-
-        .video-channel-name {
-          margin-left: 0px !important;
-        }
-      }
-    }
-
     img {
       margin-right: 0;
     }
@@ -96,6 +84,21 @@ input[type=text] {
       align-self: center;
     }
   }
+
+  .video-channel-info {
+    padding-bottom: 10px;
+    text-align: center;
+  }
+
+  .video-channel-names {
+    flex-direction: column;
+    align-items: center !important;
+    margin: auto;
+  }
+
+  .video-channel-name {
+    margin-left: 0 !important;
+  }
 }
 
 @media screen and (max-width: $mobile-view) {
index f6ba50a4883f546f5348096c725ed700d255f863..9e3bf35b4e3ba611e12464af3eebf06c83006006 100644 (file)
@@ -1,29 +1,26 @@
 import { ChartData } from 'chart.js'
 import { max, maxBy, min, minBy } from 'lodash-es'
-import { Subject } from 'rxjs'
-import { debounceTime, mergeMap } from 'rxjs/operators'
-import { Component, OnInit } from '@angular/core'
-import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core'
+import { mergeMap } from 'rxjs/operators'
+import { Component } from '@angular/core'
+import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core'
 import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
 
 @Component({
   templateUrl: './my-video-channels.component.html',
   styleUrls: [ './my-video-channels.component.scss' ]
 })
-export class MyVideoChannelsComponent implements OnInit {
+export class MyVideoChannelsComponent {
   totalItems: number
 
   videoChannels: VideoChannel[] = []
+
   videoChannelsChartData: ChartData[]
   videoChannelsMinimumDailyViews = 0
   videoChannelsMaximumDailyViews: number
 
-  channelsSearch: string
-  channelsSearchChanged = new Subject<string>()
-
   chartOptions: any
 
-  private user: User
+  search: string
 
   constructor (
     private authService: AuthService,
@@ -31,31 +28,15 @@ export class MyVideoChannelsComponent implements OnInit {
     private confirmService: ConfirmService,
     private videoChannelService: VideoChannelService,
     private screenService: ScreenService
-    ) {}
-
-  ngOnInit () {
-    this.user = this.authService.getUser()
-
-    this.loadVideoChannels()
-
-    this.channelsSearchChanged
-      .pipe(debounceTime(500))
-      .subscribe(() => {
-        this.loadVideoChannels()
-      })
-  }
+  ) {}
 
   get isInSmallView () {
     return this.screenService.isInSmallView()
   }
 
-  resetSearch () {
-    this.channelsSearch = ''
-    this.onChannelsSearchChanged()
-  }
-
-  onChannelsSearchChanged () {
-    this.channelsSearchChanged.next()
+  onSearch (search: string) {
+    this.search = search
+    this.loadVideoChannels()
   }
 
   async deleteVideoChannel (videoChannel: VideoChannel) {
@@ -85,8 +66,11 @@ channel with the same name (${videoChannel.name})!`,
 
   private loadVideoChannels () {
     this.authService.userInformationLoaded
-        .pipe(mergeMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch)))
-        .subscribe(res => {
+        .pipe(mergeMap(() => {
+          const user = this.authService.getUser()
+
+          return this.videoChannelService.listAccountVideoChannels(user.account, null, true, this.search)
+        })).subscribe(res => {
           this.videoChannels = res.data
           this.totalItems = res.total
 
index 53557ca0298101f9bfe68f3c707891946e9a4cad..c775bfdee46e931a917092394dea19b7316c213e 100644 (file)
@@ -1,6 +1,6 @@
 import { ChartModule } from 'primeng/chart'
 import { NgModule } from '@angular/core'
-import { SharedActorImageModule } from '@app/shared/shared-actor-image'
+import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
 import { SharedFormModule } from '@app/shared/shared-forms'
 import { SharedGlobalIconModule } from '@app/shared/shared-icons'
 import { SharedMainModule } from '@app/shared/shared-main'
@@ -8,6 +8,7 @@ import { MyVideoChannelCreateComponent } from './my-video-channel-create.compone
 import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
 import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module'
 import { MyVideoChannelsComponent } from './my-video-channels.component'
+import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -18,6 +19,7 @@ import { MyVideoChannelsComponent } from './my-video-channels.component'
     SharedMainModule,
     SharedFormModule,
     SharedGlobalIconModule,
+    SharedActorImageEditModule,
     SharedActorImageModule
   ],
 
index 9dec64645622067bf79cd69dd7ebd5340dc0793d..45ca37e0db6c400c12481a3d6002b895e0fc1fab 100644 (file)
@@ -5,14 +5,7 @@
 
 <div class="top-buttons">
   <div class="search-wrapper">
-    <div class="input-group has-feedback has-clear">
-      <input
-        type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history"
-        (keyup)="onSearch($event)"
-      >
-      <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-      <span class="sr-only" i18n>Clear filters</span>
-    </div>
+    <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
   </div>
 
   <div class="history-switch">
   </button>
 </div>
 
-
-<div class="no-history" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">You don't have any video in your watch history yet.</div>
-
-<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" class="videos">
-  <div class="video" *ngFor="let video of videos">
-    <my-video-miniature
-      [video]="video" [displayAsRow]="true"
-      (videoRemoved)="removeVideoFromArray(video)" (videoBlocked)="removeVideoFromArray(video)"
-    ></my-video-miniature>
-  </div>
-</div>
+<my-videos-selection
+  [pagination]="pagination"
+  [(videosModel)]="videos"
+  [miniatureDisplayOptions]="miniatureDisplayOptions"
+  [titlePage]="titlePage"
+  [getVideosObservableFunction]="getVideosObservableFunction"
+  [user]="user"
+  [loadOnInit]="false"
+  i18n-noResultMessage noResultMessage="You don't have any video in your watch history yet."
+  [enableSelection]="false"
+  #videosSelection
+></my-videos-selection>
index af4a34b4ba92a6ece0c90b4b052eed3727484be5..28b809f715edc2bb0e322c297b0ca92f626c9a70 100644 (file)
   }
 
   .delete-history {
-    grid-column: 4;
-
     @include peertube-button;
     @include grey-button;
     @include button-with-icon;
 
+    grid-column: 4;
+
     font-size: 15px;
   }
 }
index 1695bd7ad8aaf7a145ea05f6f538f8e76ecda86b..ad83db7abc41bfbd6e267b0f10f5615fdcee9636 100644 (file)
@@ -1,36 +1,55 @@
-import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
+import { Subject } from 'rxjs'
+import { tap } from 'rxjs/operators'
+import { Component, ComponentFactoryResolver, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import {
   AuthService,
   ComponentPagination,
   ConfirmService,
+  DisableForReuseHook,
   LocalStorageService,
   Notifier,
   ScreenService,
   ServerService,
+  User,
   UserService
 } from '@app/core'
 import { immutableAssign } from '@app/helpers'
-import { UserHistoryService } from '@app/shared/shared-main'
-import { AbstractVideoList } from '@app/shared/shared-video-miniature'
-import { Subject } from 'rxjs'
-import { debounceTime, tap, distinctUntilChanged } from 'rxjs/operators'
+import { UserHistoryService, Video } from '@app/shared/shared-main'
+import { MiniatureDisplayOptions, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
 
 @Component({
   templateUrl: './my-history.component.html',
   styleUrls: [ './my-history.component.scss' ]
 })
-export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
+export class MyHistoryComponent implements OnInit, DisableForReuseHook {
+  @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
+
   titlePage: string
   pagination: ComponentPagination = {
     currentPage: 1,
     itemsPerPage: 5,
     totalItems: null
   }
+
   videosHistoryEnabled: boolean
-  search: string
 
-  protected searchStream: Subject<string>
+  miniatureDisplayOptions: MiniatureDisplayOptions = {
+    date: true,
+    views: true,
+    by: true,
+    privacyLabel: false,
+    privacyText: true,
+    state: true,
+    blacklistInfo: true
+  }
+
+  getVideosObservableFunction = this.getVideosObservable.bind(this)
+
+  user: User
+
+  videos: Video[] = []
+  search: string
 
   constructor (
     protected router: Router,
@@ -45,45 +64,31 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
     private userHistoryService: UserHistoryService,
     protected cfr: ComponentFactoryResolver
   ) {
-    super()
-
     this.titlePage = $localize`My watch history`
   }
 
   ngOnInit () {
-    super.ngOnInit()
+    this.user = this.authService.getUser()
 
     this.authService.userInformationLoaded
-      .subscribe(() => {
-        this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled
-      })
-
-    this.searchStream = new Subject()
+      .subscribe(() => this.videosHistoryEnabled = this.user.videosHistoryEnabled)
+  }
 
-    this.searchStream
-      .pipe(
-        debounceTime(400),
-        distinctUntilChanged()
-      )
-      .subscribe(search => {
-        this.search = search
-        this.reloadVideos()
-      })
+  disableForReuse () {
+    this.videosSelection.disableForReuse()
   }
 
-  onSearch (event: Event) {
-    const target = event.target as HTMLInputElement
-    this.searchStream.next(target.value)
+  enabledForReuse () {
+    this.videosSelection.enabledForReuse()
   }
 
-  resetSearch () {
-    const searchInput = document.getElementById('history-search') as HTMLInputElement
-    searchInput.value = ''
-    this.searchStream.next('')
+  reloadData () {
+    this.videosSelection.reloadVideos()
   }
 
-  ngOnDestroy () {
-    super.ngOnDestroy()
+  onSearch (search: string) {
+    this.search = search
+    this.reloadData()
   }
 
   getVideosObservable (page: number) {
@@ -129,7 +134,7 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
           () => {
             this.notifier.success($localize`Videos history deleted`)
 
-            this.reloadVideos()
+            this.reloadData()
           },
 
           err => this.notifier.error(err.message)
index a5bb499b42cb52fbe1d034ee1a3aabcce10ba3c1..b32bc84e7b795b4962e6ea2b78efd888a8f85b82 100644 (file)
@@ -2,12 +2,12 @@
 @import '_mixins';
 
 .row {
+  @include sub-menu-h1;
+
   flex-direction: column;
   width: 100%;
 
-  > my-top-menu-dropdown:nth-child(1) {
+  > my-top-menu-dropdown:nth-child(1) {
     flex-grow: 1;
   }
-
-  @include sub-menu-h1;
 }
index a1d706f0bf35e2ecc61f4c776abf063795baac67..264ad03f7d19b9467d05d16099a482ab2f6c023e 100644 (file)
@@ -26,7 +26,7 @@ import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-pl
 import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component'
 import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
 import { MyVideosComponent } from './my-videos/my-videos.component'
-import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -47,7 +47,7 @@ import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/share
     SharedAbuseListModule,
     SharedShareModal,
     SharedVideoLiveModule,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
index d0eff0521bae7b008f0e9a45ca66569d259b1c2a..4c02c78fc22c8d0a21375fbd5896f01e8316e09e 100644 (file)
@@ -37,7 +37,7 @@
       <td>
         <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="chip two-lines">
-            <my-account-avatar [account]="videoChangeOwnership.initiatorAccount"></my-account-avatar>
+            <my-actor-avatar [account]="videoChangeOwnership.initiatorAccount"></my-actor-avatar>
             <div>
               {{ videoChangeOwnership.initiatorAccount.displayName }}
               <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span>
index 7cac9c9f3a6425961f159c616c8e4f285b30a173..dfc8fc99e3fe051df8d7245fb8a051bf4e5bb500 100644 (file)
   display: inline-flex;
 
   .video-table-video-image {
-    @include miniature-thumbnail;
-
     $image-height: 45px;
 
+    @include miniature-thumbnail;
+
     height: $image-height;
     width: #{(16/9) * $image-height};
     margin-right: 0.5rem;
     border-radius: 2px;
-    border: none;
+    border: 0;
     background: transparent;
     display: inline-flex;
     justify-content: center;
@@ -60,7 +60,7 @@
 
     div .glyphicon {
       font-size: 80%;
-      color: gray;
+      color: #808080;
       margin-left: 0.1rem;
     }
 
index a938023b4e4ecf0cdeb9d81416d4eaee347f6a78..aaf028474c586f617670c037b0c14d2d7c5e2fbd 100644 (file)
@@ -48,18 +48,18 @@ export class MyOwnershipComponent extends RestTable implements OnInit {
   }
 
   accepted () {
-    this.loadData()
+    this.reloadData()
   }
 
   refuse (videoChangeOwnership: VideoChangeOwnership) {
     this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id)
       .subscribe(
-        () => this.loadData(),
+        () => this.reloadData(),
         err => this.notifier.error(err.message)
       )
   }
 
-  protected loadData () {
+  protected reloadData () {
     return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
       .subscribe(
         resultList => {
index ff448ad871f70c5a02fb1446347f2bd60cc99a4d..f91cebacfbbf1a3e5904526f85837d1aebbdacdf 100644 (file)
@@ -7,21 +7,14 @@
 </h1>
 
 <div class="video-subscriptions-header">
-  <div class="has-feedback has-clear">
-    <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch"
-      (ngModelChange)="onSubscriptionsSearchChanged()" />
-    <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-    <span class="sr-only" i18n>Clear filters</span>
-  </div>
+  <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
 </div>
 
 <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscription yet.</div>
 
 <div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
   <div *ngFor="let videoChannel of videoChannels" class="video-channel">
-    <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
-      <img [src]="videoChannel.avatarUrl" alt="Avatar" />
-    </a>
+    <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar>
 
     <div class="video-channel-info">
       <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
@@ -33,7 +26,8 @@
 
       <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
         <span i18n>Created by {{ videoChannel.ownerBy }}</span>
-        <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
+
+        <my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar>
       </a>
     </div>
 
index 3c1a4d2ad0e1008845a1b232346d4006c6ddff32..6c1ddf7167a19b2714d71cad9ad47cdb0591027e 100644 (file)
@@ -8,8 +8,8 @@ input[type=text] {
 .video-channel {
   @include row-blocks;
 
-  img {
-    @include channel-avatar(80px);
+  > my-actor-avatar {
+    @include actor-avatar-size(80px);
 
     margin-right: 10px;
   }
@@ -40,13 +40,25 @@ input[type=text] {
 }
 
 .actor-owner {
-  @include actor-owner;
+  @include disable-default-a-behaviour;
+
+  font-size: 13px;
+  color: pvar(--mainForegroundColor);
 
-  margin-top: 0;
+  span:hover {
+    opacity: 0.8;
+  }
+
+  my-actor-avatar {
+    margin-left: 7px;
+    display: inline-block;
+    vertical-align: top;
+  }
 }
 
 .video-subscriptions-header {
   margin-bottom: 30px;
+  display: flex;
 }
 
 @media screen and (max-width: $small-view) {
index 3b748eccf30b0d5dc8589649173bfdcdc3063714..1f4a931a01a1a6a6745fbccb193ffea121375dc4 100644 (file)
@@ -1,6 +1,5 @@
 import { Subject } from 'rxjs'
-import { debounceTime } from 'rxjs/operators'
-import { Component, OnInit } from '@angular/core'
+import { Component } from '@angular/core'
 import { ComponentPagination, Notifier } from '@app/core'
 import { VideoChannel } from '@app/shared/shared-main'
 import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
@@ -9,7 +8,7 @@ import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
   templateUrl: './my-subscriptions.component.html',
   styleUrls: [ './my-subscriptions.component.scss' ]
 })
-export class MySubscriptionsComponent implements OnInit {
+export class MySubscriptionsComponent {
   videoChannels: VideoChannel[] = []
 
   pagination: ComponentPagination = {
@@ -20,34 +19,13 @@ export class MySubscriptionsComponent implements OnInit {
 
   onDataSubject = new Subject<any[]>()
 
-  subscriptionsSearch: string
-  subscriptionsSearchChanged = new Subject<string>()
+  search: string
 
   constructor (
     private userSubscriptionService: UserSubscriptionService,
     private notifier: Notifier
   ) {}
 
-  ngOnInit () {
-    this.loadSubscriptions()
-
-    this.subscriptionsSearchChanged
-      .pipe(debounceTime(500))
-      .subscribe(() => {
-        this.pagination.currentPage = 1
-        this.loadSubscriptions(false)
-      })
-  }
-
-  resetSearch () {
-    this.subscriptionsSearch = ''
-    this.onSubscriptionsSearchChanged()
-  }
-
-  onSubscriptionsSearchChanged () {
-    this.subscriptionsSearchChanged.next()
-  }
-
   onNearOfBottom () {
     // Last page
     if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
@@ -56,8 +34,13 @@ export class MySubscriptionsComponent implements OnInit {
     this.loadSubscriptions()
   }
 
+  onSearch (search: string) {
+    this.search = search
+    this.loadSubscriptions(false)
+  }
+
   private loadSubscriptions (more = true) {
-    this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch })
+    this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.search })
         .subscribe(
           res => {
             this.videoChannels = more
index a93c280280195ffc484026d636a91480503ac1ac..c4b847c3d1e1c330f062b3b1bfeb34522dd7f173 100644 (file)
@@ -6,7 +6,7 @@ pre {
 }
 
 .video-import-error {
-  color: red;
+  color: #ff0000;
 }
 
 .badge {
index d6d7d7a1b8c9ca1489f0924220c2447b275aa904..359535526658682e2c3d89b196df7df65539e262 100644 (file)
@@ -62,7 +62,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
     return '/videos/update/' + video.uuid
   }
 
-  protected loadData () {
+  protected reloadData () {
     this.videoImportService.getMyVideoImports(this.pagination, this.sort)
         .subscribe(
           resultList => {
index 0c68dedf6502ec6e583433eb6525964abd62d434..67587a58a915bf9a115afb89f476793572beff48 100644 (file)
@@ -25,8 +25,8 @@
 }
 
 .playlist-buttons {
-  display:flex;
-  margin: 30px 0 10px 0;
+  display: flex;
+  margin: 30px 0 10px;
 
   .share-button {
     @include peertube-button;
 .cdk-drag-preview {
   box-sizing: border-box;
   border-radius: 4px;
-  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
-  0 8px 10px 1px rgba(0, 0, 0, 0.14),
-  0 3px 14px 2px rgba(0, 0, 0, 0.12);
+  box-shadow:
+    0 5px 5px -3px rgba(0, 0, 0, 0.2),
+    0 8px 10px 1px rgba(0, 0, 0, 0.14),
+    0 3px 14px 2px rgba(0, 0, 0, 0.12);
 }
 
 .cdk-drag-placeholder {
@@ -56,7 +57,7 @@
 }
 
 .video:last-child {
-  border: none;
+  border: 0;
 }
 
 .videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) {
index b88ea3db7361687e8f7ab27d6920ccd7efc14cd7..309afcf137f8f2ca1f835722ec165a9427c2e894 100644 (file)
@@ -4,12 +4,7 @@
 </h1>
 
 <div class="video-playlists-header d-flex justify-content-between">
-  <div class="has-feedback has-clear">
-    <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch"
-      (ngModelChange)="onVideoPlaylistSearchChanged()" />
-    <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-    <span class="sr-only" i18n>Clear filters</span>
-  </div>
+  <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
 
   <a class="create-button" routerLink="create">
     <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
index f6d39492320b57d5a7d15ab9967463c2812bef3e..d90102693acab006a4dd542ef65a6124e1a45f14 100644 (file)
@@ -1,7 +1,7 @@
 import { Subject } from 'rxjs'
-import { debounceTime, mergeMap } from 'rxjs/operators'
-import { Component, OnInit } from '@angular/core'
-import { AuthService, ComponentPagination, ConfirmService, Notifier, User } from '@app/core'
+import { mergeMap } from 'rxjs/operators'
+import { Component } from '@angular/core'
+import { AuthService, ComponentPagination, ConfirmService, Notifier } from '@app/core'
 import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
 import { VideoPlaylistType } from '@shared/models'
 
@@ -9,10 +9,8 @@ import { VideoPlaylistType } from '@shared/models'
   templateUrl: './my-video-playlists.component.html',
   styleUrls: [ './my-video-playlists.component.scss' ]
 })
-export class MyVideoPlaylistsComponent implements OnInit {
-  videoPlaylistsSearch: string
+export class MyVideoPlaylistsComponent {
   videoPlaylists: VideoPlaylist[] = []
-  videoPlaylistSearchChanged = new Subject<string>()
 
   pagination: ComponentPagination = {
     currentPage: 1,
@@ -22,27 +20,14 @@ export class MyVideoPlaylistsComponent implements OnInit {
 
   onDataSubject = new Subject<any[]>()
 
-  private user: User
+  search: string
 
   constructor (
     private authService: AuthService,
     private notifier: Notifier,
     private confirmService: ConfirmService,
     private videoPlaylistService: VideoPlaylistService
-    ) {}
-
-  ngOnInit () {
-    this.user = this.authService.getUser()
-
-    this.loadVideoPlaylists()
-
-    this.videoPlaylistSearchChanged
-      .pipe(
-        debounceTime(500))
-      .subscribe(() => {
-        this.loadVideoPlaylists(true)
-      })
-  }
+  ) {}
 
   async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
     const res = await this.confirmService.confirm(
@@ -76,22 +61,20 @@ export class MyVideoPlaylistsComponent implements OnInit {
     this.loadVideoPlaylists()
   }
 
-  resetSearch () {
-    this.videoPlaylistsSearch = ''
-    this.onVideoPlaylistSearchChanged()
-  }
-
-  onVideoPlaylistSearchChanged () {
-    this.videoPlaylistSearchChanged.next()
+  onSearch (search: string) {
+    this.search = search
+    this.loadVideoPlaylists(true)
   }
 
   private loadVideoPlaylists (reset = false) {
     this.authService.userInformationLoaded
         .pipe(mergeMap(() => {
-          return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt', this.videoPlaylistsSearch)
-        }))
-        .subscribe(res => {
+          const user = this.authService.getUser()
+
+          return this.videoPlaylistService.listAccountPlaylists(user.account, this.pagination, '-updatedAt', this.search)
+        })).subscribe(res => {
           if (reset) this.videoPlaylists = []
+
           this.videoPlaylists = this.videoPlaylists.concat(res.data)
           this.pagination.totalItems = res.total
 
index a79fec179737a17d090533a8e9d5f4b5578860fd..16187bc4a18634bf8454c3400a2c2ce6e0d23dbd 100644 (file)
@@ -7,4 +7,4 @@ p-autocomplete {
 
 .form-group {
   margin: 20px 0;
-}
\ No newline at end of file
+}
index e9f4363786d5dfcd7094ebbca2b0b479d5435380..8d8b482add3756970736a71397b23fe0efb7b581 100644 (file)
 </h1>
 
 <div class="videos-header d-flex justify-content-between">
-  <div class="has-feedback has-clear">
-    <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch"
-      (ngModelChange)="onVideosSearchChanged()" />
-    <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-    <span class="sr-only" i18n>Clear filters</span>
-  </div>
+  <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
 
   <div class="peertube-select-container peertube-select-button">
     <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
@@ -46,6 +41,7 @@
   [titlePage]="titlePage"
   [getVideosObservableFunction]="getVideosObservableFunction"
   [user]="user"
+  [loadOnInit]="false"
   #videosSelection
 >
   <ng-template ptTemplate="globalButtons">
@@ -64,6 +60,5 @@
   </ng-template>
 </my-videos-selection>
 
-
 <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
 <my-live-stream-information #liveStreamInformationModal></my-live-stream-information>
index aaf21126b429c41241aec50ff10c6620b2ad7ca5..57623c36fdba034585f5bcc95e921cd7c170b5c5 100644 (file)
@@ -26,12 +26,12 @@ h1 {
 }
 
 .action-button-delete-selection {
-  display: inline-block;
-
   @include peertube-button;
   @include orange-button;
   @include button-with-icon(21px);
 
+  display: inline-block;
+
   my-global-icon {
     @include apply-svg-color(#fff);
   }
index 356e158d60ef6e816900844c1185b83accd97aa5..1e4a4406d6369d0538cdd0cc022264b8321990a3 100644 (file)
@@ -1,10 +1,11 @@
-import { concat, Observable, Subject } from 'rxjs'
-import { debounceTime, tap, toArray } from 'rxjs/operators'
+import { concat, Observable } from 'rxjs'
+import { tap, toArray } from 'rxjs/operators'
 import { Component, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
 import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
 import { immutableAssign } from '@app/helpers'
+import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
 import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
 import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
@@ -40,13 +41,21 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
   videoActions: DropdownAction<{ video: Video }>[] = []
 
   videos: Video[] = []
-  videosSearch: string
-  videosSearchChanged = new Subject<string>()
   getVideosObservableFunction = this.getVideosObservable.bind(this)
+
   sort: VideoSortField = '-publishedAt'
 
   user: User
 
+  inputFilters: AdvancedInputFilter[] = [
+    {
+      queryParams: { 'search': 'isLive:true' },
+      label: $localize`Only live videos`
+    }
+  ]
+
+  private search: string
+
   constructor (
     protected router: Router,
     protected serverService: ServerService,
@@ -64,21 +73,15 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
     this.buildActions()
 
     this.user = this.authService.getUser()
-
-    this.videosSearchChanged
-      .pipe(debounceTime(500))
-      .subscribe(() => {
-        this.videosSelection.reloadVideos()
-      })
   }
 
-  resetSearch () {
-    this.videosSearch = ''
-    this.onVideosSearchChanged()
+  onSearch (search: string) {
+    this.search = search
+    this.reloadData()
   }
 
-  onVideosSearchChanged () {
-    this.videosSearchChanged.next()
+  reloadData () {
+    this.videosSelection.reloadVideos()
   }
 
   onChangeSortColumn () {
@@ -96,7 +99,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
   getVideosObservable (page: number) {
     const newPagination = immutableAssign(this.pagination, { currentPage: page })
 
-    return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch)
+    return this.videoService.getMyVideos(newPagination, this.sort, this.search)
       .pipe(
         tap(res => this.pagination.totalItems = res.total)
       )
index 68ac6d02144d8c5b3076f96f352a5f561dba3f95..cfb7a1d98873dbacae51e31c67f25c7ad5de8c52 100644 (file)
@@ -46,7 +46,7 @@ input[type=submit] {
 
   font-weight: $font-semibold;
   display: inline-block;
-  padding: 0 10px 0 10px;
+  padding: 0 10px;
   white-space: nowrap;
   background: transparent;
 
index 65d4b6ecd63cff61b3fd9dfc58f1d6f216158938..130be75fc11f1cd2e24caee405ab174ca964552c 100644 (file)
 
   <ng-container *ngFor="let result of results">
     <div *ngIf="isVideoChannel(result)" class="entry video-channel">
-      <a class="link-avatar" *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)">
-        <img [src]="result.avatarUrl" alt="Avatar" />
-      </a>
 
-      <a class="link-avatar" *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank">
-        <img [src]="result.avatarUrl" alt="Avatar" />
-      </a>
+      <my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)"></my-actor-avatar>
 
       <div class="video-channel-info">
-        <a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)" class="video-channel-names">
+        <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names">
           <ng-container *ngTemplateOutlet="aContent"></ng-container>
         </a>
 
-        <a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank" class="video-channel-names">
+        <a *ngIf="isExternalChannelUrl()" [href]="getExternalChannelUrl(result)" target="_blank" class="video-channel-names">
           <ng-container *ngTemplateOutlet="aContent"></ng-container>
         </a>
 
index 91c8272d7f1b1e9a664feffc5de8790dfd9304e4..a8002ba886ba0d6ab14f207bfbb9ebc1def22550 100644 (file)
@@ -5,7 +5,7 @@
   $image-size: min(130px, $video-img-width);
   $margin-size: ($video-img-width - $image-size) / 2; // So we have the same width than the video miniature
 
-  @include channel-avatar($image-size);
+  @include actor-avatar-size($image-size);
 
   margin: 0 $margin-size 0 $margin-size;
 }
   max-width: 800px;
 }
 
-.video-channel {
-  img {
-    @include build-channel-img-size($video-thumbnail-width);
-  }
+.video-channel my-actor-avatar {
+  @include build-channel-img-size($video-thumbnail-width);
 }
 
 .video-channel-info {
     grid-template-columns: auto 1fr;
     grid-template-rows: auto auto;
 
-    .link-avatar {
+    my-actor-avatar {
+      @include build-channel-img-size($video-thumbnail-medium-width);
+
       grid-column: 1;
       grid-row: 1 / -1;
     }
-
-    img {
-      @include build-channel-img-size($video-thumbnail-medium-width);
-    }
   }
 
   .video-channel-info {
 }
 
 @include on-mobile-main-col {
-  .video-channel img {
+  .video-channel my-actor-avatar {
     @include build-channel-img-size($video-thumbnail-small-width);
   }
 }
index 2be952e161b9306b9afafaa1ba651dc3e4123d85..ecede19a3da6e13d392403c5db79ab44e853a608 100644 (file)
@@ -132,10 +132,6 @@ export class SearchComponent implements OnInit, OnDestroy {
     return 'internal'
   }
 
-  isExternalChannelUrl () {
-    return this.getVideoLinkType() === 'external'
-  }
-
   search () {
     forkJoin([
       this.getVideosObs(),
@@ -200,17 +196,33 @@ export class SearchComponent implements OnInit, OnDestroy {
     this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
   }
 
-  getChannelUrl (channel: VideoChannel) {
+  isExternalChannelUrl () {
+    return this.getVideoLinkType() === 'external'
+  }
+
+  getExternalChannelUrl (channel: VideoChannel) {
     // Same algorithm than videos
     if (this.getVideoLinkType() === 'external') {
       return channel.url
     }
 
-    if (this.getVideoLinkType() === 'internal') {
+    // lazy-load or internal
+    return undefined
+  }
+
+  getInternalChannelUrl (channel: VideoChannel) {
+    const linkType = this.getVideoLinkType()
+
+    if (linkType === 'internal') {
       return [ '/video-channels', channel.nameWithHost ]
     }
 
-    return [ '/search/lazy-load-channel', { url: channel.url } ]
+    if (linkType === 'lazy-load') {
+      return [ '/search/lazy-load-channel', { url: channel.url } ]
+    }
+
+    // external
+    return undefined
   }
 
   hideActions () {
index e85ae07d00d3b0ab876e7d9bc8d7171dbf8f7514..390833abc041162077d0e93795e1807f78435fe5 100644 (file)
@@ -1,4 +1,5 @@
 import { NgModule } from '@angular/core'
+import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
 import { SharedFormModule } from '@app/shared/shared-forms'
 import { SharedMainModule } from '@app/shared/shared-main'
 import { SharedSearchModule } from '@app/shared/shared-search'
@@ -18,6 +19,7 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
     SharedMainModule,
     SharedSearchModule,
     SharedFormModule,
+    SharedActorImageModule,
     SharedUserSubscriptionModule,
     SharedVideoMiniatureModule
   ],
index 16ba9e2c0e2f595ab53decf0d32a11cd37154f61..f6a846ffaf3515a5d3a7fa0735edcf2a32994621 100644 (file)
@@ -84,7 +84,7 @@ button {
       border-color: pvar(--mainColor) transparent transparent transparent;
     }
 
-    + div {
+    + div {
       font-size: 15px;
     }
   }
index fbc27c8bc1a6be224993598896f72c27f2169f8e..b302366e22ccae696a08641b2e997be77a5d537d 100644 (file)
@@ -9,19 +9,16 @@ svg {
   stroke-dashoffset: 0;
 
   &.circle {
-    -webkit-animation: dash .9s ease-in-out;
     animation: dash .9s ease-in-out;
   }
 
   &.line {
     stroke-dashoffset: 1000;
-    -webkit-animation: dash .9s .35s ease-in-out forwards;
     animation: dash .9s .35s ease-in-out forwards;
   }
 
   &.check {
     stroke-dashoffset: -100;
-    -webkit-animation: dash-check .9s .35s ease-in-out forwards;
     animation: dash-check .9s .35s ease-in-out forwards;
   }
 }
@@ -38,16 +35,6 @@ svg {
   text-align: center;
 }
 
-
-@-webkit-keyframes dash {
-  0% {
-    stroke-dashoffset: 1000;
-  }
-  100% {
-    stroke-dashoffset: 0;
-  }
-}
-
 @keyframes dash {
   0% {
     stroke-dashoffset: 1000;
@@ -57,15 +44,6 @@ svg {
   }
 }
 
-@-webkit-keyframes dash-check {
-  0% {
-    stroke-dashoffset: -100;
-  }
-  100% {
-    stroke-dashoffset: 900;
-  }
-}
-
 @keyframes dash-check {
   0% {
     stroke-dashoffset: -100;
index 9308d5bb66c3b52b80ea430f262a668b8cbac028..b4d81fe39d595ce7078a8933579d878ca620c8eb 100644 (file)
@@ -6,16 +6,16 @@
   <div class="channel-info">
 
     <ng-template #buttonsTemplate>
-        <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
-          Manage channel
-        </a>
+      <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
+        Manage channel
+      </a>
 
-        <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
+      <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
 
-        <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
-          <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>
-          <span class="icon-text" i18n>Support</span>
-        </button>
+      <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
+        <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>
+        <span class="icon-text" i18n>Support</span>
+      </button>
     </ng-template>
 
     <ng-template #ownerTemplate>
@@ -23,7 +23,7 @@
         <div class="section-label" i18n>OWNER ACCOUNT</div>
 
         <div class="avatar-row">
-          <my-account-avatar [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()" size="120"></my-account-avatar>
+          <my-actor-avatar class="account-avatar" [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
 
           <div class="actor-info">
             <h4>
@@ -49,7 +49,7 @@
     </ng-template>
 
     <div class="channel-avatar-row">
-      <img class="channel-avatar" [src]="videoChannel.avatarUrl" alt="Avatar" />
+      <my-actor-avatar class="main-avatar" [channel]="videoChannel"></my-actor-avatar>
 
       <div>
         <div class="section-label" i18n>VIDEO CHANNEL</div>
index e946707efa1aba97f418d7db01d7e49bf2c5f005..470f648789a4fc8bbac707a881d3fec4a770c9ce 100644 (file)
     display: flex;
     margin-bottom: 15px;
 
-    img {
-      @include avatar(48px);
+    .account-avatar {
+      @include actor-avatar-size(48px);
     }
 
     .actor-info {
   }
 
   .owner-description {
+    @include fade-text(120px, pvar(--mainBackgroundColor));
+
     max-height: 140px;
     word-break: break-word;
-
-    @include fade-text(120px, pvar(--mainBackgroundColor));
   }
 }
 
 }
 
 .copy-button {
-  border: none;
+  border: 0;
 }
 
 @media screen and (max-width: 1400px) {
   }
 
   .channel-description:not(.expanded) {
-    max-height: 70px;
-
     @include fade-text(30px, pvar(--channelBackgroundColor));
+
+    max-height: 70px;
   }
 
   .show-more {
     }
 
     .owner-description {
+      @include fade-text(30px, pvar(--mainBackgroundColor));
+
       grid-column: 2;
       max-height: 70px;
-
-      @include fade-text(30px, pvar(--mainBackgroundColor));
     }
 
     .view-account {
         margin-top: -5px;
       }
 
-      img {
-        @include channel-avatar(64px);
+      .account-avatar {
+        @include actor-avatar-size(64px);
 
         margin: -30px 0 0 15px;
       }
index 2e387f40177c3da287531f32afccc247cba2255b..35c39cc2ec12723637f1fc3924b2e0acbcedd8c3 100644 (file)
@@ -10,7 +10,7 @@ import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-
 import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
 import { VideoChannelsRoutingModule } from './video-channels-routing.module'
 import { VideoChannelsComponent } from './video-channels.component'
-import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -23,7 +23,7 @@ import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/share
     SharedUserSubscriptionModule,
     SharedGlobalIconModule,
     SharedSupportModal,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
index 0958b5f80837b0490b24f7ec87081e5ba9a1b661..a85cf444cf76851bd5af3a63dab7c9da672c1a74 100644 (file)
@@ -16,6 +16,6 @@ label {
 }
 
 .warning-replace-caption {
-  color: red;
+  color: #ff0000;
   margin-top: 10px;
-}
\ No newline at end of file
+}
index 6fe52af677f4480bc54d29b75419d76d84ee4171..094b4d3b3f1b7de387a52a0189cb2d39b5f24769 100644 (file)
               </ng-template>
 
               <ng-template ptTemplate="help">
-                <ng-container i18n>Some instances do not list videos containing mature or explicit content by default.</ng-container>
+                <ng-container i18n>Some instances hide videos containing mature or explicit content by default.</ng-container>
               </ng-template>
             </my-peertube-checkbox>
 
index 0b70b027014e71ef545a75d6c23aaeee1da785ca..bc32d7964715376cd0747853d46b354afc34c25a 100644 (file)
@@ -150,7 +150,7 @@ p-calendar {
   @include media-breakpoint-up(md) {
     @include make-col(7);
 
-    + .col-video-edit {
+    + .col-video-edit {
       @include make-col(5);
     }
   }
@@ -158,7 +158,7 @@ p-calendar {
   @include media-breakpoint-up(xl) {
     @include make-col(8);
 
-    + .col-video-edit {
+    + .col-video-edit {
       @include make-col(4);
     }
   }
@@ -169,7 +169,7 @@ p-calendar {
     @include media-breakpoint-up(md) {
       @include make-col(8);
 
-      + .col-video-edit {
+      + .col-video-edit {
         @include make-col(4);
       }
     }
index 17c5f63e9f96751a2f0595c7e8a1cbcd4faf0d42..dc9153b2ba067677acc24543eae7c2c996056644 100644 (file)
@@ -6,7 +6,7 @@ $width-size: 190px;
 .alert.alert-danger {
   text-align: center;
 
-  > div {
+  > div {
     font-weight: $font-semibold;
   }
 }
@@ -17,10 +17,10 @@ $width-size: 190px;
   align-items: center;
 
   .upload-icon {
+    @include apply-svg-color(#C6C6C6);
+
     width: 90px;
     margin-bottom: 25px;
-
-    @include apply-svg-color(#C6C6C6);
   }
 
   .peertube-select-container {
index 1ebee946b71d3e686edf9299a952089e2613a31a..35bca24d0871514892d283cadce5d24f94a7747d 100644 (file)
@@ -44,7 +44,7 @@ $nav-link-height: 40px;
 
 ::ng-deep .video-add-nav {
   border-bottom: $border-width $border-type $border-color;
-  margin: 20px 0 0 !important;
+  margin: 20px 0 0 !important;
 
   &.hide-nav {
     display: none !important;
index 7bd9b7c90b44ccba0098e18d30c634e498c603f8..42adfed8d73203263644d1ddc02e16ff4cf01ff9 100644 (file)
@@ -1,6 +1,6 @@
 <form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
   <div class="avatar-and-textarea">
-    <my-account-avatar [account]="user?.account" size="25"></my-account-avatar>
+    <my-actor-avatar [account]="user?.account" size="25"></my-actor-avatar>
 
     <div class="form-group">
       <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
index 1aa9255c290aa34a854b532d4c0ff9c29f2a10e8..7743bd41d1f365979e49fcfe3d5214849babfc90 100644 (file)
@@ -13,8 +13,7 @@ form {
   display: flex;
   margin-bottom: 10px;
 
-  my-account-avatar {
-    vertical-align: top;
+  my-actor-avatar {
     margin-right: 10px;
   }
 
@@ -32,7 +31,7 @@ form {
     padding-right: $markdown-icon-width + 15px !important;
 
     @media screen and (max-width: 600px) {
-      padding-right: $markdown-icon-width + 19px  !important;
+      padding-right: $markdown-icon-width + 19px !important;
     }
 
     &:focus::placeholder {
@@ -58,7 +57,9 @@ form {
       }
     }
 
-    &:focus, &:active, &:hover {
+    &:focus,
+    &:active,
+    &:hover {
       my-global-icon svg {
         background-color: #C6C6C6;
         color: pvar(--mainBackgroundColor);
index 2b0739261dfa738e5419b651719f939b727a84cf..d7ba40ef6cce432d8d3953c4d7e0e429860839b9 100644 (file)
@@ -1,12 +1,10 @@
-<div *ngIf="isCommentDisplayed()" class="root-comment">
+<div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }">
   <div class="left">
-    <my-account-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account"></my-account-avatar>
+    <my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account"></my-actor-avatar>
     <div class="vertical-border"></div>
   </div>
 
   <div class="right" [ngClass]="{ 'mb-3': firstInThread }">
-    <span *ngIf="comment.isDeleted" class="comment-avatar"></span>
-
     <div class="comment">
       <ng-container *ngIf="!comment.isDeleted">
         <div *ngIf="highlightedComment === true" class="highlighted-comment" i18n>Highlighted comment</div>
@@ -68,7 +66,7 @@
         [textValue]="redraftValue"
       ></my-video-comment-add>
 
-      <div *ngIf="commentTree" class="children">
+      <div *ngIf="commentTree">
         <div *ngFor="let commentChild of commentTree.children">
           <my-video-comment
             [comment]="commentChild.comment"
index cf33a5b0e5ab90c1965d432b60e64ab2cb4c5e91..a4d2e237cef7e390779f1b99af2478c25e3e07e5 100644 (file)
   }
 }
 
+my-actor-avatar {
+  @include actor-avatar-size(36px);
+}
+
 .comment {
   flex-grow: 1;
   // Fix word-wrap with flex
@@ -58,7 +62,7 @@
   display: inline-flex;
   padding-right: 6px;
   padding-left: 6px;
-  color: white !important;
+  color: #fff !important;
 }
 
 .comment-account {
     cursor: pointer;
     margin-right: 10px;
 
-    &:hover, &:active, &:focus, &:focus-visible {
+    &:hover,
+    &:active,
+    &:focus,
+    &:focus-visible {
       color: pvar(--mainForegroundColor);
     }
   }
@@ -148,10 +155,10 @@ my-video-comment-add {
   }
 }
 
-.children {
+.is-child {
   // Reduce avatars size for replies
-  .comment-avatar {
-    @include avatar(25px);
+  my-actor-avatar {
+    @include actor-avatar-size(25px);
   }
 
   .left {
index dd3db0c6573e29b92c8cc49425d6ba554443f1db..fd379e80e7523e6c9e34de2614e27a88fdb2984a 100644 (file)
@@ -138,6 +138,10 @@ export class VideoCommentComponent implements OnInit, OnChanges {
       (this.commentTree?.hasDisplayedChildren) // Or this is a reply that have other replies
   }
 
+  isChild () {
+    return this.parentComments.length !== 0
+  }
+
   private getUserIfNeeded (account: Account) {
     if (!account.userId) return
     if (!this.authService.isLoggedIn()) return
index e6778e1a95e8733125cea642e75028119364e6e5..a7e8580695d627247ec064efd8fc74a26758b2bc 100644 (file)
@@ -11,7 +11,8 @@
   cursor: pointer;
 }
 
-.glyphicon, .comment-thread-loading {
+.glyphicon,
+.comment-thread-loading {
   margin-right: 5px;
   display: inline-block;
   font-size: 13px;
@@ -40,7 +41,7 @@
 #dropdown-sort-comments {
   font-weight: 600;
   text-transform: uppercase;
-  border: none;
+  border: 0;
   transform: translateY(-7%);
 }
 
index e0e9f92e7603e7251ccd310ce038453eea2a5f1f..e1040feadfa04217458a631df9c847730cba3ad6 100644 (file)
@@ -15,7 +15,9 @@
     <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count">
       <my-video-miniature
         [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow"
-        (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()">
+        (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"
+        actorImageSize="32"
+      >
       </my-video-miniature>
 
       <hr *ngIf="!playlist && i == 0 && length > 1" />
index c9fae6f270140fae89d2a3a5c92822d74163ac72..5e0373afc6bdbeacf85d4644048f5f60c8899f78 100644 (file)
@@ -8,7 +8,8 @@
   margin-bottom: 25px;
   flex-wrap: wrap-reverse;
 
-  .title-page.active, .title-page.title-page-single {
+  .title-page.active,
+  .title-page.title-page-single {
     margin-bottom: unset;
     margin-right: .5rem !important;
   }
index a02373f2d6086a34081dfcdcba07520c5ce2488b..5f149cbd1155a3372a0bf3dc452e86ebe06dc550 100644 (file)
@@ -1,21 +1,11 @@
-<div class="wrapper" [ngClass]="'avatar-' + size">
-  <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel">
-    <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
-      <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
-    </a>
-
-    <my-account-avatar [account]="video.account" [title]="accountLinkTitle" [internalHref]="[ '/accounts', video.byAccount ]"></my-account-avatar>
-</ng-container>
-
-  <ng-container *ngIf="!isChannelAvatarNull() && genericChannel">
-    <my-account-avatar [account]="video.account" [title]="accountLinkTitle" [internalHref]="[ '/accounts', video.byAccount ]"></my-account-avatar>
-
-    <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
-      <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
-    </a>
-  </ng-container>
-
-  <ng-container *ngIf="isChannelAvatarNull()">
-    <my-account-avatar [account]="video.account" [title]="accountLinkTitle" [internalHref]="[ '/accounts', video.byAccount ]"></my-account-avatar>
-  </ng-container>
+<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }">
+  <my-actor-avatar
+    class="channel" [channel]="video.channel"
+    [internalHref]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"
+  ></my-actor-avatar>
+
+  <my-actor-avatar
+    class="account" [account]="video.account"
+    [internalHref]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
+  </my-actor-avatar>
 </div>
index 4998e85faa77e120832676298df550e2e0077843..20e32240cdbde340d21bfbc3e5518f1eb3acb59b 100644 (file)
@@ -1,44 +1,42 @@
 @import '_mixins';
 
+@mixin main {
+  @include actor-avatar-size(35px);
+}
+
+@mixin secondary {
+  height: 60%;
+  width: 60%;
+  position: absolute;
+  bottom: -5px;
+  right: -5px;
+  background-color: rgba(0, 0, 0, 0);
+}
+
 .wrapper {
-  $avatar-size: 35px;
+  @include actor-avatar-size(35px);
 
-  width: $avatar-size;
-  height: $avatar-size;
   position: relative;
   margin-right: 5px;
   margin-bottom: 5px;
 
-  &.avatar-sm {
-    width: 28px;
-    height: 28px;
-    margin-bottom: 3px;
-  }
+  &.generic-channel {
+    .account {
+      @include main();
+    }
 
-  a {
-    @include disable-outline;
+    .channel {
+      display: none !important;
+    }
   }
 
-  a img {
-    height: 100%;
-    object-fit: cover;
-    position: absolute;
-    top:50%;
-    left:50%;
-    transform: translate(-50%,-50%);
-    border-radius: 5px;
-
-    &:not(.channel-avatar) {
-      border-radius: 50%;
+  &:not(.generic-channel) {
+    .account {
+      @include secondary();
     }
-  }
 
-  a:nth-of-type(2) img {
-    height: 60%;
-    width: 60%;
-    border: 2px solid pvar(--mainBackgroundColor);
-    transform: translateX(15%);
-    position: relative;
-    background-color: pvar(--mainBackgroundColor);
+    .channel {
+      @include main();
+    }
   }
 }
index 0b6e796dff4b132f8b09631caff43f923339c5eb..63edd7badff7a3183bd954c2643b6252b8e8a647 100644 (file)
@@ -10,7 +10,6 @@ export class VideoAvatarChannelComponent implements OnInit {
   @Input() video: Video
   @Input() byAccount: string
 
-  @Input() size: 'md' | 'sm' = 'md'
   @Input() genericChannel: boolean
 
   channelLinkTitle = ''
index 0b0a2a899a38216cc6caa8a0913a8be11229bc01..b3f93b83c4f6bc1bd6275f550920ba9d26bdec8e 100644 (file)
@@ -45,7 +45,7 @@
 
       my-global-icon {
         &:not(.active) {
-          opacity: .5
+          opacity: .5;
         }
 
         ::ng-deep {
index eadb2148a16d6b7c8ef912d49210f1babfbce4e4..4779602d2422149fea89136ffd145a07eccb6074 100644 (file)
@@ -79,7 +79,7 @@
                   <span [innerHTML]="getRatePopoverText()"></span>
                 </ng-template>
 
-                <div class="video-actions fullWidth justify-content-end">
+                <div class="video-actions full-width justify-content-end">
                   <button
                     [ngbPopover]="getRatePopoverText() && ratePopoverText" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" (keyup.enter)="setLike()"
                     class="action-button action-button-like" [attr.aria-pressed]="userRating === 'like'" [attr.aria-label]="tooltipLike"
index e8ad10a11926f3e4a7b2cb1e3fbf07e8b0252215..30176269553b884a37c385dd8837bda186be9b21 100644 (file)
@@ -6,12 +6,12 @@
 $player-factor: 16/9;
 $video-info-margin-left: 44px;
 
-@function getPlayerHeight($width){
-  @return calc(#{$width} / #{$player-factor})
+@function getPlayerHeight ($width) {
+  @return calc(#{$width} / #{$player-factor});
 }
 
-@function getPlayerWidth($height){
-  @return calc(#{$height} * #{$player-factor})
+@function getPlayerWidth ($height) {
+  @return calc(#{$height} * #{$player-factor});
 }
 
 @mixin playlist-below-player {
@@ -24,11 +24,11 @@ $video-info-margin-left: 44px;
 
 .root {
   &.theater-enabled #video-wrapper {
+    $height: calc(100vh - #{$header-height} - #{$theater-bottom-space});
+
     flex-direction: column;
     justify-content: center;
 
-    $height: calc(100vh - #{$header-height} - #{$theater-bottom-space});
-
     #videojs-wrapper {
       width: 100%;
       height: $height;
@@ -141,7 +141,7 @@ $video-info-margin-left: 44px;
     .video-info-first-row {
       display: flex;
 
-      > div:first-child {
+      > div:first-child {
         flex-grow: 1;
       }
 
@@ -207,7 +207,7 @@ $video-info-margin-left: 44px;
       }
 
       .video-actions-rates {
-        margin: 0 0 10px 0;
+        margin: 0 0 10px;
         align-items: start;
         width: max-content;
         margin-left: auto;
@@ -231,7 +231,7 @@ $video-info-margin-left: 44px;
             font-size: 100%;
             font-weight: $font-semibold;
             display: inline-block;
-            padding: 0 10px 0 10px;
+            padding: 0 10px;
             white-space: nowrap;
             background-color: transparent !important;
             color: pvar(--actionButtonColor);
@@ -346,7 +346,8 @@ $video-info-margin-left: 44px;
         }
       }
 
-      .glyphicon, .description-loading {
+      .glyphicon,
+      .description-loading {
         margin-left: 3px;
       }
 
@@ -396,7 +397,7 @@ $video-info-margin-left: 44px;
       &.video-attribute-tags {
         .video-attribute-value:not(:nth-child(2)) {
           &::before {
-            content: ', '
+            content: ', ';
           }
         }
       }
index cf6afd852f78eb897ad94637def6f96202737dd5..62ce7be2ddf542001df235f26e7bfaa8c16ceb0c 100644 (file)
@@ -20,7 +20,7 @@ import { TimestampRouteTransformerDirective } from './timestamp-route-transforme
 import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
 import { VideoWatchRoutingModule } from './video-watch-routing.module'
 import { VideoWatchComponent } from './video-watch.component'
-import { SharedAccountAvatarModule } from '../../shared/shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../../shared/shared-actor-image/shared-actor-image.module'
 import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
 
 @NgModule({
@@ -39,7 +39,7 @@ import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
     SharedShareModal,
     SharedVideoModule,
     SharedSupportModal,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
index 639a96c43e158974a4b00eefd67524210ba923aa..e21bffb6c5ad9ebe04b696f13c14ce1043ec08c0 100644 (file)
@@ -33,7 +33,7 @@
       <div class="section channel videos" *ngFor="let object of overview.channels">
         <div class="section-title">
           <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
-            <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />
+            <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar>
 
             <h2 class="section-title">{{ object.channel.displayName }}</h2>
           </a>
index ec73c628c2e7abdcbca79d324f62525332304a1a..8fbac1b4692274d63461c62b43624fdbe59caef0 100644 (file)
@@ -16,7 +16,7 @@
     padding-top: 30px;
 
     .section-title {
-      border-top: none !important;
+      border-top: 0 !important;
     }
   }
 
     }
 
     a {
-      &:hover, &:focus:not(.focus-visible), &:active {
+      color: pvar(--mainForegroundColor);
+
+      &:hover,
+      &:focus:not(.focus-visible),
+      &:active {
         text-decoration: none;
         outline: none;
       }
-
-      color: pvar(--mainForegroundColor);
     }
   }
 
         width: fit-content;
         align-items: center;
 
-        img {
-          @include channel-avatar(28px);
+        my-actor-avatar {
+          @include actor-avatar-size(28px);
 
+          font-size: initial;
           margin-right: 8px;
         }
       }
index b3be1d7b5e5247988bc51009736edf0f95a95d26..14532ca1ed02b8a4fcbfb29d953c955c38a19921 100644 (file)
@@ -45,8 +45,8 @@ export class VideoOverviewComponent implements OnInit {
     return object.videos[0].byVideoChannel
   }
 
-  buildVideoChannelAvatarUrl (object: { videos: Video[] }) {
-    return object.videos[0].videoChannelAvatarUrl
+  buildVideoChannel (object: { videos: Video[] }) {
+    return object.videos[0].channel
   }
 
   buildVideos (videos: Video[]) {
index 923a1d67aca6953be4484cc0f385f1f9d03e0423..6daacc78e2cc81c06c093152c0de8a433535f1f2 100644 (file)
@@ -14,4 +14,4 @@
     height: 1rem;
     margin-right: .1rem;
   }
-}
\ No newline at end of file
+}
index 61d012d63375bd558d19c389c39af6289b80b3be..8a35015d654c969fd8b6410ab0333995e3e89530 100644 (file)
@@ -1,4 +1,5 @@
 import { NgModule } from '@angular/core'
+import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
 import { SharedFormModule } from '@app/shared/shared-forms'
 import { SharedGlobalIconModule } from '@app/shared/shared-icons'
 import { SharedMainModule } from '@app/shared/shared-main'
@@ -21,7 +22,8 @@ import { VideosComponent } from './videos.component'
     SharedFormModule,
     SharedVideoMiniatureModule,
     SharedUserSubscriptionModule,
-    SharedGlobalIconModule
+    SharedGlobalIconModule,
+    SharedActorImageModule
   ],
 
   declarations: [
index e7d05369baf611c23dbe69a6b5194d9c9dd6bdcc..e21ada0f18727414f7565b7932f58f52e834fc76 100644 (file)
@@ -79,7 +79,7 @@
     display: inline-block;
     width: 23px;
     height: 24px;
-    margin-right: .5rem;
+    margin-right: 0.5rem;
   }
 
   @media screen and (max-width: $mobile-view) {
index 41c59cc86da5d9f01f7db5fd969a10a3b6df8e0b..3cec6d7392ef459b4ccb50d66946c2ffc2120843 100644 (file)
@@ -24,7 +24,7 @@ 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 { SharedAccountAvatarModule } from './shared/shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
 
 registerLocaleData(localeOc, 'oc')
 
@@ -60,7 +60,7 @@ registerLocaleData(localeOc, 'oc')
     SharedUserInterfaceSettingsModule,
     SharedGlobalIconModule,
     SharedInstanceModule,
-    SharedAccountAvatarModule,
+    SharedActorImageModule,
 
     MetaModule.forRoot({
       provide: MetaLoader,
index a970260c9294546bc8a4f6e5498e3e393b187614..b39ffa98d4987344439eb59d33c7ae240cddf454 100644 (file)
   left: 0;
   color: #333;
   font-size: 1em;
-  background-color: rgba(255,255,255,0.9);
+  background-color: rgba(255, 255, 255, 0.9);
 }
 
 .cfp-hotkeys-container.fade {
   z-index: -1024;
   visibility: hidden;
   opacity: 0;
-  -webkit-transition: opacity 0.15s linear;
-  -moz-transition: opacity 0.15s linear;
-  -o-transition: opacity 0.15s linear;
   transition: opacity 0.15s linear;
 }
 
index 32c1db44638fa831e40b346b08f472ec0f43b799..a5b48f10c6f17dc85108ce7e049003c1ee0a378a 100644 (file)
@@ -1,8 +1,6 @@
 import * as debug from 'debug'
 import { LazyLoadEvent, SortMeta } from 'primeng/api'
-import { Subject } from 'rxjs'
-import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
-import { ActivatedRoute, Params, Router } from '@angular/router'
+import { ActivatedRoute, Router } from '@angular/router'
 import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
 import { RestPagination } from './rest-pagination'
 
@@ -14,14 +12,11 @@ export abstract class RestTable {
   abstract sort: SortMeta
   abstract pagination: RestPagination
 
-  search: string
   rowsPerPageOptions = [ 10, 20, 50, 100 ]
   rowsPerPage = this.rowsPerPageOptions[0]
   expandedRows = {}
 
-  baseRoute: string
-
-  protected searchStream: Subject<string>
+  search: string
 
   protected route: ActivatedRoute
   protected router: Router
@@ -30,7 +25,6 @@ export abstract class RestTable {
 
   initialize () {
     this.loadSort()
-    this.initSearch()
   }
 
   loadSort () {
@@ -58,7 +52,7 @@ export abstract class RestTable {
       count: this.rowsPerPage
     }
 
-    this.loadData()
+    this.reloadData()
     this.saveSort()
   }
 
@@ -66,55 +60,6 @@ export abstract class RestTable {
     peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
   }
 
-  initSearch () {
-    this.searchStream = new Subject()
-
-    this.searchStream
-      .pipe(
-        debounceTime(400),
-        distinctUntilChanged()
-      )
-      .subscribe(search => {
-        this.search = search
-
-        logger('On search %s.', this.search)
-
-        this.loadData()
-      })
-  }
-
-  onSearch (event: Event) {
-    const target = event.target as HTMLInputElement
-    this.searchStream.next(target.value)
-
-    this.setQueryParams((event.target as HTMLInputElement).value)
-  }
-
-  setQueryParams (search: string) {
-    if (!this.baseRoute) return
-
-    const queryParams: Params = {}
-
-    if (search) Object.assign(queryParams, { search })
-    this.router.navigate([ this.baseRoute ], { queryParams })
-  }
-
-  resetTableFilter () {
-    this.setTableFilter('')
-    this.setQueryParams('')
-    this.resetSearch()
-  }
-
-  listenToSearchChange () {
-    this.route.queryParams
-      .subscribe(params => {
-        this.search = params.search || ''
-
-        // Primeng table will run an event to load data
-        this.setTableFilter(this.search)
-      })
-  }
-
   onPage (event: { first: number, rows: number }) {
     logger('On page %o.', event)
 
@@ -125,28 +70,18 @@ export abstract class RestTable {
         count: this.rowsPerPage
       }
 
-      this.loadData()
+      this.reloadData()
     }
 
     this.expandedRows = {}
   }
 
-  setTableFilter (filter: string, triggerEvent = true) {
-    // FIXME: cannot use ViewChild, so create a component for the filter input
-    const filterInput = document.getElementById('table-filter') as HTMLInputElement
-    if (!filterInput) return
-
-    filterInput.value = filter
-
-    if (triggerEvent) filterInput.dispatchEvent(new Event('keyup'))
-  }
-
-  resetSearch () {
-    this.searchStream.next('')
-    this.setTableFilter('')
+  onSearch (search: string) {
+    this.search = search
+    this.reloadData()
   }
 
-  protected abstract loadData (): void
+  protected abstract reloadData (): void
 
   private getSortLocalStorageKey () {
     return 'rest-table-sort-' + this.getIdentifier()
index 4f1fc884846f636b6f4baee4c8738b5394c57681..1696e6709881a3fd71b23216065321e04766bf8e 100644 (file)
@@ -90,14 +90,20 @@ export class RestService {
 
       const matchedTokens = tokens.filter(t => t.startsWith(prefix))
                                   .map(t => t.slice(prefix.length)) // Keep the value filter
-                                  .map(t => t.replace(/^"|"$/g, ''))
+                                  .map(t => t.replace(/^"|"$/g, '')) // Remove ""
                                   .map(t => {
                                     if (prefixObj.handler) return prefixObj.handler(t)
 
+                                    if (prefixObj.isBoolean) {
+                                      if (t === 'true') return true
+                                      if (t === 'false') return false
+
+                                      return undefined
+                                    }
+
                                     return t
                                   })
-                                  .filter(t => !!t || t === 0)
-                                  .map(t => prefixObj.isBoolean ? t === 'true' : t)
+                                  .filter(t => t !== null && t !== undefined)
 
       if (matchedTokens.length === 0) continue
 
index 3de83152c32d992e0763ad681718815dcea1b9cc..47db985e12f00036234131e7b665a5b5b3efc005 100644 (file)
@@ -320,13 +320,7 @@ export class UserService {
       const filters = this.restService.parseQueryStringFilter(search, {
         blocked: {
           prefix: 'banned:',
-          isBoolean: true,
-          handler: v => {
-            if (v === 'true') return v
-            if (v === 'false') return v
-
-            return undefined
-          }
+          isBoolean: true
         }
       })
 
index c133b5fe91b6f41de258c79feb93059e8c1f5771..fd8268b35336cb338bc554cf9ab4b60d2d691e0e 100644 (file)
@@ -39,9 +39,9 @@ export class ScreenService {
     let numberOfVideos = 1
 
     if (screenWidth > 1850) numberOfVideos = 5
-    else if (screenWidth > 1600) numberOfVideos = 4
-    else if (screenWidth > 1370) numberOfVideos = 3
-    else if (screenWidth > 1100) numberOfVideos = 2
+    else if (screenWidth > 1410) numberOfVideos = 4
+    else if (screenWidth > 1120) numberOfVideos = 3
+    else if (screenWidth > 890) numberOfVideos = 2
 
     return numberOfVideos
   }
index c754a99d1811e5b64842df32cd1c94385df4fb0e..3e0350ba0b7184d11bbf963adbb547d94b1184cc 100644 (file)
@@ -44,7 +44,8 @@ li.suggestion {
 
   // soft border-radius for the last suggestion and the link inside
   &:last-of-type {
-    &, & ::ng-deep a {
+    &,
+    ::ng-deep a {
       border-bottom-right-radius: 3px;
       border-bottom-left-radius: 3px;
     }
@@ -74,7 +75,7 @@ li.suggestion {
 #typeahead-container {
   input {
     border: 1px solid pvar(--mainBackgroundColor) !important;
-    box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
+    box-shadow: rgba(0, 0, 0, 0.1) 0 1px 20px 0;
     flex-grow: 1;
     transition: box-shadow .3s ease, width .2s ease;
   }
@@ -95,7 +96,7 @@ li.suggestion {
     right: 10px;
   }
 
-  > div:last-child {
+  > div:last-child {
     // we have to switch the display and not the opacity,
     // to avoid clashing with the rest of the interface.
     display: none;
@@ -103,7 +104,7 @@ li.suggestion {
 
   &:focus,
   ::ng-deep &:focus-within {
-    > div:last-child {
+    > div:last-child {
       @media screen and (min-width: $mobile-view) {
         display: initial !important;
       }
@@ -111,12 +112,12 @@ li.suggestion {
       #typeahead-help,
       #typeahead-instructions,
       li.suggestion {
-        box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
+        box-shadow: rgba(0, 0, 0, 0.2) 0 10px 20px -5px;
       }
     }
 
     ::ng-deep input {
-      box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
+      box-shadow: rgba(0, 0, 0, 0.2) 0 1px 20px 0;
       border-end-start-radius: 0;
       border-end-end-radius: 0;
 
index 692a81daa01e803449288ffedcb14be8add501f8..9163de0b197c7829b0cc8070994dcf56d611f642 100644 (file)
@@ -2,9 +2,11 @@
 
 a {
   @include disable-default-a-behaviour;
+
   width: 100%;
 
-  &, &:hover {
+  &,
+  &:hover {
     color: pvar(--mainForegroundColor);
 
     &.focus-visible {
@@ -23,10 +25,10 @@ a {
 }
 
 my-global-icon {
+  @include apply-svg-color(pvar(--mainForegroundColor));
+
   width: 17px;
   position: relative;
   top: -2px;
   margin: 5px;
-
-  @include apply-svg-color(pvar(--mainForegroundColor));
 }
index 6226a85cb7dc237d95454dea0d2584e64ce89763..800b1ebef66b789b354751e71aeecdd26a91e75d 100644 (file)
@@ -5,12 +5,12 @@
   @include peertube-button-link;
   @include orange-button;
 
+  border-radius: 0;
+
   &.focus-visible,
   &:focus {
     box-shadow: none;
   }
-
-  border-radius: 0;
 }
 
 .modal-body {
index df5c7971d3d1d33eee4bd864f2f00aba52975891..2e07deca2a4cfc50ced9fafa5353277b6673a9a3 100644 (file)
@@ -5,7 +5,7 @@
         <div>
           <div class="logged-in-more" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left" [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside">
             <div ngbDropdownToggle>
-              <my-account-avatar [account]="user.account" size="34"></my-account-avatar>
+              <my-actor-avatar [account]="user.account" size="34"></my-actor-avatar>
               <div class="logged-in-info">
                 <div class="logged-in-display-name">{{ user.account?.displayName }}</div>
 
index 00d1a1f69c2c9d9c3c60d649bf432b301f5c15ce..d0edd820eab7e4bc757f30119b4771955d8ded5b 100644 (file)
@@ -24,8 +24,9 @@ $footer-links-base-opacity: .8;
     background-color: rgba(255, 255, 255, 0.15);
   }
 
-  &:hover, &.focus-visible {
-    background-color: rgba(255, 255, 255, 0.10);
+  &:hover,
+  &.focus-visible {
+    background-color: rgba(255, 255, 255, 0.1);
   }
 
   my-global-icon {
@@ -60,7 +61,8 @@ menu {
   margin: 0;
   padding: 0;
 
-  &:focus, &:hover {
+  &:focus,
+  &:hover {
     overflow-y: auto;
   }
 
@@ -125,7 +127,7 @@ my-notification {
   line-height: 1;
 
   &.show {
-    background-color: rgba(255, 255, 255, 0.20);
+    background-color: rgba(255, 255, 255, 0.2);
     box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
   }
 
@@ -158,14 +160,14 @@ my-notification {
       position: absolute;
       right: -35px;
       top: -8px;
-      color: grey;
+      color: #808080;
       width: $main-radius;
     }
   }
 
   .dropdown-toggle {
     &::after {
-      border: none;
+      border: 0;
     }
   }
 
@@ -177,7 +179,7 @@ my-notification {
   }
 }
 
-my-account-avatar {
+my-actor-avatar {
   margin-right: 10px;
 }
 
@@ -193,11 +195,11 @@ my-account-avatar {
 }
 
 .logged-in-display-name {
+  @include disable-default-a-behaviour;
+
   font-size: 16px;
   font-weight: $font-semibold;
   color: pvar(--menuForegroundColor);
-
-  @include disable-default-a-behaviour;
 }
 
 .logged-in-username {
@@ -251,7 +253,7 @@ my-account-avatar {
 }
 
 .login-buttons-block {
-  margin: 30px 25px 35px 25px;
+  margin: 30px 25px 35px;
 
   > a {
     display: block;
@@ -305,7 +307,8 @@ my-account-avatar {
 }
 
 .footer-links {
-  &, > div {
+  &,
+  > div {
     display: flex;
     flex-wrap: wrap;
   }
@@ -388,29 +391,29 @@ my-account-avatar {
   .dropdown-item:hover,
   .dropdown-item:active {
     &.settings-sensitive my-global-icon ::ng-deep svg {
-      margin-top: 0px !important;
+      margin-top: 0 !important;
     }
   }
 }
 
 my-global-icon {
-  &[iconName="playlists"] {
+  &[iconName=playlists] {
     height: 24px;
     width: 24px;
 
     margin-right: 16px;
   }
 
-  &[iconName="videos"] {
+  &[iconName=videos] {
     position: relative;
     right: -1px;
   }
 
-  &[iconName="channel"] {
+  &[iconName=channel] {
     margin-top: -2px;
   }
 
-  &[iconName="sign-out"] {
+  &[iconName='sign-out'] {
     position: relative;
     right: -2px;
     height: 20px;
index c65787779546aa4fca663bb0c9463d592bd91182..554c20ca9cc87ed0be1c031a0117125894d60d6f 100644 (file)
 .notification-inbox-popover,
 .notification-inbox-link a {
   @include apply-svg-color(#808080);
-  ::ng-deep {
-    svg {
-      transition: color .1s ease-in-out;
-    }
-  }
 
   transition: all .1s ease-in-out;
   border-radius: 25px;
   cursor: pointer;
 
-  &:hover, &:active {
-    background-color: rgba(255, 255, 255, 0.15);
+  ::ng-deep svg {
+    transition: color .1s ease-in-out;
+  }
+
+  &:hover,
+  &:active {
     @include apply-svg-color(#fff);
+
+    background-color: rgba(255, 255, 255, 0.15);
   }
 }
 
@@ -59,7 +60,7 @@
       font-size: 14px;
       font-family: $main-fonts;
       width: 400px;
-      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.30);
+      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
 
       .loader {
         display: flex;
@@ -80,7 +81,7 @@
           max-height: 500px;
         }
 
-        > my-user-notifications:nth-child(2) {
+        > my-user-notifications:nth-child(2) {
           overflow-y: auto;
           flex-grow: 1;
         }
           background: transparent;
         }
 
-        a, button {
+        a,
+        button {
           color: rgba(20, 20, 20, 0.5);
 
           &:hover:not(:disabled) {
   }
 }
 
-.notification-inbox-popover, .notification-inbox-link {
+.notification-inbox-popover,
+.notification-inbox-link {
   cursor: pointer;
   position: relative;
 
index 28d5dc49c74d6e3ece461dad9cc5449e9ab014e5..5e9e3dc5170de3366c0f5b1650a5721a36bf8824 100644 (file)
@@ -42,7 +42,7 @@ li {
   text-align: center;
   font-weight: 600;
   font-size: 18px;
-  margin: 20px 0 40px 0;
+  margin: 20px 0 40px;
 }
 
 .columns {
index 658d425374e17764ddbeda929eef9c0e1cbf2248..ca68de4b1c53713d85dfd081093b46b8b5d1c72a 100644 (file)
@@ -7,16 +7,16 @@
       <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
 
       <span class="col-9 moderation-expanded-text">
-        <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
+        <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
           class="chip"
         >
-          <my-account-avatar [account]="abuse.reporterAccount"></my-account-avatar>
+          <my-actor-avatar size="18" [account]="abuse.reporterAccount"></my-actor-avatar>
           <div>
             <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
           </div>
         </a>
 
-        <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
+        <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
           class="ml-auto text-muted abuse-details-links" i18n
         >
           {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
     <div class="d-flex" *ngIf="abuse.flaggedAccount">
       <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
       <span class="col-9 moderation-expanded-text">
-        <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
+        <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
           class="chip"
         >
-          <my-account-avatar [account]="abuse.flaggedAccount"></my-account-avatar>
+          <my-actor-avatar size="18" [account]="abuse.flaggedAccount"></my-actor-avatar>
           <div>
             <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
           </div>
         </a>
 
-        <a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
+        <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
           class="ml-auto text-muted abuse-details-links" i18n
         >
           {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@@ -53,7 +53,7 @@
     <div class="mt-3 d-flex">
       <span class="col-3 moderation-expanded-label">
         <ng-container i18n>Report</ng-container>
-        <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id  }" class="ml-1 text-muted">#{{ abuse.id }}</a>
+        <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id  }" class="ml-1 text-muted">#{{ abuse.id }}</a>
       </span>
       <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
     </div>
@@ -61,7 +61,7 @@
     <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
       <span class="col-3"></span>
       <span class="col-9">
-        <a *ngFor="let reason of getPredefinedReasons()"  [routerLink]="[ baseRoute ]"
+        <a *ngFor="let reason of getPredefinedReasons()"  [routerLink]="[ '.' ]"
           [queryParams]="{ 'search': 'tag:' + reason.id  }" class="chip rectangular bg-secondary text-light"
         >
           <div>{{ reason.label }}</div>
index e8ce7e678338a5c5182491cba0c4d5c6bf09ba11..14674c5f09c7e486f598c7ea2d1a655a0e1d43cf 100644 (file)
@@ -1,6 +1,5 @@
 import { Component, Input } from '@angular/core'
 import { durationToString } from '@app/helpers'
-import { Account } from '@app/shared/shared-main'
 import { AbusePredefinedReasonsString } from '@shared/models'
 import { ProcessedAbuse } from './processed-abuse.model'
 
@@ -12,7 +11,6 @@ import { ProcessedAbuse } from './processed-abuse.model'
 export class AbuseDetailsComponent {
   @Input() abuse: ProcessedAbuse
   @Input() isAdminView: boolean
-  @Input() baseRoute: string
 
   private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
 
index 29b51f09c20a4ba130d2fd0fbbd2326772f55e8d..b1c065c7a58edb467b28d6e0a1a51e5e705453cc 100644 (file)
@@ -1,6 +1,7 @@
 <p-table
-  [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
+  [value]="abuses" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
   (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
@@ -8,28 +9,7 @@
   <ng-template pTemplate="caption">
     <div class="caption">
       <div class="ml-auto">
-        <div class="input-group has-feedback has-clear">
-          <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
-            <div class="input-group-text" ngbDropdownToggle>
-              <span class="caret" aria-haspopup="menu" role="button"></span>
-            </div>
-
-            <div role="menu" ngbDropdownMenu>
-              <h6 class="dropdown-header" i18n>Advanced report filters</h6>
-              <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
-              <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
-              <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
-              <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
-              <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
-            </div>
-          </div>
-          <input
-            type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-            (keyup)="onSearch($event)"
-          >
-          <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
-          <span class="sr-only" i18n>Clear filters</span>
-        </div>
+        <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
@@ -65,7 +45,7 @@
       <td *ngIf="isAdminView()">
         <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="chip two-lines">
-            <my-account-avatar [account]="abuse.reporterAccount"></my-account-avatar>
+            <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar>
             <div>
               {{ abuse.reporterAccount.displayName }}
               <span>{{ abuse.reporterAccount.nameWithHost }}</span>
   <ng-template pTemplate="rowexpansion" let-abuse>
       <tr>
         <td class="expand-cell" colspan="8">
-          <my-abuse-details [abuse]="abuse" [baseRoute]="baseRoute" [isAdminView]="isAdminView()"></my-abuse-details>
+          <my-abuse-details [abuse]="abuse" [isAdminView]="isAdminView()"></my-abuse-details>
         </td>
       </tr>
   </ng-template>
index 8b5771237e30605109a1dd1ab21dbea76b6edbba..4dc2b4f10b65c3a0ce0afc9dfc05396537896e26 100644 (file)
@@ -3,7 +3,7 @@ import truncate from 'lodash-es/truncate'
 import { SortMeta } from 'primeng/api'
 import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 import { environment } from 'src/environments/environment'
-import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'
+import { Component, Input, OnInit, ViewChild } from '@angular/core'
 import { DomSanitizer } from '@angular/platform-browser'
 import { ActivatedRoute, Router } from '@angular/router'
 import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
@@ -11,6 +11,7 @@ import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared
 import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
 import { VideoCommentService } from '@app/shared/shared-video-comment'
 import { AbuseState, AdminAbuse } from '@shared/models'
+import { AdvancedInputFilter } from '../shared-forms'
 import { AbuseMessageModalComponent } from './abuse-message-modal.component'
 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
 import { ProcessedAbuse } from './processed-abuse.model'
@@ -22,9 +23,8 @@ const logger = debug('peertube:moderation:AbuseListTableComponent')
   templateUrl: './abuse-list-table.component.html',
   styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
 })
-export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
+export class AbuseListTableComponent extends RestTable implements OnInit {
   @Input() viewType: 'admin' | 'user'
-  @Input() baseRoute: string
 
   @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
   @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
@@ -36,6 +36,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
 
   abuseActions: DropdownAction<ProcessedAbuse>[][] = []
 
+  inputFilters: AdvancedInputFilter[] = [
+    {
+      queryParams: { 'search': 'state:pending' },
+      label: $localize`Unsolved reports`
+    },
+    {
+      queryParams: { 'search': 'state:accepted' },
+      label: $localize`Accepted reports`
+    },
+    {
+      queryParams: { 'search': 'state:rejected' },
+      label: $localize`Refused reports`
+    },
+    {
+      queryParams: { 'search': 'videoIs:blacklisted' },
+      label: $localize`Reports with blocked videos`
+    },
+    {
+      queryParams: { 'search': 'videoIs:deleted' },
+      label: $localize`Reports with deleted videos`
+    }
+  ]
+
   constructor (
     protected route: ActivatedRoute,
     protected router: Router,
@@ -66,11 +89,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
     ]
 
     this.initialize()
-    this.listenToSearchChange()
-  }
-
-  ngAfterViewInit () {
-    if (this.search) this.setTableFilter(this.search, false)
   }
 
   isAdminView () {
@@ -86,7 +104,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
   }
 
   onModerationCommentUpdated () {
-    this.loadData()
+    this.reloadData()
   }
 
   isAbuseAccepted (abuse: AdminAbuse) {
@@ -129,7 +147,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
     this.abuseService.removeAbuse(abuse).subscribe(
       () => {
         this.notifier.success($localize`Abuse deleted.`)
-        this.loadData()
+        this.reloadData()
       },
 
       err => this.notifier.error(err.message)
@@ -139,7 +157,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
   updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
     this.abuseService.updateAbuse(abuse, { state })
       .subscribe(
-        () => this.loadData(),
+        () => this.reloadData(),
 
         err => this.notifier.error(err.message)
       )
@@ -166,7 +184,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
     return Actor.IS_LOCAL(abuse.reporterAccount.host)
   }
 
-  protected loadData () {
+  protected reloadData () {
     logger('Loading data.')
 
     const options = {
index 19b6d456d145ea4007c69547e568918a8c5aa51c..8f3830a17211d9ff6cbe651106fed0b0cae94452 100644 (file)
@@ -10,7 +10,7 @@ import { AbuseDetailsComponent } from './abuse-details.component'
 import { AbuseListTableComponent } from './abuse-list-table.component'
 import { AbuseMessageModalComponent } from './abuse-message-modal.component'
 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
-import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -21,7 +21,7 @@ import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-accou
     SharedModerationModule,
     SharedGlobalIconModule,
     SharedVideoCommentModule,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
diff --git a/client/src/app/shared/shared-account-avatar/account-avatar.component.html b/client/src/app/shared/shared-account-avatar/account-avatar.component.html
deleted file mode 100644 (file)
index ca4ceb1..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-<ng-template #img>
-  <img [class]="class" [src]="avatarUrl" i18n-alt alt="Account avatar" />
-</ng-template>
-
-<a *ngIf="account && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
-  <ng-template *ngTemplateOutlet="img"></ng-template>
-</a>
-
-<a *ngIf="account && internalHref" [routerLink]="internalHref" [title]="title">
-  <ng-template *ngTemplateOutlet="img"></ng-template>
-</a>
-
-<ng-container *ngIf="!account || (!href && !internalHref)">
-  <ng-template *ngTemplateOutlet="img"></ng-template>
-</ng-container>
diff --git a/client/src/app/shared/shared-account-avatar/account-avatar.component.scss b/client/src/app/shared/shared-account-avatar/account-avatar.component.scss
deleted file mode 100644 (file)
index bb941d7..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-.avatar-25 {
-  @include avatar(25px);
-}
-
-.avatar-34 {
-  @include avatar(34px);
-}
-
-.avatar-36 {
-  @include avatar(36px);
-}
-
-.avatar-40 {
-  @include avatar(40px);
-}
-
-.avatar-120 {
-  @include avatar(120px);
-}
\ No newline at end of file
diff --git a/client/src/app/shared/shared-account-avatar/account-avatar.component.ts b/client/src/app/shared/shared-account-avatar/account-avatar.component.ts
deleted file mode 100644 (file)
index 02a0a18..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Component, Input } from '@angular/core'
-import { Account } from '../shared-main/account/account.model'
-
-@Component({
-  selector: 'my-account-avatar',
-  styleUrls: [ './account-avatar.component.scss' ],
-  templateUrl: './account-avatar.component.html'
-})
-export class AccountAvatarComponent {
-  @Input() account: {
-    name: string
-    avatar?: { url?: string, path: string }
-    url: string
-  }
-  @Input() size: '25' | '34' | '36' | '40' | '120' = '36'
-
-  // Use an external link
-  @Input() href: string
-  // Use routerLink
-  @Input() internalHref: string | string[]
-
-  @Input() set title (value) {
-    this._title = value
-  }
-
-  private _title: string
-
-  get title () {
-    return this._title || $localize`${this.account.name} (account page)`
-  }
-
-  get class () {
-    return `avatar avatar-${this.size}`
-  }
-
-  get avatarUrl () {
-    return Account.GET_ACTOR_AVATAR_URL(this.account)
-  }
-}
diff --git a/client/src/app/shared/shared-account-avatar/index.ts b/client/src/app/shared/shared-account-avatar/index.ts
deleted file mode 100644 (file)
index 40c742b..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './account-avatar.component'
-export * from './shared-account-avatar.module'
\ No newline at end of file
diff --git a/client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts b/client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts
deleted file mode 100644 (file)
index 17b2758..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-
-import { NgModule } from '@angular/core'
-import { SharedGlobalIconModule } from '../shared-icons'
-import { SharedMainModule } from '../shared-main/shared-main.module'
-import { AccountAvatarComponent } from './account-avatar.component'
-
-@NgModule({
-  imports: [
-    SharedMainModule,
-    SharedGlobalIconModule
-  ],
-
-  declarations: [
-    AccountAvatarComponent
-  ],
-
-  exports: [
-    AccountAvatarComponent
-  ],
-
-  providers: [ ]
-})
-export class SharedAccountAvatarModule { }
similarity index 93%
rename from client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
rename to client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
index 0829263f45e2e4af6a7d35288b92579755d5c4cf..e9c5fadcf5f8b485c46494dd0b9c413a49b4a0d3 100644 (file)
@@ -1,6 +1,6 @@
 <div class="actor" *ngIf="actor">
   <div class="d-flex">
-    <img [ngClass]="{ channel: isChannel() }" [src]="preview || actor.avatarUrl" alt="Avatar" />
+    <my-actor-avatar [channel]="getChannel()" [account]="getAccount()" [previewImage]="preview" size="100"></my-actor-avatar>
 
     <div class="actor-img-edit-container">
 
@@ -34,6 +34,7 @@
     <span for="avatarfile" i18n>Upload a new avatar</span>
     <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
   </div>
+
   <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
     <my-global-icon iconName="delete"></my-global-icon>
     <span i18n>Remove avatar</span>
similarity index 84%
rename from client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
rename to client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
index 8b0172315b6aa8a064d7cce5ecf68aa62c73456e..08e80c3b4f13a3cf037ebc54b14736c4156d7f28 100644 (file)
@@ -4,16 +4,8 @@
 .actor {
   display: flex;
 
-  img {
+  my-actor-avatar {
     margin-right: 15px;
-
-    &:not(.channel) {
-      @include avatar(100px);
-    }
-
-    &.channel {
-      @include channel-avatar(100px);
-    }
   }
 
   .actor-info {
similarity index 91%
rename from client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
rename to client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts
index d0d269489e0aa551608c5a95c93009f025c363df..840946690c8b80c3eddb1dda9a3987c31c8527d1 100644 (file)
@@ -80,4 +80,16 @@ export class ActorAvatarEditComponent implements OnInit {
   isChannel () {
     return !!(this.actor as VideoChannel).ownerAccount
   }
+
+  getChannel (): VideoChannel {
+    if (this.isChannel()) return this.actor as VideoChannel
+
+    return undefined
+  }
+
+  getAccount (): Account {
+    if (this.isChannel()) return undefined
+
+    return this.actor as Account
+  }
 }
diff --git a/client/src/app/shared/shared-actor-image-edit/index.ts b/client/src/app/shared/shared-actor-image-edit/index.ts
new file mode 100644 (file)
index 0000000..276b2e2
--- /dev/null
@@ -0,0 +1 @@
+export * from './shared-actor-image-edit.module'
diff --git a/client/src/app/shared/shared-actor-image-edit/shared-actor-image-edit.module.ts b/client/src/app/shared/shared-actor-image-edit/shared-actor-image-edit.module.ts
new file mode 100644 (file)
index 0000000..f6a397d
--- /dev/null
@@ -0,0 +1,31 @@
+
+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 { ActorAvatarEditComponent } from './actor-avatar-edit.component'
+import { ActorBannerEditComponent } from './actor-banner-edit.component'
+
+@NgModule({
+  imports: [
+    CommonModule,
+
+    SharedMainModule,
+    SharedActorImageModule,
+    SharedGlobalIconModule
+  ],
+
+  declarations: [
+    ActorAvatarEditComponent,
+    ActorBannerEditComponent
+  ],
+
+  exports: [
+    ActorAvatarEditComponent,
+    ActorBannerEditComponent
+  ],
+
+  providers: [ ]
+})
+export class SharedActorImageEditModule { }
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.html b/client/src/app/shared/shared-actor-image/actor-avatar.component.html
new file mode 100644 (file)
index 0000000..13a5385
--- /dev/null
@@ -0,0 +1,19 @@
+<ng-template #img>
+  <img *ngIf="previewImage || avatarUrl || !initial" [class]="getClass('avatar')" [src]="previewImage || avatarUrl || defaultAvatarUrl" [alt]="alt" />
+
+  <div *ngIf="!avatarUrl && initial" [class]="getClass('initial')">
+    <span>{{ initial }}</span>
+  </div>
+</ng-template>
+
+<a *ngIf="hasActor() && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
+  <ng-template *ngTemplateOutlet="img"></ng-template>
+</a>
+
+<a *ngIf="hasActor() && internalHref" [routerLink]="internalHref" [title]="title">
+  <ng-template *ngTemplateOutlet="img"></ng-template>
+</a>
+
+<ng-container *ngIf="!hasActor() || (!href && !internalHref)">
+  <ng-template *ngTemplateOutlet="img"></ng-template>
+</ng-container>
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar.component.scss
new file mode 100644 (file)
index 0000000..bf50de4
--- /dev/null
@@ -0,0 +1,101 @@
+@import '_variables';
+@import '_mixins';
+
+.avatar {
+  --avatarSize: 100%;
+  --initialFontSize: 22px;
+
+  width: var(--avatarSize);
+  height: var(--avatarSize);
+  min-width: var(--avatarSize);
+  min-height: var(--avatarSize);
+
+  &.account {
+    object-fit: cover;
+    border-radius: 50%;
+  }
+
+  &.channel {
+    border-radius: 5px;
+  }
+}
+
+.avatar-18 {
+  --avatarSize: 18px;
+  --initialFontSize: 13px;
+}
+
+.avatar-25 {
+  --avatarSize: 25px;
+}
+
+.avatar-32 {
+  --avatarSize: 32px;
+}
+
+.avatar-34 {
+  --avatarSize: 34px;
+}
+
+.avatar-36 {
+  --avatarSize: 36px;
+}
+
+.avatar-40 {
+  --avatarSize: 40px;
+}
+
+.avatar-100 {
+  --avatarSize: 100px;
+  --initialFontSize: 40px;
+}
+
+.avatar-120 {
+  --avatarSize: 120px;
+  --initialFontSize: 46px;
+}
+
+a:hover {
+  text-decoration: none;
+}
+
+.initial {
+  background-color: #3C2109;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: var(--initialFontSize);
+
+  &.blue {
+    background-color: #009FD4;
+  }
+
+  &.green {
+    background-color: #00AA55;
+  }
+
+  &.purple {
+    background-color: #B381B3;
+  }
+
+  &.gray {
+    background-color: #939393;
+  }
+
+  &.yellow {
+    background-color: #AA8F00;
+  }
+
+  &.orange {
+    background-color: #D47500;
+  }
+
+  &.red {
+    background-color: #E76E3C;
+  }
+
+  &.dark-blue {
+    background-color: #0A3055;
+  }
+}
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
new file mode 100644 (file)
index 0000000..b06c2ba
--- /dev/null
@@ -0,0 +1,111 @@
+import { Component, Input } from '@angular/core'
+import { SafeResourceUrl } from '@angular/platform-browser'
+import { VideoChannel } from '../shared-main'
+import { Account } from '../shared-main/account/account.model'
+
+type ActorInput = {
+  name: string
+  avatar?: { url?: string, path: string }
+  url: string
+}
+
+export type ActorAvatarSize = '18' | '25' | '32' | '34' | '36' | '40' | '100' | '120'
+
+@Component({
+  selector: 'my-actor-avatar',
+  styleUrls: [ './actor-avatar.component.scss' ],
+  templateUrl: './actor-avatar.component.html'
+})
+export class ActorAvatarComponent {
+  @Input() account: ActorInput
+  @Input() channel: ActorInput
+
+  @Input() previewImage: SafeResourceUrl
+
+  @Input() size: ActorAvatarSize
+
+  // Use an external link
+  @Input() href: string
+  // Use routerLink
+  @Input() internalHref: string | any[]
+
+  @Input() set title (value) {
+    this._title = value
+  }
+
+  private _title: string
+
+  get title () {
+    if (this._title) return this._title
+    if (this.account) return $localize`${this.account.name} (account page)`
+    if (this.channel) return $localize`${this.channel.name} (channel page)`
+
+    return ''
+  }
+
+  get alt () {
+    if (this.account) return $localize`Account avatar`
+    if (this.channel) return $localize`Channel avatar`
+
+    return ''
+  }
+
+  getClass (type: 'avatar' | 'initial') {
+    const base = [ 'avatar' ]
+
+    if (this.size) base.push(`avatar-${this.size}`)
+
+    if (this.channel) base.push('channel')
+    else base.push('account')
+
+    if (type === 'initial' && this.initial) {
+      base.push('initial')
+      base.push(this.getColorTheme())
+    }
+
+    return base
+  }
+
+  get defaultAvatarUrl () {
+    if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL()
+
+    return Account.GET_DEFAULT_AVATAR_URL()
+  }
+
+  get avatarUrl () {
+    if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account)
+    if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel)
+
+    return ''
+  }
+
+  get initial () {
+    const name = this.account?.name
+    if (!name) return ''
+
+    return name.slice(0, 1)
+  }
+
+  hasActor () {
+    return !!this.account || !!this.channel
+  }
+
+  private getColorTheme () {
+    // Keep consistency with CSS
+    const themes = {
+      abc: 'blue',
+      def: 'green',
+      ghi: 'purple',
+      jkl: 'gray',
+      mno: 'yellow',
+      pqr: 'orange',
+      stvu: 'red',
+      wxyz: 'dark-blue'
+    }
+
+    const theme = Object.keys(themes)
+                        .find(chars => chars.includes(this.initial))
+
+    return themes[theme]
+  }
+}
index 6044f99258f7edc4c7547fb5f0d3d739c87d9f9a..8ea4bb2bf4122883bfc25489e08a3ce8069a551a 100644 (file)
@@ -1,27 +1,21 @@
 
-import { CommonModule } from '@angular/common'
 import { NgModule } from '@angular/core'
 import { SharedGlobalIconModule } from '../shared-icons'
-import { SharedMainModule } from '../shared-main'
-import { ActorAvatarEditComponent } from './actor-avatar-edit.component'
-import { ActorBannerEditComponent } from './actor-banner-edit.component'
+import { SharedMainModule } from '../shared-main/shared-main.module'
+import { ActorAvatarComponent } from './actor-avatar.component'
 
 @NgModule({
   imports: [
-    CommonModule,
-
     SharedMainModule,
     SharedGlobalIconModule
   ],
 
   declarations: [
-    ActorAvatarEditComponent,
-    ActorBannerEditComponent
+    ActorAvatarComponent
   ],
 
   exports: [
-    ActorAvatarEditComponent,
-    ActorBannerEditComponent
+    ActorAvatarComponent
   ],
 
   providers: [ ]
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
new file mode 100644 (file)
index 0000000..10d1296
--- /dev/null
@@ -0,0 +1,24 @@
+<div class="input-group has-feedback has-clear">
+  <div *ngIf="hasFilters()" class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
+    <div class="input-group-text" ngbDropdownToggle>
+      <span class="caret" aria-haspopup="menu" role="button"></span>
+    </div>
+
+    <div role="menu" ngbDropdownMenu>
+      <h6 class="dropdown-header" i18n>Advanced filters</h6>
+
+      <a *ngFor="let filter of filters" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
+        {{ filter.label }}
+      </a>
+    </div>
+  </div>
+
+  <input
+    type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
+    [(ngModel)]="searchValue"
+    (keyup)="onInputSearch($event)"
+  >
+
+  <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetTableFilter()"></a>
+  <span class="sr-only" i18n>Clear filters</span>
+</div>
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
new file mode 100644 (file)
index 0000000..7c21989
--- /dev/null
@@ -0,0 +1,10 @@
+@import '_variables';
+@import '_mixins';
+
+input {
+  @include peertube-input-text(250px);
+}
+
+.input-group-text {
+  background-color: transparent;
+}
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts
new file mode 100644 (file)
index 0000000..c11f1ad
--- /dev/null
@@ -0,0 +1,116 @@
+import * as debug from 'debug'
+import { Subject } from 'rxjs'
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
+import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
+import { ActivatedRoute, Params, Router } from '@angular/router'
+
+export type AdvancedInputFilter = {
+  label: string
+  queryParams: Params
+}
+
+const logger = debug('peertube:AdvancedInputFilterComponent')
+
+@Component({
+  selector: 'my-advanced-input-filter',
+  templateUrl: './advanced-input-filter.component.html',
+  styleUrls: [ './advanced-input-filter.component.scss' ]
+})
+export class AdvancedInputFilterComponent implements OnInit, AfterViewInit {
+  @Input() filters: AdvancedInputFilter[] = []
+
+  @Output() search = new EventEmitter<string>()
+
+  searchValue: string
+
+  private searchStream: Subject<string>
+
+  private viewInitialized = false
+  private emitSearchAfterViewInit = false
+
+  constructor (
+    private route: ActivatedRoute,
+    private router: Router
+  ) { }
+
+  ngOnInit () {
+    this.initSearchStream()
+    this.listenToRouteSearchChange()
+  }
+
+  ngAfterViewInit () {
+    this.viewInitialized = true
+
+    // Init after view init to not send an event too early
+    if (this.emitSearchAfterViewInit) this.emitSearch()
+  }
+
+  onInputSearch (event: Event) {
+    this.scheduleSearchUpdate((event.target as HTMLInputElement).value)
+  }
+
+  onResetTableFilter () {
+    this.immediateSearchUpdate('')
+  }
+
+  hasFilters () {
+    return this.filters.length !== 0
+  }
+
+  private scheduleSearchUpdate (value: string) {
+    this.searchValue = value
+    this.searchStream.next(this.searchValue)
+  }
+
+  private immediateSearchUpdate (value: string) {
+    this.searchValue = value
+
+    this.setQueryParams(this.searchValue)
+    this.emitSearch()
+  }
+
+  private listenToRouteSearchChange () {
+    this.route.queryParams
+      .subscribe(params => {
+        const search = params.search || ''
+
+        logger('On route search change "%s".', search)
+
+        this.searchValue = search
+        this.emitSearch()
+      })
+  }
+
+  private initSearchStream () {
+    this.searchStream = new Subject()
+
+    this.searchStream
+      .pipe(
+        debounceTime(300),
+        distinctUntilChanged()
+      )
+      .subscribe(() => {
+        this.setQueryParams(this.searchValue)
+
+        this.emitSearch()
+      })
+  }
+
+  private emitSearch () {
+    if (!this.viewInitialized) {
+      this.emitSearchAfterViewInit = true
+      return
+    }
+
+    logger('On search "%s".', this.searchValue)
+
+    this.search.emit(this.searchValue)
+  }
+
+  private setQueryParams (search: string) {
+    const queryParams: Params = {}
+
+    if (search) Object.assign(queryParams, { search })
+    this.router.navigate([ ], { queryParams })
+  }
+}
index 1d859b99123fd499ce30b0ca8f7ddd3b469eb906..727416a4022cb3e3a971f6732f33952443fd322e 100644 (file)
@@ -1,12 +1,14 @@
-export * from './form-validator.service'
+export * from './advanced-input-filter.component'
 export * from './form-reactive'
-export * from './select'
-export * from './input-toggle-hidden.component'
+export * from './form-validator.service'
+export * from './form-validator.service'
 export * from './input-switch.component'
+export * from './input-toggle-hidden.component'
 export * from './markdown-textarea.component'
 export * from './peertube-checkbox.component'
 export * from './preview-upload.component'
 export * from './reactive-file.component'
+export * from './select'
+export * from './shared-form.module'
 export * from './textarea-autoresize.directive'
 export * from './timestamp-input.component'
-export * from './shared-form.module'
index c14950bd764ab15faa41f10f27549ec472231cd0..290a70db89f79d2f13af52110d65352a61730da0 100644 (file)
@@ -5,7 +5,7 @@ input {
   position: absolute;
   visibility: hidden;
 
-  + label {
+  + label {
     cursor: pointer;
     text-indent: -9999px;
     width: 35px;
@@ -16,7 +16,7 @@ input {
     position: relative;
     margin: 0;
 
-    &:after {
+    &::after {
       content: '';
       position: absolute;
       top: 3px;
@@ -28,7 +28,7 @@ input {
       transition: 0.3s ease-out;
     }
 
-    &:active:after {
+    &:active::after {
       width: 40px;
     }
   }
@@ -36,7 +36,7 @@ input {
   &:checked + label {
     background: pvar(--mainColor);
 
-    &:after {
+    &::after {
       left: calc(100% - 3px);
       transform: translateX(-100%);
     }
index 8203c7d1c46acd06011fe39ab44568d249c96953..1f72dbc322e6b7215508263235463193df3757f3 100644 (file)
@@ -18,7 +18,7 @@ $input-border-radius: 3px;
 
       font-family: monospace;
       font-size: 13px;
-      border-bottom: none;
+      border-bottom: 0;
       border-bottom-left-radius: unset;
       border-bottom-right-radius: unset;
     }
@@ -51,7 +51,8 @@ $input-border-radius: 3px;
             opacity: 0.6;
           }
 
-          &:hover, &:active {
+          &:hover,
+          &:active {
             svg {
               opacity: 1;
             }
@@ -105,6 +106,8 @@ $input-border-radius: 3px;
 }
 
 @mixin maximized-base {
+  $nav-preview-vertical-padding: 40px;
+
   flex-direction: row;
   z-index: #{z(header) - 1};
   position: fixed;
@@ -115,20 +118,18 @@ $input-border-radius: 3px;
   width: calc(100% - #{$menu-width});
   height: calc(100vh - #{$header-height}) !important;
 
-  $nav-preview-vertical-padding: 40px;
-
   .nav-preview {
     @include nav-preview-medium();
     padding-top: #{$nav-preview-vertical-padding / 2};
     padding-bottom: #{$nav-preview-vertical-padding / 2};
-    padding-left: 0px;
-    padding-right: 0px;
+    padding-left: 0;
+    padding-right: 0;
     position: absolute;
     background-color: pvar(--mainBackgroundColor);
     width: 100% !important;
-    border-top: none;
-    border-left: none;
-    border-right: none;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
 
     :last-child {
       margin-right: pvar(--horizontalMarginContent);
@@ -148,7 +149,7 @@ $input-border-radius: 3px;
     margin-top: #{$nav-preview-tab-height + $nav-preview-vertical-padding} !important;
     height: calc(100vh - #{$header-height + $nav-preview-tab-height + $nav-preview-vertical-padding}) !important;
     width: 50% !important;
-    border: none !important;
+    border: 0 !important;
     border-radius: unset !important;
   }
 
@@ -249,11 +250,11 @@ $input-border-radius: 3px;
 }
 
 @media only screen and (min-width: $small-view) {
+  @include maximized-in-medium-view();
+
   :host-context(.expanded) {
     @include in-medium-view();
   }
-
-  @include maximized-in-medium-view();
 }
 
 @media only screen and (min-width: #{$small-view + $menu-width}) {
index cf8540dc37c87a809a43aabe5f0f1430eb57e661..203b82d0b0dcf9e9e6715dd4f901e2a50ee9b7aa 100644 (file)
@@ -46,7 +46,7 @@
     line-height: 12px;
     font-weight: 500;
     color: pvar(--inputPlaceholderColor);
-    background-color: rgba(217,225,232,.1);
-    border: 1px solid rgba(217,225,232,.5);
+    background-color: rgba(217, 225, 232, .1);
+    border: 1px solid rgba(217, 225, 232, .5);
   }
-}
\ No newline at end of file
+}
index 88eccd5f7c8fd1f05110ae028edc31836eae9512..c2ee0d6a983ad5a9b13fc3da7e6cf00d5e26dc4b 100644 (file)
@@ -21,7 +21,7 @@
       max-width: 100%;
 
       &.no-image {
-        border: 2px solid grey;
+        border: 2px solid #808080;
         background-color: pvar(--mainBackgroundColor);
       }
     }
index 80196b8df4d31735c0fe57080ffc092bdc54999e..7006adab1336246aa5e9f6af6694dc6aed4aed87 100644 (file)
@@ -32,7 +32,7 @@ ng-select ::ng-deep {
 }
 
 .root {
-  display:flex;
+  display: flex;
   align-items: center;
 
   > my-select-options {
@@ -41,9 +41,9 @@ ng-select ::ng-deep {
 }
 
 my-select-options + input {
-  margin-left: 5px;
-
   @include peertube-input-text($form-base-input-width);
+
+  margin-left: 5px;
   display: block;
 }
 
index 9bdd138a147dc1ee0ff2114b24e03c1a0e8692a1..5417f7342546ca2188974ed276b8a9625b290fd1 100644 (file)
@@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 import { NgSelectModule } from '@ng-select/ng-select'
 import { SharedGlobalIconModule } from '../shared-icons'
 import { SharedMainModule } from '../shared-main/shared-main.module'
+import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
 import { DynamicFormFieldComponent } from './dynamic-form-field.component'
 import { FormValidatorService } from './form-validator.service'
 import { InputSwitchComponent } from './input-switch.component'
@@ -52,7 +53,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
     SelectCheckboxComponent,
     SelectCustomValueComponent,
 
-    DynamicFormFieldComponent
+    DynamicFormFieldComponent,
+
+    AdvancedInputFilterComponent
   ],
 
   exports: [
@@ -78,7 +81,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
     SelectCheckboxComponent,
     SelectCustomValueComponent,
 
-    DynamicFormFieldComponent
+    DynamicFormFieldComponent,
+
+    AdvancedInputFilterComponent
   ],
 
   providers: [
index 66e9aa032409c9360affac1396fa1fa6f53d3233..36f5711a61fe2db11f926668514cee4178bb3821 100644 (file)
@@ -4,8 +4,7 @@ p-inputmask {
   ::ng-deep input {
     width: 80px;
     font-size: 15px;
-
-    border: none;
+    border: 0;
 
     &:focus-within,
     &:focus {
index 2f6b420e34352ec6ba8a0d518f54ac280c989be6..615e08bcc97b82f3769eeba41920206f3e2af7cb 100644 (file)
@@ -1,6 +1,6 @@
 @import '_variables';
 @import '_mixins';
-@import "./_bootstrap-variables";
+@import './_bootstrap-variables';
 
 @import '~bootstrap/scss/functions';
 @import '~bootstrap/scss/variables';
@@ -30,7 +30,7 @@ ngb-accordion ::ng-deep {
       background-color: unset;
       padding: 0;
 
-      + .collapse.show {
+      + .collapse.show {
         background-color: var(--submenuBackgroundColor);
       }
     }
index d17e91fc2c105cc9370e3f82b636e0c2d81fb0fc..11cf1161642e63d3d540a51ba9d124eb742a72be 100644 (file)
@@ -19,7 +19,7 @@ table {
     .more-info {
       font-style: italic;
       font-weight: initial;
-      font-size: 14px
+      font-size: 14px;
     }
   }
 
index 65e6798d44decf2badb892bb3d7e3aa6f14fef35..6d9f0ee655eac7dc95b76849e5c73ebeb064d77d 100644 (file)
@@ -14,7 +14,7 @@ export class Account extends Actor implements ServerAccount {
   userId?: number
 
   static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
-    return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
+    return Actor.GET_ACTOR_AVATAR_URL(actor)
   }
 
   static GET_DEFAULT_AVATAR_URL () {
@@ -24,8 +24,6 @@ export class Account extends Actor implements ServerAccount {
   constructor (hash: ServerAccount) {
     super(hash)
 
-    this.updateComputedAttributes()
-
     this.displayName = hash.displayName
     this.description = hash.description
     this.userId = hash.userId
@@ -40,16 +38,9 @@ export class Account extends Actor implements ServerAccount {
 
   updateAvatar (newAvatar: ActorImage) {
     this.avatar = newAvatar
-
-    this.updateComputedAttributes()
   }
 
   resetAvatar () {
     this.avatar = null
-    this.avatarUrl = Account.GET_DEFAULT_AVATAR_URL()
-  }
-
-  private updateComputedAttributes () {
-    this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this)
   }
 }
index 4b036341f69e06c02184d664200169828f34950e..6ba0bb09ed7b741c18afc8df7ff437074100f4d7 100644 (file)
@@ -15,7 +15,6 @@ export abstract class Actor implements ServerActor {
   updatedAt: Date | string
 
   avatar: ActorImage
-  avatarUrl: string
 
   isLocal: boolean
 
index 724a04efcf1d1260c6fad1d143e907eb413d80ab..b9a4d46dc5c6cc0e39488e9cd1cb55a146f4327b 100644 (file)
@@ -8,6 +8,9 @@
 .action-button {
   @include peertube-button;
 
+  display: inline-block;
+  padding: 0 10px;
+
   &.button-styled {
 
     &.grey {
       @include orange-button;
     }
 
-    &:hover, &:active, &:focus {
+    &:hover,
+    &:active,
+    &:focus {
       background-color: $grey-background-color;
     }
   }
 
-  display: inline-block;
-  padding: 0 10px;
-
   &::after {
     display: none;
   }
@@ -64,7 +66,8 @@
       @include dropdown-with-icon-item;
     }
 
-    a, span {
+    a,
+    span {
       display: block;
       width: 100%;
     }
index f73b7b8087ad903809fd02ce00edbafd243e0594..09b5f95d7cfdfb0b9b4880f60e3877a40735cc19 100644 (file)
@@ -1,6 +1,16 @@
 @import '_variables';
 @import '_mixins';
 
+@mixin responsive-label {
+  .action-button {
+    padding: 0 13px;
+  }
+
+  .button-label {
+    display: none;
+  }
+}
+
 :host {
   outline: none;
 }
@@ -46,12 +56,12 @@ span[class$=-button] {
 // In a table, try to minimize the space taken by this button
 @media screen and (max-width: 1400px) {
   :host-context(td) {
-    .action-button {
-      padding: 0 13px;
-    }
+    @include responsive-label;
+  }
+}
 
-    .button-label {
-      display: none;
-    }
+@media screen and (max-width: $small-view) {
+  .responsive-label {
+    @include responsive-label;
   }
 }
index 1d2be0bf92060c6d398f542e59accb4362a63ceb..ee74b3d128cfbb54281fd3badd93e8eea3733863 100644 (file)
@@ -3,7 +3,7 @@ import { GlobalIconName } from '@app/shared/shared-icons'
 
 @Component({
   selector: 'my-button',
-  styleUrls: ['./button.component.scss'],
+  styleUrls: [ './button.component.scss' ],
   templateUrl: './button.component.html'
 })
 
@@ -14,6 +14,7 @@ export class ButtonComponent {
   @Input() title: string = undefined
   @Input() loading = false
   @Input() disabled = false
+  @Input() responsiveLabel = false
 
   getTitle () {
     return this.title || this.label
@@ -22,7 +23,8 @@ export class ButtonComponent {
   getClasses () {
     return {
       [this.className]: true,
-      disabled: this.disabled
+      disabled: this.disabled,
+      'responsive-label': this.responsiveLabel
     }
   }
 }
index c94d8d0c9c8c2ef5f561e5cfe8eabce91f3adb6d..d7a6702a7afa320c29b8be3a7119ffc82d198fca 100644 (file)
@@ -1,4 +1,7 @@
-<span class="action-button action-button-delete grey-button" [ngbTooltip]="title" role="button" tabindex="0">
+<span
+  class="action-button action-button-delete grey-button"
+  [ngClass]="{ 'responsive-label': responsiveLabel }" [ngbTooltip]="title" role="button" tabindex="0"
+>
   <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
 
   <span class="button-label" *ngIf="label">{{ label }}</span>
index 18995422a4cfc425fac044744c99abf4fd0fac54..c091f5309e40801c62a38354bb9346f33048b09f 100644 (file)
@@ -9,6 +9,7 @@ import { Component, Input, OnInit } from '@angular/core'
 export class DeleteButtonComponent implements OnInit {
   @Input() label: string
   @Input() title: string
+  @Input() responsiveLabel = false
 
   ngOnInit () {
     // <my-delete-button /> No label
index ecb709be1c8d88045d878c022851c567a9b144a4..8beeee6c40bc3d22ae252a348fc67a4953d6f78b 100644 (file)
@@ -1,4 +1,7 @@
-<a class="action-button action-button-edit grey-button" [routerLink]="routerLink" [ngbTooltip]="title">
+<a
+  class="action-button action-button-edit grey-button"
+  [ngClass]="{ 'responsive-label': responsiveLabel }" [routerLink]="routerLink" [ngbTooltip]="title"
+>
   <my-global-icon iconName="edit" aria-hidden="true"></my-global-icon>
 
   <span class="button-label" *ngIf="label">{{ label }}</span>
index 4b76551ca93c84b73aeafdf9669b7713fcd1eb60..24c8625ffa30d68cd1aa63d693f430fd78de335f 100644 (file)
@@ -5,11 +5,11 @@ import { Component, Input, OnInit } from '@angular/core'
   styleUrls: [ './button.component.scss' ],
   templateUrl: './edit-button.component.html'
 })
-
 export class EditButtonComponent implements OnInit {
   @Input() label: string
   @Input() title: string
   @Input() routerLink: string[] | string = []
+  @Input() responsiveLabel = false
 
   ngOnInit () {
     // <my-edit-button /> No label
index 86700d1d408a1ea434084a2ab027997baca74f4c..b87f7c4750a5316905ddc0da3f7d9b56d3c144b7 100644 (file)
@@ -1,5 +1,5 @@
 .date-toggle {
   &:hover {
-    cursor: default
+    cursor: default;
   }
 }
index b655ee7083461a4d642646a275e1cd53831b505d..d39f31d7070ee76611ceee47ee05000d50a7b8f1 100644 (file)
@@ -5,14 +5,14 @@
   width: 100%;
 
   a {
-    color: black;
+    color: #000;
     display: block;
   }
 }
 
 my-global-icon {
+  @include apply-svg-color(pvar(--mainForegroundColor));
+
   cursor: pointer;
   width: 100%;
-
-  @include apply-svg-color(pvar(--mainForegroundColor))
 }
index ffac9c707bc533a1b827cac28ae7feac740c9ab6..64138afe45b7709f1fa9f52335269d881e4c3068 100644 (file)
@@ -20,7 +20,7 @@
   border: 4px solid;
   border-radius: 50%;
   animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
-  border-color: #999999 transparent transparent transparent;
+  border-color: #999999 transparent transparent;
 }
 
 .loader div:nth-child(1) {
index ccc91ffab448f55a3991cd0622d9daa65fc838d0..68d7ad48f7e27ef084c2e83c88c5dea85323a8fa 100644 (file)
@@ -2,20 +2,19 @@
 @import '_mixins';
 
 .help-tooltip-button {
-  cursor: pointer;
-  border: none;
+  @include disable-outline;
 
+  cursor: pointer;
+  border: 0;
   margin: 5px;
 
   my-global-icon {
+    @include apply-svg-color(pvar(--greyForegroundColor));
+
     width: 17px;
     position: relative;
     top: -1px;
-
-    @include apply-svg-color(pvar(--greyForegroundColor))
   }
-
-  @include disable-outline;
 }
 
 ::ng-deep {
index 986572801ca909f3f5aeff54d845e7a0ae3cb22a..b2e0982f1345233ffe7658f77b4d3bd8094d4fac 100644 (file)
@@ -2,19 +2,19 @@
   <span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id">
     <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
   </span>
-  
+
   <ng-container *ngIf="isMenuDisplayed()">
     <button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()">
       <span class="glyphicon glyphicon-chevron-down"></span>
     </button>
-  
+
     <div *ngIf="!isInMobileView" class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)">
-      <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ routeActive: active }"
+      <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ 'route-active': active }"
         ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button"
       >
         <span class="glyphicon glyphicon-chevron-down"></span>
       </button>
-  
+
       <div ngbDropdownMenu>
         <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length"
           [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item">
index 1ec044489a087ff384023f551a675183c6577607..7e31d3850652f1145e64c1363b413c890643cce3 100644 (file)
 
 button {
   width: 30px;
-  border: none;
+  border: 0;
 
   &::after {
     display: none;
   }
 
-  &.routeActive {
+  &.route-active {
     &::after {
       display: inherit;
       border: 2px solid pvar(--mainColor);
@@ -36,7 +36,7 @@ button {
   margin-top: 0 !important;
   position: static;
   right: auto;
-  bottom: auto
+  bottom: auto;
 }
 
 .modal-body {
index 84dd7dce3375ae96db321147a8d2ff80c350c64a..ffabb364651db629fe247cbf0cf48146511a6ee2 100644 (file)
   }
 }
 
-::ng-deep .dropdown-toggle::after {
+.sub-menu ::ng-deep .dropdown-toggle::after {
   position: relative;
   top: 2px;
 }
 
-::ng-deep .dropdown-menu {
+.sub-menu ::ng-deep .dropdown-menu {
   margin-top: 0 !important;
 }
 
index 88a4811da0681ec350e267bd1a789f3dcc3ccd1e..ed5791794d1a05e1c88a6207e2bc3b5c0def2fc5 100644 (file)
@@ -258,10 +258,10 @@ export class UserNotification implements UserNotificationServer {
   }
 
   private setAccountAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
-    actor.avatarUrl = Account.GET_ACTOR_AVATAR_URL(actor)
+    actor.avatarUrl = Account.GET_ACTOR_AVATAR_URL(actor) || Account.GET_DEFAULT_AVATAR_URL()
   }
 
   private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
-    actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor)
+    actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor) || VideoChannel.GET_DEFAULT_AVATAR_URL()
   }
 }
index 5166bd559d7f19fedadbb889960b18652a8fc615..b69d4b5d6c51909dad50711a4f37e9d5ab4fe023 100644 (file)
   }
 
   my-global-icon {
+    @include apply-svg-color(#333);
+
     width: 24px;
     margin-right: 11px;
     margin-left: 3px;
-
-    @include apply-svg-color(#333);
   }
 
   .avatar {
-    @include avatar(30px);
-
+    width: 30px;
+    height: 30px;
+    min-width: 30px;
+    min-height: 30px;
+    border-radius: 5px;
     margin-right: 10px;
   }
 
index c670559d33a5abf0e097bcefe52ef8ae7c3fa81d..c06cafe2929ae47db6e3b64eb9e5f8bea876f542 100644 (file)
@@ -11,7 +11,8 @@ label {
     margin-right: 5px;
   }
 
-  &, .progress {
+  &,
+  .progress {
     width: 100% !important;
   }
 
index 1ba3fcc0e651bac6de069ade7b712443c0b872fc..c40dd53119e1278bf8f008bf15d772d0ddb9a8ec 100644 (file)
@@ -18,14 +18,13 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
 
   ownerAccount?: ServerAccount
   ownerBy?: string
-  ownerAvatarUrl?: string
 
   videosCount?: number
 
   viewsPerDay?: ViewsPerDate[]
 
   static GET_ACTOR_AVATAR_URL (actor: object) {
-    return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
+    return Actor.GET_ACTOR_AVATAR_URL(actor)
   }
 
   static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
@@ -67,7 +66,6 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
     if (hash.ownerAccount) {
       this.ownerAccount = hash.ownerAccount
       this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
-      this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
     }
 
     this.updateComputedAttributes()
@@ -94,7 +92,6 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
   }
 
   updateComputedAttributes () {
-    this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
     this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
   }
 }
index 14c507295abe9c45a900f574c8ae82fa6b4dd31f..526d10e32faae2c3c62430f0ca2e37666d18d1ab 100644 (file)
@@ -20,8 +20,6 @@ export class Video implements VideoServerModel {
   byVideoChannel: string
   byAccount: string
 
-  videoChannelAvatarUrl: string
-
   createdAt: Date
   updatedAt: Date
   publishedAt: Date
@@ -143,7 +141,6 @@ export class Video implements VideoServerModel {
 
     this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
     this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host)
-    this.videoChannelAvatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this.channel)
 
     this.category.label = peertubeTranslate(this.category.label, translations)
     this.licence.label = peertubeTranslate(this.licence.label, translations)
index 0b708b692c358f3bdfbc168a0c4c2495f0f01586..7b17bd2ab4b0c3c1e0aa2f489be5682f5c0306dd 100644 (file)
@@ -124,7 +124,17 @@ export class VideoService implements VideosProvider {
 
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
-    params = this.restService.addObjectParams(params, { search })
+
+    if (search) {
+      const filters = this.restService.parseQueryStringFilter(search, {
+        isLive: {
+          prefix: 'isLive:',
+          isBoolean: true
+        }
+      })
+
+      params = this.restService.addObjectParams(params, filters)
+    }
 
     return this.authHttp
                .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
index 3f2f5555945d7f6c6a3187ec9e617c22d232390c..a9fac08102aa6ed1faa80c2308f538748cc5150f 100644 (file)
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto has-feedback has-clear">
-        <input
-          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-          (keyup)="onSearch($event)"
-        >
-        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-        <span class="sr-only" i18n>Clear filters</span>
+      <div class="ml-auto">
+        <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
@@ -38,7 +33,7 @@
       <td>
         <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="chip two-lines">
-            <my-account-avatar [account]="accountBlock.blockedAccount"></my-account-avatar>
+            <my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar>
             <div>
               {{ accountBlock.blockedAccount.displayName }}
               <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
index 3eede44eb72b103524af921aa4bfdf97afa7ee13..bc441811e3a25e11273106f0f8042aab27d7b44a 100644 (file)
@@ -1,15 +1,6 @@
 @import '_variables';
 @import '_mixins';
 
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-    flex-grow: 1;
-  }
-}
-
 .chip {
   @include chip;
 }
@@ -17,4 +8,4 @@
 .unblock-button {
   @include peertube-button;
   @include grey-button;
-}
\ No newline at end of file
+}
index 1bce65bf02722b46acd97908d78ef90f7de93161..1146aeec0b77326e3ae217089d3781b11076e527 100644 (file)
@@ -44,12 +44,12 @@ export class GenericAccountBlocklistComponent extends RestTable implements OnIni
             : $localize`Account ${blockedAccount.nameWithHost} unmuted by your instance.`
         )
 
-        this.loadData()
+        this.reloadData()
       }
     )
   }
 
-  protected loadData () {
+  protected reloadData () {
     const operation = this.mode === BlocklistComponentType.Account
       ? this.blocklistService.getUserAccountBlocklist({
         pagination: this.pagination,
index cdcc12fe072d60d197181ab51024b60afdb8cc1a..b13d06f03d50790ec55b5eb451617fcc436d910d 100644 (file)
     word-wrap: break-word;
 
     ::ng-deep p:last-child {
-      margin-bottom: 0px !important;
+      margin-bottom: 0 !important;
     }
   }
 }
 
 .screenratio {
-  div {
-    @include miniature-thumbnail;
-
-    display: inline-flex;
-    justify-content: center;
-    align-items: center;
-    color: pvar(--inputPlaceholderColor);
-  }
-
   @include block-ratio($selector: 'div, ::ng-deep iframe') {
     width: 100% !important;
     height: 100% !important;
     left: 0;
   };
-}
 
-.input-group {
-  @include peertube-input-group(300px);
+  div {
+    @include miniature-thumbnail;
 
-  .dropdown-toggle::after {
-    margin-left: 0;
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    color: pvar(--inputPlaceholderColor);
   }
 }
 
   @include chip;
 }
 
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-    flex-grow: 1;
-  }
-}
-
 my-action-dropdown.show {
   ::ng-deep .dropdown-root {
     display: block !important;
@@ -93,15 +76,15 @@ my-action-dropdown.show {
   display: inline-flex;
 
   .table-video-image {
-    @include miniature-thumbnail;
-
     $image-height: 45px;
 
+    @include miniature-thumbnail;
+
     height: $image-height;
     width: #{(16/9) * $image-height};
     margin-right: 0.5rem;
     border-radius: 2px;
-    border: none;
+    border: 0;
     background: transparent;
     display: inline-flex;
     justify-content: center;
@@ -139,7 +122,7 @@ my-action-dropdown.show {
 
     div .glyphicon {
       font-size: 80%;
-      color: gray;
+      color: #808080;
       margin-left: 0.1rem;
     }
 
index 537186f05d92de5bef7ae484bfbdd34b0d9872b1..c6d29bb21946dfbc2066ab108855554019fc3e62 100644 (file)
@@ -4,8 +4,9 @@
 </h1>
 
 <p-table
-  [value]="blockedServers" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
+  [value]="blockedServers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onPage)="onPage($event)"
+  [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted instances"
 >
         </a>
       </div>
 
-      <div class="ml-auto has-feedback has-clear">
-        <input
-          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-          (keyup)="onSearch($event)"
-        >
-        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-        <span class="sr-only" i18n>Clear filters</span>
+      <div class="ml-auto">
+        <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
     </div>
   </ng-template>
index 31db4d92ba41c5e66d1971ce44561568db16b30a..a22972c5f46c750f16ee59aa5bdd38418649a733 100644 (file)
@@ -5,7 +5,8 @@ a {
   @include disable-default-a-behaviour;
   display: inline-block;
 
-  &, &:hover {
+  &,
+  &:hover {
     color: pvar(--mainForegroundColor);
   }
 
@@ -15,15 +16,6 @@ a {
   }
 }
 
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-    flex-grow: 1;
-  }
-}
-
 .unblock-button {
   @include peertube-button;
   @include grey-button;
@@ -33,15 +25,6 @@ a {
   @include create-button;
 }
 
-.caption {
-  justify-content: flex-end;
-
-  input {
-    @include peertube-input-text(250px);
-    flex-grow: 1;
-  }
-}
-
 .chip {
   @include chip;
 }
index 546fd53c395eebf6ee913d2eb9e1491357fc5931..274d8f6e9cf4eb2cd1632ffd912aa24ba45a6f8a 100644 (file)
@@ -46,7 +46,7 @@ export class GenericServerBlocklistComponent extends RestTable implements OnInit
             : $localize`Instance ${host} unmuted by your instance.`
         )
 
-        this.loadData()
+        this.reloadData()
       }
     )
   }
@@ -69,13 +69,13 @@ export class GenericServerBlocklistComponent extends RestTable implements OnInit
               : $localize`Instance ${domain} muted by your instance.`
           )
 
-          this.loadData()
+          this.reloadData()
         }
       )
     })
   }
 
-  protected loadData () {
+  protected reloadData () {
     const operation = this.mode === BlocklistComponentType.Account
       ? this.blocklistService.getUserServerBlocklist({
         pagination: this.pagination,
index c7e201792c419fc461857b09556561eedefc4ff3..95213e2bd969376308dadbe607d80906fc8ff53f 100644 (file)
@@ -13,7 +13,7 @@ import { UserBanModalComponent } from './user-ban-modal.component'
 import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
 import { VideoBlockComponent } from './video-block.component'
 import { VideoBlockService } from './video-block.service'
-import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -21,7 +21,7 @@ import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-accou
     SharedFormModule,
     SharedGlobalIconModule,
     SharedVideoCommentModule,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
index afa0d96f796c8d0878fde6af0035455ccae5c891..a6e33070bc5c68296044bdc828cfd449367f2cc4 100644 (file)
@@ -7,5 +7,5 @@ textarea {
 
 .live-info {
   font-size: 15px;
-  margin: 40px 0 20px 0;
+  margin: 40px 0 20px;
 }
index 30badc8faf20ef52a07e5ecc23965b250df5fa70..0e3924841a38fa182c2cbad8b1ba61505d947dec 100644 (file)
@@ -1,4 +1,4 @@
-import { NSFWQuery, SearchTargetType } from '@shared/models'
+import { BooleanBothQuery, SearchTargetType } from '@shared/models'
 
 export class AdvancedSearch {
   startDate: string // ISO 8601
@@ -7,7 +7,7 @@ export class AdvancedSearch {
   originallyPublishedStartDate: string // ISO 8601
   originallyPublishedEndDate: string // ISO 8601
 
-  nsfw: NSFWQuery
+  nsfw: BooleanBothQuery
 
   categoryOneOf: string
 
@@ -33,7 +33,7 @@ export class AdvancedSearch {
     endDate?: string
     originallyPublishedStartDate?: string
     originallyPublishedEndDate?: string
-    nsfw?: NSFWQuery
+    nsfw?: BooleanBothQuery
     categoryOneOf?: string
     licenceOneOf?: string
     languageOneOf?: string
index ea59ab346b155193a287fdae239b37dd3578a054..e678d6edfdfa531601521bc8bd6e37159942abdd 100644 (file)
@@ -11,7 +11,7 @@
   width: 100%;
   position: absolute;
   bottom: 0;
-  background-color: rgba(0, 0, 0, 0.20);
+  background-color: rgba(0, 0, 0, 0.2);
 
   div {
     height: 100%;
@@ -39,8 +39,8 @@
   top: 5px;
   font-weight: $font-bold;
 
-  &.warning { background-color: orange; }
-  &.danger { background-color: red; }
+  &.warning { background-color: #ffa500; }
+  &.danger { background-color: #ff0000; }
 }
 
 .video-thumbnail-duration-overlay,
@@ -77,9 +77,9 @@
   padding: 3px;
 
   my-global-icon {
+    @include apply-svg-color(#fff);
+
     width: 22px;
     height: 22px;
-
-    @include apply-svg-color(#fff);
   }
 }
index aa261fdce49de51f2a9aadf3cd4e9d12136b4907..a49e114858f95e20d7a62ac863b44fa8db782f4c 100644 (file)
@@ -5,7 +5,7 @@
     <my-help>
       <ng-template ptTemplate="customHtml">
         <ng-container i18n>
-          With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
+          With <strong>Hide</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
         </ng-container>
       </ng-template>
     </my-help>
@@ -13,7 +13,7 @@
     <div class="peertube-select-container">
       <select id="nsfwPolicy" formControlName="nsfwPolicy" class="form-control">
         <option i18n value="undefined" disabled>Policy for sensitive videos</option>
-        <option i18n value="do_not_list">Do not list</option>
+        <option i18n value="do_not_list">Hide</option>
         <option i18n value="blur">Blur thumbnails</option>
         <option i18n value="display">Display</option>
       </select>
index d74c2b2d827cef6149af754b32561e8bfe1dcf72..ae95030c7543a5f0cd01d92f89e006525658df46 100644 (file)
@@ -38,8 +38,6 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
   ngOnInit () {
     this.allLanguagesGroup = $localize`All languages`
 
-    let oldForm: any
-
     this.buildForm({
       nsfwPolicy: null,
       webTorrentEnabled: null,
@@ -73,16 +71,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
         videoLanguages
       })
 
-      if (this.reactiveUpdate) {
-        oldForm = { ...this.form.value }
-
-        this.formValuesWatcher = this.form.valueChanges.subscribe((formValue: any) => {
-          const updatedKey = Object.keys(formValue).find(k => formValue[k] !== oldForm[k])
-          oldForm = { ...this.form.value }
-
-          this.updateDetails([ updatedKey ])
-        })
-      }
+      if (this.reactiveUpdate) this.handleReactiveUpdate()
     })
   }
 
@@ -96,7 +85,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
     const autoPlayVideo = this.form.value['autoPlayVideo']
     const autoPlayNextVideo = this.form.value['autoPlayNextVideo']
 
-    const videoLanguagesForm = this.form.value['videoLanguages']
+    let videoLanguagesForm = this.form.value['videoLanguages']
 
     if (Array.isArray(videoLanguagesForm)) {
       if (videoLanguagesForm.length > 20) {
@@ -104,13 +93,14 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
         return
       }
 
+      // Automatically use "All languages" if the user did not select any language
       if (videoLanguagesForm.length === 0) {
-        this.notifier.error($localize`You need to enable at least 1 video language.`)
-        return
+        videoLanguagesForm = [ this.allLanguagesGroup ]
+        this.form.patchValue({ videoLanguages: [ { group: this.allLanguagesGroup } ] })
       }
     }
 
-    const videoLanguages = this.getVideoLanguages(videoLanguagesForm)
+    const videoLanguages = this.buildLanguagesFromForm(videoLanguagesForm)
 
     let details: UserUpdateMe = {
       nsfwPolicy,
@@ -127,22 +117,13 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
     if (onlyKeys) details = pick(details, onlyKeys)
 
     if (this.authService.isLoggedIn()) {
-      this.userService.updateMyProfile(details).subscribe(
-        () => {
-          this.authService.refreshUserInformation()
-
-          if (this.notifyOnUpdate) this.notifier.success($localize`Video settings updated.`)
-        },
-
-        err => this.notifier.error(err.message)
-      )
-    } else {
-      this.userService.updateMyAnonymousProfile(details)
-      if (this.notifyOnUpdate) this.notifier.success($localize`Display/Video settings updated.`)
+      return this.updateLoggedProfile(details)
     }
+
+    return this.updateAnonymousProfile(details)
   }
 
-  private getVideoLanguages (videoLanguages: ItemSelectCheckboxValue[]) {
+  private buildLanguagesFromForm (videoLanguages: ItemSelectCheckboxValue[]) {
     if (!Array.isArray(videoLanguages)) return undefined
 
     // null means "All"
@@ -166,4 +147,34 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
       return l.id + ''
     })
   }
+
+  private handleReactiveUpdate () {
+    let oldForm = { ...this.form.value }
+
+    this.formValuesWatcher = this.form.valueChanges.subscribe((formValue: any) => {
+      const updatedKey = Object.keys(formValue)
+                               .find(k => formValue[k] !== oldForm[k])
+
+      oldForm = { ...this.form.value }
+
+      this.updateDetails([ updatedKey ])
+    })
+  }
+
+  private updateLoggedProfile (details: UserUpdateMe) {
+    this.userService.updateMyProfile(details).subscribe(
+      () => {
+        this.authService.refreshUserInformation()
+
+        if (this.notifyOnUpdate) this.notifier.success($localize`Video settings updated.`)
+      },
+
+      err => this.notifier.error(err.message)
+    )
+  }
+
+  private updateAnonymousProfile (details: UserUpdateMe) {
+    this.userService.updateMyAnonymousProfile(details)
+    if (this.notifyOnUpdate) this.notifier.success($localize`Display/Video settings updated.`)
+  }
 }
index 698c5866abdb1ae2ccf18c9ae6a30b684a4f5e06..73db0d090ddaed33d4ae1284c8938a5765ac1082 100644 (file)
@@ -3,4 +3,4 @@
 .btn-remote-follow {
   @include peertube-button;
   @include orange-button;
-}
\ No newline at end of file
+}
index f6cdc11c002cb92378a7c11b6bb4c053fec46376..897ee7799ff872066a8e5a07ddea46cd1a958c6f 100644 (file)
@@ -8,8 +8,8 @@
   float: right;
   padding: 0;
 
-  > .btn,
-  > .dropdown > .dropdown-toggle {
+  > .btn,
+  > .dropdown > .dropdown-toggle {
     font-size: 15px;
   }
 
@@ -20,7 +20,7 @@
   &.big {
     height: 35px;
 
-    > button:first-child {
+    > button:first-child {
       width: max-content;
       min-width: 175px;
     }
@@ -29,7 +29,7 @@
       span:first-child {
         line-height: 80%;
       }
-    
+
       span:not(:first-child) {
         font-size: 75%;
       }
   }
 
   // Unlogged
-  > .dropdown > .dropdown-toggle span {
+  > .dropdown > .dropdown-toggle span {
     padding-right: 3px;
   }
 
   // Logged
-  > .btn {
+  > .btn {
     padding-right: 4px;
 
-    + .dropdown > button {
+    + .dropdown > button {
       padding-left: 2px;
 
       &::after {
index 0f09778df5be694cde3e688186a49b05016f90fa..c5aeb3c12e86ec24e6df9b46b29d172e1adb3f21 100644 (file)
@@ -190,13 +190,7 @@ export class VideoCommentService {
     const filters = this.restService.parseQueryStringFilter(search, {
       isLocal: {
         prefix: 'local:',
-        isBoolean: true,
-        handler: v => {
-          if (v === 'true') return v
-          if (v === 'false') return v
-
-          return undefined
-        }
+        isBoolean: true
       },
 
       searchAccount: { prefix: 'account:' },
index 467ca1d2c7c62f869cb82c8f686c63245bf45459..d9cf7a14ff0bb78fc0fdbdbd6fd143414d7c25ad 100644 (file)
@@ -3,7 +3,7 @@
 @import '_mixins';
 @import '_miniature';
 
-$iconSize: 16px;
+$icon-size: 16px;
 
 ::ng-deep my-video-list-header {
   display: flex;
@@ -17,20 +17,19 @@ $iconSize: 16px;
 
   my-feed {
     display: inline-block;
-    width: calc(#{$iconSize} - 2px);
+    width: calc(#{$icon-size} - 2px);
   }
 
   .moderation-block {
-
-    my-global-icon {
-      position: relative;
-      width: $iconSize;
-    }
-
     margin-left: .4rem;
     display: flex;
     justify-content: flex-end;
     align-items: center;
+
+    my-global-icon {
+      position: relative;
+      width: $icon-size;
+    }
   }
 }
 
@@ -72,7 +71,7 @@ $iconSize: 16px;
 
     .title-page {
       margin-bottom: 10px;
-      margin-right: 0px;
+      margin-right: 0;
     }
   }
 }
index 32cfdfd685125245c04ff9b13fad7d6d914663cd..03be6d2ffd396621decaae0ac3d78b51c8cb58be 100644 (file)
@@ -13,7 +13,7 @@ import { VideoDownloadComponent } from './video-download.component'
 import { VideoMiniatureComponent } from './video-miniature.component'
 import { VideosSelectionComponent } from './videos-selection.component'
 import { VideoListHeaderComponent } from './video-list-header.component'
-import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-account-avatar.module'
+import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -25,7 +25,7 @@ import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-accou
     SharedGlobalIconModule,
     SharedVideoLiveModule,
     SharedVideoModule,
-    SharedAccountAvatarModule
+    SharedActorImageModule
   ],
 
   declarations: [
index 7f6e03c87f1f13be9398ecb03641315e9af6a9b5..b689b1046635480192f00e3ff855b575b4c54dfe 100644 (file)
@@ -28,7 +28,7 @@
 
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
-  border-right: none;
+  border-right: 0;
 
   select {
     height: inherit;
@@ -85,7 +85,7 @@
   &.metadata-attribute-tags {
     .metadata-attribute-value:not(:nth-child(2)) {
       &::before {
-        content: ', '
+        content: ', ';
       }
     }
   }
index bc19127aaf5759b0f421df0cde49bb4c052406d2..645be92bd3d4336dc8f64b4866e7df224e6abc8c 100644 (file)
   <div class="video-bottom">
     <div class="video-miniature-information">
       <div class="d-flex video-miniature-meta">
-        <a *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" class="channel-avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
-          <img [src]="getAvatarUrl()" alt="" />
-        </a>
+        <my-actor-avatar
+          *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle"
+          [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]"
+        ></my-actor-avatar>
 
-        <my-account-avatar
+        <my-actor-avatar
           *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle"
-          [account]="video.account" size="40" [internalHref]="'/video-channels/' + video.byVideoChannel"
-        ></my-account-avatar>
+          [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]"
+        ></my-actor-avatar>
 
         <div class="w-100 d-flex flex-column">
           <a *ngIf="!videoHref" tabindex="-1" class="video-miniature-name"
index f6f2925f03c035d621273b4fffd42a7373ee2492..5df89d019deccd23854f411c1e49c3015b29a319 100644 (file)
@@ -12,15 +12,10 @@ $more-button-width: 40px;
   width: calc(100% - #{$more-button-width});
 }
 
-my-account-avatar,
-.channel-avatar {
+my-actor-avatar {
   margin: 10px 10px 0 0;
 }
 
-.channel-avatar img{
-  @include channel-avatar(40px);
-}
-
 .video-miniature-created-at-views {
   font-size: 13px;
 }
@@ -46,7 +41,7 @@ my-account-avatar,
 }
 
 .video-info-blocked {
-  color: red;
+  color: #ff0000;
 
   .blocked-reason::before {
     content: ' - ';
@@ -54,7 +49,7 @@ my-account-avatar,
 }
 
 .video-info-nsfw {
-  color: red;
+  color: #ff0000;
 }
 
 .video-actions {
index 8d66aaee2c9a42bc495dd3f83e103896b6ce149e..b58c118beac7e35706c46c98ebe1a56abeed90de 100644 (file)
@@ -12,6 +12,7 @@ import {
 } from '@angular/core'
 import { AuthService, ScreenService, ServerService, User } from '@app/core'
 import { ServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models'
+import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
 import { Video } from '../shared-main'
 import { VideoPlaylistService } from '../shared-video-playlist'
 import { VideoActionsDisplayType } from './video-actions-dropdown.component'
@@ -51,6 +52,8 @@ export class VideoMiniatureComponent implements OnInit {
   }
   @Input() displayVideoActions = true
 
+  @Input() actorImageSize: ActorAvatarSize = '40'
+
   @Input() displayAsRow = false
 
   @Input() videoLinkType: VideoLinkType = 'internal'
@@ -180,14 +183,6 @@ export class VideoMiniatureComponent implements OnInit {
     return ''
   }
 
-  getAvatarUrl () {
-    if (this.displayOwnerAccount()) {
-      return this.video.account.avatar?.url
-    }
-
-    return this.video.videoChannelAvatarUrl
-  }
-
   loadActions () {
     if (this.displayVideoActions) this.showActions = true
 
index dec9e99f335e5ea79250c612dfda89c31e3626ba..4ee90ce7fc456c4e7c9e5d01a3ee87496d28cfc8 100644 (file)
@@ -1,9 +1,9 @@
-<div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div>
+<div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">{{ noResultMessage }}</div>
 
 <div myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()" class="videos">
   <div class="video" *ngFor="let video of videos; let i = index; trackBy: videoById">
 
-    <div class="checkbox-container">
+    <div class="checkbox-container" *ngIf="enableSelection">
       <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="_selection[video.id]"></my-peertube-checkbox>
     </div>
 
index f8c3800d7fa1e96a77da3a5b2083e26172b79b4e..d64ee9b981ecc3c94f84c5b935466614b403c8da 100644 (file)
@@ -31,6 +31,9 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
   @Input() pagination: ComponentPagination
   @Input() titlePage: string
   @Input() miniatureDisplayOptions: MiniatureDisplayOptions
+  @Input() noResultMessage = $localize`No results.`
+  @Input() enableSelection = true
+  @Input() loadOnInit = true
 
   @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
 
index b84cacece486f8f4f58b6c0b6f756dad9ce4fa68..cb116819680fbd40164017591901462c0fd27cac 100644 (file)
@@ -126,7 +126,7 @@ $timestamp-margin-right: 10px;
   border-top: 1px solid $separator-border-color;
 }
 
-.new-playlist-button  {
+.new-playlist-button {
   cursor: pointer;
 
   my-global-icon {
index 572f7d7a88e905aea17f5b5aabdd5dd2cc6b6915..9ccd039127aec2471320090883d950296c359088 100644 (file)
@@ -84,21 +84,23 @@ my-video-thumbnail,
         width: auto;
       }
 
-      .video-info-account, .video-info-timestamp {
+      .video-info-account,
+      .video-info-timestamp {
         color: pvar(--greyForegroundColor);
       }
     }
   }
 
   .video-info-name {
+    @include ellipsis;
+
     font-size: 18px;
     font-weight: $font-semibold;
     display: inline-block;
-
-    @include ellipsis;
   }
 
-  .more, my-edit-button {
+  .more,
+  my-edit-button {
     justify-self: flex-end;
     margin-left: auto;
     cursor: pointer;
@@ -118,7 +120,7 @@ my-video-thumbnail,
       display: flex;
 
       &::after {
-        border: none;
+        border: 0;
       }
     }
   }
index 99089166caabd5454cca0f9eddc693a8b14855a1..a46a6e47525de167a2d5c48b2a0c6e482eb50afa 100644 (file)
@@ -6,7 +6,7 @@
   display: inline-block;
   width: 100%;
 
-  &.no-videos:not(.to-manage){
+  &.no-videos:not(.to-manage) {
     a {
       cursor: default !important;
     }
index 9bec16d7768190d39b235672e5e3c20a02399c5d..5b6ba9dbf2bc1d089f94d71b2f2c871a2b9ee1e8 100644 (file)
@@ -37,10 +37,8 @@ export class VideoPlaylist implements ServerVideoPlaylist {
   embedUrl: string
 
   ownerBy: string
-  ownerAvatarUrl: string
 
   videoChannelBy?: string
-  videoChannelAvatarUrl?: string
 
   private thumbnailVersion: number
   private originThumbnailUrl: string
@@ -78,12 +76,10 @@ export class VideoPlaylist implements ServerVideoPlaylist {
 
     this.ownerAccount = hash.ownerAccount
     this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
-    this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
 
     if (hash.videoChannel) {
       this.videoChannel = hash.videoChannel
       this.videoChannelBy = Actor.CREATE_BY_STRING(hash.videoChannel.name, hash.videoChannel.host)
-      this.videoChannelAvatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this.videoChannel)
     }
 
     this.privacy.label = peertubeTranslate(this.privacy.label, translations)
diff --git a/client/src/assets/player/images/info.svg b/client/src/assets/player/images/info.svg
new file mode 100644 (file)
index 0000000..bd1d9c6
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
\ No newline at end of file
index e97925ab50bb3512aab7263422a49e1d26ed6080..4275a5e5e49452da487e0eb59a933d65c481f24b 100644 (file)
@@ -1,10 +1,10 @@
+import * as Hlsjs from 'hls.js/dist/hls.light.js'
+import { Events, Segment } from 'p2p-media-loader-core'
+import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
 import videojs from 'video.js'
 import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings'
-import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
-import { Events, Segment } from 'p2p-media-loader-core'
 import { timeToInt } from '../utils'
 import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
-import * as Hlsjs from 'hls.js/dist/hls.light.js'
 
 registerConfigPlugin(videojs)
 registerSourceHandler(videojs)
@@ -36,6 +36,9 @@ class P2pMediaLoaderPlugin extends Plugin {
 
   private networkInfoInterval: any
 
+  private hlsjsCurrentLevel: number
+  private hlsjsLevels: Hlsjs.Level[]
+
   constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
     super(player)
 
@@ -84,6 +87,16 @@ class P2pMediaLoaderPlugin extends Plugin {
     clearInterval(this.networkInfoInterval)
   }
 
+  getCurrentLevel () {
+    return this.hlsjsLevels.find(l => l.level === this.hlsjsCurrentLevel)
+  }
+
+  getLiveLatency () {
+    return undefined as number
+    // FIXME: Use latency when hls >= V1
+    // return this.hlsjs.latency
+  }
+
   getHLSJS () {
     return this.hlsjs
   }
@@ -140,6 +153,14 @@ class P2pMediaLoaderPlugin extends Plugin {
     this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
     this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--)
 
+    this.hlsjs.on(Hlsjs.Events.MANIFEST_PARSED, (_e, manifest) => {
+      this.hlsjsCurrentLevel = manifest.firstLevel
+      this.hlsjsLevels = manifest.levels
+    })
+    this.hlsjs.on(Hlsjs.Events.LEVEL_LOADED, (_e, level) => {
+      this.hlsjsCurrentLevel = level.levelId || (level as any).id
+    })
+
     this.networkInfoInterval = setInterval(() => {
       const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload)
       const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
@@ -166,7 +187,8 @@ class P2pMediaLoaderPlugin extends Plugin {
           numPeers: this.statsP2PBytes.numPeers,
           downloaded: this.statsP2PBytes.totalDownload,
           uploaded: this.statsP2PBytes.totalUpload
-        }
+        },
+        bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8
       } as PlayerNetworkInfo)
     }, this.CONSTANTS.INFO_SCHEDULER)
   }
index cf2cfb472e6b4bbdf80e41e1eecd810847f5ea40..80aceb2395801e0b9490df264230dfa151284fa4 100644 (file)
@@ -45,6 +45,7 @@ function saveTheaterInStore (enabled: boolean) {
 }
 
 function saveAverageBandwidth (value: number) {
+  /** used to choose the most fitting resolution */
   return setLocalStorage('average-bandwidth', value.toString())
 }
 
index ed82e0496595611024ac9fe0703d0aa5bd4bc0bd..62dff82859320aa1dc8b6e041a850ee022d9922b 100644 (file)
@@ -4,6 +4,8 @@ import 'videojs-contextmenu-pt'
 import 'videojs-contrib-quality-levels'
 import './upnext/end-card'
 import './upnext/upnext-plugin'
+import './stats/stats-card'
+import './stats/stats-plugin'
 import './bezels/bezels-plugin'
 import './peertube-plugin'
 import './videojs-components/next-previous-video-button'
@@ -170,6 +172,11 @@ export class PeertubePlayerManager {
         self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
 
         player.bezels()
+        player.stats({
+          videoUUID: options.common.videoUUID,
+          videoIsLive: options.common.isLive,
+          mode
+        })
 
         return res(player)
       })
@@ -538,6 +545,14 @@ export class PeertubePlayerManager {
         })
       }
 
+      items.push({
+        icon: 'info',
+        label: player.localize('Stats for nerds'),
+        listener: () => {
+          player.stats().show()
+        }
+      })
+
       return items.map(i => ({
         ...i,
         label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
index 4a6c8024767cd20f15c45a1e941ba60315879fcf..8afb424a780c7b3602729f6d9b0fb3f0711bb1ef 100644 (file)
@@ -7,7 +7,9 @@ import { PlayerMode } from './peertube-player-manager'
 import { PeerTubePlugin } from './peertube-plugin'
 import { PlaylistPlugin } from './playlist/playlist-plugin'
 import { EndCardOptions } from './upnext/end-card'
+import { StatsCardOptions } from './stats/stats-card'
 import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
+import { StatsForNerdsPlugin } from './stats/stats-plugin'
 
 declare module 'video.js' {
 
@@ -36,6 +38,8 @@ declare module 'video.js' {
 
     bezels (): void
 
+    stats (options?: StatsCardOptions): StatsForNerdsPlugin
+
     qualityLevels (): QualityLevels
 
     textTracks (): TextTrackList & {
@@ -195,6 +199,9 @@ type PlayerNetworkInfo = {
     uploaded: number
     numPeers: number
   }
+
+  // In bytes
+  bandwidthEstimate: number
 }
 
 type PlaylistItemOptions = {
diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts
new file mode 100644 (file)
index 0000000..d9f0d2f
--- /dev/null
@@ -0,0 +1,273 @@
+import videojs from 'video.js'
+import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
+import { bytes, secondsToTime } from '../utils'
+
+interface StatsCardOptions extends videojs.ComponentOptions {
+  videoUUID: string
+  videoIsLive: boolean
+  mode: 'webtorrent' | 'p2p-media-loader'
+}
+
+interface PlayerNetworkInfo {
+  downloadSpeed?: string
+  uploadSpeed?: string
+  totalDownloaded?: string
+  totalUploaded?: string
+  numPeers?: number
+  averageBandwidth?: string
+
+  downloadedFromServer?: string
+  downloadedFromPeers?: string
+}
+
+const Component = videojs.getComponent('Component')
+class StatsCard extends Component {
+  options_: StatsCardOptions
+
+  container: HTMLDivElement
+
+  list: HTMLDivElement
+  closeButton: HTMLElement
+
+  updateInterval: any
+
+  mode: 'webtorrent' | 'p2p-media-loader'
+
+  metadataStore: any = {}
+
+  intervalMs = 300
+  playerNetworkInfo: PlayerNetworkInfo = {}
+
+  constructor (player: videojs.Player, options: StatsCardOptions) {
+    super(player, options)
+  }
+
+  createEl () {
+    const container = super.createEl('div', {
+      className: 'vjs-stats-content',
+      innerHTML: this.getMainTemplate()
+    }) as HTMLDivElement
+    this.container = container
+    this.container.style.display = 'none'
+
+    this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
+    this.closeButton.onclick = () => this.hide()
+
+    this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
+
+    this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
+      if (!data) return // HTTP fallback
+
+      this.mode = data.source
+
+      const p2pStats = data.p2p
+      const httpStats = data.http
+
+      this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
+      this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
+      this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
+      this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
+      this.playerNetworkInfo.numPeers = p2pStats.numPeers
+      this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
+
+      if (data.source === 'p2p-media-loader') {
+        this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
+        this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
+      }
+    })
+
+    return container
+  }
+
+  toggle () {
+    this.updateInterval
+      ? this.hide()
+      : this.show()
+  }
+
+  show () {
+    this.container.style.display = 'block'
+    this.updateInterval = setInterval(async () => {
+      try {
+        const options = this.mode === 'webtorrent'
+          ? await this.buildWebTorrentOptions()
+          : await this.buildHLSOptions()
+
+        this.list.innerHTML = this.getListTemplate(options)
+      } catch (err) {
+        console.error('Cannot update stats.', err)
+        clearInterval(this.updateInterval)
+      }
+    }, this.intervalMs)
+  }
+
+  hide () {
+    clearInterval(this.updateInterval)
+    this.container.style.display = 'none'
+  }
+
+  private async buildHLSOptions () {
+    const p2pMediaLoader = this.player_.p2pMediaLoader()
+    const level = p2pMediaLoader.getCurrentLevel()
+
+    const codecs = level?.videoCodec || level?.audioCodec
+      ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
+      : undefined
+
+    const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
+    const buffer = this.timeRangesToString(this.player().buffered())
+
+    let progress: number
+    let latency: string
+
+    if (this.options_.videoIsLive) {
+      latency = secondsToTime(p2pMediaLoader.getLiveLatency())
+    } else {
+      progress = this.player().bufferedPercent()
+    }
+
+    return {
+      playerNetworkInfo: this.playerNetworkInfo,
+      resolution,
+      codecs,
+      buffer,
+      latency,
+      progress
+    }
+  }
+
+  private async buildWebTorrentOptions () {
+    const videoFile = this.player_.webtorrent().getCurrentVideoFile()
+
+    if (!this.metadataStore[videoFile.fileUrl]) {
+      this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
+    }
+
+    const metadata = this.metadataStore[videoFile.fileUrl]
+
+    let colorSpace = 'unknown'
+    let codecs = 'unknown'
+
+    if (metadata?.streams[0]) {
+      const stream = metadata.streams[0]
+
+      colorSpace = stream['color_space'] !== 'unknown'
+        ? stream['color_space']
+        : 'bt709'
+
+      codecs = stream['codec_name'] || 'avc1'
+    }
+
+    const resolution = videoFile?.resolution.label + videoFile?.fps
+    const buffer = this.timeRangesToString(this.player().buffered())
+    const progress = this.player_.webtorrent().getTorrent()?.progress
+
+    return {
+      playerNetworkInfo: this.playerNetworkInfo,
+      progress,
+      colorSpace,
+      codecs,
+      resolution,
+      buffer
+    }
+  }
+
+  private getListTemplate (options: {
+    playerNetworkInfo: PlayerNetworkInfo
+    progress: number
+    codecs: string
+    resolution: string
+    buffer: string
+
+    latency?: string
+    colorSpace?: string
+  }) {
+    const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
+    const player = this.player()
+
+    const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
+    const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
+    const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
+    const pr = (window.devicePixelRatio || 1).toFixed(2)
+    const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
+
+    const duration = player.duration()
+
+    let volume = `${Math.round(player.volume() * 100)}`
+    if (player.muted()) volume += ' (muted)'
+
+    const networkActivity = playerNetworkInfo.downloadSpeed
+      ? `${playerNetworkInfo.downloadSpeed} &dArr; / ${playerNetworkInfo.uploadSpeed} &uArr;`
+      : undefined
+
+    const totalTransferred = playerNetworkInfo.totalDownloaded
+      ? `${playerNetworkInfo.totalDownloaded} &dArr; / ${playerNetworkInfo.totalUploaded} &uArr;`
+      : undefined
+    const downloadBreakdown = playerNetworkInfo.downloadedFromServer
+      ? `${playerNetworkInfo.downloadedFromServer} from server Â· ${playerNetworkInfo.downloadedFromPeers} from peers`
+      : undefined
+
+    const bufferProgress = progress !== undefined
+      ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
+      : undefined
+
+    return `
+      ${this.buildElement(player.localize('Player mode'), this.options_.mode)}
+
+      ${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)}
+
+      ${this.buildElement(player.localize('Viewport / Frames'), frames)}
+
+      ${this.buildElement(player.localize('Resolution'), resolution)}
+
+      ${this.buildElement(player.localize('Volume'), volume)}
+
+      ${this.buildElement(player.localize('Codecs'), codecs)}
+      ${this.buildElement(player.localize('Color'), colorSpace)}
+
+      ${this.buildElement(player.localize('Connection Speed'), playerNetworkInfo.averageBandwidth)}
+
+      ${this.buildElement(player.localize('Network Activity'), networkActivity)}
+      ${this.buildElement(player.localize('Total Transfered'), totalTransferred)}
+      ${this.buildElement(player.localize('Download Breakdown'), downloadBreakdown)}
+
+      ${this.buildElement(player.localize('Buffer Progress'), bufferProgress)}
+      ${this.buildElement(player.localize('Buffer State'), buffer)}
+
+      ${this.buildElement(player.localize('Live Latency'), latency)}
+    `
+  }
+
+  private getMainTemplate () {
+    return `
+      <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
+      <div class="vjs-stats-list"></div>
+    `
+  }
+
+  private buildElement (label: string, value?: string) {
+    if (!value) return ''
+
+    return `<div><div>${label}</div><span>${value}</span></div>`
+  }
+
+  private timeRangesToString (r: videojs.TimeRange) {
+    let result = ''
+
+    for (let i = 0; i < r.length; i++) {
+      const start = Math.floor(r.start(i))
+      const end = Math.floor(r.end(i))
+
+      result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
+    }
+
+    return result
+  }
+}
+
+videojs.registerComponent('StatsCard', StatsCard)
+
+export {
+  StatsCard,
+  StatsCardOptions
+}
diff --git a/client/src/assets/player/stats/stats-plugin.ts b/client/src/assets/player/stats/stats-plugin.ts
new file mode 100644 (file)
index 0000000..8aad80e
--- /dev/null
@@ -0,0 +1,31 @@
+import videojs from 'video.js'
+import { StatsCard, StatsCardOptions } from './stats-card'
+
+const Plugin = videojs.getPlugin('plugin')
+
+class StatsForNerdsPlugin extends Plugin {
+  private statsCard: StatsCard
+
+  constructor (player: videojs.Player, options: StatsCardOptions) {
+    const settings = {
+      ...options
+    }
+
+    super(player)
+
+    this.player.ready(() => {
+      player.addClass('vjs-stats-for-nerds')
+    })
+
+    this.statsCard = new StatsCard(player, options)
+
+    player.addChild(this.statsCard, settings)
+  }
+
+  show () {
+    this.statsCard.show()
+  }
+}
+
+videojs.registerPlugin('stats', StatsForNerdsPlugin)
+export { StatsForNerdsPlugin }
index e67a3da0637b087ace2c21ffc7e2af82e44fb9a4..74788a8971bdf1855c72a1acad03da82a64c6d86 100644 (file)
@@ -95,11 +95,6 @@ class SettingsButton extends Button {
       }
     }
 
-    document.removeEventListener('click', this.documentClickHandler)
-    if (this.isInIframe()) {
-      window.removeEventListener('blur', this.documentClickHandler)
-    }
-
     this.hideDialog()
 
     if (this.settingsButtonOptions.entries.length === 0) {
@@ -107,6 +102,14 @@ class SettingsButton extends Button {
     }
   }
 
+  dispose () {
+    document.removeEventListener('click', this.documentClickHandler)
+
+    if (this.isInIframe()) {
+      window.removeEventListener('blur', this.documentClickHandler)
+    }
+  }
+
   onAddSettingsItem (event: any, data: any) {
     const [ entry, options ] = data
 
index e557fe722e3f7d81989d8d0769355fcb02548fcd..6f5fbe4c97bfd404c581d512bcce7567c9d3f93a 100644 (file)
@@ -506,7 +506,8 @@ class WebTorrentPlugin extends Plugin {
           uploadSpeed: this.torrent.uploadSpeed,
           downloaded: this.torrent.downloaded,
           uploaded: this.torrent.uploaded
-        }
+        },
+        bandwidthEstimate: this.webtorrent.downloadSpeed
       } as PlayerNetworkInfo)
     }, this.CONSTANTS.INFO_SCHEDULER)
   }
index fa9c0d9924d2ff7edc6580da515d7729f13bec65..89b6f0c4cceb17508efd2dfa3fb1083949b457b2 100644 (file)
@@ -8,9 +8,9 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
 @import './bootstrap';
 @import './primeng-custom';
-@import './ng-select.scss';
+@import './ng-select';
 
-@import './classes.scss';
+@import './classes';
 
 [hidden] {
   display: none !important;
@@ -89,14 +89,16 @@ input.readonly {
   background-color: pvar(--inputBackgroundColor) !important;
 }
 
-input, textarea {
+input,
+textarea {
   outline: none;
   color: pvar(--inputForegroundColor);
 }
 
 button {
-  background: unset;
   @include disable-outline;
+
+  background: unset;
 }
 
 label {
@@ -121,12 +123,12 @@ code {
   margin-top: 5px;
 }
 
-.input-error
+.input-error,
 my-input-toggle-hidden ::ng-deep input {
   border-color: $red !important;
 }
 
-.fullWidth {
+.full-width {
   width: 100%;
   margin-left: auto;
   margin-right: auto;
@@ -134,7 +136,7 @@ my-input-toggle-hidden ::ng-deep input {
 }
 
 .glyphicon-black {
-  color: black;
+  color: #000;
 }
 
 .row {
@@ -184,26 +186,26 @@ my-input-toggle-hidden ::ng-deep input {
     width: 100%;
   }
 
-  &.lock-scroll .main-row > router-outlet + * {
+  &.lock-scroll .main-row > router-outlet + * {  /* stylelint-disable-line selector-max-compound-selectors */
     // Lock and hide body scrollbars
     position: fixed;
 
     // Lock and hide sub-menu scrollbars
-    .sub-menu {
+    .sub-menu { /* stylelint-disable-line */
       overflow-x: hidden;
     }
   }
 }
 
 .title-page {
+  @include disable-default-a-behaviour;
+
   opacity: 0.6;
   color: pvar(--mainForegroundColor);
   font-size: 16px;
   display: inline-block;
   margin-right: 55px;
   font-weight: $font-semibold;
-  @include disable-default-a-behaviour;
-
   border-bottom: 2px solid transparent;
 
   &.title-page-single {
@@ -219,13 +221,19 @@ my-input-toggle-hidden ::ng-deep input {
     font-size: 125%;
   }
 
-  &:hover, &:active, &:focus {
+  &:hover,
+  &:active,
+  &:focus {
     color: pvar(--mainForegroundColor);
   }
 
-  &.active, &:hover,  &:active, &:focus, &.title-page-single {
+  &.active,
+  &:hover,
+  &:active,
+  &:focus,
+  &.title-page-single {
     opacity: 1;
-    outline: 0px hidden !important;
+    outline: 0 hidden !important;
   }
 
   @media screen and (max-width: $mobile-view) {
@@ -262,7 +270,10 @@ my-input-toggle-hidden ::ng-deep input {
       background-color: pvar(--submenuBackgroundColor);
     }
 
-    &.active, &:hover, &:active, &:focus {
+    &.active,
+    &:hover,
+    &:active,
+    &:focus {
       opacity: 1;
     }
   }
@@ -275,8 +286,13 @@ my-input-toggle-hidden ::ng-deep input {
 
 // In tables, don't have a hover different background
 table {
-  .action-button-edit, .action-button-delete {
-    &:hover, &:active, &:focus, &[disabled], &.disabled {
+  .action-button-edit,
+  .action-button-delete {
+    &:hover,
+    &:active,
+    &:focus,
+    &[disabled],
+    &.disabled {
       background-color: $grey-background-color !important;
     }
   }
@@ -329,15 +345,12 @@ ngx-loading-bar {
 
 @media screen and (max-width: #{breakpoint(xxl)}) {
   .main-col {
-    & {
-      --horizontalMarginContent: #{$not-expanded-horizontal-margins / 2};
-    }
+    --horizontalMarginContent: #{$not-expanded-horizontal-margins / 2};
+    --videosHorizontalMarginContent: 30px;
 
     &.expanded {
       --horizontalMarginContent: #{$expanded-horizontal-margins / 2};
     }
-
-    --videosHorizontalMarginContent: 30px;
   }
 }
 
index 0ab6230c8c75c854079feae1829d1e284dcef9e6..548e55e1e7d054e9d05e5483bbc88519481ea1ba 100644 (file)
@@ -6,7 +6,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
 // Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
 .glyphicon-refresh-animate {
-  animation: spin .7s infinite linear;
+  animation: spin 0.7s infinite linear;
 }
 
 .glyphicon-duplicate {
@@ -25,6 +25,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   from {
     transform: scale(1) rotate(0deg);
   }
+
   to {
     transform: scale(1) rotate(360deg);
   }
@@ -70,7 +71,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
     &.active {
       color: pvar(--mainBackgroundColor) !important;
       background-color: pvar(--mainHoverColor);
-      opacity: .9;
+      opacity: 0.9;
     }
 
     &:active {
@@ -97,9 +98,9 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 }
 
 @media screen and (min-width: #{breakpoint(md)}) {
-  .modal:before {
+  .modal::before {
     vertical-align: middle;
-    content: " ";
+    content: ' ';
     height: 100%;
   }
 
@@ -123,7 +124,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   }
 
   .modal-header {
-    border-bottom: none;
+    border-bottom: 0;
     margin-bottom: 5px;
 
     .modal-title {
@@ -140,10 +141,11 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
       margin: 0;
       padding: 0;
-      opacity: .5;
+      opacity: 0.5;
 
-      &[iconName="cross"] {
+      &[iconName=cross] { /* stylelint-disable-line selector-max-compound-selectors */
         @include icon(16px);
+
         top: -3px;
       }
     }
@@ -154,7 +156,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
     text-align: right;
 
     > .peertube-button:not(:first-child) {
-      margin-left: 10px
+      margin-left: 10px;
     }
   }
 }
@@ -168,7 +170,8 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
 // On touchscreen devices, simply overflow: hidden to avoid detached overlay on scroll
 @media (hover: none) and (pointer: coarse) {
-  .modal-open, .menu-open {
+  .modal-open,
+  .menu-open {
     overflow: hidden !important;
   }
 
@@ -176,7 +179,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   .menu-open {
     .main-col {
       &::before {
-        background-color: black;
+        background-color: #000;
         width: 100vw;
         height: 100vh;
         opacity: 0.75;
@@ -204,7 +207,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   .nav-link {
     opacity: 0.6 !important;
 
-    &.active, &:hover, &:active, &:focus {
+    &.active,
+    &:hover,
+    &:active,
+    &:focus {
       opacity: 1 !important;
     }
   }
@@ -221,7 +227,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
   color: pvar(--mainForegroundColor);
   font-weight: $font-semibold;
-  border: none;
+  border: 0;
   border-bottom: 2px solid transparent;
   opacity: 0.6;
 
@@ -231,7 +237,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
     border-bottom-color: pvar(--mainColor);
   }
 
-  &.active, &:hover, &:active, &:focus {
+  &.active,
+  &:hover,
+  &:active,
+  &:focus {
     opacity: 1;
   }
 }
@@ -314,9 +323,10 @@ ngb-tooltip-window {
 }
 
 .input-group {
-  > .form-control {
+  > .form-control {
     flex: initial;
   }
+
   input.form-control {
     width: unset !important;
     flex-grow: 1;
@@ -366,7 +376,7 @@ ngb-tooltip-window {
   border: 1px solid #eee;
   border-radius: .25rem;
 
-  > label {
+  > label {
     position: relative;
     top: -5px;
     left: -10px;
index a4798ce1d68ff58ee9111b2df9f2d847c20074e2..38bd90ae6dec5622b5dcd1374d150b0f14f9234d 100644 (file)
@@ -17,7 +17,7 @@
 @mixin show-more-description {
   color: pvar(--mainColor);
   cursor: pointer;
-  margin: 10px auto 45px auto;
+  margin: 10px auto 45px;
 }
 
 @mixin avatar-row-responsive ($img-margin, $grey-font-size) {
@@ -25,8 +25,8 @@
   grid-column: 1;
   margin-bottom: 30px;
 
-  .channel-avatar {
-    @include channel-avatar(120px);
+  .main-avatar {
+    @include actor-avatar-size(120px);
   }
 
   > div {
       font-size: 22px;
     }
 
-    .channel-avatar {
-      @include channel-avatar(80px);
-    }
-
-    .account-avatar {
-      @include avatar(120px);
+    .main-avatar {
+      @include actor-avatar-size(80px);
     }
   }
 }
index b1a23be6bcad130aeb39a2aaf48235727bfe2453..d9e5efc02da2407314e68b8374395a79b6b0dfb1 100644 (file)
@@ -1,4 +1,4 @@
-@import "./_bootstrap-variables";
+@import './_bootstrap-variables';
 
 @import '~bootstrap/scss/functions';
 @import '~bootstrap/scss/variables';
index 6313736e0e07c0d9a613af1cfffe27df294116e7..514261d01394c81686f1f13a1fa7973bc5bbffa5 100644 (file)
@@ -1,4 +1,4 @@
-@font-face{
+@font-face {
   font-family: 'Source Sans Pro';
   font-weight: 200 900;
   font-style: normal;
@@ -7,7 +7,7 @@
   src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Roman.ttf.woff2') format('woff2');
 }
 
-@font-face{
+@font-face {
   font-family: 'Source Sans Pro';
   font-weight: 200 900;
   font-style: italic;
index 3b86f29b4fcc59657624e2e77e4f20c7eac5be94..070aa339868d397915d8fb95e9eb202b45f887b9 100644 (file)
@@ -3,9 +3,8 @@
 
 @mixin miniature-name {
   @include ellipsis-multiline(1.1em, 2);
+  @include peertube-word-wrap(false);
 
-  word-break: break-all;
-  word-wrap: break-word;
   transition: color 0.2s;
   font-weight: $font-semibold;
   color: pvar(--mainForegroundColor);
 }
 
 @mixin miniature-thumbnail {
-  @include disable-outline;
-
   $play-overlay-transition: 0.2s ease;
   $play-overlay-height: 26px;
   $play-overlay-width: 18px;
 
+  @include disable-outline;
+
   display: flex;
   flex-direction: column;
   position: relative;
@@ -47,7 +46,8 @@
     opacity: 0;
     background-color: rgba(0, 0, 0, 0.3);
 
-    &, .icon {
+    &,
+    .icon {
       transition: all $play-overlay-transition;
     }
 
@@ -79,7 +79,7 @@
 
     &.blur-filter {
       filter: blur(20px);
-      transform : scale(1.03);
+      transform: scale(1.03);
     }
   }
 }
       column-gap: 30px;
       grid-template-columns: repeat(
         auto-fill,
-        minmax(
-          var(--miniatureMinWidth),
-          1fr
-        )
+        minmax(var(--miniatureMinWidth), 1fr)
       );
 
       .video-wrapper,
index e03201ceffb658a91b81b6995406ce8e77cf015f..b2083e51653d3445e4745e062f22178cd43a7536 100644 (file)
@@ -1,7 +1,9 @@
 @import '_variables';
 
 @mixin disable-default-a-behaviour {
-  &:hover, &:focus, &:active {
+  &:hover,
+  &:focus,
+  &:active {
     text-decoration: none !important;
     outline: none !important;
   }
@@ -22,7 +24,7 @@
 @mixin ellipsis-multiline($font-size: 16px, $number-of-lines: 2) {
   display: block;
   /* Fallback for non-webkit */
-  display: -webkit-box;
+  display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */
   -webkit-line-clamp: $number-of-lines;
   /* Fallback for non-webkit */
   font-size: $font-size;
@@ -36,7 +38,7 @@
   position: relative;
   overflow: hidden;
 
-  &:after {
+  &::after {
     content: '';
     pointer-events: none;
     width: 100%;
   }
 }
 
-@mixin peertube-word-wrap {
+@mixin peertube-word-wrap ($with-hyphen: true) {
   word-break: break-word;
   word-wrap: break-word;
   overflow-wrap: break-word;
-  hyphens: auto;
+
+  @if $with-hyphen {
+    hyphens: auto;
+  }
 }
 
 @mixin apply-svg-color ($color) {
   padding-bottom: 0;
   flex-wrap: nowrap;
 
-  .input-group-text{
+  .input-group-text {
     font-size: 14px;
-    color: gray;
+    color: #808080;
   }
 }
 
 @mixin orange-button {
   @include button-focus(pvar(--mainColorLightest));
 
-  &, &:active, &:focus {
+  &,
+  &:active,
+  &:focus {
     color: #fff;
     background-color: pvar(--mainColor);
   }
     background-color: pvar(--mainHoverColor);
   }
 
-  &[disabled], &.disabled {
+  &[disabled],
+  &.disabled {
     cursor: default;
     color: #fff;
     background-color: #C6C6C6;
   }
 
   my-global-icon {
-    @include apply-svg-color(#fff)
+    @include apply-svg-color(#fff);
   }
 }
 
   border: 2px solid pvar(--mainColor);
   font-weight: $font-semibold;
 
-  &, &:active, &:focus {
+  &,
+  &:active,
+  &:focus {
     color: pvar(--mainColor);
     background-color: pvar(--mainBackgroundColor);
   }
     background-color: pvar(--mainColorLightest);
   }
 
-  &[disabled], &.disabled {
+  &[disabled],
+  &.disabled {
     cursor: default;
     color: pvar(--mainColor);
     background-color: #C6C6C6;
   }
 
   my-global-icon {
-    @include apply-svg-color(pvar(--mainColor))
+    @include apply-svg-color(pvar(--mainColor));
   }
 }
 
   color: pvar(--greyForegroundColor);
   background-color: transparent;
 
-  &[disabled], &.disabled {
+  &[disabled],
+  .disabled {
     cursor: default;
   }
 
   my-global-icon {
-    @include apply-svg-color(transparent)
+    @include apply-svg-color(transparent);
   }
 }
 
   background-color: $grey-background-color;
   color: pvar(--greyForegroundColor);
 
-  &:hover, &:active, &:focus, &[disabled], &.disabled {
+  &:hover,
+  &:active,
+  &:focus,
+  &[disabled],
+  &.disabled {
     color: pvar(--greyForegroundColor);
     background-color: $grey-background-hover-color;
   }
 
-  &[disabled], &.disabled {
+  &[disabled],
+  &.disabled {
     cursor: default;
   }
 
   my-global-icon {
-    @include apply-svg-color(pvar(--greyForegroundColor))
+    @include apply-svg-color(pvar(--greyForegroundColor));
   }
 }
 
   $text: #fff6f5;
 
   @include button-focus(scale-color($color, $alpha: -95%));
+
   background-color: $color;
   color: $text;
 
-  &:hover, &:active, &:focus, &[disabled], &.disabled {
+  &:hover,
+  &:active,
+  &:focus,
+  &[disabled],
+  &.disabled {
     background-color: lighten($color: $color, $amount: 10);
   }
 
-  &[disabled], &.disabled {
+  &[disabled],
+  &.disabled {
     cursor: default;
   }
 
   my-global-icon {
-    @include apply-svg-color($text)
+    @include apply-svg-color($text);
   }
 }
 
 @mixin peertube-button {
-  border: none;
+  border: 0;
   font-weight: $font-semibold;
   font-size: 15px;
   height: $button-height;
 }
 
 @mixin peertube-button-link {
-  display: inline-block;
-
   @include disable-default-a-behaviour;
   @include peertube-button;
-}
 
-@mixin peertube-button-outline {
   display: inline-block;
+}
 
+@mixin peertube-button-outline {
   @include disable-default-a-behaviour;
   @include peertube-button;
 
+  display: inline-block;
   border: 1px solid;
 }
 
     filter: alpha(opacity=0);
     opacity: 0;
     outline: none;
-    background: white;
+    background: #fff;
     cursor: inherit;
     display: block;
   }
 }
 
 @mixin peertube-button-file ($width) {
-  width: $width;
-
   @include peertube-file;
   @include peertube-button;
+
+  width: $width;
 }
 
 @mixin icon ($size) {
 @mixin select-arrow-down {
   top: 50%;
   right: calc(0% + 15px);
-  content: " ";
+  content: ' ';
   height: 0;
   width: 0;
   position: absolute;
     width: 100%;
   }
 
-  &:after {
+  &::after {
     @include select-arrow-down;
   }
 
     option {
       font-weight: $font-semibold;
       color: pvar(--greyForegroundColor);
-      border: none;
+      border: 0;
     }
   }
 }
 
 // Thanks: https://codepen.io/triss90/pen/XNEdRe/
 @mixin peertube-radio-container {
-  input[type="radio"] {
+  input[type=radio] {
     display: none;
 
-    + label {
+    + label {
       font-weight: $font-regular;
       cursor: pointer;
 
-      &:before {
+      &::before {
         position: relative;
         top: -2px;
         content: '';
       }
     }
 
-    &:checked + label:before {
+    &:checked + label::before {
       background-color: #000;
       box-shadow: inset 0 0 0 4px #fff;
     }
 
-    &:focus + label:before {
+    &:focus + label::before {
       outline: none;
       border-color: #000;
     }
     box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
   }
 
-  + span {
+  + span {
     position: relative;
     width: 18px;
     min-width: 18px;
     vertical-align: middle;
     cursor: pointer;
 
-    &:after {
+    &::after {
       content: '';
       position: absolute;
       top: calc(2px - #{$border-width});
     background: pvar(--mainColor);
     animation: jelly 0.6s ease;
 
-    &:after {
+    &::after {
       opacity: 1;
       transform: rotate(45deg) scale(1);
     }
   }
 
-  + span + span {
+  + span + span {
     font-size: 15px;
     font-weight: $font-regular;
     margin-left: 5px;
   }
 
   &[disabled] + span,
-  &[disabled] + span + span{
+  &[disabled] + span + span {
     opacity: 0.5;
     cursor: default;
   }
   }
 }
 
-@mixin avatar ($size) {
-  object-fit: cover;
-  border-radius: 50%;
-  width: $size;
-  height: $size;
-  min-width: $size;
-  min-height: $size;
-}
-
-@mixin channel-avatar ($size) {
+@mixin actor-avatar-size ($size) {
+  display: inline-block;
   width: $size;
   height: $size;
   min-width: $size;
   min-height: $size;
-  border-radius: 5px;
 }
 
 @mixin chevron ($size, $border-width) {
   margin-bottom: 10px;
 }
 
-@mixin actor-owner {
-  @include disable-default-a-behaviour;
-
-  font-size: 13px;
-  margin-top: 4px;
-  color: pvar(--mainForegroundColor);
-
-  span:hover {
-    opacity: 0.8;
-  }
-
-  img {
-    @include avatar(18px);
-
-    margin-left: 7px;
-    position: relative;
-    top: -2px;
-  }
-}
-
 @mixin create-button {
   @include peertube-button-link;
   @include orange-button;
       color: pvar(--mainColor);
     }
 
-    + .breadcrumb-item {
+    + .breadcrumb-item {
       padding-left: 0.5rem;
       &::before {
         display: inline-block;
         padding-right: 0.5rem;
         color: #6c757d;
-        content: "/";
+        content: '/';
       }
     }
 
   flex-wrap: wrap;
   margin: 0 -5px;
 
-  > div {
+  > div {
     box-sizing: border-box;
     flex: 0 0 percentage(1/3);
     padding: 0 5px;
     margin-bottom: 10px;
 
-    > a {
+    > a {
       @include disable-default-a-behaviour;
 
       text-decoration: none;
       }
     }
 
-    > a,
-    > div {
+    > a,
+    > div {
       padding: 20px;
       background: pvar(--submenuBackgroundColor);
       border-radius: 4px;
     }
   }
 
-  .dashboard-num, .dashboard-text {
+  .dashboard-num,
+  .dashboard-text {
     text-align: center;
     font-size: 130%;
     color: pvar(--mainForegroundColor);
     --chip-padding: .2rem .3rem;
   }
 
-  .avatar {
+  my-actor-avatar {
     margin-left: -.4rem;
     margin-right: .2rem;
-    height: $avatar-height;
-    width: $avatar-height;
-
-    border-radius: 50%;
-    display: inline-block;
-    line-height: 1.25;
-    position: relative;
-    vertical-align: middle;
   }
 
   &.two-lines {
 
     height: $avatar-height;
 
-    .avatar {
-      height: $avatar-height;
-      width: $avatar-height;
+    my-actor-avatar {
+      @include actor-avatar-size($avatar-height);
     }
 
     div {
   flex-direction: column;
 
   .form-sub-title {
-    margin-right: 0px !important;
+    margin-right: 0 !important;
     margin-bottom: 10px;
     text-align: center;
   }
     padding-bottom: 15px;
     margin-bottom: $sub-menu-margin-bottom;
 
+    > span > my-global-icon,
     > my-global-icon {
       margin-right: 10px;
-      vertical-align: bottom;
       width: 24px;
       height: 24px;
+      vertical-align: top;
     }
 
     .badge {
       margin-left: 7px;
+      vertical-align: top;
     }
   }
 }
index d2a5d2bd9fb8f07b6d3fb6da81d5c9ab049c95eb..d54563df6e644afdc1de20f9a95d36916d160102 100644 (file)
@@ -60,7 +60,7 @@ $max-channels-width: 1200px;
 $footer-height: 30px;
 $footer-margin: 30px;
 
-$separator-border-color: rgba(0, 0, 0, 0.10);
+$separator-border-color: rgba(0, 0, 0, 0.1);
 
 $video-miniature-margin-bottom: 15px;
 
@@ -90,7 +90,7 @@ $markdown-textarea-background-color: $grey-background-hover-color;
 $sub-menu-margin-bottom: 30px;
 $sub-menu-margin-bottom-small-view: 10px;
 
-$activated-action-button-color: black;
+$activated-action-button-color: #000;
 
 $focus-box-shadow-form: 0 0 0 .2rem;
 
@@ -147,7 +147,7 @@ $variables: (
   @if map-has-key($variables, $variable) {
     @return map-get($variables, $variable);
   } @else {
-    @error "ERROR: Variable #{$variable} does not exist";
+    @error 'ERROR: Variable #{$variable} does not exist';
   }
 }
 
index 61da6d2664722e7b914d5c23bb63cb5d919cc2da..13b2012b2befb51bd4619a1b6d4648beae5fac63 100644 (file)
@@ -14,7 +14,7 @@ $ng-select-height: 30px;
 $ng-select-value-padding-left: 15px;
 $ng-select-value-font-size: 15px;
 
-@import "~@ng-select/ng-select/scss/default.theme.scss";
+@import '~@ng-select/ng-select/scss/default.theme';
 
 .ng-select {
   font-size: $ng-select-value-font-size;
@@ -31,13 +31,13 @@ $ng-select-value-font-size: 15px;
   }
 
   .ng-arrow-wrapper {
-    padding-right: 12px
+    padding-right: 12px;
   }
 
   &.ng-select-single .ng-value-container .ng-value {
     color: pvar(--inputForegroundColor);
 
-    .ng-value-label {
+    .ng-value-label { /* stylelint-disable-line */
       display: flex;
       align-items: center;
     }
@@ -45,7 +45,8 @@ $ng-select-value-font-size: 15px;
 
   &.ng-select-multiple .ng-select-container .ng-value-container {
     padding-left: 12px;
-    .ng-value {
+
+    .ng-value { /* stylelint-disable-line */
       margin-left: 3px;
     }
   }
index df78916c6605dee6a0c9a05ca690babd368acffe..45cee3e7759d00aa915f6a435f866026386d0645 100644 (file)
@@ -8,7 +8,7 @@ $context-menu-width: 350px;
 
 .video-js .vjs-contextmenu-ui-menu {
   position: absolute;
-  background-color: rgba(0, 0, 0, 0.5);
+  background-color: $primary-background-color;
   padding: 8px 0;
   border-radius: 4px;
   width: $context-menu-width;
@@ -31,26 +31,26 @@ $context-menu-width: 350px;
       background-color: rgba(255, 255, 255, 0.2);
     }
 
-    [class^="vjs-icon-"] {
+    [class^='vjs-icon-'] {
+      $icons: 'link-2', 'repeat', 'code', 'tick-white', 'info';
+
       display: inline-flex;
       position: relative;
       top: 2px;
       cursor: pointer;
       width: 14px;
       height: 14px;
-      background-color: white;
+      background-color: #fff;
       mask-size: cover;
       margin-right: 0.8rem !important;
 
-      $icons: 'link-2', 'repeat', 'code', 'tick-white';
-
       @each $icon in $icons {
         &[class$="-#{$icon}"] {
           mask-image: url('#{$assets-path}/player/images/#{$icon}.svg');
         }
       }
 
-      &[class$="-tick-white"] {
+      &[class$='-tick-white'] {
         float: right;
         margin: 0 !important;
       }
index fe92ce5e0637c7bf9c4c1434fa8da49431d7a515..e674fa2f6963a38fad069daf3160b2ff85b5ac77 100644 (file)
@@ -4,5 +4,6 @@
 @import './settings-menu';
 @import './spinner';
 @import './upnext';
-@import './bezels.scss';
-@import './playlist.scss';
+@import './bezels';
+@import './playlist';
+@import './stats';
index c2fa855abcf124306b2f04b56d00370193c8cc96..26066d2187901148b6bf035c6768a13cb6e8e0b6 100644 (file)
@@ -13,4 +13,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
index 81aacf1d769b0980e6b7a198b7c95edf5646c11a..8fe2e054d7d698ef8eeabdd0f8dd69fdcd4520f9 100644 (file)
@@ -52,12 +52,12 @@ body {
   }
 
   .vjs-big-play-button {
-    outline: 0;
-    font-size: 6em;
-
     $big-play-width: 1.2em;
     $big-play-height: 1.2em;
 
+    outline: 0;
+    font-size: 6em;
+
     border: 2px solid #fff;
     border-radius: 100%;
 
@@ -72,7 +72,7 @@ body {
 
     &::-moz-focus-inner {
       border: 0;
-      padding: 0
+      padding: 0;
     }
 
     .vjs-icon-placeholder::before {
@@ -82,8 +82,9 @@ body {
       background-image: url('#{$assets-path}/player/images/big-play-button.svg');
     }
 
-    &.focus-visible, &:hover {
-      background-color: var(--mainColor, dimgray);
+    &.focus-visible,
+    &:hover {
+      background-color: var(--mainColor, #696969);
     }
 
   }
@@ -91,16 +92,19 @@ body {
   // Small effect when we click on the play button
   &.vjs-has-big-play-button-clicked {
 
-    .vjs-big-play-button, .vjs-poster {
+    .vjs-big-play-button,
+    .vjs-poster {
       display: block;
       visibility: hidden;
 
-      &.vjs-big-play-button, &.vjs-big-play-button::before {
+      &.vjs-big-play-button,
+      &.vjs-big-play-button::before {
         opacity: 0;
         transition: visibility 0.2s, opacity 0.2s;
       }
 
-      &.vjs-poster, &.vjs-poster::before {
+      &.vjs-poster,
+      &.vjs-poster::before {
         opacity: 0;
         transition: visibility 0.3s, opacity 0.3s;
         transition-delay: 0.05s;
@@ -165,8 +169,7 @@ body {
     .vjs-fullscreen-control,
     .vjs-peertube-link,
     .vjs-theater-control,
-    .vjs-settings
-    {
+    .vjs-settings {
       color: pvar(--embedForegroundColor) !important;
 
       opacity: $primary-foreground-opacity;
@@ -217,7 +220,8 @@ body {
         }
 
         .vjs-load-progress {
-          &, & div {
+          &,
+          div {
             background: rgba(255, 255, 255, .2);
           }
         }
@@ -266,7 +270,7 @@ body {
           line-height: calc(#{$control-bar-height} - 1px);
 
           &::after {
-            content: "/";
+            content: '/';
             margin: 0 1px 0 2px;
           }
         }
@@ -308,11 +312,17 @@ body {
         display: none;
       }
 
-      .download-speed-number, .upload-speed-number, .peers-number, .http-fallback {
+      .download-speed-number,
+      .upload-speed-number,
+      .peers-number,
+      .http-fallback {
         font-weight: $font-semibold;
       }
 
-      .download-speed-text, .upload-speed-text, .peers-text, .http-fallback {
+      .download-speed-text,
+      .upload-speed-text,
+      .peers-text,
+      .http-fallback {
         margin-right: 15px;
       }
 
@@ -336,10 +346,8 @@ body {
         &.icon-next,
         &.icon-previous {
           mask-image: url('#{$assets-path}/player/images/next.svg');
-          -webkit-mask-image: url('#{$assets-path}/player/images/next.svg');
-          background-color: white;
+          background-color: #fff;
           mask-size: cover;
-          -webkit-mask-size: cover;
           width: 11px;
           height: 11px;
           margin-top: -2px;
@@ -410,7 +418,7 @@ body {
     }
 
     .vjs-volume-bar {
-      background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcCAQAAACw95UnAAAAMElEQVRIx2NgoBL4n4YKGUYNHkEG4zJg1OCRYDCpBowaPJwMppbLRg0eNXjUYBLEAXWNUA6QNm1lAAAAAElFTkSuQmCC) no-repeat;
+      background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcCAQAAACw95UnAAAAMElEQVRIx2NgoBL4n4YKGUYNHkEG4zJg1OCRYDCpBowaPJwMppbLRg0eNXjUYBLEAXWNUA6QNm1lAAAAAElFTkSuQmCC') no-repeat;
       background-size: 22px 14px;
       height: 100%;
       width: 100%;
@@ -421,7 +429,7 @@ body {
       top: 3px;
 
       .vjs-volume-level {
-        background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcAQAAAAAyhWABAAAAAnRSTlMAAHaTzTgAAAAZSURBVHgBYwAB/g9EUv+JokCqiaT+U4MCAPKPS7WUUOc1AAAAAElFTkSuQmCC) no-repeat;
+        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcAQAAAAAyhWABAAAAAnRSTlMAAHaTzTgAAAAZSURBVHgBYwAB/g9EUv+JokCqiaT+U4MCAPKPS7WUUOc1AAAAAElFTkSuQmCC') no-repeat;
         background-size: 22px 14px;
         max-width: 22px;
         max-height: 14px;
index ebbed02d9bc434879e5a588525fffb9a6a0c08e4..8558fc837a18bdb008414c2d5fdd4d3a9d9f8565 100644 (file)
@@ -44,10 +44,8 @@ $playlist-menu-width: 350px;
       width: 20px;
       height: 20px;
       mask-image: url('#{$assets-path}/images/feather/x.svg');
-      -webkit-mask-image: url('#{$assets-path}/images/feather/x.svg');
-      background-color: white;
+      background-color: #fff;
       mask-size: cover;
-      -webkit-mask-size: cover;
     }
   }
 }
@@ -90,10 +88,8 @@ $playlist-menu-width: 350px;
   width: 22px;
   height: 22px;
   mask-image: url('#{$assets-path}/images/feather/list.svg');
-  -webkit-mask-image: url('#{$assets-path}/images/feather/list.svg');
-  background-color: white;
+  background-color: #fff;
   mask-size: cover;
-  -webkit-mask-size: cover;
   margin-bottom: 3px;
 }
 
@@ -133,9 +129,9 @@ $playlist-menu-width: 350px;
   }
 
   .item-player {
-    display: none;
-
     @include play-icon(20px, 16px);
+
+    display: none;
   }
 
   &.vjs-selected {
index 09c872ef74b3a6ac5c18d985f805368eeabbc6c4..74eee7d645bccc71ed88a5c930be4caffc56e083 100644 (file)
@@ -149,7 +149,7 @@ $setting-transition-easing: ease-out;
             background-color: inherit;
             padding: 8px 8px 13px 12px;
             margin-bottom: 5px;
-            border-bottom: 1px solid grey;
+            border-bottom: 1px solid #808080;
             text-align: left;
 
             &::before {
index a6af8da3303b3af0890124540f92cfc56618982b..94f4d1008e610621f78a2b28a960bbe955a23875 100644 (file)
@@ -51,4 +51,4 @@
       opacity: 1;
     }
   }
-}
\ No newline at end of file
+}
diff --git a/client/src/sass/player/stats.scss b/client/src/sass/player/stats.scss
new file mode 100644 (file)
index 0000000..6fcbcd9
--- /dev/null
@@ -0,0 +1,41 @@
+@import './_player-variables';
+
+$stats-width: 420px;
+$contextmenu-background-color: rgba(0, 0, 0, 0.6);
+
+.video-js {
+
+  .vjs-stats-content {
+    @include transition(opacity 0.1s);
+
+    position: absolute;
+    background-color: $contextmenu-background-color;
+    padding: 5px 0;
+    border-radius: 4px;
+    width: $stats-width;
+    min-width: 27em;
+    max-width: calc(100vw - 20px);
+    left: 10px;
+    top: 10px;
+    z-index: 64;
+    font-size: 12px;
+    line-height: 1.2;
+  }
+
+  .vjs-stats-close {
+    cursor: pointer;
+    position: absolute;
+    right: 3px;
+    top: 3px;
+    padding: 0;
+  }
+
+  .vjs-stats-list > div > div {
+    display: inline-block;
+    font-weight: 600;
+    padding: 0 .5em;
+    text-align: right;
+    width: 11.5em;
+    white-space: nowrap;
+  }
+}
index 7614bb3b67a01b9ba6c57637066027432bb5f4dd..8c9a6f784a3e66046a906b2bfd4752c618e79908 100644 (file)
@@ -11,15 +11,15 @@ $browser-context: 16;
 .video-js {
 
   .vjs-upnext-content {
+    @include transition(opacity 0.1s);
+
     font-size: 1.8em;
     pointer-events: auto;
     position: absolute;
     top: 0;
     bottom: 0;
-    background: rgba(0,0,0,0.6);
+    background: rgba(0, 0, 0, 0.6);
     width: 100%;
-
-    @include transition(opacity 0.1s);
   }
 
   .vjs-upnext-top {
@@ -77,7 +77,7 @@ $browser-context: 16;
     float: none;
     padding: 10px !important;
     font-size: 16px !important;
-    border: none;
+    border: 0;
   }
 
   .vjs-upnext-cancel-button,
@@ -86,7 +86,7 @@ $browser-context: 16;
   }
 
   .vjs-upnext-cancel-button:hover {
-    background-color: rgba(255,255,255,0.25);
+    background-color: rgba(255, 255, 255, 0.25);
     border-radius: 2px;
   }
 
@@ -95,6 +95,8 @@ $browser-context: 16;
   }
 
   .vjs-upnext-autoplay-icon {
+    @include transition(stroke-dasharray 0.1s cubic-bezier(0.4,0,1,1));
+
     position: absolute;
     top: 50%;
     left: 50%;
@@ -102,8 +104,6 @@ $browser-context: 16;
     height: 98px;
     margin: -49px 0 0 -49px;
     cursor: pointer;
-
-    @include transition(stroke-dasharray 0.1s cubic-bezier(0.4,0,1,1));
   }
 
 }
index 544d0039a5d3f1d26492334488b0e5c3a51e2b6a..6a4d89dff8722cb9d6dc8010885dc93ebfdaf8fd 100644 (file)
@@ -1,13 +1,18 @@
 @import '_variables';
 @import '_mixins';
 
+/* stylelint-disable */
 @import '~primeng/resources/primeng.css';
 
 // Override primeng style we don't want
-input[type="button"] {
+input[type=button] {
   border-radius: inherit;
 }
 
+p-table .p-datatable-header .caption {
+  margin-bottom: 15px;
+}
+
 // Taken from old nova light theme
 
 body .p-disabled {
@@ -511,10 +516,6 @@ p-table {
       .left-buttons {
         padding-left: 15px;
       }
-
-      .input-group-text {
-        background-color: transparent;
-      }
     }
   }
 
index cbe6bdd012896d4e3b7204a740d35f488d4b5b93..e32cce54ef9576caed1b0794263319c701c2f165 100644 (file)
@@ -21,7 +21,8 @@ video {
 }
 
 /* fill the entire space */
-html, body {
+html,
+body {
   height: 100%;
   margin: 0;
   background-color: #000;
@@ -70,18 +71,18 @@ html, body {
   text-align: center;
   width: 100%;
   height: 100%;
-  color: white;
+  color: #fff;
   box-sizing: border-box;
   font-family: sans-serif;
+}
 
-  #error-title {
-    font-size: 45px;
-    margin-bottom: 5px;
-  }
+#error-title {
+  font-size: 45px;
+  margin-bottom: 5px;
+}
 
-  #error-content {
-    font-size: 24px;
-  }
+#error-content {
+  font-size: 24px;
 }
 
 #placeholder-preview {
@@ -97,10 +98,10 @@ html, body {
 @media screen and (max-width: 300px) {
   #error-block {
     font-size: 36px;
+  }
 
-    #error-content {
-      font-size: 14px;
-    }
+  #error-content {
+    font-size: 14px;
   }
 }
 
index 85ce4e0f7ba771b810021992b361b6c5e32531c9..b9ac3e74e608015f0b2afc473a989c7a71e2b532 100644 (file)
@@ -15,7 +15,7 @@ body {
 }
 
 iframe {
-  border: none;
+  border: 0;
   border-radius: 8px;
   min-width: 200px;
   width: 100%;
@@ -41,7 +41,7 @@ aside {
   .icon {
     height: 100%;
     padding: 0 18px 0 32px;
-    background: white;
+    background: #fff;
     display: flex;
     align-items: center;
     margin-right: 0.5em;
@@ -62,13 +62,13 @@ header {
   width: 100%;
   height: 3.2em;
   background-color: #F1680D;
-  color: white;
+  color: #fff;
   //background-image: url(../../assets/images/backdrop/network-o.png);
   display: flex;
   flex-direction: row;
   align-items: center;
   margin-bottom: 1em;
-  box-shadow: 1px 0px 10px rgba(0,0,0,0.6);
+  box-shadow: 1px 0 10px rgba(0, 0, 0, 0.6);
   background-size: 50%;
   background-position: top left;
   padding-right: 1em;
@@ -87,13 +87,13 @@ header {
   display: flex;
   flex-wrap: wrap;
 
-  > * {
+  > * {
     flex-grow: 0;
   }
 }
 
 fieldset {
-  border: none;
+  border: 0;
   min-width: 8em;
   legend {
     border-bottom: 1px solid #ccc;
@@ -103,12 +103,12 @@ fieldset {
 
 button {
   background: #F1680D;
-  color: white;
+  color: #fff;
   font-weight: bold;
   border-radius: 5px;
   margin: 0;
   padding: 1em 1.25em;
-  border: none;
+  border: 0;
 }
 
 a {
@@ -118,7 +118,11 @@ a {
     text-decoration: underline;
   }
 
-  &, &:hover, &:focus, &:visited, &:active {
+  &,
+  &:hover,
+  &:focus,
+  &:visited,
+  &:active {
     color: #F44336;
   }
 }
index 8cd17204286a2416133f2d959b72950f17d05a6e..571314f221a075d4a83a2ce6d3669c2b6caeef80 100644 (file)
   dependencies:
     "@babel/highlight" "^7.12.13"
 
-"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8":
+"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.15", "@babel/compat-data@^7.13.8":
   version "7.13.15"
   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4"
   integrity sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@>=7.9.0":
+  version "7.13.16"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.16.tgz#7756ab24396cc9675f1c3fcd5b79fcce192ea96a"
+  integrity sha512-sXHpixBiWWFti0AV2Zq7avpTasr6sIAu7Y396c608541qAU2ui4a193m0KSQmfPSKFZLnQ3cvlKDOm3XkuXm3Q==
+  dependencies:
+    "@babel/code-frame" "^7.12.13"
+    "@babel/generator" "^7.13.16"
+    "@babel/helper-compilation-targets" "^7.13.16"
+    "@babel/helper-module-transforms" "^7.13.14"
+    "@babel/helpers" "^7.13.16"
+    "@babel/parser" "^7.13.16"
+    "@babel/template" "^7.12.13"
+    "@babel/traverse" "^7.13.15"
+    "@babel/types" "^7.13.16"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.2"
+    json5 "^2.1.2"
+    semver "^6.3.0"
+    source-map "^0.5.0"
+
 "@babel/core@^7.7.5", "@babel/core@^7.8.6":
   version "7.13.15"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.15.tgz#a6d40917df027487b54312202a06812c4f7792d0"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.13.16":
+  version "7.13.16"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.16.tgz#0befc287031a201d84cdfc173b46b320ae472d14"
+  integrity sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==
+  dependencies:
+    "@babel/types" "^7.13.16"
+    jsesc "^2.5.1"
+    source-map "^0.5.0"
+
 "@babel/helper-annotate-as-pure@^7.12.13":
   version "7.12.13"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab"
     browserslist "^4.14.5"
     semver "^6.3.0"
 
+"@babel/helper-compilation-targets@^7.13.16":
+  version "7.13.16"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz#6e91dccf15e3f43e5556dffe32d860109887563c"
+  integrity sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==
+  dependencies:
+    "@babel/compat-data" "^7.13.15"
+    "@babel/helper-validator-option" "^7.12.17"
+    browserslist "^4.14.5"
+    semver "^6.3.0"
+
 "@babel/helper-create-class-features-plugin@^7.13.0":
   version "7.13.11"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6"
     "@babel/traverse" "^7.13.0"
     "@babel/types" "^7.13.0"
 
+"@babel/helpers@^7.13.16":
+  version "7.13.17"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.17.tgz#b497c7a00e9719d5b613b8982bda6ed3ee94caf6"
+  integrity sha512-Eal4Gce4kGijo1/TGJdqp3WuhllaMLSrW6XcL0ulyUAQOuxHcCafZE8KHg9857gcTehsm/v7RcOx2+jp0Ryjsg==
+  dependencies:
+    "@babel/template" "^7.12.13"
+    "@babel/traverse" "^7.13.17"
+    "@babel/types" "^7.13.17"
+
 "@babel/highlight@^7.12.13":
   version "7.13.10"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8"
   integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==
 
+"@babel/parser@^7.13.16":
+  version "7.13.16"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.16.tgz#0f18179b0448e6939b1f3f5c4c355a3a9bcdfd37"
+  integrity sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==
+
 "@babel/plugin-proposal-async-generator-functions@^7.12.1":
   version "7.13.15"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz#80e549df273a3b3050431b148c892491df1bcc5b"
     debug "^4.1.0"
     globals "^11.1.0"
 
+"@babel/traverse@^7.13.17":
+  version "7.13.17"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.17.tgz#c85415e0c7d50ac053d758baec98b28b2ecfeea3"
+  integrity sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==
+  dependencies:
+    "@babel/code-frame" "^7.12.13"
+    "@babel/generator" "^7.13.16"
+    "@babel/helper-function-name" "^7.12.13"
+    "@babel/helper-split-export-declaration" "^7.12.13"
+    "@babel/parser" "^7.13.16"
+    "@babel/types" "^7.13.17"
+    debug "^4.1.0"
+    globals "^11.1.0"
+
 "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.14", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6":
   version "7.13.14"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.13.16", "@babel/types@^7.13.17":
+  version "7.13.17"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.17.tgz#48010a115c9fba7588b4437dd68c9469012b38b4"
+  integrity sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.12.11"
+    to-fast-properties "^2.0.0"
+
 "@discoveryjs/json-ext@0.5.2", "@discoveryjs/json-ext@^0.5.0":
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
     semver "7.3.4"
     semver-intersect "1.4.0"
 
+"@stylelint/postcss-css-in-js@^0.37.2":
+  version "0.37.2"
+  resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2"
+  integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==
+  dependencies:
+    "@babel/core" ">=7.9.0"
+
+"@stylelint/postcss-markdown@^0.36.2":
+  version "0.36.2"
+  resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz#0a540c4692f8dcdfc13c8e352c17e7bfee2bb391"
+  integrity sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==
+  dependencies:
+    remark "^13.0.0"
+    unist-util-find-all-after "^3.0.2"
+
 "@tootallnate/once@1":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
     "@types/linkify-it" "*"
     "@types/mdurl" "*"
 
+"@types/mdast@^3.0.0":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb"
+  integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/mdurl@*":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
   integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
 
+"@types/minimist@^1.2.0":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
+  integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
+
 "@types/mousetrap@1.6.3", "@types/mousetrap@^1.6.0":
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
   integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
 
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
+  integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   dependencies:
     source-map "^0.6.1"
 
+"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
+  integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
+
 "@types/video.js@^7.3.8":
   version "7.3.15"
   resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.15.tgz#42ff1bf598384ae5966bf6f64560197357e99033"
@@ -1893,28 +2006,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
-acorn-jsx@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
-  integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=
-  dependencies:
-    acorn "^3.0.4"
-
 acorn-walk@^8.0.0:
   version "8.0.2"
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
   integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
 
-acorn@^3.0.4:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-  integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
-
-acorn@^5.5.0:
-  version "5.7.4"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
-  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
-
 acorn@^6.4.1:
   version "6.4.2"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
@@ -1989,11 +2085,6 @@ ajv-errors@^1.0.0:
   resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
   integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
 
-ajv-keywords@^1.0.0:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
-  integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw=
-
 ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
@@ -2009,13 +2100,15 @@ ajv@6.12.6, ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^4.7.0:
-  version "4.11.8"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
-  integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=
+ajv@^8.0.1:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.2.0.tgz#c89d3380a784ce81b2085f48811c4c101df4c602"
+  integrity sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA==
   dependencies:
-    co "^4.6.0"
-    json-stable-stringify "^1.0.1"
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
 
 alphanum-sort@^1.0.0:
   version "1.0.2"
@@ -2048,11 +2141,6 @@ ansi-colors@^3.0.0:
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
   integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
 
-ansi-escapes@^1.1.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
-  integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
-
 ansi-escapes@^4.2.1:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -2210,7 +2298,7 @@ array-unique@^0.3.2:
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-arrify@^1.0.0:
+arrify@^1.0.0, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
@@ -2255,6 +2343,11 @@ ast-types-flow@0.0.7:
   resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
   integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
 
+astral-regex@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+  integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+
 async-each@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
@@ -2299,6 +2392,19 @@ autoprefixer@10.2.4:
     normalize-range "^0.1.2"
     postcss-value-parser "^4.1.0"
 
+autoprefixer@^9.8.6:
+  version "9.8.6"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
+  integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==
+  dependencies:
+    browserslist "^4.12.0"
+    caniuse-lite "^1.0.30001109"
+    colorette "^1.2.1"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^7.0.32"
+    postcss-value-parser "^4.1.0"
+
 aws-sign2@~0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -2338,11 +2444,21 @@ backo2@~1.0.2:
   resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
   integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
 
+bail@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
+  integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+balanced-match@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
+  integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
+
 base64-arraybuffer@0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
@@ -2692,6 +2808,17 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.1, browserslist@^4
     escalade "^3.1.1"
     node-releases "^1.1.71"
 
+browserslist@^4.12.0:
+  version "4.16.5"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.5.tgz#952825440bca8913c62d0021334cbe928ef062ae"
+  integrity sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==
+  dependencies:
+    caniuse-lite "^1.0.30001214"
+    colorette "^1.2.2"
+    electron-to-chromium "^1.3.719"
+    escalade "^3.1.1"
+    node-releases "^1.1.71"
+
 browserstack@^1.5.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3"
@@ -2893,13 +3020,6 @@ caller-callsite@^2.0.0:
   dependencies:
     callsites "^2.0.0"
 
-caller-path@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
-  integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=
-  dependencies:
-    callsites "^0.2.0"
-
 caller-path@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
@@ -2907,11 +3027,6 @@ caller-path@^2.0.0:
   dependencies:
     caller-callsite "^2.0.0"
 
-callsites@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
-  integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=
-
 callsites@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
@@ -2930,7 +3045,16 @@ camel-case@^4.1.1:
     pascal-case "^3.1.2"
     tslib "^2.0.3"
 
-camelcase@5.3.1, camelcase@^5.0.0:
+camelcase-keys@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
+  integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
+  dependencies:
+    camelcase "^5.3.1"
+    map-obj "^4.0.0"
+    quick-lru "^4.0.1"
+
+camelcase@5.3.1, camelcase@^5.0.0, camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2960,6 +3084,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001181, can
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9"
   integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==
 
+caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001214:
+  version "1.0.30001219"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz#5bfa5d0519f41f993618bd318f606a4c4c16156b"
+  integrity sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ==
+
 canonical-path@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d"
@@ -2977,7 +3106,7 @@ caseless@~0.12.0:
   dependencies:
     traverse ">=0.3.0 <0.4"
 
-chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -3005,6 +3134,21 @@ chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+character-entities-legacy@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
+  integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
+
+character-entities@^1.0.0:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
+  integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
+
+character-reference-invalid@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
+  integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
+
 chardet@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -3125,11 +3269,6 @@ circular-dependency-plugin@5.2.2:
   resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz#39e836079db1d3cf2f988dc48c5188a44058b600"
   integrity sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==
 
-circular-json@^0.3.1:
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
-  integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
-
 class-utils@^0.3.5:
   version "0.3.6"
   resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
@@ -3152,13 +3291,6 @@ clean-stack@^2.0.0:
   resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
   integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
 
-cli-cursor@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
-  integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=
-  dependencies:
-    restore-cursor "^1.0.1"
-
 cli-cursor@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -3171,11 +3303,6 @@ cli-spinners@^2.5.0:
   resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939"
   integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==
 
-cli-width@^2.0.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
-  integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
-
 cli-width@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
@@ -3226,16 +3353,18 @@ clone-deep@^4.0.1:
     kind-of "^6.0.2"
     shallow-clone "^3.0.0"
 
+clone-regexp@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
+  integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==
+  dependencies:
+    is-regexp "^2.0.0"
+
 clone@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
   integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
 
-co@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
-  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
-
 coa@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
@@ -3335,7 +3464,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.11.0, commander@^2.12.1, commander@^2.20.0, commander@^2.8.1:
+commander@^2.11.0, commander@^2.12.1, commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -3404,7 +3533,7 @@ concat-map@0.0.1:
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
-concat-stream@^1.4.6, concat-stream@^1.5.0:
+concat-stream@^1.5.0:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
   integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -3939,7 +4068,7 @@ date-format@^3.0.0:
   resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95"
   integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==
 
-debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3:
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -3967,7 +4096,15 @@ debug@~3.1.0:
   dependencies:
     ms "2.0.0"
 
-decamelize@^1.1.1, decamelize@^1.2.0:
+decamelize-keys@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
+  integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
+  dependencies:
+    decamelize "^1.1.0"
+    map-obj "^1.0.0"
+
+decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3996,11 +4133,6 @@ deep-equal@^1.0.1:
     object-keys "^1.1.1"
     regexp.prototype.flags "^1.2.0"
 
-deep-is@~0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
-  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
-
 deepmerge@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
@@ -4178,14 +4310,6 @@ doctrine@0.7.2:
     esutils "^1.1.6"
     isarray "0.0.1"
 
-doctrine@^1.2.2:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
-  integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
-  dependencies:
-    esutils "^2.0.2"
-    isarray "^1.0.0"
-
 dom-converter@^0.2:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@@ -4326,6 +4450,11 @@ electron-to-chromium@^1.3.712:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.712.tgz#ae467ffe5f95961c6d41ceefe858fc36eb53b38f"
   integrity sha512-3kRVibBeCM4vsgoHHGKHmPocLqtFAGTrebXxxtgKs87hNUzXrX2NuS3jnBys7IozCnw7viQlozxKkmty2KNfrw==
 
+electron-to-chromium@^1.3.719:
+  version "1.3.723"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.723.tgz#52769a75635342a4db29af5f1e40bd3dad02c877"
+  integrity sha512-L+WXyXI7c7+G1V8ANzRsPI5giiimLAUDC6Zs1ojHHPhYXb3k/iTABFmWjivEtsWrRQymjnO66/rO2ZTABGdmWg==
+
 elliptic@^6.5.3:
   version "6.5.4"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
@@ -4522,7 +4651,7 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14:
+es5-ext@^0.10.35, es5-ext@^0.10.50:
   version "0.10.53"
   resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
   integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
@@ -4531,7 +4660,7 @@ es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14:
     es6-symbol "~3.1.3"
     next-tick "~1.0.0"
 
-es6-iterator@2.0.3, es6-iterator@^2.0.3, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
+es6-iterator@2.0.3, es6-iterator@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
   integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
@@ -4540,18 +4669,6 @@ es6-iterator@2.0.3, es6-iterator@^2.0.3, es6-iterator@~2.0.1, es6-iterator@~2.0.
     es5-ext "^0.10.35"
     es6-symbol "^3.1.1"
 
-es6-map@^0.1.3:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
-  integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=
-  dependencies:
-    d "1"
-    es5-ext "~0.10.14"
-    es6-iterator "~2.0.1"
-    es6-set "~0.1.5"
-    es6-symbol "~3.1.1"
-    event-emitter "~0.3.5"
-
 es6-promise@^4.0.3:
   version "4.2.8"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
@@ -4564,26 +4681,7 @@ es6-promisify@^5.0.0:
   dependencies:
     es6-promise "^4.0.3"
 
-es6-set@~0.1.5:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
-  integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=
-  dependencies:
-    d "1"
-    es5-ext "~0.10.14"
-    es6-iterator "~2.0.1"
-    es6-symbol "3.1.1"
-    event-emitter "~0.3.5"
-
-es6-symbol@3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
-  integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
-  dependencies:
-    d "1"
-    es5-ext "~0.10.14"
-
-es6-symbol@^3.1.1, es6-symbol@~3.1.1, es6-symbol@~3.1.3:
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
   integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
@@ -4591,16 +4689,6 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.1, es6-symbol@~3.1.3:
     d "^1.0.1"
     ext "^1.1.2"
 
-es6-weak-map@^2.0.1:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
-  integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
-  dependencies:
-    d "1"
-    es5-ext "^0.10.46"
-    es6-iterator "^2.0.3"
-    es6-symbol "^3.1.1"
-
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4621,16 +4709,6 @@ escape-string-regexp@^4.0.0:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
-escope@^3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
-  integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=
-  dependencies:
-    es6-map "^0.1.3"
-    es6-weak-map "^2.0.1"
-    esrecurse "^4.1.0"
-    estraverse "^4.1.1"
-
 eslint-scope@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
@@ -4639,53 +4717,6 @@ eslint-scope@^4.0.3:
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
-eslint@^2.7.0:
-  version "2.13.1"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11"
-  integrity sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=
-  dependencies:
-    chalk "^1.1.3"
-    concat-stream "^1.4.6"
-    debug "^2.1.1"
-    doctrine "^1.2.2"
-    es6-map "^0.1.3"
-    escope "^3.6.0"
-    espree "^3.1.6"
-    estraverse "^4.2.0"
-    esutils "^2.0.2"
-    file-entry-cache "^1.1.1"
-    glob "^7.0.3"
-    globals "^9.2.0"
-    ignore "^3.1.2"
-    imurmurhash "^0.1.4"
-    inquirer "^0.12.0"
-    is-my-json-valid "^2.10.0"
-    is-resolvable "^1.0.0"
-    js-yaml "^3.5.1"
-    json-stable-stringify "^1.0.0"
-    levn "^0.3.0"
-    lodash "^4.0.0"
-    mkdirp "^0.5.0"
-    optionator "^0.8.1"
-    path-is-absolute "^1.0.0"
-    path-is-inside "^1.0.1"
-    pluralize "^1.2.1"
-    progress "^1.1.8"
-    require-uncached "^1.0.2"
-    shelljs "^0.6.0"
-    strip-json-comments "~1.0.1"
-    table "^3.7.8"
-    text-table "~0.2.0"
-    user-home "^2.0.0"
-
-espree@^3.1.6:
-  version "3.5.4"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
-  integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==
-  dependencies:
-    acorn "^5.5.0"
-    acorn-jsx "^3.0.0"
-
 esprima@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
@@ -4698,7 +4729,7 @@ esrecurse@^4.1.0:
   dependencies:
     estraverse "^5.2.0"
 
-estraverse@^4.1.1, estraverse@^4.2.0:
+estraverse@^4.1.1:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
   integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
@@ -4728,14 +4759,6 @@ etag@~1.8.1:
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 
-event-emitter@~0.3.5:
-  version "0.3.5"
-  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
-  integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
-  dependencies:
-    d "1"
-    es5-ext "~0.10.14"
-
 eventemitter3@^4.0.0, eventemitter3@^4.0.3:
   version "4.0.7"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@@ -4802,10 +4825,12 @@ execa@^5.0.0:
     signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
-exit-hook@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
-  integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
+execall@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45"
+  integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==
+  dependencies:
+    clone-regexp "^2.1.0"
 
 exit@^0.1.2:
   version "0.1.2"
@@ -4926,7 +4951,7 @@ fast-deep-equal@^3.1.1:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
-fast-glob@^3.1.1, fast-glob@^3.2.4:
+fast-glob@^3.1.1, fast-glob@^3.2.4, fast-glob@^3.2.5:
   version "3.2.5"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
   integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==
@@ -4943,11 +4968,6 @@ fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@^2.0.0:
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-fast-levenshtein@~2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
-  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-
 fastest-levenshtein@^1.0.12:
   version "1.0.12"
   resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
@@ -4977,14 +4997,6 @@ figgy-pudding@^3.5.1:
   resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
   integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
 
-figures@^1.3.5:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
-  integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=
-  dependencies:
-    escape-string-regexp "^1.0.5"
-    object-assign "^4.1.0"
-
 figures@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -4992,13 +5004,12 @@ figures@^3.0.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
-file-entry-cache@^1.1.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8"
-  integrity sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
   dependencies:
-    flat-cache "^1.2.1"
-    object-assign "^4.0.1"
+    flat-cache "^3.0.4"
 
 file-loader@6.2.0, file-loader@^6.0.0:
   version "6.2.0"
@@ -5091,21 +5102,24 @@ find-up@^4.0.0, find-up@^4.1.0:
     locate-path "^5.0.0"
     path-exists "^4.0.0"
 
-flat-cache@^1.2.1:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f"
-  integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
   dependencies:
-    circular-json "^0.3.1"
-    graceful-fs "^4.1.2"
-    rimraf "~2.6.2"
-    write "^0.2.1"
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
 
 flatted@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
   integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
 
+flatted@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
+  integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
+
 flush-write-stream@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -5178,13 +5192,6 @@ from2@^2.1.0:
     inherits "^2.0.1"
     readable-stream "^2.0.0"
 
-front-matter@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb"
-  integrity sha1-91mDufL0E75ljJPf172M5AePXNs=
-  dependencies:
-    js-yaml "^3.4.6"
-
 fs-chunk-store@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.3.tgz#21e51f1833a84a07cb5e911d058dae084030375a"
@@ -5206,15 +5213,6 @@ fs-extra@4.0.2:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs-extra@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
-  integrity sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=
-  dependencies:
-    graceful-fs "^4.1.2"
-    jsonfile "^3.0.0"
-    universalify "^0.1.0"
-
 fs-extra@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
@@ -5278,20 +5276,6 @@ gauge@~2.7.3:
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
-generate-function@^2.0.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
-  integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
-  dependencies:
-    is-property "^1.0.2"
-
-generate-object-property@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
-  integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=
-  dependencies:
-    is-property "^1.0.0"
-
 gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
   version "1.0.0-beta.2"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -5382,7 +5366,7 @@ glob@7.1.2:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1:
+glob@7.1.6, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -5394,6 +5378,22 @@ glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glo
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+global-modules@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
+  integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
+  dependencies:
+    global-prefix "^3.0.0"
+
+global-prefix@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
+  integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
+  dependencies:
+    ini "^1.3.5"
+    kind-of "^6.0.2"
+    which "^1.3.1"
+
 global@4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
@@ -5415,12 +5415,7 @@ globals@^11.1.0:
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
   integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 
-globals@^9.2.0:
-  version "9.18.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
-  integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
-
-globby@^11.0.1:
+globby@^11.0.1, globby@^11.0.3:
   version "11.0.3"
   resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb"
   integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==
@@ -5455,21 +5450,17 @@ globby@^6.1.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-globule@^1.0.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4"
-  integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==
-  dependencies:
-    glob "~7.1.1"
-    lodash "~4.17.10"
-    minimatch "~3.0.2"
+globjoin@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
+  integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=
 
-gonzales-pe-sl@^4.2.3:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6"
-  integrity sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y=
+gonzales-pe@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
+  integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
   dependencies:
-    minimist "1.1.x"
+    minimist "^1.2.5"
 
 graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4:
   version "4.2.6"
@@ -5501,6 +5492,11 @@ har-validator@~5.1.3:
     ajv "^6.12.3"
     har-schema "^2.0.0"
 
+hard-rejection@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
+  integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -5699,6 +5695,11 @@ html-minifier-terser@^5.0.1, html-minifier-terser@^5.1.1:
     relateurl "^0.2.7"
     terser "^4.6.3"
 
+html-tags@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
+  integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
+
 html-webpack-plugin@^4.0.3:
   version "4.5.2"
   resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz#76fc83fa1a0f12dd5f7da0404a54e2699666bc12"
@@ -5714,7 +5715,7 @@ html-webpack-plugin@^4.0.3:
     tapable "^1.1.3"
     util.promisify "1.0.0"
 
-htmlparser2@^3.10.1:
+htmlparser2@^3.10.0, htmlparser2@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
   integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@@ -5912,12 +5913,7 @@ ignore-walk@^3.0.3:
   dependencies:
     minimatch "^3.0.4"
 
-ignore@^3.1.2:
-  version "3.3.10"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
-  integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
-
-ignore@^5.1.4:
+ignore@^5.1.4, ignore@^5.1.8:
   version "5.1.8"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
   integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@@ -5955,6 +5951,11 @@ import-fresh@^3.2.1:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
 
+import-lazy@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
+  integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
+
 import-local@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
@@ -6024,7 +6025,7 @@ ini@2.0.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
   integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
 
-ini@^1.3.4:
+ini@^1.3.4, ini@^1.3.5:
   version "1.3.8"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
   integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
@@ -6048,25 +6049,6 @@ inquirer@7.3.3:
     strip-ansi "^6.0.0"
     through "^2.3.6"
 
-inquirer@^0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
-  integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=
-  dependencies:
-    ansi-escapes "^1.1.0"
-    ansi-regex "^2.0.0"
-    chalk "^1.0.0"
-    cli-cursor "^1.0.1"
-    cli-width "^2.0.0"
-    figures "^1.3.5"
-    lodash "^4.3.0"
-    readline2 "^1.0.1"
-    run-async "^0.1.0"
-    rx-lite "^3.1.2"
-    string-width "^1.0.1"
-    strip-ansi "^3.0.0"
-    through "^2.3.6"
-
 internal-ip@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
@@ -6136,6 +6118,19 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-alphabetical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
+  integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
+
+is-alphanumerical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
+  integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
+  dependencies:
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+
 is-arguments@^1.0.4:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
@@ -6189,6 +6184,11 @@ is-buffer@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
+is-buffer@^2.0.0:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
+  integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
+
 is-callable@^1.1.4, is-callable@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
@@ -6232,6 +6232,11 @@ is-date-object@^1.0.1:
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
   integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
 
+is-decimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
+  integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
+
 is-descriptor@^0.1.0:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@@ -6318,6 +6323,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
+is-hexadecimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
+  integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
+
 is-interactive@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
@@ -6328,22 +6338,6 @@ is-lambda@^1.0.1:
   resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
   integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=
 
-is-my-ip-valid@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
-  integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==
-
-is-my-json-valid@^2.10.0:
-  version "2.20.5"
-  resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz#5eca6a8232a687f68869b7361be1612e7512e5df"
-  integrity sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A==
-  dependencies:
-    generate-function "^2.0.0"
-    generate-object-property "^1.1.0"
-    is-my-ip-valid "^1.0.0"
-    jsonpointer "^4.0.0"
-    xtend "^4.0.0"
-
 is-negative-zero@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
@@ -6409,6 +6403,16 @@ is-path-inside@^2.1.0:
   dependencies:
     path-is-inside "^1.0.2"
 
+is-plain-obj@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+  integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+
+is-plain-obj@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
 is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -6421,11 +6425,6 @@ is-plain-object@^5.0.0:
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
   integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
 
-is-property@^1.0.0, is-property@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
-  integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
-
 is-regex@^1.0.4, is-regex@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
@@ -6434,6 +6433,11 @@ is-regex@^1.0.4, is-regex@^1.1.2:
     call-bind "^1.0.2"
     has-symbols "^1.0.1"
 
+is-regexp@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
+  integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
+
 is-resolvable@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
@@ -6628,7 +6632,7 @@ js-tokens@^4.0.0:
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
-js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4:
+js-yaml@^3.13.1:
   version "3.14.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
   integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@@ -6671,18 +6675,16 @@ json-schema-traverse@^0.4.1:
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
 json-schema@0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
   integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
 
-json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
-  integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
-  dependencies:
-    jsonify "~0.0.0"
-
 json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -6712,13 +6714,6 @@ jsonc-parser@3.0.0:
   resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22"
   integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==
 
-jsonfile@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
-  integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
 jsonfile@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@@ -6726,21 +6721,11 @@ jsonfile@^4.0.0:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
-jsonify@~0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-  integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
-
 jsonparse@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
 
-jsonpointer@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc"
-  integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==
-
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -6887,7 +6872,7 @@ kind-of@^5.0.0:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
   integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
 
-kind-of@^6.0.0, kind-of@^6.0.2:
+kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
   version "6.0.3"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@@ -6897,10 +6882,10 @@ klona@^2.0.3, klona@^2.0.4:
   resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
   integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
 
-known-css-properties@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4"
-  integrity sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ==
+known-css-properties@^0.21.0:
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.21.0.tgz#15fbd0bbb83447f3ce09d8af247ed47c68ede80d"
+  integrity sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw==
 
 last-one-wins@^1.0.4:
   version "1.0.4"
@@ -6940,14 +6925,6 @@ less@4.1.1:
     needle "^2.5.2"
     source-map "~0.6.0"
 
-levn@^0.3.0, levn@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
-  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
-  dependencies:
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-
 license-webpack-plugin@2.3.11:
   version "2.3.11"
   resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz#0d93188a31fce350a44c86212badbaf33dcd29d8"
@@ -7061,32 +7038,37 @@ lodash-es@^4.17.4:
   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
   integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
 
-lodash.capitalize@^4.1.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
-  integrity sha1-+CbJtOKoUR2E46yinbBeGk87cqk=
+lodash.clonedeep@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
 
-lodash.kebabcase@^4.0.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
-  integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY=
+lodash.flatten@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
 
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
+lodash.truncate@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
+  integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
+
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.3.0, lodash@~4.17.10:
+lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-log-symbols@^4.0.0:
+log-symbols@^4.0.0, log-symbols@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
   integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
@@ -7110,6 +7092,11 @@ loglevel@^1.6.8:
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
   integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
 
+longest-streak@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
+  integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
+
 lower-case@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@@ -7220,6 +7207,16 @@ map-cache@^0.2.2:
   resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
   integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
 
+map-obj@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+  integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
+
+map-obj@^4.0.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7"
+  integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==
+
 map-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -7238,6 +7235,11 @@ markdown-it@12.0.4:
     mdurl "^1.0.1"
     uc.micro "^1.0.5"
 
+mathml-tag-names@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
+  integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
+
 md5.js@^1.3.4:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -7247,6 +7249,34 @@ md5.js@^1.3.4:
     inherits "^2.0.1"
     safe-buffer "^5.1.2"
 
+mdast-util-from-markdown@^0.8.0:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c"
+  integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==
+  dependencies:
+    "@types/mdast" "^3.0.0"
+    mdast-util-to-string "^2.0.0"
+    micromark "~2.11.0"
+    parse-entities "^2.0.0"
+    unist-util-stringify-position "^2.0.0"
+
+mdast-util-to-markdown@^0.6.0:
+  version "0.6.5"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz#b33f67ca820d69e6cc527a93d4039249b504bebe"
+  integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    longest-streak "^2.0.0"
+    mdast-util-to-string "^2.0.0"
+    parse-entities "^2.0.0"
+    repeat-string "^1.0.0"
+    zwitch "^1.0.0"
+
+mdast-util-to-string@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b"
+  integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==
+
 mdn-data@2.0.14:
   version "2.0.14"
   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
@@ -7306,6 +7336,24 @@ memory-fs@^0.5.0:
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
+meow@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
+  integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
+  dependencies:
+    "@types/minimist" "^1.2.0"
+    camelcase-keys "^6.2.2"
+    decamelize "^1.2.0"
+    decamelize-keys "^1.1.0"
+    hard-rejection "^2.1.0"
+    minimist-options "4.1.0"
+    normalize-package-data "^3.0.0"
+    read-pkg-up "^7.0.1"
+    redent "^3.0.0"
+    trim-newlines "^3.0.0"
+    type-fest "^0.18.0"
+    yargs-parser "^20.2.3"
+
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -7328,16 +7376,19 @@ merge2@^1.3.0:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-merge@^1.2.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
-  integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
-
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 
+micromark@~2.11.0:
+  version "2.11.4"
+  resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a"
+  integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==
+  dependencies:
+    debug "^4.0.0"
+    parse-entities "^2.0.0"
+
 micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@@ -7357,7 +7408,7 @@ micromatch@^3.1.10, micromatch@^3.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-micromatch@^4.0.0, micromatch@^4.0.2:
+micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
   integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
@@ -7417,6 +7468,11 @@ min-document@^2.19.0:
   dependencies:
     dom-walk "^0.1.0"
 
+min-indent@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+
 mini-css-extract-plugin@1.3.5:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz#252166e78879c106e0130f229d44e0cbdfcebed3"
@@ -7445,17 +7501,21 @@ minimalistic-crypto-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
 
-minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@3.0.4, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@1.1.x:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
-  integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=
+minimist-options@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
+  integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
+  dependencies:
+    arrify "^1.0.1"
+    is-plain-obj "^1.1.0"
+    kind-of "^6.0.3"
 
 minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.5"
@@ -7553,7 +7613,7 @@ mkdirp-classic@^0.5.2:
   resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
   integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
 
-mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
+mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
   integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -7655,11 +7715,6 @@ multistream@^4.0.1, multistream@^4.1.0:
     once "^1.4.0"
     readable-stream "^3.6.0"
 
-mute-stream@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
-  integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=
-
 mute-stream@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
@@ -7818,7 +7873,7 @@ nopt@^5.0.0:
   dependencies:
     abbrev "1"
 
-normalize-package-data@^2.3.2:
+normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
   integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -7828,6 +7883,16 @@ normalize-package-data@^2.3.2:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-package-data@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.2.tgz#cae5c410ae2434f9a6c1baa65d5bc3b9366c8699"
+  integrity sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==
+  dependencies:
+    hosted-git-info "^4.0.1"
+    resolve "^1.20.0"
+    semver "^7.3.4"
+    validate-npm-package-license "^3.0.1"
+
 normalize-path@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@@ -7845,6 +7910,11 @@ normalize-range@^0.1.2:
   resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
   integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
 
+normalize-selector@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
+  integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
+
 normalize-url@^3.0.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
@@ -7961,6 +8031,11 @@ nth-check@^1.0.2:
   dependencies:
     boolbase "~1.0.0"
 
+num2fraction@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+  integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -8070,11 +8145,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
   dependencies:
     wrappy "1"
 
-onetime@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
-  integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
-
 onetime@^5.1.0, onetime@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
@@ -8102,18 +8172,6 @@ opn@^5.5.0:
   dependencies:
     is-wsl "^1.1.0"
 
-optionator@^0.8.1:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
-  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
-  dependencies:
-    deep-is "~0.1.3"
-    fast-levenshtein "~2.0.6"
-    levn "~0.3.0"
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-    word-wrap "~1.2.3"
-
 ora@5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f"
@@ -8140,11 +8198,6 @@ os-browserify@^0.3.0:
   resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
   integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
 
-os-homedir@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-
 os-locale@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
@@ -8328,6 +8381,18 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
     pbkdf2 "^3.0.3"
     safe-buffer "^5.1.1"
 
+parse-entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
+  integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
+  dependencies:
+    character-entities "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    character-reference-invalid "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 parse-json@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@@ -8572,11 +8637,6 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
   dependencies:
     find-up "^4.0.0"
 
-pluralize@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
-  integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=
-
 pngjs@^3.3.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
@@ -8659,6 +8719,13 @@ postcss-discard-overridden@^4.0.1:
   dependencies:
     postcss "^7.0.0"
 
+postcss-html@^0.36.0:
+  version "0.36.0"
+  resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204"
+  integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==
+  dependencies:
+    htmlparser2 "^3.10.0"
+
 postcss-import@14.0.0:
   version "14.0.0"
   resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.0.0.tgz#3ed1dadac5a16650bde3f4cdea6633b9c3c78296"
@@ -8668,6 +8735,13 @@ postcss-import@14.0.0:
     read-cache "^1.0.0"
     resolve "^1.1.7"
 
+postcss-less@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad"
+  integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==
+  dependencies:
+    postcss "^7.0.14"
+
 postcss-loader@4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-4.2.0.tgz#f6993ea3e0f46600fb3ee49bbd010448123a7db4"
@@ -8679,6 +8753,11 @@ postcss-loader@4.2.0:
     schema-utils "^3.0.0"
     semver "^7.3.4"
 
+postcss-media-query-parser@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
+  integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=
+
 postcss-merge-longhand@^4.0.11:
   version "4.0.11"
   resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24"
@@ -8879,6 +8958,33 @@ postcss-reduce-transforms@^4.0.2:
     postcss "^7.0.0"
     postcss-value-parser "^3.0.0"
 
+postcss-resolve-nested-selector@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
+  integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=
+
+postcss-safe-parser@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96"
+  integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==
+  dependencies:
+    postcss "^7.0.26"
+
+postcss-sass@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3"
+  integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==
+  dependencies:
+    gonzales-pe "^4.3.0"
+    postcss "^7.0.21"
+
+postcss-scss@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383"
+  integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==
+  dependencies:
+    postcss "^7.0.6"
+
 postcss-selector-parser@^3.0.0:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270"
@@ -8898,6 +9004,22 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
     uniq "^1.0.1"
     util-deprecate "^1.0.2"
 
+postcss-selector-parser@^6.0.5:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.5.tgz#042d74e137db83e6f294712096cb413f5aa612c4"
+  integrity sha512-aFYPoYmXbZ1V6HZaSvat08M97A8HqO6Pjz+PiNpw/DhuRrC72XWAdp3hL6wusDCN31sSmcZyMGa2hZEuX+Xfhg==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-sorting@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-5.0.1.tgz#10d5d0059eea8334dacc820c0121864035bc3f11"
+  integrity sha512-Y9fUFkIhfrm6i0Ta3n+89j56EFqaNRdUKqXyRp6kvTcSXnmgEjaVowCXH+JBe9+YKWqd4nc28r2sgwnzJalccA==
+  dependencies:
+    lodash "^4.17.14"
+    postcss "^7.0.17"
+
 postcss-svgo@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e"
@@ -8907,6 +9029,11 @@ postcss-svgo@^4.0.3:
     postcss-value-parser "^3.0.0"
     svgo "^1.0.0"
 
+postcss-syntax@^0.36.2:
+  version "0.36.2"
+  resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c"
+  integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==
+
 postcss-unique-selectors@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac"
@@ -8944,7 +9071,7 @@ postcss@8.2.4:
     nanoid "^3.1.20"
     source-map "^0.6.1"
 
-postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27:
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.31, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6:
   version "7.0.35"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
   integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
@@ -8962,11 +9089,6 @@ postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.8:
     nanoid "^3.1.22"
     source-map "^0.6.1"
 
-prelude-ls@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
-  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
-
 pretty-bytes@^5.3.0:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -9002,11 +9124,6 @@ process@~0.5.1:
   resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
   integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=
 
-progress@^1.1.8:
-  version "1.1.8"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
-  integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
-
 promise-inflight@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@@ -9190,6 +9307,11 @@ queue-microtask@^1.2.0, queue-microtask@^1.2.2:
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
+quick-lru@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
+  integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
+
 random-access-file@^2.0.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.0.tgz#b49b999efefb374afb7587f219071fec5ce66546"
@@ -9286,6 +9408,15 @@ read-pkg-up@^2.0.0:
     find-up "^2.0.0"
     read-pkg "^2.0.0"
 
+read-pkg-up@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
+  dependencies:
+    find-up "^4.1.0"
+    read-pkg "^5.2.0"
+    type-fest "^0.8.1"
+
 read-pkg@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
@@ -9295,6 +9426,16 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
+read-pkg@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
 "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@@ -9333,15 +9474,6 @@ readdirp@~3.5.0:
   dependencies:
     picomatch "^2.2.1"
 
-readline2@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
-  integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=
-  dependencies:
-    code-point-at "^1.0.0"
-    is-fullwidth-code-point "^1.0.0"
-    mute-stream "0.0.5"
-
 rechoir@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca"
@@ -9354,6 +9486,14 @@ record-cache@^1.0.2:
   resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.0.tgz#f8a467a691a469584b26e88d36b18afdb3932037"
   integrity sha512-u8rbtLEJV7HRacl/ZYwSBFD8NFyB3PfTTfGLP37IW3hftQCwu6z4Q2RLyxo1YJUNRTEzJfpLpGwVuEYdaIkG9Q==
 
+redent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
+  integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+  dependencies:
+    indent-string "^4.0.0"
+    strip-indent "^3.0.0"
+
 reflect-metadata@^0.1.2:
   version "0.1.13"
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@@ -9433,6 +9573,29 @@ relateurl@^0.2.7:
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
   integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
 
+remark-parse@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640"
+  integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==
+  dependencies:
+    mdast-util-from-markdown "^0.8.0"
+
+remark-stringify@^9.0.0:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-9.0.1.tgz#576d06e910548b0a7191a71f27b33f1218862894"
+  integrity sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==
+  dependencies:
+    mdast-util-to-markdown "^0.6.0"
+
+remark@^13.0.0:
+  version "13.0.0"
+  resolved "https://registry.yarnpkg.com/remark/-/remark-13.0.0.tgz#d15d9bf71a402f40287ebe36067b66d54868e425"
+  integrity sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==
+  dependencies:
+    remark-parse "^9.0.0"
+    remark-stringify "^9.0.0"
+    unified "^9.1.0"
+
 remove-trailing-separator@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -9472,7 +9635,7 @@ repeat-element@^1.1.2:
   resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
   integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
 
-repeat-string@^1.6.1:
+repeat-string@^1.0.0, repeat-string@^1.6.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
   integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
@@ -9508,6 +9671,11 @@ require-directory@^2.1.1:
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
   integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
 
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
 require-main-filename@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
@@ -9518,14 +9686,6 @@ require-main-filename@^2.0.0:
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
   integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
 
-require-uncached@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
-  integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=
-  dependencies:
-    caller-path "^0.1.0"
-    resolve-from "^1.0.0"
-
 requires-port@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@@ -9545,11 +9705,6 @@ resolve-cwd@^3.0.0:
   dependencies:
     resolve-from "^5.0.0"
 
-resolve-from@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
-  integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=
-
 resolve-from@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@@ -9594,7 +9749,7 @@ resolve@1.19.0:
     is-core-module "^2.1.0"
     path-parse "^1.0.6"
 
-resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.9.0:
+resolve@^1.1.7, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.9.0:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -9602,14 +9757,6 @@ resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.9.0:
     is-core-module "^2.2.0"
     path-parse "^1.0.6"
 
-restore-cursor@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
-  integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=
-  dependencies:
-    exit-hook "^1.0.0"
-    onetime "^1.0.0"
-
 restore-cursor@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@@ -9680,13 +9827,6 @@ rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3:
   dependencies:
     glob "^7.1.3"
 
-rimraf@~2.6.2:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
-  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
-  dependencies:
-    glob "^7.1.3"
-
 ripemd160@^2.0.0, ripemd160@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -9717,13 +9857,6 @@ rollup@2.38.4:
   optionalDependencies:
     fsevents "~2.3.1"
 
-run-async@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
-  integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=
-  dependencies:
-    once "^1.3.0"
-
 run-async@^2.4.0:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -9767,11 +9900,6 @@ rust-result@^1.0.0:
   dependencies:
     individual "^2.0.0"
 
-rx-lite@^3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
-  integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=
-
 rxjs@6.6.3:
   version "6.6.3"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
@@ -9828,26 +9956,6 @@ sanitize-html@^2.1.2:
     parse-srcset "^1.0.2"
     postcss "^8.0.2"
 
-sass-lint@^1.13.1:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.13.1.tgz#5fd2b2792e9215272335eb0f0dc607f61e8acc8f"
-  integrity sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==
-  dependencies:
-    commander "^2.8.1"
-    eslint "^2.7.0"
-    front-matter "2.1.2"
-    fs-extra "^3.0.1"
-    glob "^7.0.0"
-    globule "^1.0.0"
-    gonzales-pe-sl "^4.2.3"
-    js-yaml "^3.5.4"
-    known-css-properties "^0.3.0"
-    lodash.capitalize "^4.1.0"
-    lodash.kebabcase "^4.0.0"
-    merge "^1.2.0"
-    path-is-absolute "^1.0.0"
-    util "^0.10.3"
-
 sass-loader@10.1.1, sass-loader@^10:
   version "10.1.1"
   resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.1.1.tgz#4ddd5a3d7638e7949065dd6e9c7c04037f7e663d"
@@ -10125,11 +10233,6 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-shelljs@^0.6.0:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8"
-  integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=
-
 signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@@ -10202,10 +10305,14 @@ slash@^3.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
-slice-ansi@0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
-  integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
+slice-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+  integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
+  dependencies:
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
 
 smart-buffer@^4.1.0:
   version "4.1.0"
@@ -10447,6 +10554,11 @@ spdy@^4.0.2:
     select-hose "^2.0.0"
     spdy-transport "^3.0.0"
 
+specificity@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
+  integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
+
 speed-measure-webpack-plugin@1.4.2:
   version "1.4.2"
   resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.4.2.tgz#1608e62d3bdb45f01810010e1b5bfedefedfa58f"
@@ -10634,7 +10746,7 @@ string-width@^3.0.0, string-width@^3.1.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
-string-width@^4.1.0, string-width@^4.2.0:
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
   integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
@@ -10724,10 +10836,12 @@ strip-final-newline@^2.0.0:
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-json-comments@~1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
-  integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
+strip-indent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
+  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
+  dependencies:
+    min-indent "^1.0.0"
 
 style-loader@2.0.0:
   version "2.0.0"
@@ -10737,6 +10851,11 @@ style-loader@2.0.0:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
 
+style-search@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
+  integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
+
 stylehacks@^4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@@ -10746,6 +10865,88 @@ stylehacks@^4.0.0:
     postcss "^7.0.0"
     postcss-selector-parser "^3.0.0"
 
+stylelint-config-sass-guidelines@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/stylelint-config-sass-guidelines/-/stylelint-config-sass-guidelines-8.0.0.tgz#e92279aa052a04e822dd096d7c46c8e37d4b3406"
+  integrity sha512-v21iDWtzpfhuKJlYKpoE1vjp+GT8Cr6ZBWwMx/jf+eCEblZgAIDVVjgAELoDLhVj17DcEFwlIKJBMfrdAmXg0Q==
+  dependencies:
+    stylelint-order "^4.0.0"
+    stylelint-scss "^3.18.0"
+
+stylelint-order@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-4.1.0.tgz#692d05b7d0c235ac66fcf5ea1d9e5f08a76747f6"
+  integrity sha512-sVTikaDvMqg2aJjh4r48jsdfmqLT+nqB1MOsaBnvM3OwLx4S+WXcsxsgk5w18h/OZoxZCxuyXMh61iBHcj9Qiw==
+  dependencies:
+    lodash "^4.17.15"
+    postcss "^7.0.31"
+    postcss-sorting "^5.0.1"
+
+stylelint-scss@^3.18.0:
+  version "3.19.0"
+  resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.19.0.tgz#528006d5a4c5a0f1f4d709b02fd3f626ed66d742"
+  integrity sha512-Ic5bsmpS4wVucOw44doC1Yi9f5qbeVL4wPFiEOaUElgsOuLEN6Ofn/krKI8BeNL2gAn53Zu+IcVV4E345r6rBw==
+  dependencies:
+    lodash "^4.17.15"
+    postcss-media-query-parser "^0.2.3"
+    postcss-resolve-nested-selector "^0.1.1"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+stylelint@^13.13.0:
+  version "13.13.0"
+  resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.13.0.tgz#1a33bffde765920ac985f16ae6250ff914b27804"
+  integrity sha512-jvkM1iuH88vAvjdKPwPm6abiMP2/D/1chbfb+4GVONddOOskHuCXc0loyrLdxO1AwwH6jdnjYskkTKHQD7cXwQ==
+  dependencies:
+    "@stylelint/postcss-css-in-js" "^0.37.2"
+    "@stylelint/postcss-markdown" "^0.36.2"
+    autoprefixer "^9.8.6"
+    balanced-match "^2.0.0"
+    chalk "^4.1.0"
+    cosmiconfig "^7.0.0"
+    debug "^4.3.1"
+    execall "^2.0.0"
+    fast-glob "^3.2.5"
+    fastest-levenshtein "^1.0.12"
+    file-entry-cache "^6.0.1"
+    get-stdin "^8.0.0"
+    global-modules "^2.0.0"
+    globby "^11.0.3"
+    globjoin "^0.1.4"
+    html-tags "^3.1.0"
+    ignore "^5.1.8"
+    import-lazy "^4.0.0"
+    imurmurhash "^0.1.4"
+    known-css-properties "^0.21.0"
+    lodash "^4.17.21"
+    log-symbols "^4.1.0"
+    mathml-tag-names "^2.1.3"
+    meow "^9.0.0"
+    micromatch "^4.0.4"
+    normalize-selector "^0.2.0"
+    postcss "^7.0.35"
+    postcss-html "^0.36.0"
+    postcss-less "^3.1.4"
+    postcss-media-query-parser "^0.2.3"
+    postcss-resolve-nested-selector "^0.1.1"
+    postcss-safe-parser "^4.0.2"
+    postcss-sass "^0.4.4"
+    postcss-scss "^2.1.1"
+    postcss-selector-parser "^6.0.5"
+    postcss-syntax "^0.36.2"
+    postcss-value-parser "^4.1.0"
+    resolve-from "^5.0.0"
+    slash "^3.0.0"
+    specificity "^0.4.1"
+    string-width "^4.2.2"
+    strip-ansi "^6.0.0"
+    style-search "^0.1.0"
+    sugarss "^2.0.0"
+    svg-tags "^1.0.0"
+    table "^6.5.1"
+    v8-compile-cache "^2.3.0"
+    write-file-atomic "^3.0.3"
+
 stylus-loader@4.3.3:
   version "4.3.3"
   resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-4.3.3.tgz#381bb6341272ac50bcdfd0b877707eac99b6b757"
@@ -10771,6 +10972,13 @@ stylus@0.54.8:
     semver "^6.3.0"
     source-map "^0.7.3"
 
+sugarss@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d"
+  integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==
+  dependencies:
+    postcss "^7.0.2"
+
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -10797,6 +11005,11 @@ supports-color@^7.0.0, supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+svg-tags@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
+  integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
+
 svgo@^1.0.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
@@ -10821,17 +11034,18 @@ symbol-observable@3.0.0:
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-3.0.0.tgz#eea8f6478c651018e059044268375c408c15c533"
   integrity sha512-6tDOXSHiVjuCaasQSWTmHUWn4PuG7qa3+1WT031yTc/swT7+rLiw3GOrFxaH1E3lLP09dH3bVuVDf2gK5rxG3Q==
 
-table@^3.7.8:
-  version "3.8.3"
-  resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
-  integrity sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=
+table@^6.5.1:
+  version "6.6.0"
+  resolved "https://registry.yarnpkg.com/table/-/table-6.6.0.tgz#905654b79df98d9e9a973de1dd58682532c40e8e"
+  integrity sha512-iZMtp5tUvcnAdtHpZTWLPF0M7AgiQsURR2DwmxnJwSy8I3+cY+ozzVvYha3BOLG2TB+L0CqjIz+91htuj6yCXg==
   dependencies:
-    ajv "^4.7.0"
-    ajv-keywords "^1.0.0"
-    chalk "^1.1.1"
-    lodash "^4.0.0"
-    slice-ansi "0.0.4"
-    string-width "^2.0.0"
+    ajv "^8.0.1"
+    lodash.clonedeep "^4.5.0"
+    lodash.flatten "^4.4.0"
+    lodash.truncate "^4.4.2"
+    slice-ansi "^4.0.0"
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
 
 tapable@^1.0.0, tapable@^1.1.3:
   version "1.1.3"
@@ -10912,7 +11126,7 @@ terser@^5.3.4:
     source-map "~0.7.2"
     source-map-support "~0.5.19"
 
-text-table@0.2.0, text-table@~0.2.0:
+text-table@0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
@@ -11074,6 +11288,16 @@ tree-kill@1.2.2:
   resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
   integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
 
+trim-newlines@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
+  integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
+
+trough@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
+  integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
+
 ts-loader@^8.0.14:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.1.0.tgz#d6292487df279c7cc79b6d3b70bb9d31682b693e"
@@ -11181,18 +11405,26 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
-type-check@~0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
-  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
-  dependencies:
-    prelude-ls "~1.1.2"
+type-fest@^0.18.0:
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
+  integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
 
 type-fest@^0.21.3:
   version "0.21.3"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
   integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
 
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
+type-fest@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
 type-is@~1.6.17, type-is@~1.6.18:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -11211,7 +11443,7 @@ type@^2.0.0:
   resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
   integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
 
-typedarray-to-buffer@^3.0.0:
+typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5:
   version "3.1.5"
   resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
   integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
@@ -11283,6 +11515,18 @@ unicode-property-aliases-ecmascript@^1.0.4:
   resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
   integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
 
+unified@^9.1.0:
+  version "9.2.1"
+  resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.1.tgz#ae18d5674c114021bfdbdf73865ca60f410215a3"
+  integrity sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==
+  dependencies:
+    bail "^1.0.0"
+    extend "^3.0.0"
+    is-buffer "^2.0.0"
+    is-plain-obj "^2.0.0"
+    trough "^1.0.0"
+    vfile "^4.0.0"
+
 union-value@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@@ -11317,6 +11561,25 @@ unique-slug@^2.0.0:
   dependencies:
     imurmurhash "^0.1.4"
 
+unist-util-find-all-after@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz#fdfecd14c5b7aea5e9ef38d5e0d5f774eeb561f6"
+  integrity sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==
+  dependencies:
+    unist-util-is "^4.0.0"
+
+unist-util-is@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797"
+  integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==
+
+unist-util-stringify-position@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da"
+  integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==
+  dependencies:
+    "@types/unist" "^2.0.2"
+
 universal-analytics@0.4.23:
   version "0.4.23"
   resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.23.tgz#d915e676850c25c4156762471bdd7cf2eaaca8ac"
@@ -11402,13 +11665,6 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
-user-home@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
-  integrity sha1-nHC/2Babwdy/SGBODwS4tJzenp8=
-  dependencies:
-    os-homedir "^1.0.0"
-
 ut_metadata@^3.5.2:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/ut_metadata/-/ut_metadata-3.5.2.tgz#2351c9348759e929978fa6a08d56ef6f584749e7"
@@ -11465,13 +11721,6 @@ util@0.10.3:
   dependencies:
     inherits "2.0.1"
 
-util@^0.10.3:
-  version "0.10.4"
-  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
-  integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
-  dependencies:
-    inherits "2.0.3"
-
 util@^0.11.0:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
@@ -11510,7 +11759,7 @@ uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
-v8-compile-cache@^2.2.0:
+v8-compile-cache@^2.2.0, v8-compile-cache@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
   integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
@@ -11549,6 +11798,24 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+vfile-message@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
+  integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-stringify-position "^2.0.0"
+
+vfile@^4.0.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624"
+  integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    is-buffer "^2.0.0"
+    unist-util-stringify-position "^2.0.0"
+    vfile-message "^2.0.0"
+
 "video.js@^6 || ^7", video.js@^7, video.js@^7.6.0:
   version "7.11.8"
   resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.11.8.tgz#1fa27c56f30a436b06b44f21560f223e264aec51"
@@ -11938,7 +12205,7 @@ which-module@^2.0.0:
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
-which@^1.2.1, which@^1.2.9:
+which@^1.2.1, which@^1.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -11964,11 +12231,6 @@ wildcard@^2.0.0:
   resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
   integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
 
-word-wrap@~1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
-  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
-
 worker-farm@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
@@ -12023,12 +12285,15 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-write@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
-  integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=
+write-file-atomic@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+  integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
   dependencies:
-    mkdirp "^0.5.1"
+    imurmurhash "^0.1.4"
+    is-typedarray "^1.0.0"
+    signal-exit "^3.0.2"
+    typedarray-to-buffer "^3.1.5"
 
 ws@^6.2.1:
   version "6.2.1"
@@ -12121,7 +12386,7 @@ yargs-parser@^18.1.2:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs-parser@^20.2.2:
+yargs-parser@^20.2.2, yargs-parser@^20.2.3:
   version "20.2.7"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
   integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
@@ -12219,3 +12484,8 @@ zone.js@~0.11.3:
   integrity sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==
   dependencies:
     tslib "^2.0.0"
+
+zwitch@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
+  integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==
index 9481e63f437ebc8c9bff45f62dd2b36851fdec0e..8552a9d60c0e96d60fd0ab43d3bb15fa00bf767a 100644 (file)
@@ -5,7 +5,7 @@
   "private": true,
   "licence": "AGPL-3.0",
   "engines": {
-    "node": ">=10.x <=15",
+    "node": ">=10.x",
     "yarn": ">=1.x",
     "postgres": ">=10.x",
     "redis-server": ">=2.8.18",
   },
   "typings": "*.d.ts",
   "scripts": {
-    "e2e": "scripty",
-    "e2e:local": "scripty",
-    "setup:cli": "scripty",
-    "build": "scripty",
-    "build:embed": "scripty",
-    "build:server": "scripty",
-    "build:client": "scripty",
-    "clean:client": "scripty",
-    "clean:server": "scripty",
-    "clean:server:test": "scripty",
-    "danger:clean:dev": "scripty",
-    "danger:clean:prod": "scripty",
-    "danger:clean:modules": "scripty",
-    "i18n:update": "scripty",
+    "e2e": "sh ./scripts/e2e/index.sh",
+    "e2e:local": "sh ./scripts/e2e/local.sh",
+    "setup:cli": "sh ./scripts/setup/cli.sh",
+    "build": "sh ./scripts/build/index.sh",
+    "build:embed": "sh ./scripts/build/embed.sh",
+    "build:server": "sh ./scripts/build/server.sh",
+    "build:client": "sh ./scripts/build/client.sh",
+    "clean:client": "sh ./scripts/clean/client/index.sh",
+    "clean:server:test": "sh ./scripts/clean/server/test.sh",
+    "i18n:update": "sh ./scripts/i18n/update.sh",
     "plugin:install": "node ./dist/scripts/plugin/install.js",
     "plugin:uninstall": "node ./dist/scripts/plugin/uninstall.js",
     "i18n:create-custom-files": "node ./dist/scripts/i18n/create-custom-files.js",
     "reset-password": "node ./dist/scripts/reset-password.js",
-    "play": "scripty",
     "dev": "sh ./scripts/dev/index.sh",
     "dev:server": "sh ./scripts/dev/server.sh",
-    "dev:embed": "scripty",
+    "dev:embed": "sh ./scripts/dev/embed.sh",
     "dev:client": "sh ./scripts/dev/client.sh",
-    "dev:cli": "scripty",
+    "dev:cli": "sh ./scripts/dev/cli.sh",
     "start": "node dist/server",
     "start:server": "node dist/server --no-client",
     "update-host": "node ./dist/scripts/update-host.js",
@@ -56,9 +51,9 @@
     "regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js",
     "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js",
     "print-transcode-command": "node ./dist/scripts/print-transcode-command.js",
-    "test": "scripty",
-    "help": "scripty",
-    "generate-cli-doc": "scripty",
+    "test": "sh ./scripts/test.sh",
+    "help": "sh ./scripts/help.sh",
+    "generate-cli-doc": "sh ./scripts/generate-cli-doc.sh",
     "parse-log": "node ./dist/scripts/parse-log.js",
     "prune-storage": "node ./dist/scripts/prune-storage.js",
     "optimize-old-videos": "node ./dist/scripts/optimize-old-videos.js",
     "ts-node": "ts-node",
     "eslint": "eslint",
     "concurrently": "concurrently",
-    "sasslint": "sass-lint --verbose --no-exit",
-    "sasslint:fix": "sass-lint-auto-fix -c .sass-lint.yml --verbose",
     "mocha": "mocha",
-    "ci": "scripty",
-    "release": "scripty",
-    "release-embed-api": "scripty",
-    "nightly": "scripty",
-    "openapi-clients": "scripty",
-    "client-report": "scripty",
-    "swagger-cli": "swagger-cli",
-    "sass-lint": "sass-lint"
+    "ci": "sh ./scripts/ci.sh",
+    "release": "sh ./scripts/release.sh",
+    "release-embed-api": "sh ./scripts/release-embed-api.sh",
+    "nightly": "sh ./scripts/nightly.sh",
+    "openapi-clients": "sh ./scripts/openapi-clients.sh",
+    "client-report": "sh ./scripts/client-report.sh",
+    "swagger-cli": "swagger-cli"
   },
   "dependencies": {
     "apicache": "1.6.2",
     "redis": "^3.0.2",
     "reflect-metadata": "^0.1.12",
     "sanitize-html": "2.x",
-    "scripty": "^2.0.0",
     "sequelize": "6.6.2",
     "sequelize-typescript": "^2.0.0-beta.1",
     "sitemap": "^6.1.0",
     "ts-node": "9.1.1",
     "typescript": "^4.0.5"
   },
-  "scripty": {
-    "silent": true
-  },
-  "sasslintConfig": "client/.sass-lint.yml",
   "bundlewatch": {
     "files": [
       {
diff --git a/scripts/clean/server/dist.sh b/scripts/clean/server/dist.sh
deleted file mode 100755 (executable)
index 50722cb..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-
-set -eu
-
-rm -rf dist/
diff --git a/scripts/danger/clean/cleaner.ts b/scripts/danger/clean/cleaner.ts
deleted file mode 100644 (file)
index 69e8a63..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import { registerTSPaths } from '../../../server/helpers/register-ts-paths'
-registerTSPaths()
-
-import * as Promise from 'bluebird'
-import * as rimraf from 'rimraf'
-import { initDatabaseModels, sequelizeTypescript } from '../../../server/initializers/database'
-import { CONFIG } from '../../../server/initializers/config'
-
-initDatabaseModels(true)
-  .then(() => {
-    return sequelizeTypescript.drop()
-  })
-  .then(() => {
-    console.info('Tables of %s deleted.', CONFIG.DATABASE.DBNAME)
-
-    const STORAGE = CONFIG.STORAGE
-    return Promise.mapSeries(Object.keys(STORAGE), storage => {
-      const storageDir = STORAGE[storage]
-
-      return new Promise((res, rej) => {
-        rimraf(storageDir, err => {
-          if (err) return rej(err)
-
-          console.info('%s deleted.', storageDir)
-          return res()
-        })
-      })
-    })
-    .then(() => process.exit(0))
-  })
-  .catch(err => {
-    console.error(err)
-    process.exit(-1)
-  })
diff --git a/scripts/danger/clean/dev.sh b/scripts/danger/clean/dev.sh
deleted file mode 100755 (executable)
index 14b45e1..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-read -p "This will remove all directories and SQL tables. Are you sure? (y/*) " -n 1 -r
-echo
-
-if [[ "$REPLY" =~ ^[Yy]$ ]]; then
-  NODE_ENV=test npm run ts-node -- --type-check "scripts/danger/clean/cleaner"
-fi
diff --git a/scripts/danger/clean/modules.sh b/scripts/danger/clean/modules.sh
deleted file mode 100755 (executable)
index f59d6b6..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-read -p "This will remove all node server and client modules. Are you sure? " -n 1 -r
-
-if [[ "$REPLY" =~ ^[Yy]$ ]]; then
-  rm -rf node_modules client/node_modules
-fi
diff --git a/scripts/danger/clean/prod.sh b/scripts/danger/clean/prod.sh
deleted file mode 100755 (executable)
index 0675600..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-read -p "This will remove all directories and SQL tables. Are you sure? (y/*) " -n 1 -r
-echo
-
-if [[ "$REPLY" =~ ^[Yy]$ ]]; then
-  NODE_ENV=production npm run ts-node -- --type-check "./scripts/danger/clean/cleaner"
-fi
index bc38bdb40025abaa5a6ab434b6b42b27321f5fe5..da127092dafb91a328efa6abd6d370710d5c3b83 100755 (executable)
@@ -9,11 +9,6 @@ printf "  build:server                -> Build the server for production\n"
 printf "  build:client                -> Build the client for production\n"
 printf "  clean:client                -> Clean the client build files (dist directory)\n"
 printf "  clean:server:test           -> Clean logs, uploads, database... of the test instances\n"
-printf "  watch:client                -> Watch and compile on the fly the client files\n"
-printf "  danger:clean:dev            -> /!\ Clean certificates, logs, uploads, thumbnails, torrents and database specified in the development environment\n"
-printf "  danger:clean:prod           -> /!\ Clean certificates, logs, uploads, thumbnails, torrents and database specified by the production environment\n"
-printf "  danger:clean:modules        -> /!\ Clean node and typescript modules\n"
-printf "  play                        -> Run 3 fresh nodes so that you can test the communication between them\n"
 printf "  reset-password -- -u [user] -> Reset the password of user [user]\n"
 printf "  create-transcoding-job -- -v [video UUID] \n"
 printf "                              -> Create a transcoding job for a particular video\n"
index d4d5b44f05a00030bb89e6a56a73e0f8c8e25c25..b26e92d93dd9915fb53184e5da811b665759b715 100755 (executable)
@@ -36,7 +36,22 @@ const playerKeys = {
   'From servers: ': 'From servers: ',
   'From peers: ': 'From peers: ',
   'Normal mode': 'Normal mode',
-  'Theater mode': 'Theater mode'
+  'Stats for nerds': 'Stats for nerds',
+  'Theater mode': 'Theater mode',
+  'Video UUID': 'Video UUID',
+  'Viewport / Frames': 'Viewport / Frames',
+  'Resolution': 'Resolution',
+  'Volume': 'Volume',
+  'Codecs': 'Codecs',
+  'Color': 'Color',
+  'Connection Speed': 'Connection Speed',
+  'Network Activity': 'Network Activity',
+  'Total Transfered': 'Total Transfered',
+  'Download Breakdown': 'Download Breakdown',
+  'Buffer Progress': 'Buffer Progress',
+  'Buffer State': 'Buffer State',
+  'Live Latency': 'Live Latency',
+  'Player mode': 'Player mode'
 }
 Object.assign(playerKeys, videojs)
 
diff --git a/scripts/play.sh b/scripts/play.sh
deleted file mode 100755 (executable)
index 69725da..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/sh
-
-set -eu
-
-if [ ! -f "dist/server.js" ]; then
-  echo "Missing server file (server.js)."
-  exit -1
-fi
-
-max=${1:-3}
-
-for i in $(seq 1 "$max"); do
-  NODE_ENV=test NODE_APP_INSTANCE=$i node dist/server.js &
-  sleep 1
-done
index bdfb335c61aeb7a28d4c74b94b510b7cf6aef3d4..32314b0b7c134ebace445b756bbd185a150bd82c 100755 (executable)
@@ -34,6 +34,8 @@ async function run () {
 
   let toDelete: string[] = []
 
+  console.log('Detecting files to remove, it could take a while...')
+
   toDelete = toDelete.concat(
     await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesVideoExist(true)),
     await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesVideoExist(true)),
index e31924a9410b5d87acfccf3d45a9aeedb954daff..49a8e3195028bdeb6eccffb5c7bbd6b65f649a86 100644 (file)
@@ -1,9 +1,10 @@
 import * as express from 'express'
 import { getServerActor } from '@server/models/application/application'
+import { VideosWithSearchCommonQuery } from '@shared/models'
 import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
 import { getFormattedObjects } from '../../helpers/utils'
-import { Hooks } from '../../lib/plugins/hooks'
 import { JobQueue } from '../../lib/job-queue'
+import { Hooks } from '../../lib/plugins/hooks'
 import {
   asyncMiddleware,
   authenticate,
@@ -158,25 +159,27 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
   const account = res.locals.account
   const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
   const countVideos = getCountVideos(req)
+  const query = req.query as VideosWithSearchCommonQuery
 
   const apiOptions = await Hooks.wrapObject({
     followerActorId,
-    start: req.query.start,
-    count: req.query.count,
-    sort: req.query.sort,
+    start: query.start,
+    count: query.count,
+    sort: query.sort,
     includeLocalVideos: true,
-    categoryOneOf: req.query.categoryOneOf,
-    licenceOneOf: req.query.licenceOneOf,
-    languageOneOf: req.query.languageOneOf,
-    tagsOneOf: req.query.tagsOneOf,
-    tagsAllOf: req.query.tagsAllOf,
-    filter: req.query.filter,
-    nsfw: buildNSFWFilter(res, req.query.nsfw),
+    categoryOneOf: query.categoryOneOf,
+    licenceOneOf: query.licenceOneOf,
+    languageOneOf: query.languageOneOf,
+    tagsOneOf: query.tagsOneOf,
+    tagsAllOf: query.tagsAllOf,
+    filter: query.filter,
+    isLive: query.isLive,
+    nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     accountId: account.id,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
     countVideos,
-    search: req.query.search
+    search: query.search
   }, 'filter:api.accounts.videos.list.params')
 
   const resultList = await Hooks.wrapPromiseFun(
index 9f9d2d77ff5cb0d8b04411cea1b81f401c8d0a11..0763d1900767d4e31bf539804ca60428dc8774e4 100644 (file)
@@ -111,7 +111,8 @@ async function getUserVideos (req: express.Request, res: express.Response) {
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
-    search: req.query.search
+    search: req.query.search,
+    isLive: req.query.isLive
   }, 'filter:api.user.me.videos.list.params')
 
   const resultList = await Hooks.wrapPromiseFun(
index e8949ee5964750ea6726cca8b97e81f6f9c73f0f..56b93276fda47e91d6f8bbd2bae623dd0f5b8ccc 100644 (file)
@@ -2,8 +2,8 @@ import 'multer'
 import * as express from 'express'
 import { sendUndoFollow } from '@server/lib/activitypub/send'
 import { VideoChannelModel } from '@server/models/video/video-channel'
+import { VideosCommonQuery } from '@shared/models'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
 import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { WEBSERVER } from '../../../initializers/constants'
@@ -170,19 +170,20 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
 async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.User
   const countVideos = getCountVideos(req)
+  const query = req.query as VideosCommonQuery
 
   const resultList = await VideoModel.listForApi({
-    start: req.query.start,
-    count: req.query.count,
-    sort: req.query.sort,
+    start: query.start,
+    count: query.count,
+    sort: query.sort,
     includeLocalVideos: false,
-    categoryOneOf: req.query.categoryOneOf,
-    licenceOneOf: req.query.licenceOneOf,
-    languageOneOf: req.query.languageOneOf,
-    tagsOneOf: req.query.tagsOneOf,
-    tagsAllOf: req.query.tagsAllOf,
-    nsfw: buildNSFWFilter(res, req.query.nsfw),
-    filter: req.query.filter as VideoFilter,
+    categoryOneOf: query.categoryOneOf,
+    licenceOneOf: query.licenceOneOf,
+    languageOneOf: query.languageOneOf,
+    tagsOneOf: query.tagsOneOf,
+    tagsAllOf: query.tagsAllOf,
+    nsfw: buildNSFWFilter(res, query.nsfw),
+    filter: query.filter,
     withFiles: false,
     followerActorId: user.Account.Actor.id,
     user,
index 149d6cfb4a58d093d8cdd9e3d182897b7ac32860..a755d7e5724fa13df0a019fec068b65c0e26faa4 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { getServerActor } from '@server/models/application/application'
 import { MChannelBannerAccountDefault } from '@server/types/models'
-import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
+import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared'
 import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
 import { resetSequelizeInstance } from '../../helpers/database-utils'
@@ -312,20 +312,21 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
   const videoChannelInstance = res.locals.videoChannel
   const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
   const countVideos = getCountVideos(req)
+  const query = req.query as VideosCommonQuery
 
   const apiOptions = await Hooks.wrapObject({
     followerActorId,
-    start: req.query.start,
-    count: req.query.count,
-    sort: req.query.sort,
+    start: query.start,
+    count: query.count,
+    sort: query.sort,
     includeLocalVideos: true,
-    categoryOneOf: req.query.categoryOneOf,
-    licenceOneOf: req.query.licenceOneOf,
-    languageOneOf: req.query.languageOneOf,
-    tagsOneOf: req.query.tagsOneOf,
-    tagsAllOf: req.query.tagsAllOf,
-    filter: req.query.filter,
-    nsfw: buildNSFWFilter(res, req.query.nsfw),
+    categoryOneOf: query.categoryOneOf,
+    licenceOneOf: query.licenceOneOf,
+    languageOneOf: query.languageOneOf,
+    tagsOneOf: query.tagsOneOf,
+    tagsAllOf: query.tagsAllOf,
+    filter: query.filter,
+    nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     videoChannelId: videoChannelInstance.id,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
index 7fee278f28c528eb41db89d11c0425e30330f948..6ec6478e4666f1e10210f83eb0d684fb8ccc10f4 100644 (file)
@@ -10,9 +10,8 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail
 import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import { getServerActor } from '@server/models/application/application'
 import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared'
+import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
 import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
 import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
 import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
@@ -494,20 +493,22 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
 }
 
 async function listVideos (req: express.Request, res: express.Response) {
+  const query = req.query as VideosCommonQuery
   const countVideos = getCountVideos(req)
 
   const apiOptions = await Hooks.wrapObject({
-    start: req.query.start,
-    count: req.query.count,
-    sort: req.query.sort,
+    start: query.start,
+    count: query.count,
+    sort: query.sort,
     includeLocalVideos: true,
-    categoryOneOf: req.query.categoryOneOf,
-    licenceOneOf: req.query.licenceOneOf,
-    languageOneOf: req.query.languageOneOf,
-    tagsOneOf: req.query.tagsOneOf,
-    tagsAllOf: req.query.tagsAllOf,
-    nsfw: buildNSFWFilter(res, req.query.nsfw),
-    filter: req.query.filter as VideoFilter,
+    categoryOneOf: query.categoryOneOf,
+    licenceOneOf: query.licenceOneOf,
+    languageOneOf: query.languageOneOf,
+    tagsOneOf: query.tagsOneOf,
+    tagsAllOf: query.tagsAllOf,
+    nsfw: buildNSFWFilter(res, query.nsfw),
+    isLive: query.isLive,
+    filter: query.filter,
     withFiles: false,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
     countVideos
index 429fcafcf9525f94e392f88baeb687a559d88a36..a8f25883832c4588b122eee4a801904a5bcf136a 100644 (file)
@@ -11,7 +11,7 @@ function isStringArray (value: any) {
   return isArray(value) && value.every(v => typeof v === 'string')
 }
 
-function isNSFWQueryValid (value: any) {
+function isBooleanBothQueryValid (value: any) {
   return value === 'true' || value === 'false' || value === 'both'
 }
 
@@ -32,6 +32,6 @@ function isSearchTargetValid (value: SearchTargetType) {
 export {
   isNumberArray,
   isStringArray,
-  isNSFWQueryValid,
+  isBooleanBothQueryValid,
   isSearchTargetValid
 }
index 37a963760830bf4200b7fd4f77ca43897961a3ed..d390fd95e9a9b3c1f6722ec802dcbf63b1b10619 100644 (file)
@@ -188,10 +188,7 @@ const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } =
   }
 }
 const JOB_PRIORITY = {
-  TRANSCODING: {
-    OPTIMIZER: 10,
-    NEW_RESOLUTION: 100
-  }
+  TRANSCODING: 100
 }
 
 const BROADCAST_CONCURRENCY = 30 // How many requests in parallel we do in activitypub-http-broadcast job
index 4ee2b2df2228199c63f7f58e7809aebb87a39144..010b95b059057ae5b391c164220f15e0581069fc 100644 (file)
@@ -1,7 +1,6 @@
 import * as Bull from 'bull'
 import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils'
-import { JOB_PRIORITY } from '@server/initializers/constants'
-import { getJobTranscodingPriorityMalus, publishAndFederateIfNeeded } from '@server/lib/video'
+import { getTranscodingJobPriority, publishAndFederateIfNeeded } from '@server/lib/video'
 import { getVideoFilePath } from '@server/lib/video-paths'
 import { UserModel } from '@server/models/account/user'
 import { MUser, MUserId, MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
@@ -215,7 +214,7 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
   if (!payload || CONFIG.TRANSCODING.HLS.ENABLED !== true) return false
 
   const jobOptions = {
-    priority: JOB_PRIORITY.TRANSCODING.NEW_RESOLUTION + await getJobTranscodingPriorityMalus(user)
+    priority: await getTranscodingJobPriority(user)
   }
 
   const hlsTranscodingPayload: HLSTranscodingPayload = {
@@ -272,7 +271,7 @@ async function createLowerResolutionsJobs (
     resolutionCreated.push(resolution)
 
     const jobOptions = {
-      priority: JOB_PRIORITY.TRANSCODING.NEW_RESOLUTION + await getJobTranscodingPriorityMalus(user)
+      priority: await getTranscodingJobPriority(user)
     }
 
     JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }, jobOptions)
index e381e0a69930e3b342270ee68fb5c0cc37d414ba..9469b817899dcce5fd3292afefbd15ceacc074bc 100644 (file)
@@ -121,19 +121,19 @@ async function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile,
   }
 
   const jobOptions = {
-    priority: JOB_PRIORITY.TRANSCODING.OPTIMIZER + await getJobTranscodingPriorityMalus(user)
+    priority: await getTranscodingJobPriority(user)
   }
 
   return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput }, jobOptions)
 }
 
-async function getJobTranscodingPriorityMalus (user: MUserId) {
+async function getTranscodingJobPriority (user: MUserId) {
   const now = new Date()
   const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
 
   const videoUploadedByUser = await VideoModel.countVideosUploadedByUserSince(user.id, lastWeek)
 
-  return videoUploadedByUser
+  return JOB_PRIORITY.TRANSCODING + videoUploadedByUser
 }
 
 // ---------------------------------------------------------------------------
@@ -144,5 +144,5 @@ export {
   buildVideoThumbnailsFromReq,
   setVideoTags,
   addOptimizeOrMergeAudioJob,
-  getJobTranscodingPriorityMalus
+  getTranscodingJobPriority
 }
index 4d31d3dcb076e6cd4a1644fe2b9719ca519e1b38..bb617d77c3dd83a0a309cd88e3e25065bef48586 100644 (file)
@@ -20,7 +20,7 @@ import {
   toIntOrNull,
   toValueOrNull
 } from '../../../helpers/custom-validators/misc'
-import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
+import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
 import {
   isScheduleVideoUpdatePrivacyValid,
@@ -439,7 +439,11 @@ const commonVideosFiltersValidator = [
     .custom(isStringArray).withMessage('Should have a valid all of tags array'),
   query('nsfw')
     .optional()
-    .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
+    .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
+  query('isLive')
+    .optional()
+    .customSanitizer(toBooleanOrNull)
+    .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
   query('filter')
     .optional()
     .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
index 4d95ddee29a8b1fcaf5f7a561baae499c73e8a5c..155afe64be00ce66bbc7db1f4a5c51ba0c3d6224 100644 (file)
@@ -16,9 +16,11 @@ export type BuildVideosQueryOptions = {
   start: number
   sort: string
 
+  nsfw?: boolean
   filter?: VideoFilter
+  isLive?: boolean
+
   categoryOneOf?: number[]
-  nsfw?: boolean
   licenceOneOf?: number[]
   languageOneOf?: string[]
   tagsOneOf?: string[]
@@ -199,10 +201,14 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
 
   if (options.nsfw === true) {
     and.push('"video"."nsfw" IS TRUE')
+  } else if (options.nsfw === false) {
+    and.push('"video"."nsfw" IS FALSE')
   }
 
-  if (options.nsfw === false) {
-    and.push('"video"."nsfw" IS FALSE')
+  if (options.isLive === true) {
+    and.push('"video"."isLive" IS TRUE')
+  } else if (options.isLive === false) {
+    and.push('"video"."isLive" IS FALSE')
   }
 
   if (options.categoryOneOf) {
index 422bf6deb94c7bef8f0a770424136072dad96558..e55a21a6b69c0c031ebc195423adf42e03975862 100644 (file)
@@ -1021,14 +1021,28 @@ export class VideoModel extends Model {
     start: number
     count: number
     sort: string
+    isLive?: boolean
     search?: string
   }) {
-    const { accountId, start, count, sort, search } = options
+    const { accountId, start, count, sort, search, isLive } = options
 
     function buildBaseQuery (): FindOptions {
-      let baseQuery = {
+      const where: WhereOptions = {}
+
+      if (search) {
+        where.name = {
+          [Op.iLike]: '%' + search + '%'
+        }
+      }
+
+      if (isLive) {
+        where.isLive = isLive
+      }
+
+      const baseQuery = {
         offset: start,
         limit: count,
+        where,
         order: getVideoSort(sort),
         include: [
           {
@@ -1047,16 +1061,6 @@ export class VideoModel extends Model {
         ]
       }
 
-      if (search) {
-        baseQuery = Object.assign(baseQuery, {
-          where: {
-            name: {
-              [Op.iLike]: '%' + search + '%'
-            }
-          }
-        })
-      }
-
       return baseQuery
     }
 
@@ -1084,23 +1088,34 @@ export class VideoModel extends Model {
     start: number
     count: number
     sort: string
+
     nsfw: boolean
+    filter?: VideoFilter
+    isLive?: boolean
+
     includeLocalVideos: boolean
     withFiles: boolean
+
     categoryOneOf?: number[]
     licenceOneOf?: number[]
     languageOneOf?: string[]
     tagsOneOf?: string[]
     tagsAllOf?: string[]
-    filter?: VideoFilter
+
     accountId?: number
     videoChannelId?: number
+
     followerActorId?: number
+
     videoPlaylistId?: number
+
     trendingDays?: number
+
     user?: MUserAccountId
     historyOfUser?: MUserId
+
     countVideos?: boolean
+
     search?: string
   }) {
     if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
@@ -1128,6 +1143,7 @@ export class VideoModel extends Model {
       followerActorId,
       serverAccountId: serverActor.Account.id,
       nsfw: options.nsfw,
+      isLive: options.isLive,
       categoryOneOf: options.categoryOneOf,
       licenceOneOf: options.licenceOneOf,
       languageOneOf: options.languageOneOf,
@@ -1160,6 +1176,7 @@ export class VideoModel extends Model {
     originallyPublishedStartDate?: string
     originallyPublishedEndDate?: string
     nsfw?: boolean
+    isLive?: boolean
     categoryOneOf?: number[]
     licenceOneOf?: number[]
     languageOneOf?: string[]
@@ -1171,23 +1188,32 @@ export class VideoModel extends Model {
     filter?: VideoFilter
   }) {
     const serverActor = await getServerActor()
+
     const queryOptions = {
       followerActorId: serverActor.id,
       serverAccountId: serverActor.Account.id,
+
       includeLocalVideos: options.includeLocalVideos,
       nsfw: options.nsfw,
+      isLive: options.isLive,
+
       categoryOneOf: options.categoryOneOf,
       licenceOneOf: options.licenceOneOf,
       languageOneOf: options.languageOneOf,
+
       tagsOneOf: options.tagsOneOf,
       tagsAllOf: options.tagsAllOf,
+
       user: options.user,
       filter: options.filter,
+
       start: options.start,
       count: options.count,
       sort: options.sort,
+
       startDate: options.startDate,
       endDate: options.endDate,
+
       originallyPublishedStartDate: options.originallyPublishedStartDate,
       originallyPublishedEndDate: options.originallyPublishedEndDate,
 
index d48e2a8ee67eae835484fe1a815f2b3b611fdef5..57fb58150246d3a83f924ffaf8bd210402f6d549 100644 (file)
@@ -19,10 +19,12 @@ import {
   doubleFollow,
   flushAndRunMultipleServers,
   getLive,
+  getMyVideosWithFilter,
   getPlaylist,
   getVideo,
   getVideoIdFromUUID,
   getVideosList,
+  getVideosWithFilters,
   killallServers,
   makeRawRequest,
   removeVideo,
@@ -37,6 +39,7 @@ import {
   testImage,
   updateCustomSubConfig,
   updateLive,
+  uploadVideoAndGetId,
   viewVideo,
   wait,
   waitJobs,
@@ -229,6 +232,68 @@ describe('Test live', function () {
     })
   })
 
+  describe('Live filters', function () {
+    let command: any
+    let liveVideoId: string
+    let vodVideoId: string
+
+    before(async function () {
+      this.timeout(120000)
+
+      vodVideoId = (await uploadVideoAndGetId({ server: servers[0], videoName: 'vod video' })).uuid
+
+      const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].videoChannel.id }
+      const resLive = await createLive(servers[0].url, servers[0].accessToken, liveOptions)
+      liveVideoId = resLive.body.video.uuid
+
+      command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+      await waitUntilLivePublishedOnAllServers(liveVideoId)
+      await waitJobs(servers)
+    })
+
+    it('Should only display lives', async function () {
+      const res = await getVideosWithFilters(servers[0].url, { isLive: true })
+
+      expect(res.body.total).to.equal(1)
+      expect(res.body.data).to.have.lengthOf(1)
+      expect(res.body.data[0].name).to.equal('live')
+    })
+
+    it('Should not display lives', async function () {
+      const res = await getVideosWithFilters(servers[0].url, { isLive: false })
+
+      expect(res.body.total).to.equal(1)
+      expect(res.body.data).to.have.lengthOf(1)
+      expect(res.body.data[0].name).to.equal('vod video')
+    })
+
+    it('Should display my lives', async function () {
+      this.timeout(60000)
+
+      await stopFfmpeg(command)
+      await waitJobs(servers)
+
+      const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: true })
+      const videos = res.body.data as Video[]
+
+      const result = videos.every(v => v.isLive)
+      expect(result).to.be.true
+    })
+
+    it('Should not display my lives', async function () {
+      const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: false })
+      const videos = res.body.data as Video[]
+
+      const result = videos.every(v => !v.isLive)
+      expect(result).to.be.true
+    })
+
+    after(async function () {
+      await removeVideo(servers[0].url, servers[0].accessToken, vodVideoId)
+      await removeVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+    })
+  })
+
   describe('Stream checks', function () {
     let liveVideo: LiveVideo & VideoDetails
     let rtmpUrl: string
index e05c3a2691d07641bea71746ec5b58631814066d..5b8907961d4c8f1e2dfcfbccc5de67ce8646d84c 100644 (file)
@@ -1,17 +1,24 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
+import { VideoPrivacy } from '@shared/models'
 import {
   advancedVideosSearch,
   cleanupTests,
+  createLive,
   flushAndRunServer,
   immutableAssign,
   searchVideo,
+  sendRTMPStreamInVideo,
   ServerInfo,
   setAccessTokensToServers,
+  setDefaultVideoChannel,
+  stopFfmpeg,
+  updateCustomSubConfig,
   uploadVideo,
-  wait
+  wait,
+  waitUntilLivePublished
 } from '../../../../shared/extra-utils'
 import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
 
@@ -28,6 +35,7 @@ describe('Test videos search', function () {
     server = await flushAndRunServer(1)
 
     await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
 
     {
       const attributes1 = {
@@ -449,6 +457,43 @@ describe('Test videos search', function () {
     expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
   })
 
+  it('Should search by live', async function () {
+    this.timeout(30000)
+
+    {
+      const options = {
+        search: {
+          searchIndex: { enabled: false }
+        },
+        live: { enabled: true }
+      }
+      await updateCustomSubConfig(server.url, server.accessToken, options)
+    }
+
+    {
+      const res = await advancedVideosSearch(server.url, { isLive: true })
+
+      expect(res.body.total).to.equal(0)
+      expect(res.body.data).to.have.lengthOf(0)
+    }
+
+    {
+      const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.videoChannel.id }
+      const resLive = await createLive(server.url, server.accessToken, liveOptions)
+      const liveVideoId = resLive.body.video.uuid
+
+      const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId)
+      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
+
+      const res = await advancedVideosSearch(server.url, { isLive: true })
+
+      expect(res.body.total).to.equal(1)
+      expect(res.body.data[0].name).to.equal('live')
+
+      await stopFfmpeg(command)
+    }
+  })
+
   after(async function () {
     await cleanupTests([ server ])
   })
index da90223b820ba17abbb885e5706a88fcbe567f3a..a79648bf7442a147b7b00b97f407dff6bb85906a 100644 (file)
@@ -387,11 +387,11 @@ describe('Test a single server', function () {
   })
 
   it('Should filter by tags and category', async function () {
-    const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 })
+    const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
     expect(res1.body.total).to.equal(1)
     expect(res1.body.data[0].name).to.equal('my super video updated')
 
-    const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 })
+    const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
     expect(res2.body.total).to.equal(0)
   })
 
index 1058baaa313d50f5672debdca057784b9817e885..1c99f26df27544bf4278a8f8d8ca70f02d0967b4 100644 (file)
@@ -721,12 +721,7 @@ describe('Test video transcoding', function () {
       expect(webtorrentJobs).to.have.lengthOf(6)
       expect(optimizeJobs).to.have.lengthOf(1)
 
-      for (const j of optimizeJobs) {
-        expect(j.priority).to.be.greaterThan(11)
-        expect(j.priority).to.be.lessThan(50)
-      }
-
-      for (const j of hlsJobs.concat(webtorrentJobs)) {
+      for (const j of optimizeJobs.concat(hlsJobs.concat(webtorrentJobs))) {
         expect(j.priority).to.be.greaterThan(100)
         expect(j.priority).to.be.lessThan(150)
       }
index 67fe82d41883f743cb319b0af743823b003f453b..a0143b0efef50ac8f227a78372b7ff3af9e084c4 100644 (file)
@@ -8,6 +8,7 @@ import * as request from 'supertest'
 import { v4 as uuidv4 } from 'uuid'
 import validator from 'validator'
 import { HttpStatusCode } from '@shared/core-utils'
+import { VideosCommonQuery } from '@shared/models'
 import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
 import { VideoDetails, VideoPrivacy } from '../../models/videos'
 import {
@@ -195,6 +196,18 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu
     .expect('Content-Type', /json/)
 }
 
+function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
+  const path = '/api/v1/users/me/videos'
+
+  return makeGetRequest({
+    url,
+    path,
+    token: accessToken,
+    query,
+    statusCodeExpected: HttpStatusCode.OK_200
+  })
+}
+
 function getAccountVideos (
   url: string,
   accessToken: string,
@@ -295,7 +308,7 @@ function getVideosListSort (url: string, sort: string) {
           .expect('Content-Type', /json/)
 }
 
-function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
+function getVideosWithFilters (url: string, query: VideosCommonQuery) {
   const path = '/api/v1/videos'
 
   return request(url)
@@ -751,6 +764,7 @@ export {
   completeVideoCheck,
   checkVideoFilesWereRemoved,
   getPlaylistVideos,
+  getMyVideosWithFilter,
   uploadVideoAndGetId,
   getLocalIdByUUID,
   getVideoIdFromUUID
diff --git a/shared/models/search/boolean-both-query.model.ts b/shared/models/search/boolean-both-query.model.ts
new file mode 100644 (file)
index 0000000..57b0e8d
--- /dev/null
@@ -0,0 +1 @@
+export type BooleanBothQuery = 'true' | 'false' | 'both'
index e2d0ab62056d67998f8659130157b867209b5b3c..697ceccb1645cac5bb75acef82bee513c7bb811f 100644 (file)
@@ -1,4 +1,5 @@
-export * from './nsfw-query.model'
+export * from './boolean-both-query.model'
 export * from './search-target-query.model'
+export * from './videos-common-query.model'
 export * from './videos-search-query.model'
 export * from './video-channels-search-query.model'
diff --git a/shared/models/search/nsfw-query.model.ts b/shared/models/search/nsfw-query.model.ts
deleted file mode 100644 (file)
index 6b6ad19..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export type NSFWQuery = 'true' | 'false' | 'both'
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
new file mode 100644 (file)
index 0000000..bd02489
--- /dev/null
@@ -0,0 +1,28 @@
+import { VideoFilter } from '../videos'
+import { BooleanBothQuery } from './boolean-both-query.model'
+
+// These query parameters can be used with any endpoint that list videos
+export interface VideosCommonQuery {
+  start?: number
+  count?: number
+  sort?: string
+
+  nsfw?: BooleanBothQuery
+
+  isLive?: boolean
+
+  categoryOneOf?: number[]
+
+  licenceOneOf?: number[]
+
+  languageOneOf?: string[]
+
+  tagsOneOf?: string[]
+  tagsAllOf?: string[]
+
+  filter?: VideoFilter
+}
+
+export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
+  search?: string
+}
index 3ce4ff73e3f66edabb41ecef8160e75f0630c614..406f6cab22b808dec9d456b48c358c2ef3c199a5 100644 (file)
@@ -1,33 +1,15 @@
-import { VideoFilter } from '../videos'
-import { NSFWQuery } from './nsfw-query.model'
 import { SearchTargetQuery } from './search-target-query.model'
+import { VideosCommonQuery } from './videos-common-query.model'
 
-export interface VideosSearchQuery extends SearchTargetQuery {
+export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery {
   search?: string
 
-  start?: number
-  count?: number
-  sort?: string
-
   startDate?: string // ISO 8601
   endDate?: string // ISO 8601
 
   originallyPublishedStartDate?: string // ISO 8601
   originallyPublishedEndDate?: string // ISO 8601
 
-  nsfw?: NSFWQuery
-
-  categoryOneOf?: number[]
-
-  licenceOneOf?: number[]
-
-  languageOneOf?: string[]
-
-  tagsOneOf?: string[]
-  tagsAllOf?: string[]
-
   durationMin?: number // seconds
   durationMax?: number // seconds
-
-  filter?: VideoFilter
 }
index a63ee79835b096b5bf9c06d01b0cd5f56a239c4b..d4fe1566401a9dcedff8cf686b4dc4a21aaca103 100644 (file)
@@ -210,6 +210,7 @@ paths:
       parameters:
         - $ref: '#/components/parameters/name'
         - $ref: '#/components/parameters/categoryOneOf'
+        - $ref: '#/components/parameters/isLive'
         - $ref: '#/components/parameters/tagsOneOf'
         - $ref: '#/components/parameters/tagsAllOf'
         - $ref: '#/components/parameters/licenceOneOf'
@@ -781,6 +782,7 @@ paths:
         - Videos
       parameters:
         - $ref: '#/components/parameters/categoryOneOf'
+        - $ref: '#/components/parameters/isLive'
         - $ref: '#/components/parameters/tagsOneOf'
         - $ref: '#/components/parameters/tagsAllOf'
         - $ref: '#/components/parameters/licenceOneOf'
@@ -1086,6 +1088,7 @@ paths:
         - Video
       parameters:
         - $ref: '#/components/parameters/categoryOneOf'
+        - $ref: '#/components/parameters/isLive'
         - $ref: '#/components/parameters/tagsOneOf'
         - $ref: '#/components/parameters/tagsAllOf'
         - $ref: '#/components/parameters/licenceOneOf'
@@ -1226,6 +1229,8 @@ paths:
                 name:
                   description: Video name
                   type: string
+                  minLength: 3
+                  maxLength: 120
                 tags:
                   description: Video tags (maximum 5 tags each between 2 and 30 characters)
                   type: array
@@ -1397,6 +1402,8 @@ paths:
                 name:
                   description: Video name
                   type: string
+                  minLength: 3
+                  maxLength: 120
                 tags:
                   description: Video tags (maximum 5 tags each between 2 and 30 characters)
                   type: array
@@ -1523,6 +1530,8 @@ paths:
                 name:
                   description: Video name
                   type: string
+                  minLength: 3
+                  maxLength: 120
                 tags:
                   description: Video tags (maximum 5 tags each between 2 and 30 characters)
                   type: array
@@ -1627,6 +1636,8 @@ paths:
                 name:
                   description: Live video/replay name
                   type: string
+                  minLength: 3
+                  maxLength: 120
                 tags:
                   description: Live video/replay tags (maximum 5 tags each between 2 and 30 characters)
                   type: array
@@ -1816,10 +1827,10 @@ paths:
                 reason:
                   description: Reason why the user reports this video
                   type: string
-                  minLength: 4
+                  minLength: 2
+                  maxLength: 3000
                 predefinedReasons:
                   $ref: '#/components/schemas/PredefinedAbuseReasons'
-
                 video:
                   type: object
                   properties:
@@ -1875,6 +1886,8 @@ paths:
                 moderationComment:
                   type: string
                   description: Update the report comment visible only to the moderation team
+                  minLength: 2
+                  maxLength: 3000
       responses:
         '204':
           description: successful operation
@@ -1932,6 +1945,8 @@ paths:
                 message:
                   description: Message to send
                   type: string
+                  minLength: 2
+                  maxLength: 3000
               required:
                 - message
       responses:
@@ -2182,6 +2197,7 @@ paths:
       parameters:
         - $ref: '#/components/parameters/channelHandle'
         - $ref: '#/components/parameters/categoryOneOf'
+        - $ref: '#/components/parameters/isLive'
         - $ref: '#/components/parameters/tagsOneOf'
         - $ref: '#/components/parameters/tagsAllOf'
         - $ref: '#/components/parameters/licenceOneOf'
@@ -2369,7 +2385,7 @@ paths:
                       id:
                         type: integer
                       uuid:
-                        type: string
+                        $ref: '#/components/schemas/UUIDv4'
       requestBody:
         content:
           multipart/form-data:
@@ -2379,6 +2395,8 @@ paths:
                 displayName:
                   description: Video playlist display name
                   type: string
+                  minLength: 1
+                  maxLength: 120
                 thumbnailfile:
                   description: Video playlist thumbnail file
                   type: string
@@ -2432,6 +2450,8 @@ paths:
                 displayName:
                   description: Video playlist display name
                   type: string
+                  minLength: 1
+                  maxLength: 120
                 thumbnailfile:
                   description: Video playlist thumbnail file
                   type: string
@@ -2825,6 +2845,7 @@ paths:
           schema:
             type: string
         - $ref: '#/components/parameters/categoryOneOf'
+        - $ref: '#/components/parameters/isLive'
         - $ref: '#/components/parameters/tagsOneOf'
         - $ref: '#/components/parameters/tagsAllOf'
         - $ref: '#/components/parameters/licenceOneOf'
@@ -3732,9 +3753,7 @@ components:
           - type: integer
             minimum: 0
             example: 42
-          - type: string
-            format: uuid
-            example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          - $ref: '#/components/schemas/UUIDv4'
     playlistElementId:
       name: playlistElementId
       in: path
@@ -3793,6 +3812,13 @@ components:
       description: The comment id
       schema:
         type: integer
+    isLive:
+      name: isLive
+      in: query
+      required: false
+      description: whether or not the video is a live
+      schema:
+        type: boolean
     categoryOneOf:
       name: categoryOneOf
       in: query
@@ -3955,16 +3981,35 @@ components:
             moderator: Moderator scope
             user: User scope
   schemas:
-    VideoConstantNumber:
+    UUIDv4:
+      type: string
+      format: uuid
+      example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+      pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
+      # the regex above limits the length;
+      # however, some tools might require explicit settings:
+      minLength: 36
+      maxLength: 36
+
+    VideoConstantNumber-Category:
+      properties:
+        id:
+          type: integer
+          description: category id of the video (see [/videos/categories](#tag/Video/paths/~1videos~1categories/get))
+        label:
+          type: string
+    VideoConstantNumber-Licence:
       properties:
         id:
           type: integer
+          description: licence id of the video (see [/videos/licences](#tag/Video/paths/~1videos~1licences/get))
         label:
           type: string
-    VideoConstantString:
+    VideoConstantString-Language:
       properties:
         id:
           type: string
+          description: language id of the video (see [/videos/languages](#tag/Video/paths/~1videos~1languages/get))
         label:
           type: string
 
@@ -4171,14 +4216,21 @@ components:
           type: string
           format: url
     VideoStreamingPlaylists:
+      allOf:
+        - type: object
+          properties:
+            id:
+              type: integer
+            type:
+              type: integer
+              enum:
+                - 1
+              description: |
+                Playlist type:
+                - `1`: HLS
+        - $ref: '#/components/schemas/VideoStreamingPlaylists-HLS'
+    VideoStreamingPlaylists-HLS:
       properties:
-        id:
-          type: integer
-        type:
-          type: integer
-          enum:
-            - 1
-          description: 'Playlist type (HLS = `1`)'
         playlistUrl:
           type: string
           format: url
@@ -4187,7 +4239,10 @@ components:
           format: url
         files:
           type: array
-          description: 'Video files associated to this playlist. The difference with the root "files" property is that these files are fragmented, so they can be used in this streaming playlist (HLS etc)'
+          description: |
+            Video files associated to this playlist.
+
+            The difference with the root `files` property is that these files are fragmented, so they can be used in this streaming playlist (HLS, etc.)
           items:
             $ref: '#/components/schemas/VideoFile'
         redundancies:
@@ -4203,52 +4258,69 @@ components:
         id:
           type: integer
         uuid:
-          type: string
-          format: uuid
-          example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          $ref: '#/components/schemas/UUIDv4'
         name:
           type: string
+          minLength: 3
+          maxLength: 120
     Video:
       properties:
         id:
           type: integer
           example: 8
         uuid:
-          type: string
-          format: uuid
-          example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          $ref: '#/components/schemas/UUIDv4'
         isLive:
           type: boolean
         createdAt:
           type: string
           format: date-time
+          example: 2017-10-01T10:52:46.396Z
+          description: time at which the video object was first drafted
         publishedAt:
           type: string
           format: date-time
+          example: 2018-10-01T10:52:46.396Z
+          description: time at which the video was marked as ready for playback (with restrictions depending on `privacy`). Usually set after a `state` evolution.
         updatedAt:
           type: string
           format: date-time
+          example: 2021-05-04T08:01:01.502Z
+          description: last time the video's metadata was modified
         originallyPublishedAt:
           type: string
           format: date-time
+          example: 2010-10-01T10:52:46.396Z
+          description: used to represent a date of first publication, prior to the practical publication date of `publishedAt`
         category:
-          $ref: '#/components/schemas/VideoConstantNumber'
+          $ref: '#/components/schemas/VideoConstantNumber-Category'
         licence:
-          $ref: '#/components/schemas/VideoConstantNumber'
+          $ref: '#/components/schemas/VideoConstantNumber-Licence'
         language:
-          $ref: '#/components/schemas/VideoConstantString'
+          $ref: '#/components/schemas/VideoConstantString-Language'
         privacy:
           $ref: '#/components/schemas/VideoPrivacyConstant'
         description:
           type: string
+          example: |
+            **[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n
+            **Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**\r\n*A decentralized video hosting network, based on fr...
+          minLength: 3
+          maxLength: 250
+          description: |
+            truncated description of the video, written in Markdown.
+            Resolve `descriptionPath` to get the full description of maximum `10000` characters.
         duration:
           type: integer
           example: 1419
+          description: duration of the video in seconds
         isLocal:
           type: boolean
         name:
           type: string
           example: What is PeerTube?
+          minLength: 3
+          maxLength: 120
         thumbnailPath:
           type: string
           example: /static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg
@@ -4301,24 +4373,27 @@ components:
           properties:
             descriptionPath:
               type: string
+              example: /api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/description
+              description: path at which to get the full description of maximum `10000` characters
             support:
               type: string
               description: A text tell the audience how to support the video creator
               example: Please support my work on <insert crowdfunding plateform>! <3
+              minLength: 3
+              maxLength: 1000
             channel:
               $ref: '#/components/schemas/VideoChannel'
             account:
               $ref: '#/components/schemas/Account'
             tags:
-              type: array
-              items:
-                type: string
               example: [flowers, gardening]
-            files:
               type: array
-              description: 'WebTorrent/raw video files. Can be empty if WebTorrent is disabled on the server. In this case, video files will be in the "streamingPlaylists[].files" property'
+              minItems: 1
+              maxItems: 5
               items:
-                $ref: '#/components/schemas/VideoFile'
+                type: string
+                minLength: 2
+                maxLength: 30
             commentsEnabled:
               type: boolean
             downloadEnabled:
@@ -4328,10 +4403,24 @@ components:
               items:
                 type: string
                 format: url
+            files:
+              type: array
+              items:
+                $ref: '#/components/schemas/VideoFile'
+              description: |
+                WebTorrent/raw video files. If WebTorrent is disabled on the server:
+
+                - field will be empty
+                - video files will be found in `streamingPlaylists[].files` field
             streamingPlaylists:
               type: array
               items:
                 $ref: '#/components/schemas/VideoStreamingPlaylists'
+              description: |
+                HLS playlists/manifest files. If HLS is disabled on the server:
+
+                - field will be empty
+                - video files will be found in `files` field
     FileRedundancyInformation:
       properties:
         id:
@@ -4367,9 +4456,7 @@ components:
           type: string
           format: url
         uuid:
-          type: string
-          format: uuid
-          example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          $ref: '#/components/schemas/UUIDv4'
         redundancies:
           type: object
           properties:
@@ -4428,6 +4515,8 @@ components:
         reason:
           type: string
           example: The video is a spam
+          minLength: 2
+          maxLength: 3000
         predefinedReasons:
           $ref: '#/components/schemas/AbusePredefinedReasons'
         reporterAccount:
@@ -4437,17 +4526,10 @@ components:
         moderationComment:
           type: string
           example: Decided to ban the server since it spams us regularly
+          minLength: 2
+          maxLength: 3000
         video:
-          type: object
-          properties:
-            id:
-              type: integer
-            name:
-              type: string
-            uuid:
-              type: string
-              format: uuid
-              example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          $ref: '#/components/schemas/VideoInfo'
         createdAt:
           type: string
           format: date-time
@@ -4457,6 +4539,8 @@ components:
           type: integer
         message:
           type: string
+          minLength: 2
+          maxLength: 3000 
         byModerator:
           type: boolean
         createdAt:
@@ -4478,12 +4562,14 @@ components:
           format: date-time
         name:
           type: string
+          minLength: 3
+          maxLength: 120
         uuid:
-          type: string
-          format: uuid
-          example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          $ref: '#/components/schemas/UUIDv4'
         description:
           type: string
+          minLength: 3
+          maxLength: 10000
         duration:
           type: integer
         views:
@@ -4498,8 +4584,12 @@ components:
       properties:
         displayName:
           type: string
+          minLength: 1
+          maxLength: 120
         description:
           type: string
+          minLength: 3
+          maxLength: 1000
         isLocal:
           type: boolean
         ownerAccount:
@@ -4508,9 +4598,7 @@ components:
             id:
               type: integer
             uuid:
-              type: string
-              format: uuid
-              example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+              $ref: '#/components/schemas/UUIDv4'
     VideoPlaylist:
       properties:
         id:
@@ -4523,12 +4611,14 @@ components:
           format: date-time
         description:
           type: string
+          minLength: 3
+          maxLength: 1000
         uuid:
-          type: string
-          format: uuid
-          example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+          $ref: '#/components/schemas/UUIDv4'
         displayName:
           type: string
+          minLength: 1
+          maxLength: 120
         isLocal:
           type: boolean
         videoLength:
@@ -4552,6 +4642,8 @@ components:
           format: url
         text:
           type: string
+          minLength: 1
+          maxLength: 10000
         threadId:
           type: integer
         inReplyToCommentId:
@@ -4581,7 +4673,7 @@ components:
     VideoCaption:
       properties:
         language:
-          $ref: '#/components/schemas/VideoConstantString'
+          $ref: '#/components/schemas/VideoConstantString-Language'
         captionPath:
           type: string
     ActorImage:
@@ -5119,9 +5211,7 @@ components:
               type: integer
               example: 8
             uuid:
-              type: string
-              format: uuid
-              example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+              $ref: '#/components/schemas/UUIDv4'
     CommentThreadResponse:
       properties:
         total:
@@ -5370,34 +5460,41 @@ components:
         - username
         - password
         - email
-    VideoChannelCreate:
+
+    VideoChannelCommon:
       properties:
-        name:
-          type: string
         displayName:
           type: string
+          minLength: 1
+          maxLength: 120
         description:
           type: string
+          minLength: 3
+          maxLength: 1000
         support:
           type: string
           description: 'A text shown by default on all videos of this channel, to tell the audience how to support it'
           example: Please support my work on <insert crowdfunding plateform>! <3
+          minLength: 3
+          maxLength: 1000
+    VideoChannelCreate:
+      allOf:
+        - $ref: '#/components/schemas/VideoChannelCommon'
+        - properties:
+            name:
+              type: string
+              minLength: 1
+              maxLength: 120
       required:
         - name
         - displayName
     VideoChannelUpdate:
-      properties:
-        displayName:
-          type: string
-        description:
-          type: string
-        support:
-          type: string
-          description: 'A text shown by default on all videos of this channel, to tell the audience how to support it'
-          example: Please support my work on <insert crowdfunding plateform>! <3
-        bulkVideosSupportUpdate:
-          type: boolean
-          description: 'Update the support field for all videos of this channel'
+      allOf:
+        - $ref: '#/components/schemas/VideoChannelCommon'
+        - properties:
+            bulkVideosSupportUpdate:
+              type: boolean
+              description: 'Update the support field for all videos of this channel'
 
     MRSSPeerLink:
       type: object
index 8b13041f8f678628e2545fb9baad1ac7a2744ba8..8f3a17f124f8175d27dd7c33d7296a7b1d39e706 100644 (file)
@@ -6,7 +6,7 @@
 ## Installation
 
 Please don't install PeerTube for production on a device behind a low bandwidth connection (example: your ADSL link).
-If you want information about the appropriate hardware to run PeerTube, please see the [FAQ](https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md#should-i-have-a-big-server-to-run-peertube).
+If you want information about the appropriate hardware to run PeerTube, please see the [FAQ](https://joinpeertube.org/en_US/faq#should-i-have-a-big-server-to-run-peertube).
 
 ### Dependencies
 
index 99a316f715541c77ff674d1e093c9d8e18d44e94..3ce730fbdcb2c7acfc2627e95444c29271dc01c3 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -1349,13 +1349,6 @@ async@>=0.2.9, async@^3.0.1, async@^3.1.0:
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
   integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
 
-async@^2.6.1:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
-  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
-  dependencies:
-    lodash "^4.17.14"
-
 async@~0.9.0:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@@ -3837,7 +3830,7 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
   dependencies:
     is-glob "^4.0.1"
 
-glob@7.1.6, glob@^7.0.3, glob@^7.1.3:
+glob@7.1.6, glob@^7.1.3:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -5014,7 +5007,7 @@ lodash@4.17.19:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
   integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
 
-lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
+lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6902,23 +6895,11 @@ resolve-alpn@^1.0.0:
   resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.1.1.tgz#4a006a7d533c81a5dd04681612090fde227cd6e1"
   integrity sha512-0KbFjFPR2bnJhNx1t8Ad6RqVc8+QPJC4y561FYyC/Q/6OzB3fhUzB5PEgitYhPK6aifwR5gXBSnDMllaDWixGQ==
 
-resolve-from@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
-  integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=
-
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
-resolve-pkg@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-1.0.0.tgz#e19a15e78aca2e124461dc92b2e3943ef93494d9"
-  integrity sha1-4ZoV54rKLhJEYdySsuOUPvk0lNk=
-  dependencies:
-    resolve-from "^2.0.0"
-
 resolve@^1.10.0, resolve@^1.10.1, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
@@ -7054,16 +7035,6 @@ sax@>=0.6.0, sax@^1.2.4:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
-scripty@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/scripty/-/scripty-2.0.0.tgz#25761bb2e237a7563f705d87357db07791d38459"
-  integrity sha512-vbd4FPeuNwYNGtRtYa1wDZLPCx5PpW6VrldCEiBGqPz7Je1xZOgNvVPD2axymvqNghBIRiXxAU+JwYrOzvuLJg==
-  dependencies:
-    async "^2.6.1"
-    glob "^7.0.3"
-    lodash "^4.17.11"
-    resolve-pkg "^1.0.0"
-
 semver-diff@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"