Add sorting dialog

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent a276a37d3d
commit 7737fce36a

@ -1,12 +1,12 @@
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module'; import {AppRoutingModule} from './app-routing.module';
import { AppComponent } from './app.component'; import {AppComponent} from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { RepositoriesComponent } from './pages/repositories/repositories.component'; import {RepositoriesComponent} from './pages/repositories/repositories.component';
import { HomeComponent } from './pages/home/home.component'; import {HomeComponent} from './pages/home/home.component';
import { RepositoryCardComponent } from './pages/repositories/repository-card/repository-card.component'; import {RepositoryCardComponent} from './pages/repositories/repository-card/repository-card.component';
import {MatCardModule} from "@angular/material/card"; import {MatCardModule} from "@angular/material/card";
import {MatListModule} from "@angular/material/list"; import {MatListModule} from "@angular/material/list";
import {MatButtonModule} from "@angular/material/button"; import {MatButtonModule} from "@angular/material/button";
@ -15,11 +15,11 @@ import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatFormFieldModule} from "@angular/material/form-field"; import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input"; import {MatInputModule} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import { RepoFormComponent } from './pages/repositories/repo-form/repo-form.component'; import {RepoFormComponent} from './pages/repositories/repo-form/repo-form.component';
import { FileGridComponent } from './components/file-grid/file-grid.component'; import {FileGridComponent} from './components/file-grid/file-grid.component';
import {MatSidenavModule} from "@angular/material/sidenav"; import {MatSidenavModule} from "@angular/material/sidenav";
import {MatGridListModule} from "@angular/material/grid-list"; import {MatGridListModule} from "@angular/material/grid-list";
import { FileGridEntryComponent } from './components/file-grid/file-grid-entry/file-grid-entry.component'; import {FileGridEntryComponent} from './components/file-grid/file-grid-entry/file-grid-entry.component';
import {MatProgressBarModule} from "@angular/material/progress-bar"; import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatPaginatorModule} from "@angular/material/paginator"; import {MatPaginatorModule} from "@angular/material/paginator";
import {ScrollingModule} from "@angular/cdk/scrolling"; import {ScrollingModule} from "@angular/cdk/scrolling";
@ -27,11 +27,14 @@ import {LightboxModule} from "ngx-lightbox";
import {MatChipsModule} from "@angular/material/chips"; import {MatChipsModule} from "@angular/material/chips";
import {MatIconModule} from "@angular/material/icon"; import {MatIconModule} from "@angular/material/icon";
import {MatAutocompleteModule} from "@angular/material/autocomplete"; import {MatAutocompleteModule} from "@angular/material/autocomplete";
import { FileSearchComponent } from './components/file-search/file-search.component'; import {FileSearchComponent} from './components/file-search/file-search.component';
import {MatTabsModule} from "@angular/material/tabs"; import {MatTabsModule} from "@angular/material/tabs";
import { SearchPageComponent } from './pages/home/search-page/search-page.component'; import {SearchPageComponent} from './pages/home/search-page/search-page.component';
import {FlexModule, GridModule} from "@angular/flex-layout"; import {FlexModule, GridModule} from "@angular/flex-layout";
import {MatRippleModule} from "@angular/material/core"; import {MatRippleModule} from "@angular/material/core";
import {FilterDialogComponent} from './components/file-search/filter-dialog/filter-dialog.component';
import {MatDialogModule} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -44,6 +47,7 @@ import {MatRippleModule} from "@angular/material/core";
FileGridEntryComponent, FileGridEntryComponent,
FileSearchComponent, FileSearchComponent,
SearchPageComponent, SearchPageComponent,
FilterDialogComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -69,9 +73,12 @@ import {MatRippleModule} from "@angular/material/core";
MatTabsModule, MatTabsModule,
FlexModule, FlexModule,
GridModule, GridModule,
MatRippleModule MatRippleModule,
MatDialogModule,
MatSelectModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule {
}

