: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
+++ /dev/null
-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
--- /dev/null
+{
+ "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"
+ }
+}
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')
"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",
{{ 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>
justify-content: flex-start;
}
-.showMore {
+.show-more {
@include peertube-button-link;
@include grey-button;
-
+
margin-top: 1%;
}
position: relative;
- &:hover, &:active {
+ &:hover,
+ &:active {
&::after {
content: '#';
display: inline-block;
}
}
- .middle-title, .section-title {
+ .middle-title,
+ .section-title {
display: inline-block;
}
.p2p-privacy,
my-about-peertube-contributors {
::ng-deep {
- p, li {
+ p,
+ li {
font-size: 15px;
}
}
<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">
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));
}
}
<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>
}
.copy-button {
- border: none;
+ border: 0;
}
.account-info {
}
.description:not(.expanded) {
- max-height: 70px;
-
@include fade-text(30px, pvar(--submenuBackgroundColor));
+
+ max-height: 70px;
}
.show-more {
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: [
SharedModerationModule,
SharedVideoMiniatureModule,
SharedGlobalIconModule,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
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 {
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: [
SharedGlobalIconModule,
SharedAbuseListModule,
SharedVideoCommentModule,
- SharedAccountAvatarModule,
SharedActorImageModule,
+ SharedActorImageEditModule,
TableModule,
SelectButtonModule,
display: flex;
margin-left: auto;
- & + .form-error {
+ + .form-error {
display: inline;
margin-left: 5px;
}
}
.disabled-checkbox-extra {
- &, ::ng-deep label {
+ &,
+ ::ng-deep label {
opacity: .5;
pointer-events: none;
}
<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>
</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>
@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);
}
const handle = follow.follower.name + '@' + follow.follower.host
this.notifier.success($localize`${handle} rejected from instance followers`)
- this.loadData()
+ this.reloadData()
},
err => {
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 => {
</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>
@include disable-default-a-behaviour;
display: inline-block;
- &, &:hover {
+ &,
+ &:hover {
color: pvar(--mainForegroundColor);
}
}
}
-.caption {
- justify-content: flex-end;
-
- input {
- @include peertube-input-text(250px);
- }
-}
-
.follow-button {
@include create-button;
}
this.followService.follow(hosts).subscribe(
() => {
this.notifier.success($localize`Follow request(s) sent!`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message)
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 => {
-@import "mixins";
+@import 'mixins';
.form-sub-title {
flex-grow: 0;
@include disable-default-a-behaviour;
display: inline-block;
- &, &:hover {
+ &,
+ &:hover {
color: pvar(--mainForegroundColor);
}
this.pagination.start = 0
this.saveSelectLocalStorage()
- this.loadData()
+ this.reloadData()
}
getRedundancyStrategy (redundancy: VideoRedundancy) {
.subscribe(
() => {
this.notifier.success($localize`Video redundancies removed!`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message)
}
- protected loadData () {
+ protected reloadData () {
const options = {
pagination: this.pagination,
sort: this.sort,
<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>
<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>
<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>
.unblock-button {
@include peertube-button;
@include grey-button;
-}
\ No newline at end of file
+}
</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>
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;
}
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'
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 }
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,
).subscribe(
() => {
this.notifier.success($localize`Video ${videoBlock.video.name} switched to manual block.`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message)
})
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'
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)
)
}
- protected loadData () {
+ protected reloadData () {
this.videoBlocklistService.listBlocks({
pagination: this.pagination,
sort: this.sort,
<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>
<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>
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;
max-height: 22px;
}
- div, p {
+ div,
+ p {
@include ellipsis;
}
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'
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 }
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()
}
ngOnInit () {
this.initialize()
- this.listenToSearchChange()
this.bulkCommentActions = [
{
]
}
- ngAfterViewInit () {
- if (this.search) this.setTableFilter(this.search, false)
- }
-
getIdentifier () {
return 'VideoCommentListComponent'
}
return this.selectedComments.length !== 0
}
- protected loadData () {
+ protected reloadData () {
this.videoCommentService.getAdminVideoComments({
pagination: this.pagination,
sort: this.sort,
this.videoCommentService.deleteVideoComments(commentArgs).subscribe(
() => {
this.notifier.success($localize`${commentArgs.length} comments deleted.`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message),
private deleteComment (comment: VideoCommentAdmin) {
this.videoCommentService.deleteVideoComment(comment.video.id, comment.id)
.subscribe(
- () => this.loadData(),
+ () => this.reloadData(),
err => this.notifier.error(err.message)
)
</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>
@import '_variables';
@import '_mixins';
-.update-button[disabled="true"] ::ng-deep .action-button {
+.update-button[disabled=true] ::ng-deep .action-button {
cursor: default !important;
}
<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>
margin-bottom: 20px;
}
-input[type=submit], button {
+input[type=submit],
+button {
@include peertube-button;
@include orange-button;
+++ /dev/null
-@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;
- }
- }
- }
- }
-}
import { Component } from '@angular/core'
@Component({
- templateUrl: './plugins.component.html',
- styleUrls: [ './plugins.component.scss' ]
+ templateUrl: './plugins.component.html'
})
export class PluginsComponent {
}
my-global-icon {
@include apply-svg-color(pvar(--greyForegroundColor));
- &[iconName="npm"] {
+ &[iconName=npm] {
@include fill-svg-color(pvar(--greyForegroundColor));
}
}
.buttons {
margin-left: auto;
width: max-content;
+
> *:not(:last-child) {
margin-right: 10px;
}
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;
+ }
+ }
+}
}
.job-error {
- color: red;
+ color: #ff0000;
}
.badge {
onJobStateOrTypeChanged () {
this.pagination.start = 0
- this.loadData()
+ this.reloadData()
this.saveJobStateAndType()
}
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
ng-select,
my-button {
width: 100% !important;
- margin-left: 0px !important;
+ margin-left: 0 !important;
margin-bottom: 10px !important;
}
ng-select,
my-button {
width: 100% !important;
- margin-left: 0px !important;
+ margin-left: 0 !important;
margin-bottom: 10px !important;
}
display: block;
}
-input[type=submit], button {
+input[type=submit],
+button {
@include peertube-button;
@include orange-button;
display: block;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
- border-right: none;
+ border-right: 0;
}
input[type=submit] {
<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>
.table-email {
@include disable-default-a-behaviour;
+
color: pvar(--mainForegroundColor);
}
.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;
.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;
- }
-}
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'
@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
.subscribe(config => this.serverConfig = config)
this.initialize()
- this.listenToSearchChange()
this.bulkUserActions = [
[
}
onUserChanged () {
- this.loadData()
+ this.reloadData()
}
async unbanUsers (users: User[]) {
.subscribe(
() => {
this.notifier.success($localize`${users.length} users unbanned.`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message)
this.userService.removeUser(users).subscribe(
() => {
this.notifier.success($localize`${users.length} users deleted.`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message)
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)
return this.selectedUsers.length !== 0
}
- protected loadData () {
+ protected reloadData () {
this.selectedUsers = []
this.userService.getUsers({
}
}
-.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);
justify-content: space-around;
flex-wrap: wrap;
- & > div {
+ > div {
flex: 1 1;
}
form {
margin: 0;
- &, input {
+ &,
+ input {
width: 100%;
}
color: var(--mainColor);
- &:hover, &:active {
+ &:hover,
+ &:active {
color: var(--mainHoverColor);
}
}
min-width: 100px;
&:hover {
- background-color: rgba(209, 215, 224, 0.5)
+ background-color: rgba(209, 215, 224, 0.5);
}
}
}
}
}
-@mixin columnReverseDisplay {
+@mixin column-reverse-display {
flex-direction: column-reverse;
.login-form-and-externals,
@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();
}
}
}
<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>
display: flex;
margin-left: auto;
- & + .form-error {
+ + .form-error {
display: inline;
margin-left: 5px;
}
.header {
flex-direction: column;
- & >:first-child, .peertube-select-container {
+ > :first-child,
+ .peertube-select-container {
margin-bottom: 15px;
}
@include danger-button;
@include disable-outline;
}
-}
\ No newline at end of file
+}
font-size: 16px;
}
- & > div {
+ > div {
padding: 10px;
&:first-child {
@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;
}
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'
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: [
SharedGlobalIconModule,
SharedAbuseListModule,
SharedShareModal,
- SharedAccountAvatarModule,
- SharedActorImageModule
+ SharedActorImageModule,
+ SharedActorImageEditModule
],
declarations: [
width: auto !important;
}
- label[for=name] + div, textarea {
+ label[for=name] + div,
+ textarea {
width: 100%;
}
}
<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">
@import '_variables';
@import '_mixins';
+h1 my-global-icon {
+ position: relative;
+ top: -2px;
+}
+
.create-button {
@include create-button;
}
@include peertube-input-text(300px);
}
-::ng-deep .action-button {
- &.action-button-edit {
- margin-right: 10px;
- }
+my-edit-button {
+ margin-right: 10px;
}
.video-channel {
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 {
.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;
}
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) {
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,
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) {
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
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'
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: [
SharedMainModule,
SharedFormModule,
SharedGlobalIconModule,
+ SharedActorImageEditModule,
SharedActorImageModule
],
<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>
}
.delete-history {
- grid-column: 4;
-
@include peertube-button;
@include grey-button;
@include button-with-icon;
+ grid-column: 4;
+
font-size: 15px;
}
}
-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,
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) {
() => {
this.notifier.success($localize`Videos history deleted`)
- this.reloadVideos()
+ this.reloadData()
},
err => this.notifier.error(err.message)
@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;
}
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: [
SharedAbuseListModule,
SharedShareModal,
SharedVideoLiveModule,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
<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>
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;
div .glyphicon {
font-size: 80%;
- color: gray;
+ color: #808080;
margin-left: 0.1rem;
}
}
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 => {
</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">
<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>
.video-channel {
@include row-blocks;
- img {
- @include channel-avatar(80px);
+ > my-actor-avatar {
+ @include actor-avatar-size(80px);
margin-right: 10px;
}
}
.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) {
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'
templateUrl: './my-subscriptions.component.html',
styleUrls: [ './my-subscriptions.component.scss' ]
})
-export class MySubscriptionsComponent implements OnInit {
+export class MySubscriptionsComponent {
videoChannels: VideoChannel[] = []
pagination: ComponentPagination = {
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
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
}
.video-import-error {
- color: red;
+ color: #ff0000;
}
.badge {
return '/videos/update/' + video.uuid
}
- protected loadData () {
+ protected reloadData () {
this.videoImportService.getMyVideoImports(this.pagination, this.sort)
.subscribe(
resultList => {
}
.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 {
}
.video:last-child {
- border: none;
+ border: 0;
}
.videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) {
</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>
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'
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,
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(
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
.form-group {
margin: 20px 0;
-}
\ No newline at end of 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">
[titlePage]="titlePage"
[getVideosObservableFunction]="getVideosObservableFunction"
[user]="user"
+ [loadOnInit]="false"
#videosSelection
>
<ng-template ptTemplate="globalButtons">
</ng-template>
</my-videos-selection>
-
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
<my-live-stream-information #liveStreamInformationModal></my-live-stream-information>
}
.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);
}
-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'
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,
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 () {
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)
)
font-weight: $font-semibold;
display: inline-block;
- padding: 0 10px 0 10px;
+ padding: 0 10px;
white-space: nowrap;
background: transparent;
<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>
$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);
}
}
return 'internal'
}
- isExternalChannelUrl () {
- return this.getVideoLinkType() === 'external'
- }
-
search () {
forkJoin([
this.getVideosObs(),
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 () {
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'
SharedMainModule,
SharedSearchModule,
SharedFormModule,
+ SharedActorImageModule,
SharedUserSubscriptionModule,
SharedVideoMiniatureModule
],
border-color: pvar(--mainColor) transparent transparent transparent;
}
- & + div {
+ + div {
font-size: 15px;
}
}
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;
}
}
text-align: center;
}
-
-@-webkit-keyframes dash {
- 0% {
- stroke-dashoffset: 1000;
- }
- 100% {
- stroke-dashoffset: 0;
- }
-}
-
@keyframes dash {
0% {
stroke-dashoffset: 1000;
}
}
-@-webkit-keyframes dash-check {
- 0% {
- stroke-dashoffset: -100;
- }
- 100% {
- stroke-dashoffset: 900;
- }
-}
-
@keyframes dash-check {
0% {
stroke-dashoffset: -100;
<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>
<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>
</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>
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;
}
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: [
SharedUserSubscriptionModule,
SharedGlobalIconModule,
SharedSupportModal,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
}
.warning-replace-caption {
- color: red;
+ color: #ff0000;
margin-top: 10px;
-}
\ No newline at end of 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>
@include media-breakpoint-up(md) {
@include make-col(7);
- & + .col-video-edit {
+ + .col-video-edit {
@include make-col(5);
}
}
@include media-breakpoint-up(xl) {
@include make-col(8);
- & + .col-video-edit {
+ + .col-video-edit {
@include make-col(4);
}
}
@include media-breakpoint-up(md) {
@include make-col(8);
- & + .col-video-edit {
+ + .col-video-edit {
@include make-col(4);
}
}
.alert.alert-danger {
text-align: center;
- & > div {
+ > div {
font-weight: $font-semibold;
}
}
align-items: center;
.upload-icon {
+ @include apply-svg-color(#C6C6C6);
+
width: 90px;
margin-bottom: 25px;
-
- @include apply-svg-color(#C6C6C6);
}
.peertube-select-container {
::ng-deep .video-add-nav {
border-bottom: $border-width $border-type $border-color;
- margin: 20px 0 0 0 !important;
+ margin: 20px 0 0 !important;
&.hide-nav {
display: none !important;
<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
display: flex;
margin-bottom: 10px;
- my-account-avatar {
- vertical-align: top;
+ my-actor-avatar {
margin-right: 10px;
}
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 {
}
}
- &:focus, &:active, &:hover {
+ &:focus,
+ &:active,
+ &:hover {
my-global-icon svg {
background-color: #C6C6C6;
color: pvar(--mainBackgroundColor);
-<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>
[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"
}
}
+my-actor-avatar {
+ @include actor-avatar-size(36px);
+}
+
.comment {
flex-grow: 1;
// Fix word-wrap with flex
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);
}
}
}
}
-.children {
+.is-child {
// Reduce avatars size for replies
- .comment-avatar {
- @include avatar(25px);
+ my-actor-avatar {
+ @include actor-avatar-size(25px);
}
.left {
(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
cursor: pointer;
}
-.glyphicon, .comment-thread-loading {
+.glyphicon,
+.comment-thread-loading {
margin-right: 5px;
display: inline-block;
font-size: 13px;
#dropdown-sort-comments {
font-weight: 600;
text-transform: uppercase;
- border: none;
+ border: 0;
transform: translateY(-7%);
}
<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" />
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;
}
-<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>
@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();
+ }
}
}
@Input() video: Video
@Input() byAccount: string
- @Input() size: 'md' | 'sm' = 'md'
@Input() genericChannel: boolean
channelLinkTitle = ''
my-global-icon {
&:not(.active) {
- opacity: .5
+ opacity: .5;
}
::ng-deep {
<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"
$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 {
.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;
.video-info-first-row {
display: flex;
- & > div:first-child {
+ > div:first-child {
flex-grow: 1;
}
}
.video-actions-rates {
- margin: 0 0 10px 0;
+ margin: 0 0 10px;
align-items: start;
width: max-content;
margin-left: auto;
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);
}
}
- .glyphicon, .description-loading {
+ .glyphicon,
+ .description-loading {
margin-left: 3px;
}
&.video-attribute-tags {
.video-attribute-value:not(:nth-child(2)) {
&::before {
- content: ', '
+ content: ', ';
}
}
}
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({
SharedShareModal,
SharedVideoModule,
SharedSupportModal,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
<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>
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;
}
}
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[]) {
height: 1rem;
margin-right: .1rem;
}
-}
\ No newline at end of file
+}
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'
SharedFormModule,
SharedVideoMiniatureModule,
SharedUserSubscriptionModule,
- SharedGlobalIconModule
+ SharedGlobalIconModule,
+ SharedActorImageModule
],
declarations: [
display: inline-block;
width: 23px;
height: 24px;
- margin-right: .5rem;
+ margin-right: 0.5rem;
}
@media screen and (max-width: $mobile-view) {
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')
SharedUserInterfaceSettingsModule,
SharedGlobalIconModule,
SharedInstanceModule,
- SharedAccountAvatarModule,
+ SharedActorImageModule,
MetaModule.forRoot({
provide: MetaLoader,
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;
}
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'
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
initialize () {
this.loadSort()
- this.initSearch()
}
loadSort () {
count: this.rowsPerPage
}
- this.loadData()
+ this.reloadData()
this.saveSort()
}
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)
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()
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
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
}
})
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
}
// 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;
}
#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;
}
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;
&:focus,
::ng-deep &:focus-within {
- & > div:last-child {
+ > div:last-child {
@media screen and (min-width: $mobile-view) {
display: initial !important;
}
#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;
a {
@include disable-default-a-behaviour;
+
width: 100%;
- &, &:hover {
+ &,
+ &:hover {
color: pvar(--mainForegroundColor);
&.focus-visible {
}
my-global-icon {
+ @include apply-svg-color(pvar(--mainForegroundColor));
+
width: 17px;
position: relative;
top: -2px;
margin: 5px;
-
- @include apply-svg-color(pvar(--mainForegroundColor));
}
@include peertube-button-link;
@include orange-button;
+ border-radius: 0;
+
&.focus-visible,
&:focus {
box-shadow: none;
}
-
- border-radius: 0;
}
.modal-body {
<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>
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 {
margin: 0;
padding: 0;
- &:focus, &:hover {
+ &:focus,
+ &:hover {
overflow-y: auto;
}
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);
}
position: absolute;
right: -35px;
top: -8px;
- color: grey;
+ color: #808080;
width: $main-radius;
}
}
.dropdown-toggle {
&::after {
- border: none;
+ border: 0;
}
}
}
}
-my-account-avatar {
+my-actor-avatar {
margin-right: 10px;
}
}
.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 {
}
.login-buttons-block {
- margin: 30px 25px 35px 25px;
+ margin: 30px 25px 35px;
> a {
display: block;
}
.footer-links {
- &, > div {
+ &,
+ > div {
display: flex;
flex-wrap: wrap;
}
.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;
.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);
}
}
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;
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;
text-align: center;
font-weight: 600;
font-size: 18px;
- margin: 20px 0 40px 0;
+ margin: 20px 0 40px;
}
.columns {
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
<span class="col-9 moderation-expanded-text">
- <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
+ <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
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:"' + abuse.reporterAccount.displayName + '"' }"
+ <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
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:"' +abuse.flaggedAccount.displayName + '"' }"
+ <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
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:"' +abuse.flaggedAccount.displayName + '"' }"
+ <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
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>
<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>
<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>
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'
export class AbuseDetailsComponent {
@Input() abuse: ProcessedAbuse
@Input() isAdminView: boolean
- @Input() baseRoute: string
private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
<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"
<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>
<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>
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'
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'
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
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,
]
this.initialize()
- this.listenToSearchChange()
- }
-
- ngAfterViewInit () {
- if (this.search) this.setTableFilter(this.search, false)
}
isAdminView () {
}
onModerationCommentUpdated () {
- this.loadData()
+ this.reloadData()
}
isAbuseAccepted (abuse: AdminAbuse) {
this.abuseService.removeAbuse(abuse).subscribe(
() => {
this.notifier.success($localize`Abuse deleted.`)
- this.loadData()
+ this.reloadData()
},
err => this.notifier.error(err.message)
updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
this.abuseService.updateAbuse(abuse, { state })
.subscribe(
- () => this.loadData(),
+ () => this.reloadData(),
err => this.notifier.error(err.message)
)
return Actor.IS_LOCAL(abuse.reporterAccount.host)
}
- protected loadData () {
+ protected reloadData () {
logger('Loading data.')
const options = {
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: [
SharedModerationModule,
SharedGlobalIconModule,
SharedVideoCommentModule,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
+++ /dev/null
-<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>
+++ /dev/null
-@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
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-export * from './account-avatar.component'
-export * from './shared-account-avatar.module'
\ No newline at end of file
+++ /dev/null
-
-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 { }
<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">
<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>
.actor {
display: flex;
- img {
+ my-actor-avatar {
margin-right: 15px;
-
- &:not(.channel) {
- @include avatar(100px);
- }
-
- &.channel {
- @include channel-avatar(100px);
- }
}
.actor-info {
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
+ }
}
--- /dev/null
+export * from './shared-actor-image-edit.module'
--- /dev/null
+
+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 { }
--- /dev/null
+<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>
--- /dev/null
+@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;
+ }
+}
--- /dev/null
+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]
+ }
+}
-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: [ ]
--- /dev/null
+<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>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input {
+ @include peertube-input-text(250px);
+}
+
+.input-group-text {
+ background-color: transparent;
+}
--- /dev/null
+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 })
+ }
+}
-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'
position: absolute;
visibility: hidden;
- & + label {
+ + label {
cursor: pointer;
text-indent: -9999px;
width: 35px;
position: relative;
margin: 0;
- &:after {
+ &::after {
content: '';
position: absolute;
top: 3px;
transition: 0.3s ease-out;
}
- &:active:after {
+ &:active::after {
width: 40px;
}
}
&:checked + label {
background: pvar(--mainColor);
- &:after {
+ &::after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
font-family: monospace;
font-size: 13px;
- border-bottom: none;
+ border-bottom: 0;
border-bottom-left-radius: unset;
border-bottom-right-radius: unset;
}
opacity: 0.6;
}
- &:hover, &:active {
+ &:hover,
+ &:active {
svg {
opacity: 1;
}
}
@mixin maximized-base {
+ $nav-preview-vertical-padding: 40px;
+
flex-direction: row;
z-index: #{z(header) - 1};
position: fixed;
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);
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;
}
}
@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}) {
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
+}
max-width: 100%;
&.no-image {
- border: 2px solid grey;
+ border: 2px solid #808080;
background-color: pvar(--mainBackgroundColor);
}
}
}
.root {
- display:flex;
+ display: flex;
align-items: center;
> my-select-options {
}
my-select-options + input {
- margin-left: 5px;
-
@include peertube-input-text($form-base-input-width);
+
+ margin-left: 5px;
display: block;
}
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'
SelectCheckboxComponent,
SelectCustomValueComponent,
- DynamicFormFieldComponent
+ DynamicFormFieldComponent,
+
+ AdvancedInputFilterComponent
],
exports: [
SelectCheckboxComponent,
SelectCustomValueComponent,
- DynamicFormFieldComponent
+ DynamicFormFieldComponent,
+
+ AdvancedInputFilterComponent
],
providers: [
::ng-deep input {
width: 80px;
font-size: 15px;
-
- border: none;
+ border: 0;
&:focus-within,
&:focus {
@import '_variables';
@import '_mixins';
-@import "./_bootstrap-variables";
+@import './_bootstrap-variables';
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
background-color: unset;
padding: 0;
- & + .collapse.show {
+ + .collapse.show {
background-color: var(--submenuBackgroundColor);
}
}
.more-info {
font-style: italic;
font-weight: initial;
- font-size: 14px
+ font-size: 14px;
}
}
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 () {
constructor (hash: ServerAccount) {
super(hash)
- this.updateComputedAttributes()
-
this.displayName = hash.displayName
this.description = hash.description
this.userId = hash.userId
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)
}
}
updatedAt: Date | string
avatar: ActorImage
- avatarUrl: string
isLocal: boolean
.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;
}
@include dropdown-with-icon-item;
}
- a, span {
+ a,
+ span {
display: block;
width: 100%;
}
@import '_variables';
@import '_mixins';
+@mixin responsive-label {
+ .action-button {
+ padding: 0 13px;
+ }
+
+ .button-label {
+ display: none;
+ }
+}
+
:host {
outline: none;
}
// 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;
}
}
@Component({
selector: 'my-button',
- styleUrls: ['./button.component.scss'],
+ styleUrls: [ './button.component.scss' ],
templateUrl: './button.component.html'
})
@Input() title: string = undefined
@Input() loading = false
@Input() disabled = false
+ @Input() responsiveLabel = false
getTitle () {
return this.title || this.label
getClasses () {
return {
[this.className]: true,
- disabled: this.disabled
+ disabled: this.disabled,
+ 'responsive-label': this.responsiveLabel
}
}
}
-<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>
export class DeleteButtonComponent implements OnInit {
@Input() label: string
@Input() title: string
+ @Input() responsiveLabel = false
ngOnInit () {
// <my-delete-button /> No label
-<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>
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
.date-toggle {
&:hover {
- cursor: default
+ cursor: default;
}
}
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))
}
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) {
@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 {
<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">
button {
width: 30px;
- border: none;
+ border: 0;
&::after {
display: none;
}
- &.routeActive {
+ &.route-active {
&::after {
display: inherit;
border: 2px solid pvar(--mainColor);
margin-top: 0 !important;
position: static;
right: auto;
- bottom: auto
+ bottom: auto;
}
.modal-body {
}
}
-::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;
}
}
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()
}
}
}
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;
}
margin-right: 5px;
}
- &, .progress {
+ &,
+ .progress {
width: 100% !important;
}
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) {
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()
}
updateComputedAttributes () {
- this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
}
}
byVideoChannel: string
byAccount: string
- videoChannelAvatarUrl: string
-
createdAt: Date
updatedAt: Date
publishedAt: Date
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)
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 })
>
<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>
<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>
@import '_variables';
@import '_mixins';
-.caption {
- justify-content: flex-end;
-
- input {
- @include peertube-input-text(250px);
- flex-grow: 1;
- }
-}
-
.chip {
@include chip;
}
.unblock-button {
@include peertube-button;
@include grey-button;
-}
\ No newline at end of file
+}
: $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,
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;
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;
div .glyphicon {
font-size: 80%;
- color: gray;
+ color: #808080;
margin-left: 0.1rem;
}
</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>
@include disable-default-a-behaviour;
display: inline-block;
- &, &:hover {
+ &,
+ &:hover {
color: pvar(--mainForegroundColor);
}
}
}
-.caption {
- justify-content: flex-end;
-
- input {
- @include peertube-input-text(250px);
- flex-grow: 1;
- }
-}
-
.unblock-button {
@include peertube-button;
@include grey-button;
@include create-button;
}
-.caption {
- justify-content: flex-end;
-
- input {
- @include peertube-input-text(250px);
- flex-grow: 1;
- }
-}
-
.chip {
@include chip;
}
: $localize`Instance ${host} unmuted by your instance.`
)
- this.loadData()
+ this.reloadData()
}
)
}
: $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,
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: [
SharedFormModule,
SharedGlobalIconModule,
SharedVideoCommentModule,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
.live-info {
font-size: 15px;
- margin: 40px 0 20px 0;
+ margin: 40px 0 20px;
}
-import { NSFWQuery, SearchTargetType } from '@shared/models'
+import { BooleanBothQuery, SearchTargetType } from '@shared/models'
export class AdvancedSearch {
startDate: string // ISO 8601
originallyPublishedStartDate: string // ISO 8601
originallyPublishedEndDate: string // ISO 8601
- nsfw: NSFWQuery
+ nsfw: BooleanBothQuery
categoryOneOf: string
endDate?: string
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
- nsfw?: NSFWQuery
+ nsfw?: BooleanBothQuery
categoryOneOf?: string
licenceOneOf?: string
languageOneOf?: string
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%;
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,
padding: 3px;
my-global-icon {
+ @include apply-svg-color(#fff);
+
width: 22px;
height: 22px;
-
- @include apply-svg-color(#fff);
}
}
<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="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>
ngOnInit () {
this.allLanguagesGroup = $localize`All languages`
- let oldForm: any
-
this.buildForm({
nsfwPolicy: null,
webTorrentEnabled: null,
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()
})
}
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) {
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,
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"
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.`)
+ }
}
.btn-remote-follow {
@include peertube-button;
@include orange-button;
-}
\ No newline at end of file
+}
float: right;
padding: 0;
- & > .btn,
- & > .dropdown > .dropdown-toggle {
+ > .btn,
+ > .dropdown > .dropdown-toggle {
font-size: 15px;
}
&.big {
height: 35px;
- & > button:first-child {
+ > button:first-child {
width: max-content;
min-width: 175px;
}
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 {
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:' },
@import '_mixins';
@import '_miniature';
-$iconSize: 16px;
+$icon-size: 16px;
::ng-deep my-video-list-header {
display: flex;
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;
+ }
}
}
.title-page {
margin-bottom: 10px;
- margin-right: 0px;
+ margin-right: 0;
}
}
}
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: [
SharedGlobalIconModule,
SharedVideoLiveModule,
SharedVideoModule,
- SharedAccountAvatarModule
+ SharedActorImageModule
],
declarations: [
border-top-right-radius: 0;
border-bottom-right-radius: 0;
- border-right: none;
+ border-right: 0;
select {
height: inherit;
&.metadata-attribute-tags {
.metadata-attribute-value:not(:nth-child(2)) {
&::before {
- content: ', '
+ content: ', ';
}
}
}
<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"
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;
}
}
.video-info-blocked {
- color: red;
+ color: #ff0000;
.blocked-reason::before {
content: ' - ';
}
.video-info-nsfw {
- color: red;
+ color: #ff0000;
}
.video-actions {
} 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'
}
@Input() displayVideoActions = true
+ @Input() actorImageSize: ActorAvatarSize = '40'
+
@Input() displayAsRow = false
@Input() videoLinkType: VideoLinkType = 'internal'
return ''
}
- getAvatarUrl () {
- if (this.displayOwnerAccount()) {
- return this.video.account.avatar?.url
- }
-
- return this.video.videoChannelAvatarUrl
- }
-
loadActions () {
if (this.displayVideoActions) this.showActions = true
-<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>
@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>>
border-top: 1px solid $separator-border-color;
}
-.new-playlist-button {
+.new-playlist-button {
cursor: pointer;
my-global-icon {
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;
display: flex;
&::after {
- border: none;
+ border: 0;
}
}
}
display: inline-block;
width: 100%;
- &.no-videos:not(.to-manage){
+ &.no-videos:not(.to-manage) {
a {
cursor: default !important;
}
embedUrl: string
ownerBy: string
- ownerAvatarUrl: string
videoChannelBy?: string
- videoChannelAvatarUrl?: string
private thumbnailVersion: number
private originThumbnailUrl: string
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)
--- /dev/null
+<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
+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)
private networkInfoInterval: any
+ private hlsjsCurrentLevel: number
+ private hlsjsLevels: Hlsjs.Level[]
+
constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
super(player)
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
}
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)
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)
}
}
function saveAverageBandwidth (value: number) {
+ /** used to choose the most fitting resolution */
return setLocalStorage('average-bandwidth', value.toString())
}
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'
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)
})
})
}
+ 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
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' {
bezels (): void
+ stats (options?: StatsCardOptions): StatsForNerdsPlugin
+
qualityLevels (): QualityLevels
textTracks (): TextTrackList & {
uploaded: number
numPeers: number
}
+
+ // In bytes
+ bandwidthEstimate: number
}
type PlaylistItemOptions = {
--- /dev/null
+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} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
+ : undefined
+
+ const totalTransferred = playerNetworkInfo.totalDownloaded
+ ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
+ : 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
+}
--- /dev/null
+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 }
}
}
- document.removeEventListener('click', this.documentClickHandler)
- if (this.isInIframe()) {
- window.removeEventListener('blur', this.documentClickHandler)
- }
-
this.hideDialog()
if (this.settingsButtonOptions.entries.length === 0) {
}
}
+ dispose () {
+ document.removeEventListener('click', this.documentClickHandler)
+
+ if (this.isInIframe()) {
+ window.removeEventListener('blur', this.documentClickHandler)
+ }
+ }
+
onAddSettingsItem (event: any, data: any) {
const [ entry, options ] = data
uploadSpeed: this.torrent.uploadSpeed,
downloaded: this.torrent.downloaded,
uploaded: this.torrent.uploaded
- }
+ },
+ bandwidthEstimate: this.webtorrent.downloadSpeed
} as PlayerNetworkInfo)
}, this.CONSTANTS.INFO_SCHEDULER)
}
@import './bootstrap';
@import './primeng-custom';
-@import './ng-select.scss';
+@import './ng-select';
-@import './classes.scss';
+@import './classes';
[hidden] {
display: none !important;
background-color: pvar(--inputBackgroundColor) !important;
}
-input, textarea {
+input,
+textarea {
outline: none;
color: pvar(--inputForegroundColor);
}
button {
- background: unset;
@include disable-outline;
+
+ background: unset;
}
label {
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;
}
.glyphicon-black {
- color: black;
+ color: #000;
}
.row {
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 {
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) {
background-color: pvar(--submenuBackgroundColor);
}
- &.active, &:hover, &:active, &:focus {
+ &.active,
+ &:hover,
+ &:active,
+ &:focus {
opacity: 1;
}
}
// 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;
}
}
@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;
}
}
// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
.glyphicon-refresh-animate {
- animation: spin .7s infinite linear;
+ animation: spin 0.7s infinite linear;
}
.glyphicon-duplicate {
from {
transform: scale(1) rotate(0deg);
}
+
to {
transform: scale(1) rotate(360deg);
}
&.active {
color: pvar(--mainBackgroundColor) !important;
background-color: pvar(--mainHoverColor);
- opacity: .9;
+ opacity: 0.9;
}
&:active {
}
@media screen and (min-width: #{breakpoint(md)}) {
- .modal:before {
+ .modal::before {
vertical-align: middle;
- content: " ";
+ content: ' ';
height: 100%;
}
}
.modal-header {
- border-bottom: none;
+ border-bottom: 0;
margin-bottom: 5px;
.modal-title {
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;
}
}
text-align: right;
> .peertube-button:not(:first-child) {
- margin-left: 10px
+ margin-left: 10px;
}
}
}
// 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;
}
.menu-open {
.main-col {
&::before {
- background-color: black;
+ background-color: #000;
width: 100vw;
height: 100vh;
opacity: 0.75;
.nav-link {
opacity: 0.6 !important;
- &.active, &:hover, &:active, &:focus {
+ &.active,
+ &:hover,
+ &:active,
+ &:focus {
opacity: 1 !important;
}
}
color: pvar(--mainForegroundColor);
font-weight: $font-semibold;
- border: none;
+ border: 0;
border-bottom: 2px solid transparent;
opacity: 0.6;
border-bottom-color: pvar(--mainColor);
}
- &.active, &:hover, &:active, &:focus {
+ &.active,
+ &:hover,
+ &:active,
+ &:focus {
opacity: 1;
}
}
}
.input-group {
- & > .form-control {
+ > .form-control {
flex: initial;
}
+
input.form-control {
width: unset !important;
flex-grow: 1;
border: 1px solid #eee;
border-radius: .25rem;
- & > label {
+ > label {
position: relative;
top: -5px;
left: -10px;
@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) {
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);
}
}
}
-@import "./_bootstrap-variables";
+@import './_bootstrap-variables';
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
-@font-face{
+@font-face {
font-family: 'Source Sans Pro';
font-weight: 200 900;
font-style: normal;
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;
@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;
opacity: 0;
background-color: rgba(0, 0, 0, 0.3);
- &, .icon {
+ &,
+ .icon {
transition: all $play-overlay-transition;
}
&.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,
@import '_variables';
@mixin disable-default-a-behaviour {
- &:hover, &:focus, &:active {
+ &:hover,
+ &:focus,
+ &:active {
text-decoration: none !important;
outline: none !important;
}
@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;
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;
}
}
}
$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;
$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;
@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';
}
}
$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;
}
.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;
}
&.ng-select-multiple .ng-select-container .ng-value-container {
padding-left: 12px;
- .ng-value {
+
+ .ng-value { /* stylelint-disable-line */
margin-left: 3px;
}
}
.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;
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;
}
@import './settings-menu';
@import './spinner';
@import './upnext';
-@import './bezels.scss';
-@import './playlist.scss';
+@import './bezels';
+@import './playlist';
+@import './stats';
}
}
}
-}
\ No newline at end of file
+}
}
.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%;
&::-moz-focus-inner {
border: 0;
- padding: 0
+ padding: 0;
}
.vjs-icon-placeholder::before {
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);
}
}
// 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;
.vjs-fullscreen-control,
.vjs-peertube-link,
.vjs-theater-control,
- .vjs-settings
- {
+ .vjs-settings {
color: pvar(--embedForegroundColor) !important;
opacity: $primary-foreground-opacity;
}
.vjs-load-progress {
- &, & div {
+ &,
+ div {
background: rgba(255, 255, 255, .2);
}
}
line-height: calc(#{$control-bar-height} - 1px);
&::after {
- content: "/";
+ content: '/';
margin: 0 1px 0 2px;
}
}
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;
}
&.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;
}
.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%;
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;
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;
}
}
}
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;
}
}
.item-player {
- display: none;
-
@include play-icon(20px, 16px);
+
+ display: none;
}
&.vjs-selected {
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 {
opacity: 1;
}
}
-}
\ No newline at end of file
+}
--- /dev/null
+@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;
+ }
+}
.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 {
float: none;
padding: 10px !important;
font-size: 16px !important;
- border: none;
+ border: 0;
}
.vjs-upnext-cancel-button,
}
.vjs-upnext-cancel-button:hover {
- background-color: rgba(255,255,255,0.25);
+ background-color: rgba(255, 255, 255, 0.25);
border-radius: 2px;
}
}
.vjs-upnext-autoplay-icon {
+ @include transition(stroke-dasharray 0.1s cubic-bezier(0.4,0,1,1));
+
position: absolute;
top: 50%;
left: 50%;
height: 98px;
margin: -49px 0 0 -49px;
cursor: pointer;
-
- @include transition(stroke-dasharray 0.1s cubic-bezier(0.4,0,1,1));
}
}
@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 {
.left-buttons {
padding-left: 15px;
}
-
- .input-group-text {
- background-color: transparent;
- }
}
}
}
/* fill the entire space */
-html, body {
+html,
+body {
height: 100%;
margin: 0;
background-color: #000;
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 {
@media screen and (max-width: 300px) {
#error-block {
font-size: 36px;
+ }
- #error-content {
- font-size: 14px;
- }
+ #error-content {
+ font-size: 14px;
}
}
}
iframe {
- border: none;
+ border: 0;
border-radius: 8px;
min-width: 200px;
width: 100%;
.icon {
height: 100%;
padding: 0 18px 0 32px;
- background: white;
+ background: #fff;
display: flex;
align-items: center;
margin-right: 0.5em;
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;
display: flex;
flex-wrap: wrap;
- & > * {
+ > * {
flex-grow: 0;
}
}
fieldset {
- border: none;
+ border: 0;
min-width: 8em;
legend {
border-bottom: 1px solid #ccc;
button {
background: #F1680D;
- color: white;
+ color: #fff;
font-weight: bold;
border-radius: 5px;
margin: 0;
padding: 1em 1.25em;
- border: none;
+ border: 0;
}
a {
text-decoration: underline;
}
- &, &:hover, &:focus, &:visited, &:active {
+ &,
+ &:hover,
+ &:focus,
+ &:visited,
+ &:active {
color: #F44336;
}
}
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"
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"
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"
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"
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"
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=
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"
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"
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"
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"
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"
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"
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==
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"
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=
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"
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"
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"
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"
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"
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==
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==
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==
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=
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"
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"
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"
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==
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=
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"
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==
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"
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"
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"
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==
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"
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"
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==
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"
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"
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"
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"
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"
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"
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"
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==
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"
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==
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"
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"
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"
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==
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==
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"
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==
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"
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"
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"
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"
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"
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"
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"
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"
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"
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==
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"
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"
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"
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==
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"
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"
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==
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"
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"
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"
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"
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"
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"
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==
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"
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"
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==
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"
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==
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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==
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"
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"
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"
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"
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"
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"
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"
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"
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=
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"
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"
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"
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==
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"
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"
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"
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"
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"
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"
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"
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"
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==
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"
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"
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"
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"
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"
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"
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=
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"
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"
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==
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"
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"
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"
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"
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==
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"
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==
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"
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"
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==
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==
"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",
"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": [
{
+++ /dev/null
-#!/bin/sh
-
-set -eu
-
-rm -rf dist/
+++ /dev/null
-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)
- })
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
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"
'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)
+++ /dev/null
-#!/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
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)),
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,
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(
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(
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'
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,
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'
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,
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'
}
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
return isArray(value) && value.every(v => typeof v === 'string')
}
-function isNSFWQueryValid (value: any) {
+function isBooleanBothQueryValid (value: any) {
return value === 'true' || value === 'false' || value === 'both'
}
export {
isNumberArray,
isStringArray,
- isNSFWQueryValid,
+ isBooleanBothQueryValid,
isSearchTargetValid
}
}
}
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
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'
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 = {
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)
}
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
}
// ---------------------------------------------------------------------------
buildVideoThumbnailsFromReq,
setVideoTags,
addOptimizeOrMergeAudioJob,
- getJobTranscodingPriorityMalus
+ getTranscodingJobPriority
}
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,
.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'),
start: number
sort: string
+ nsfw?: boolean
filter?: VideoFilter
+ isLive?: boolean
+
categoryOneOf?: number[]
- nsfw?: boolean
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: string[]
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) {
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: [
{
]
}
- if (search) {
- baseQuery = Object.assign(baseQuery, {
- where: {
- name: {
- [Op.iLike]: '%' + search + '%'
- }
- }
- })
- }
-
return baseQuery
}
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)) {
followerActorId,
serverAccountId: serverActor.Account.id,
nsfw: options.nsfw,
+ isLive: options.isLive,
categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf,
languageOneOf: options.languageOneOf,
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
nsfw?: boolean
+ isLive?: boolean
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
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,
doubleFollow,
flushAndRunMultipleServers,
getLive,
+ getMyVideosWithFilter,
getPlaylist,
getVideo,
getVideoIdFromUUID,
getVideosList,
+ getVideosWithFilters,
killallServers,
makeRawRequest,
removeVideo,
testImage,
updateCustomSubConfig,
updateLive,
+ uploadVideoAndGetId,
viewVideo,
wait,
waitJobs,
})
})
+ 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
/* 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'
server = await flushAndRunServer(1)
await setAccessTokensToServers([ server ])
+ await setDefaultVideoChannel([ server ])
{
const attributes1 = {
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 ])
})
})
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)
})
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)
}
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 {
.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,
.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)
completeVideoCheck,
checkVideoFilesWereRemoved,
getPlaylistVideos,
+ getMyVideosWithFilter,
uploadVideoAndGetId,
getLocalIdByUUID,
getVideoIdFromUUID
--- /dev/null
+export type BooleanBothQuery = 'true' | 'false' | 'both'
-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'
+++ /dev/null
-export type NSFWQuery = 'true' | 'false' | 'both'
--- /dev/null
+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
+}
-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
}
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'
- Videos
parameters:
- $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf'
- Video
parameters:
- $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf'
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
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
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
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
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:
moderationComment:
type: string
description: Update the report comment visible only to the moderation team
+ minLength: 2
+ maxLength: 3000
responses:
'204':
description: successful operation
message:
description: Message to send
type: string
+ minLength: 2
+ maxLength: 3000
required:
- message
responses:
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'
id:
type: integer
uuid:
- type: string
+ $ref: '#/components/schemas/UUIDv4'
requestBody:
content:
multipart/form-data:
displayName:
description: Video playlist display name
type: string
+ minLength: 1
+ maxLength: 120
thumbnailfile:
description: Video playlist thumbnail file
type: string
displayName:
description: Video playlist display name
type: string
+ minLength: 1
+ maxLength: 120
thumbnailfile:
description: Video playlist thumbnail file
type: string
schema:
type: string
- $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf'
- 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
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
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
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
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:
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
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:
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:
type: string
format: url
uuid:
- type: string
- format: uuid
- example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+ $ref: '#/components/schemas/UUIDv4'
redundancies:
type: object
properties:
reason:
type: string
example: The video is a spam
+ minLength: 2
+ maxLength: 3000
predefinedReasons:
$ref: '#/components/schemas/AbusePredefinedReasons'
reporterAccount:
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
type: integer
message:
type: string
+ minLength: 2
+ maxLength: 3000
byModerator:
type: boolean
createdAt:
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:
properties:
displayName:
type: string
+ minLength: 1
+ maxLength: 120
description:
type: string
+ minLength: 3
+ maxLength: 1000
isLocal:
type: boolean
ownerAccount:
id:
type: integer
uuid:
- type: string
- format: uuid
- example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+ $ref: '#/components/schemas/UUIDv4'
VideoPlaylist:
properties:
id:
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:
format: url
text:
type: string
+ minLength: 1
+ maxLength: 10000
threadId:
type: integer
inReplyToCommentId:
VideoCaption:
properties:
language:
- $ref: '#/components/schemas/VideoConstantString'
+ $ref: '#/components/schemas/VideoConstantString-Language'
captionPath:
type: string
ActorImage:
type: integer
example: 8
uuid:
- type: string
- format: uuid
- example: 9c9de5e8-0a1e-484a-b099-e80766180a6d
+ $ref: '#/components/schemas/UUIDv4'
CommentThreadResponse:
properties:
total:
- 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
## 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
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"
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==
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==
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"
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"