title: $localize`Advanced filters`,
children: [
{
- queryParams: { search: 'type:auto' },
+ value: 'type:auto',
label: $localize`Automatic blocks`
},
{
- queryParams: { search: 'type:manual' },
+ value: 'type:manual',
label: $localize`Manual blocks`
}
]
title: $localize`Advanced filters`,
children: [
{
- queryParams: { search: 'local:true' },
+ value: 'local:true',
label: $localize`Local comments`
},
{
- queryParams: { search: 'local:false' },
+ value: 'local:false',
label: $localize`Remote comments`
}
]
title: $localize`Advanced filters`,
children: [
{
- queryParams: { search: 'banned:true' },
+ value: 'banned:true',
label: $localize`Banned users`
}
]
title: $localize`Video type`,
children: [
{
- queryParams: { search: 'isLive:false' },
+ value: 'isLive:false',
label: $localize`VOD`
},
{
- queryParams: { search: 'isLive:true' },
+ value: 'isLive:true',
label: $localize`Live`
}
]
title: $localize`Video files`,
children: [
{
- queryParams: { search: 'webtorrent:true' },
+ value: 'webtorrent:true',
label: $localize`With WebTorrent`
},
{
- queryParams: { search: 'webtorrent:false' },
+ value: 'webtorrent:false',
label: $localize`Without WebTorrent`
},
{
- queryParams: { search: 'hls:true' },
+ value: 'hls:true',
label: $localize`With HLS`
},
{
- queryParams: { search: 'hls:false' },
+ value: 'hls:false',
label: $localize`Without HLS`
}
]
title: $localize`Videos scope`,
children: [
{
- queryParams: { search: 'isLocal:false' },
+ value: 'isLocal:false',
label: $localize`Remote videos`
},
{
- queryParams: { search: 'isLocal:true' },
+ value: 'isLocal:true',
label: $localize`Local videos`
}
]
title: $localize`Exclude`,
children: [
{
- queryParams: { search: 'excludeMuted' },
+ value: 'excludeMuted',
label: $localize`Exclude muted accounts`
}
]
}
getPrivacyBadgeClass (video: Video) {
- if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-blue'
+ if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green'
return 'badge-yellow'
}
this.auth.userInformationLoaded.subscribe(() => {
const channelFilters = this.auth.getUser().videoChannels.map(c => {
return {
- queryParams: { search: 'channel:' + c.name },
+ value: 'channel:' + c.name,
label: c.name
}
})
const channelFilters = this.userChannels.map(c => {
return {
- queryParams: { search: 'channel:' + c.name },
+ value: 'channel:' + c.name,
label: c.name
}
})
title: $localize`Advanced filters`,
children: [
{
- queryParams: { search: 'isLive:true' },
+ value: 'isLive:true',
label: $localize`Only live videos`
}
]
parseQueryStringFilter <T extends QueryStringFilterPrefixes> (q: string, prefixes: T): ParseQueryStringFiltersResult<keyof T> {
if (!q) return {}
- // Tokenize the strings using spaces that are not in quotes
- const tokens = q.match(/(?:[^\s"]+|"[^"]*")+/g)
- .filter(token => !!token)
+ const tokens = this.tokenizeString(q)
// Build prefix array
const prefixeStrings = Object.values(prefixes)
- .map(p => p.prefix)
+ .map(p => p.prefix)
logger(`Built tokens "${tokens.join(', ')}" for prefixes "${prefixeStrings.join(', ')}"`)
...additionalFilters
}
}
+
+ tokenizeString (q: string) {
+ if (!q) return []
+
+ // Tokenize the strings using spaces that are not in quotes
+ return q.match(/(?:[^\s"]+|"[^"]*")+/g)
+ .filter(token => !!token)
+ }
}
title: $localize`Advanced filters`,
children: [
{
- queryParams: { search: 'state:pending' },
+ value: 'state:pending',
label: $localize`Unsolved reports`
},
{
- queryParams: { search: 'state:accepted' },
+ value: 'state:accepted',
label: $localize`Accepted reports`
},
{
- queryParams: { search: 'state:rejected' },
+ value: 'state:rejected',
label: $localize`Refused reports`
},
{
- queryParams: { search: 'videoIs:blacklisted' },
+ value: 'videoIs:blacklisted',
label: $localize`Reports with blocked videos`
},
{
- queryParams: { search: 'videoIs:deleted' },
+ value: 'videoIs:deleted',
label: $localize`Reports with deleted videos`
}
]
<ng-container *ngFor="let group of filters">
<h6 class="dropdown-header">{{ group.title }}</h6>
- <a *ngFor="let filter of group.children" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
+ <button *ngFor="let filter of group.children" (click)="onFilterClick(filter)" class="dropdown-item">
+ <my-global-icon [ngClass]="{ 'no-visible': !isFilterEnabled(filter) }" iconName="tick"></my-global-icon>
+
{{ filter.label }}
- </a>
+ </button>
</ng-container>
</div>
</div>
@use '_variables' as *;
@use '_mixins' as *;
+.dropdown-item {
+ font-size: 14px;
+}
+
input {
@include peertube-input-text(250px);
}
.input-group-text {
background-color: transparent;
}
+
+my-global-icon {
+ $size: 18px;
+ $margin: 2px;
+
+ @include margin-right($margin);
+
+ opacity: 1;
+ width: 18px;
+ height: 18px;
+
+
+ &.no-visible {
+ @include margin-right($size + $margin);
+
+ width: 0;
+ height: 0;
+ }
+}
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { ActivatedRoute, Params, Router } from '@angular/router'
+import { RestService } from '@app/core'
export type AdvancedInputFilter = {
title: string
- children: {
- label: string
- queryParams: Params
- }[]
+ children: AdvancedInputFilterChild[]
+}
+
+export type AdvancedInputFilterChild = {
+ label: string
+ value: string
}
const logger = debug('peertube:AdvancedInputFilterComponent')
searchValue: string
+ private enabledFilters = new Set<string>()
+
private searchStream: Subject<string>
private viewInitialized = false
constructor (
private route: ActivatedRoute,
+ private restService: RestService,
private router: Router
) { }
return this.filters && this.filters.length !== 0
}
+ isFilterEnabled (filter: AdvancedInputFilterChild) {
+ return this.enabledFilters.has(filter.value)
+ }
+
+ onFilterClick (filter: AdvancedInputFilterChild) {
+ const newSearch = this.isFilterEnabled(filter)
+ ? this.removeFilterToSearch(this.searchValue, filter)
+ : this.addFilterToSearch(this.searchValue, filter)
+
+ this.router.navigate([ '.' ], { relativeTo: this.route, queryParams: { search: newSearch.trim() } })
+ }
+
private scheduleSearchUpdate (value: string) {
this.searchValue = value
this.searchStream.next(this.searchValue)
this.searchValue = value
this.setQueryParams(this.searchValue)
+ this.parseFilters(this.searchValue)
this.emitSearch()
}
if (this.searchValue === search) return
this.searchValue = search
+
+ this.parseFilters(this.searchValue)
+
this.emitSearch()
})
}
)
.subscribe(() => {
this.setQueryParams(this.searchValue)
+ this.parseFilters(this.searchValue)
this.emitSearch()
})
if (search) Object.assign(queryParams, { search })
this.router.navigate([ ], { queryParams })
}
+
+ private removeFilterToSearch (search: string, removedFilter: AdvancedInputFilterChild) {
+ return search.replace(removedFilter.value, '')
+ }
+
+ private addFilterToSearch (search: string, newFilter: AdvancedInputFilterChild) {
+ const prefix = newFilter.value.split(':').shift()
+
+ // Tokenize search and remove a potential existing filter
+ const tokens = this.restService.tokenizeString(search)
+ .filter(t => !t.startsWith(prefix))
+
+ tokens.push(newFilter.value)
+
+ return tokens.join(' ')
+ }
+
+ private parseFilters (search: string) {
+ const tokens = this.restService.tokenizeString(search)
+
+ this.enabledFilters = new Set()
+
+ for (const group of this.filters) {
+ for (const filter of group.children) {
+ if (tokens.includes(filter.value)) {
+ this.enabledFilters.add(filter.value)
+ }
+ }
+ }
+ }
}
--- /dev/null
+function findCommonElement <T> (array1: T[], array2: T[]) {
+ for (const a of array1) {
+ for (const b of array2) {
+ if (a === b) return a
+ }
+ }
+
+ return null
+}
+
+export {
+ findCommonElement
+}
+export * from './array'
export * from './object'