@ -45,7 +45,7 @@ export class FileGridEntryComponent implements OnInit, OnDestroy {
try { try {
const thumbnails = await this.fileService.getThumbnails(this.gridEntry.file.hash); const thumbnails = await this.fileService.getThumbnails(this.gridEntry.file.hash);
let thumbnail = thumbnails.find(t => (t.height > 250 || t.width > 250) && (t.height < 500 && t.width < 500)); let thumbnail = thumbnails.find(t => (t.height > 250 || t.width > 250) && (t.height < 500 && t.width < 500));
this.selectedThumbnail = thumbnail; this.selectedThumbnail = thumbnail ?? thumbnails[0];
if (!thumbnail) { if (!thumbnail) {
console.log("Thumbnail is empty?!", thumbnails); console.log("Thumbnail is empty?!", thumbnails);

@ -14,6 +14,10 @@ $warn-palette: map.get($color-config, 'warn');
mat-form-field:focus { mat-form-field:focus {
background-color: dimgrey; background-color: dimgrey;
} }
#sort-button {
background-color: darken(dimgrey, 5);
}
} }
@mixin typography($theme) { @mixin typography($theme) {

@ -1,7 +1,8 @@
<div class="tag-input-list-and-actions"> <div class="tag-input-list-and-actions">
<div class="tag-input-list" #tagInputList> <div class="tag-input-list" #tagInputList>
<div class="tag-input-list-inner"> <div class="tag-input-list-inner">
<div class="tag-input-item" *ngFor="let tag of searchTags" mat-ripple (click)="removeSearchTag(tag)">{{tag.getNormalizedTag()}}</div> <div class="tag-input-item" *ngFor="let tag of searchTags" mat-ripple
(click)="removeSearchTag(tag)">{{tag.getNormalizedTag()}}</div>
</div> </div>
</div> </div>
<button id="delete-all-tags-button" mat-button (click)="removeAllSearchTags()"> <button id="delete-all-tags-button" mat-button (click)="removeAllSearchTags()">
@ -11,13 +12,14 @@
<mat-form-field class="full-width" appearance="fill"> <mat-form-field class="full-width" appearance="fill">
<mat-label>Enter tags to filter for</mat-label> <mat-label>Enter tags to filter for</mat-label>
<input matInput <input matInput
#tagInput #tagInput
(keydown)="addSearchTagByInput($event)" (keydown)="addSearchTagByInput($event)"
[matAutocomplete]="auto" [matAutocomplete]="auto"
[formControl]="formControl"/> [formControl]="formControl"/>
<mat-autocomplete #auto (optionSelected)="addSearchTagByAutocomplete($event)"> <mat-autocomplete #auto (optionSelected)="addSearchTagByAutocomplete($event)">
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag"> <mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
{{tag}} {{tag}}
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<button mat-flat-button id="sort-button" (click)="openSortDialog()">Sort: {{sortExpression.join(", ")}}</button>

@ -29,6 +29,16 @@
top: 0; top: 0;
} }
#sort-button {
width: 100%;
}
::ng-deep #sort-button {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.tag-input-list-inner { .tag-input-list-inner {
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;

@ -7,11 +7,13 @@ import {
import {TagService} from "../../services/tag/tag.service"; import {TagService} from "../../services/tag/tag.service";
import {FileService} from "../../services/file/file.service"; import {FileService} from "../../services/file/file.service";
import {FormControl} from "@angular/forms"; import {FormControl} from "@angular/forms";
import {COMMA} from "@angular/cdk/keycodes";
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete"; import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
import {map, startWith} from "rxjs/operators"; import {map, startWith} from "rxjs/operators";
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {TagQuery} from "../../models/TagQuery"; import {TagQuery} from "../../models/TagQuery";
import {SortKey} from "../../models/SortKey";
import {MatDialog} from "@angular/material/dialog";
import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component";
@Component({ @Component({
selector: 'app-file-search', selector: 'app-file-search',
@ -23,7 +25,8 @@ export class FileSearchComponent implements AfterViewChecked {
this.inputList.nativeElement.scrollLeft = this.inputList.nativeElement.scrollWidth; this.inputList.nativeElement.scrollLeft = this.inputList.nativeElement.scrollWidth;
} }
public searchInputSeparators = [COMMA]; public sortExpression: SortKey[] = [new SortKey("FileImportedTime",
"Ascending", undefined)];
public formControl = new FormControl(); public formControl = new FormControl();
public searchTags: TagQuery[] = []; public searchTags: TagQuery[] = [];
public suggestionTags: Observable<string[]>; public suggestionTags: Observable<string[]>;
@ -32,7 +35,7 @@ export class FileSearchComponent implements AfterViewChecked {
@ViewChild("tagInput") tagInput!: ElementRef<HTMLInputElement>; @ViewChild("tagInput") tagInput!: ElementRef<HTMLInputElement>;
@ViewChild("tagInputList") inputList!: ElementRef; @ViewChild("tagInputList") inputList!: ElementRef;
constructor(private tagService: TagService, private fileService: FileService) { constructor(private tagService: TagService, private fileService: FileService, public dialog: MatDialog) {
this.tagService.tags.subscribe( this.tagService.tags.subscribe(
(tag) => this.allTags = tag.map(t => t.getNormalizedOutput())); (tag) => this.allTags = tag.map(t => t.getNormalizedOutput()));
@ -45,7 +48,7 @@ export class FileSearchComponent implements AfterViewChecked {
} }
public async searchForFiles() { public async searchForFiles() {
await this.fileService.findFiles(this.searchTags); await this.fileService.findFiles(this.searchTags, this.sortExpression);
} }
public addSearchTag(tag: string) { public addSearchTag(tag: string) {
@ -63,7 +66,7 @@ export class FileSearchComponent implements AfterViewChecked {
async removeAllSearchTags() { async removeAllSearchTags() {
this.searchTags = []; this.searchTags = [];
await this.searchForFiles(); await this.searchForFiles();
} }
async removeSearchTag(tag: TagQuery) { async removeSearchTag(tag: TagQuery) {
@ -92,4 +95,18 @@ export class FileSearchComponent implements AfterViewChecked {
this.tagInput.nativeElement.value = ''; this.tagInput.nativeElement.value = '';
await this.searchForFiles(); await this.searchForFiles();
} }
openSortDialog() {
const sortEntries = this.sortExpression.map(key => JSON.parse(JSON.stringify(key))).map(key => new SortKey(key.sortType, key.sortDirection, key.namespaceName))
const openedDialog = this.dialog.open(FilterDialogComponent, {
minWidth: "40vw",
data: {
sortEntries,
},
});
openedDialog.afterClosed().subscribe(async (sortExpression) => {
this.sortExpression = sortExpression;
await this.searchForFiles();
});
}
} }

@ -0,0 +1,37 @@
<h1 mat-dialog-title>Sort Entries</h1>
<div mat-dialog-content>
<div class="sort-input-list">
<div class="sort-input-row" *ngFor="let sortKey of sortEntries">
<mat-form-field>
<mat-label>Key</mat-label>
<mat-select required [(value)]="sortKey.sortType">
<mat-option value="Namespace">Namespace</mat-option>
<mat-option value="FileName">File Name</mat-option>
<mat-option value="FileSize">File Size</mat-option>
<mat-option value="FileImportedTime">Time Imported</mat-option>
<mat-option value="FileCreatedTime">Time Created</mat-option>
<mat-option value="FileChangeTime">Time Changed</mat-option>
<mat-option value="FileType">File Type</mat-option>
<mat-option value="NumTags">Number of Tags</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="sortKey.sortType === 'Namespace'">
<mat-label>Namespace Name</mat-label>
<input #namespaceInput matInput required [value]="sortKey.namespaceName ?? ''" (change)="sortKey.namespaceName = namespaceInput.value">
</mat-form-field>
<div class="filler" *ngIf="sortKey.sortType !== 'Namespace'"></div>
<mat-form-field>
<mat-label>Direction</mat-label>
<mat-select [(value)]="sortKey.sortDirection" required>
<mat-option value="Ascending">Ascending</mat-option>
<mat-option value="Descending">Descending</mat-option>
</mat-select>
</mat-form-field>
<button *ngIf="sortEntries.indexOf(sortKey) === sortEntries.length -1" mat-flat-button (click)="addNewSortKey()"><mat-icon>add</mat-icon></button>
<button *ngIf="sortEntries.indexOf(sortKey) !== sortEntries.length -1" mat-flat-button (click)="removeSortKey(sortKey)"><mat-icon>remove</mat-icon></button>
</div>
</div>
</div>
<div class="dialog-actions" mat-dialog-actions>
<button mat-flat-button color="primary" (click)="confirmSort()">Sort</button>
</div>

@ -0,0 +1,24 @@
mat-dialog-content {
height: 100%;
}
.sort-input-list {
display: table;
width: 100%;
}
.sort-input-row {
display: table-row;
width: 100%;
}
mat-form-field, .filler {
display: table-cell;
padding: 0 1em;
}
.dialog-actions {
align-items: center;
text-align: center;
width: 100%;
}

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterDialogComponent } from './filter-dialog.component';
describe('FilterDialogComponent', () => {
let component: FilterDialogComponent;
let fixture: ComponentFixture<FilterDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,31 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {SortKey} from "../../../models/SortKey";
@Component({
selector: 'app-filter-dialog',
templateUrl: './filter-dialog.component.html',
styleUrls: ['./filter-dialog.component.scss']
})
export class FilterDialogComponent {
public sortEntries: SortKey[] = []
constructor(public dialogRef: MatDialogRef<FilterDialogComponent>, @Inject(MAT_DIALOG_DATA) data: any) {
this.sortEntries = data.sortEntries;
}
addNewSortKey() {
const sortKey = new SortKey("FileName", "Ascending", undefined);
this.sortEntries.push(sortKey)
}
public removeSortKey(sortKey: SortKey): void {
const index = this.sortEntries.indexOf(sortKey);
this.sortEntries.splice(index, 1);
}
public confirmSort(): void {
this.dialogRef.close(this.sortEntries);
}
}

@ -0,0 +1,29 @@
export class SortKey {
constructor(
public sortType: "Namespace" | "FileName" | "FileSize" | "FileImportedTime" | "FileCreatedTime" | "FileChangeTime" | "FileType" | "NumTags",
public sortDirection: "Ascending" | "Descending",
public namespaceName: string | undefined
) {}
public toString(): string {
if (this.sortType == "Namespace") {
return `${this.sortType} '${this.namespaceName}' ${this.sortDirection}`
} else {
return `${this.sortType} ${this.sortDirection}`
}
}
public toBackendType(): any {
if (this.sortType == "Namespace") {
return {"Namespace": {direction: this.sortDirection, tag: this.namespaceName}}
} else {
let returnObj: any = {};
returnObj[this.sortType] = this.sortDirection;
return returnObj;
}
}
}

@ -1,7 +1,7 @@
<mat-drawer-container class="page"> <mat-drawer-container class="page">
<mat-drawer mode="side" opened disableClose> <mat-drawer mode="side" opened disableClose>
<div fxLayout="column" class="drawer-sidebar-inner"> <div fxLayout="column" class="drawer-sidebar-inner">
<div id="file-search-input" fxFlex="150px"> <div id="file-search-input" fxFlex="220px">
<app-file-search #filesearch></app-file-search> <app-file-search #filesearch></app-file-search>
</div> </div>
<div id="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill> <div id="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill>

@ -7,6 +7,7 @@ import {FileService} from "../../../services/file/file.service";
import {TagService} from "../../../services/tag/tag.service"; import {TagService} from "../../../services/tag/tag.service";
import {Lightbox, LIGHTBOX_EVENT, LightboxEvent} from "ngx-lightbox"; import {Lightbox, LIGHTBOX_EVENT, LightboxEvent} from "ngx-lightbox";
import {MatSelectionListChange} from "@angular/material/list"; import {MatSelectionListChange} from "@angular/material/list";
import {SortKey} from "../../../models/SortKey";
@Component({ @Component({
selector: 'app-search-page', selector: 'app-search-page',
@ -31,6 +32,7 @@ export class SearchPageComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.fileService.displayedFiles.subscribe((files) => this.files = files); this.fileService.displayedFiles.subscribe((files) => this.files = files);
await this.fileService.findFiles([], [new SortKey("FileImportedTime", "Ascending", undefined)])
} }
async onFileMultiSelect(files: File[]) { async onFileMultiSelect(files: File[]) {

@ -12,13 +12,11 @@ export class DataloaderService {
constructor( constructor(
private erroBroker: ErrorBrokerService, private erroBroker: ErrorBrokerService,
private fileService: FileService,
private tagService: TagService) { } private tagService: TagService) { }
public async loadData() { public async loadData() {
try { try {
await this.tagService.loadTags(); await this.tagService.loadTags();
await this.fileService.findFiles([]);
} catch (err) { } catch (err) {
this.erroBroker.showError(err); this.erroBroker.showError(err);
} }

@ -5,6 +5,7 @@ import {invoke} from "@tauri-apps/api/tauri";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
import {Thumbnail} from "../../models/Thumbnail"; import {Thumbnail} from "../../models/Thumbnail";
import {TagQuery} from "../../models/TagQuery"; import {TagQuery} from "../../models/TagQuery";
import {SortKey} from "../../models/SortKey";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -22,15 +23,8 @@ export class FileService {
this.displayedFiles.next(all_files); this.displayedFiles.next(all_files);
} }
public async findFiles(tags: TagQuery[]) { public async findFiles(tags: TagQuery[], sortBy: SortKey[]) {
const sortBy: any[] = [ let files = await invoke<File[]>("plugin:mediarepo|find_files", {tags, sortBy: sortBy.map(k => k.toBackendType())});
{Namespace: {tag: "creator", direction: "Descending"}},
{Namespace: {tag: "series", direction: "Ascending"}},
{Namespace: {tag: "title", direction: "Ascending"}},
{Namespace: {tag: "page", direction: "Ascending"}},
{Namespace: {tag: "panel", direction: "Ascending"}},
];
let files = await invoke<File[]>("plugin:mediarepo|find_files", {tags, sortBy});
this.displayedFiles.next(files); this.displayedFiles.next(files);
} }

Loading…
Cancel
Save