diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts index f8a82d4..dceb63e 100644 --- a/mediarepo-ui/src/app/app.module.ts +++ b/mediarepo-ui/src/app/app.module.ts @@ -57,6 +57,7 @@ import {MatCheckboxModule} from "@angular/material/checkbox"; import { FilterDialogComponent } from './components/file-search/filter-dialog/filter-dialog.component'; import { TagFilterListItemComponent } from './components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component'; import { TagInputComponent } from './components/inputs/tag-input/tag-input.component'; +import { ContextMenuComponent } from './components/context-menu/context-menu.component'; @NgModule({ declarations: [ @@ -84,6 +85,7 @@ import { TagInputComponent } from './components/inputs/tag-input/tag-input.compo FilterDialogComponent, TagFilterListItemComponent, TagInputComponent, + ContextMenuComponent, ], imports: [ BrowserModule, diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.html b/mediarepo-ui/src/app/components/context-menu/context-menu.component.html new file mode 100644 index 0000000..d182a4b --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.scss b/mediarepo-ui/src/app/components/context-menu/context-menu.component.scss new file mode 100644 index 0000000..960a1df --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.scss @@ -0,0 +1,4 @@ +.menu-anchor { + visibility: hidden; + position: fixed; +} diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts b/mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts new file mode 100644 index 0000000..55a3414 --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContextMenuComponent } from './context-menu.component'; + +describe('ContextMenuComponent', () => { + let component: ContextMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ContextMenuComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContextMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.ts b/mediarepo-ui/src/app/components/context-menu/context-menu.component.ts new file mode 100644 index 0000000..38e0105 --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.ts @@ -0,0 +1,32 @@ +import { + Component, + ComponentFactoryResolver, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import {MatMenuTrigger} from "@angular/material/menu"; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + styleUrls: ['./context-menu.component.scss'] +}) +export class ContextMenuComponent { + + + public x: string = "0"; + public y: string = "0"; + + @ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger + + constructor() { + } + + public onContextMenu(event: MouseEvent) { + event.preventDefault(); + this.x = event.clientX + "px"; + this.y = event.clientY + "px"; + this.menuTrigger.openMenu(); + } +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html index ce83f9a..9e8801e 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html @@ -1,8 +1,9 @@

Filters

- - + + @@ -13,3 +14,7 @@
+ + + + diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss index 49c51ef..bea8807 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss @@ -15,8 +15,14 @@ mat-list-item.filter-list-item { height: 100%; padding: 0.5em 0; + user-select: none; + cursor: pointer; } app-tag-filter-list-item { width: 100%; } + +.selected { + background-color: #5c5c5c; +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts index fdea7c4..e5e9f45 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts @@ -1,14 +1,9 @@ -import { - Component, - ElementRef, - HostListener, - Inject, - ViewChild -} from '@angular/core'; +import {Component, HostListener, Inject, ViewChildren} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {SortDialogComponent} from "../sort-dialog/sort-dialog.component"; import { - FilterExpression, OrFilterExpression, + FilterExpression, + OrFilterExpression, SingleFilterExpression } from "../../../models/FilterExpression"; import {TagQuery} from "../../../models/TagQuery"; @@ -27,9 +22,16 @@ export class FilterDialogComponent { public availableTags: Tag[] = []; public mode: "AND" | "OR" = "AND"; + @ViewChildren( + TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[]; + + private selectedQueries: TagQuery[] = []; + constructor(public dialogRef: MatDialogRef, @Inject( MAT_DIALOG_DATA) data: any) { - this.filters = data.filterEntries.map((f: FilterExpression) => new Selectable(f, false)) ?? []; + this.filters = data.filterEntries.map( + (f: FilterExpression) => new Selectable(f, + false)) ?? []; this.availableTags = data.availableTags ?? []; } @@ -47,13 +49,16 @@ export class FilterDialogComponent { if (index >= 0) { this.filters.splice(index, 1); } + this.unselectAll(); } public addFilter(tag: string) { const query = TagQuery.fromString(tag); if (this.mode === "AND" || this.filters.length === 0) { - this.filters.push(new Selectable(new SingleFilterExpression(query), false)); + this.filters.push( + new Selectable(new SingleFilterExpression(query), + false)); tag = tag.replace(/^-/g, ''); if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) { @@ -64,8 +69,82 @@ export class FilterDialogComponent { let queryList = this.filters.pop()?.data.queryList() ?? []; queryList.push(query); - this.filters.push(new Selectable(new OrFilterExpression(queryList), false)); + const filterExpression = new OrFilterExpression(queryList); + filterExpression.removeDuplicates(); + this.filters.push( + new Selectable(filterExpression, + false)); + } + this.unselectAll(); + } + + public addToSelection(query: TagQuery): void { + this.selectedQueries.push(query); + } + + public removeFromSelection(query: TagQuery): void { + const index = this.selectedQueries.indexOf(query); + if (index > 0) { + this.selectedQueries.splice(index, 1); + } + } + + public unselectAll() { + this.filters.forEach(filter => filter.selected = false); + this.selectedQueries = []; + this.filterListItems.forEach(i => i.selectedIndices = []); + } + + public convertSelectionToAndExpression(): void { + for (const query of this.selectedQueries) { + this.filters.push(new Selectable(new SingleFilterExpression(query), false)); + } + this.removeFilterDuplicates(); + this.unselectAll(); + } + + public convertSelectionToOrExpression(): void { + const queries = this.selectedQueries; + const expression = new OrFilterExpression(queries); + this.filters.push(new Selectable(expression, false)); + this.removeFilterDuplicates(); + this.unselectAll(); + } + + private removeFilterDuplicates() { + const filters = this.filters; + let newFilters: Selectable[] = []; + + for (const filterItem of filters) { + if (filterItem.data.filter_type == "OrExpression") { + (filterItem.data as OrFilterExpression).removeDuplicates(); + } + if (newFilters.findIndex(f => FilterDialogComponent.checkFiltersEqual(f.data, filterItem.data)) < 0) { + if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) { + filterItem.data = new SingleFilterExpression(filterItem.data.queryList()[0]); + } + newFilters.push(filterItem); + } + } + this.filters = newFilters; + } + + private static checkFiltersEqual(l: FilterExpression, r: FilterExpression): boolean { + const lTags = l.queryList().map(q => q.getNormalizedTag()).sort(); + const rTags = r.queryList().map(q => q.getNormalizedTag()).sort(); + let match = false; + + if (lTags.length == rTags.length) { + match = true; + + for (const tag of lTags) { + match = rTags.includes(tag); + if (!match) { + break; + } + } } + return match; } @HostListener("window:keydown", ["$event"]) diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html index 8b573d9..b1116e3 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html @@ -1,15 +1,16 @@ - +
{{expression.data.getDisplayName()}} - +
- + OR {{entry[1].getNormalizedTag()}} - diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss index e64d496..37a6050 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss @@ -14,6 +14,7 @@ mat-list { width: 100%; display: block; background-color: #353535; + padding: 0; border-radius: 0.25em; } @@ -21,6 +22,9 @@ mat-list-item.or-filter-list-item { padding: 0.5em 0; height: 100%; width: 100%; + border-collapse: collapse; + cursor: pointer; + user-select: none; ::ng-deep .mat-list-item-content { padding-right: 0; @@ -31,3 +35,7 @@ mat-list-item.or-filter-list-item { .or-span { margin-right: 0.5em; } + +.selected { + background-color: #5c5c5c; +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts index abe1391..f2d0b03 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts @@ -3,9 +3,9 @@ import { Component, EventEmitter, Inject, - Input, + Input, OnChanges, OnInit, - Output + Output, SimpleChanges } from '@angular/core'; import { FilterExpression, @@ -19,12 +19,22 @@ import {Selectable} from "../../../../models/Selectable"; templateUrl: './tag-filter-list-item.component.html', styleUrls: ['./tag-filter-list-item.component.scss'] }) -export class TagFilterListItemComponent { +export class TagFilterListItemComponent implements OnChanges { @Input() expression!: Selectable; @Output() removeClicked = new EventEmitter(); + @Output() querySelect = new EventEmitter(); + @Output() queryUnselect = new EventEmitter(); - constructor() { } + public selectedIndices: number[] = []; + + constructor(private changeDetector: ChangeDetectorRef) { } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes["expression"]) { + this.selectedIndices = []; + } + } public enumerate(items: T[]): [number, T][] { return items.map((value, index) => [index, value]); @@ -40,4 +50,26 @@ export class TagFilterListItemComponent { this.expression.data = new SingleFilterExpression(expression.filter[0]); } } + + public selectInnerIndex(index: number): void { + const expression = this.expression.data as OrFilterExpression; + + if (this.selectedIndices.includes(index)) { + const elementIndex = this.selectedIndices.indexOf(index); + this.selectedIndices.splice(elementIndex, 1); + this.queryUnselect.emit(expression.filter[index]); + } else { + this.selectedIndices.push(index); + this.querySelect.emit(expression.filter[index]); + } + } + + public onSelect(): void { + this.expression.selected = !this.expression.selected; + if (this.expression.selected) { + this.querySelect.emit(this.expression.data.filter as TagQuery); + } else { + this.queryUnselect.emit(this.expression.data.filter as TagQuery); + } + } } diff --git a/mediarepo-ui/src/app/models/FilterExpression.ts b/mediarepo-ui/src/app/models/FilterExpression.ts index 7929872..e2e8d62 100644 --- a/mediarepo-ui/src/app/models/FilterExpression.ts +++ b/mediarepo-ui/src/app/models/FilterExpression.ts @@ -47,6 +47,18 @@ export class OrFilterExpression implements FilterExpression{ public removeQueryEntry(index: number) { this.filter.splice(index, 1); } + + public removeDuplicates() { + const filters = this.filter.reverse(); + let newEntries: TagQuery[] = []; + + for (const entry of filters) { + if (newEntries.findIndex(f => f.tag === entry.tag) < 0) { + newEntries.push(entry); + } + } + this.filter = newEntries.reverse(); + } } export class SingleFilterExpression implements FilterExpression {