Add extended filtering implementation

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 2 years ago
parent 8007bf64a7
commit 501a3c9df0

@ -9,8 +9,27 @@ insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = double
trim_trailing_whitespace = true
charset = utf-8
ij_typescript_align_multiline_chained_methods = true
ij_typescript_force_quote_style = true
ij_typescript_space_after_colon = true
ij_typescript_space_after_comma = true
ij_typescript_space_after_property_colon = true
ij_typescript_space_after_type_colon = true
ij_any_spaces_within_braces = true
ij_typescript_spaces_within_object_type_braces = true
ij_typescript_spaces_within_object_literal_braces = true
ij_any_call_parameters_wrap = on_every_item
ij_any_method_parameters_wrap = on_every_item
ij_any_method_call_chain_wrap = on_every_item
ij_typescript_method_parameters_wrap = on_every_item
ij_any_call_parameters_new_line_after_left_paren = true
ij_any_call_parameters_right_paren_on_new_line = true
ij_any_method_parameters_new_line_after_left_paren = true
ij_any_method_parameters_right_paren_on_new_line = true
max_line_length = 120
[*.md]
max_line_length = off
trim_trailing_whitespace = false
trim_trailing_whitespace = false

@ -7,6 +7,9 @@
# Only exists if Bazel was run
/bazel-out
# angular stuff
.angular
# dependencies
/node_modules

@ -1,61 +1,60 @@
{
"name": "mediarepo-ui",
"version": "0.12.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"lint": "ng lint",
"tauri": "tauri"
},
"private": true,
"dependencies": {
"@angular/animations": "~12.2.0",
"@angular/cdk": "12.2.9",
"@angular/common": "~12.2.0",
"@angular/compiler": "~12.2.0",
"@angular/core": "~12.2.0",
"@angular/flex-layout": "^12.0.0-beta.35",
"@angular/forms": "~12.2.0",
"@angular/material": "12.2.9",
"@angular/platform-browser": "~12.2.0",
"@angular/platform-browser-dynamic": "~12.2.0",
"@angular/router": "~12.2.0",
"@ng-icons/core": "^13.1.1",
"@ng-icons/feather-icons": "^13.1.1",
"@ng-icons/material-icons": "^13.1.1",
"@tauri-apps/api": "^1.0.0-beta.8",
"ngx-lightbox": "^2.5.1",
"primeicons": "^4.1.0",
"primeng": "^12.2.1",
"rxjs": "~6.6.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~12.2.9",
"@angular-eslint/builder": "12.5.0",
"@angular-eslint/eslint-plugin": "12.5.0",
"@angular-eslint/eslint-plugin-template": "12.5.0",
"@angular-eslint/schematics": "12.5.0",
"@angular-eslint/template-parser": "12.5.0",
"@angular/cli": "~12.2.9",
"@angular/compiler-cli": "~12.2.0",
"@tauri-apps/cli": "^1.0.0-beta.10",
"@types/file-saver": "^2.0.3",
"@types/jasmine": "~3.8.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "4.28.2",
"@typescript-eslint/parser": "4.28.2",
"eslint": "^7.26.0",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.3.5"
}
"name": "mediarepo-ui",
"version": "0.12.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"lint": "ng lint",
"tauri": "tauri"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.1.1",
"@angular/cdk": "^13.1.1",
"@angular/common": "~13.1.1",
"@angular/compiler": "~13.1.1",
"@angular/core": "~13.1.1",
"@angular/flex-layout": "^13.0.0-beta.36",
"@angular/forms": "~13.1.1",
"@angular/material": "^13.1.1",
"@angular/platform-browser": "~13.1.1",
"@angular/platform-browser-dynamic": "~13.1.1",
"@angular/router": "~13.1.1",
"@ng-icons/core": "^13.2.0",
"@ng-icons/feather-icons": "^13.2.0",
"@ng-icons/material-icons": "13.1.0",
"@tauri-apps/api": "^1.0.0-beta.8",
"primeicons": "^5.0.0",
"primeng": "^13.0.4",
"rxjs": "~7.5.2",
"tslib": "^2.3.1",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.1.2",
"@angular-eslint/builder": "^13.0.1",
"@angular-eslint/eslint-plugin": "^13.0.1",
"@angular-eslint/eslint-plugin-template": "^13.0.1",
"@angular-eslint/schematics": "^13.0.1",
"@angular-eslint/template-parser": "^13.0.1",
"@angular/cli": "~13.1.2",
"@angular/compiler-cli": "~13.1.1",
"@tauri-apps/cli": "^1.0.0-beta.10",
"@types/file-saver": "^2.0.4",
"@types/jasmine": "~3.10.3",
"@types/node": "^16.11.19",
"@typescript-eslint/eslint-plugin": "5.9.1",
"@typescript-eslint/parser": "^5.9.1",
"eslint": "^8.6.0",
"jasmine-core": "~4.0.0",
"karma": "~6.3.10",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.5.4"
}
}

@ -1489,8 +1489,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "mediarepo-api"
version = "0.25.0"
source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=cd7bcc8d688d05275679d20af71231098602009b#cd7bcc8d688d05275679d20af71231098602009b"
version = "0.26.0"
source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=bd21f48f41aa2943f76b21addf137b2e58d492ca#bd21f48f41aa2943f76b21addf137b2e58d492ca"
dependencies = [
"async-trait",
"bromine",

@ -25,7 +25,7 @@ features = [ "env-filter" ]
[dependencies.mediarepo-api]
git = "https://github.com/Trivernis/mediarepo-api.git"
rev = "cd7bcc8d688d05275679d20af71231098602009b"
rev = "bd21f48f41aa2943f76b21addf137b2e58d492ca"
features = [ "tauri-plugin" ]
[features]

@ -25,12 +25,9 @@ import {
StartDaemonRequest,
UpdateFileNameRequest
} from "./api-types/requests";
import {
RepositoryData,
RepositoryMetadata,
SizeMetadata
} from "./api-types/repo";
import {RepositoryData, RepositoryMetadata, SizeMetadata} from "./api-types/repo";
import {NamespaceData, TagData} from "./api-types/tags";
import {ShortCache} from "./ShortCache";
export class MediarepoApi {
@ -99,7 +96,7 @@ export class MediarepoApi {
}
public static async findFiles(request: FindFilesRequest): Promise<FileBasicData[]> {
return this.invokePlugin(ApiFunction.FindFiles, request);
return ShortCache.cached(request, () => this.invokePlugin(ApiFunction.FindFiles, request), 5000, "findFiles");
}
public static async getFileMetadata(request: GetFileMetadataRequest): Promise<FileMetadata> {
@ -123,7 +120,7 @@ export class MediarepoApi {
}
public static async getAllTags(): Promise<TagData[]> {
return this.invokePlugin(ApiFunction.GetAllTags);
return ShortCache.cached("all-tags", () => this.invokePlugin(ApiFunction.GetAllTags), 2000);
}
public static async getAllNamespaces(): Promise<NamespaceData[]> {
@ -131,7 +128,12 @@ export class MediarepoApi {
}
public static async getTagsForFiles(request: GetTagsForFilesRequest): Promise<TagData[]> {
return this.invokePlugin(ApiFunction.GetTagsForFiles, request);
return ShortCache.cached(
request,
() => this.invokePlugin(ApiFunction.GetTagsForFiles, request),
1000,
"getTagsForFiles"
);
}
public static async createTags(request: CreateTagsRequest): Promise<TagData[]> {
@ -151,7 +153,7 @@ export class MediarepoApi {
}
public static async getFrontendState(): Promise<string> {
return this.invokePlugin(ApiFunction.GetFrontendState);
return ShortCache.cached("frontend-state", () => this.invokePlugin(ApiFunction.GetFrontendState), 1000);
}
public static async setFrontendState(request: SetFrontendStateRequest): Promise<void> {

@ -0,0 +1,69 @@
type CacheEntry<T> = {
ttl: number,
value: T,
}
const cacheMap: {
[key: string]: CacheEntry<any>
} = {};
export class ShortCache {
public static async cached<T>(
key: any,
producer: () => Promise<T>,
ttl: number = 1000,
prefix: string = ""
): Promise<T> {
const cacheKey = prefix + JSON.stringify(key);
const entry = this.getCacheEntry<T>(cacheKey, ttl);
if (entry) {
console.debug("cache hit for key", cacheKey);
return entry;
} else {
console.debug("cache miss key", cacheKey);
const value = await producer();
this.addCacheEntry(cacheKey, value, ttl);
return value;
}
}
public static startTicking() {
(async () => {
while (true) {
ShortCache.tick();
await new Promise(resolve => setTimeout(resolve, 100));
}
})();
}
private static addCacheEntry(key: string, value: any, ttl: number) {
cacheMap[key] = {
ttl,
value
};
console.debug("added cache entry with key", key);
}
private static getCacheEntry<T>(key: string, ttl: number): T | undefined {
const entry = cacheMap[key];
if (entry) {
entry.ttl = ttl;
}
return entry?.value;
}
private static tick() {
for (let key in cacheMap) {
cacheMap[key].ttl -= 100;
if (cacheMap[key].ttl < 0) {
console.debug("purged cache entry with key", key);
delete cacheMap[key];
}
}
}
}
ShortCache.startTicking();

@ -1,12 +1,56 @@
export type FilterExpression =
{ OrExpression: TagQuery[] }
| { Query: TagQuery };
export type FilterExpression = FilterExpressionOrExpression | FilterExpressionQuery;
export type FilterExpressionOrExpression = {
OrExpression: FilterQuery[],
};
export type FilterExpressionQuery = {
Query: FilterQuery;
};
export type FilterQuery = FilterQueryTag | FilterQueryProperty;
export type FilterQueryTag = { Tag: TagQuery };
export type FilterQueryProperty = { Property: PropertyQuery };
export type TagQuery = {
negate: boolean,
tag: string,
};
export type PropertyKeys =
"Status"
| "FileSize"
| "ImportedTime"
| "ChangedTime"
| "CreatedTime"
| "TagCount"
| "Cd"
| "Id";
export type PropertyQuery = PropertyQueryStatus
| PropertyQueryFileSize
| PropertyQueryImportedTime
| PropertyQueryChangedTime
| PropertyQueryCreatedTime
| PropertyQueryTagCount
| PropertyQueryCd
| PropertyQueryId;
export type PropertyQueryStatus = { Status: FileStatus };
export type PropertyQueryFileSize = { FileSize: ValueComparator<number> };
export type PropertyQueryImportedTime = { ImportedTime: ValueComparator<Date> };
export type PropertyQueryChangedTime = { ChangedTime: ValueComparator<Date> };
export type PropertyQueryCreatedTime = { CreatedTime: ValueComparator<Date> };
export type PropertyQueryTagCount = { TagCount: ValueComparator<number> };
export type PropertyQueryCd = { Cd: string };
export type PropertyQueryId = { Id: number };
export type ValueComparator<T> =
{ Less: T }
| { Equal: T }
| { Greater: T }
| { Between: T[] }
export type SortKey = { Namespace: SortNamespace }
| { FileName: SortDirection }
| { FileSize: SortDirection }

@ -0,0 +1,292 @@
import {FileStatus, FilterQuery, PropertyQuery, ValueComparator} from "../api-types/files";
export type Comparator = "Less" | "Equal" | "Greater" | "Between";
export type PropertyType =
"Status"
| "FileSize"
| "ImportedTime"
| "ChangedTime"
| "CreatedTime"
| "TagCount"
| "Cd"
| "Id";
export class FilterQueryBuilder {
public static tag(tag: string, negate: boolean): FilterQuery {
return { Tag: { tag, negate } };
}
public static status(status: FileStatus): FilterQuery {
return filterQuery({ Status: status });
}
public static fileSize(size: number, comparator: Comparator, max_size?: number): FilterQuery {
return filterQuery(
{ FileSize: valuesToCompareEnum(size, comparator, max_size) });
}
public static importedTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
return filterQuery({
ImportedTime: valuesToCompareEnum(date, comparator,
max_date
)
});
}
public static changedTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
return filterQuery({
ChangedTime: valuesToCompareEnum(date, comparator, max_date)
});
}
public static createdTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
return filterQuery({
CreatedTime: valuesToCompareEnum(date, comparator, max_date)
});
}
public static tagCount(count: number, comparator: Comparator, max_count: number): FilterQuery {
return filterQuery({
TagCount: valuesToCompareEnum(count, comparator, max_count)
});
}
public static contentDescriptor(descriptor: string): FilterQuery {
return filterQuery({ Cd: descriptor });
}
public static fileId(id: number): FilterQuery {
return filterQuery({ Id: id });
}
public static buildFilterFromString(filterStr: string): FilterQuery {
filterStr = filterStr.trim();
if (filterStr.startsWith(".")) {
const cleanFilter = filterStr.replace(/^\./, "");
const parsedPropertyFilter = this.parsePropertyFilterQuery(cleanFilter);
if (parsedPropertyFilter) {
return parsedPropertyFilter;
}
} else if (filterStr.startsWith("-")) {
const tag = filterStr.replace(/^-/, "").trim();
return this.tag(tag, true);
}
return this.tag(filterStr, false);
}
private static parsePropertyFilterQuery(expression: string): FilterQuery | undefined {
let propertyName = "";
let compareValue = "";
let rawComparator = "";
let comparatorStarted = false;
let valueStarted = false;
for (const char of expression) {
console.log(char);
if (!valueStarted) {
switch (char) {
case " ":
break;
case "=":
case "!":
case ">":
case "<":
rawComparator += char;
comparatorStarted = true;
break;
default:
valueStarted = comparatorStarted;
if (valueStarted) {
compareValue += char;
} else {
propertyName += char;
}
}
} else {
compareValue += char;
}
}
return this.parseQueryFromParts(propertyName, rawComparator, compareValue);
}
private static parseQueryFromParts(
propertyName: string,
rawComparator: string,
compareValue: string
): FilterQuery | undefined {
const property = this.parsePropertyName(propertyName);
const comparator = this.parseComparator(rawComparator);
console.log("Parts: ", propertyName, rawComparator, compareValue);
if (property && comparator) {
let value;
switch (property) {
case "Status":
value = parseStatus(compareValue);
if (comparator === "Equal" && value != undefined) {
return this.status(value);
}
break;
case "FileSize":
value = this.parsePropertyValue(compareValue, parseNumber);
if (value != undefined) {
return this.fileSize(value[0], comparator, value[1]);
}
break;
case "ImportedTime":
value = this.parsePropertyValue(compareValue, parseDate);
if (value != undefined) {
return this.importedTime(value[0], comparator, value[1]);
}
break;
case "ChangedTime":
value = this.parsePropertyValue(compareValue, parseDate);
if (value != undefined) {
return this.changedTime(value[0], comparator, value[1]);
}
break;
case "CreatedTime":
value = this.parsePropertyValue(compareValue, parseDate);
if (value != undefined) {
return this.createdTime(value[0], comparator, value[1]);
}
break;
case "TagCount":
value = this.parsePropertyValue(compareValue, parseNumber);
if (value != undefined) {
return this.tagCount(value[0], comparator, value[1]);
}
break;
case "Cd":
if (compareValue) {
return this.contentDescriptor(compareValue);
}
break;
case "Id":
value = parseNumber(compareValue);
if (value != undefined) {
return this.fileId(value);
}
break;
}
}
return undefined;
}
private static parseComparator(comparatorStr: string): Comparator | undefined {
switch (comparatorStr) {
case "=":
case "==":
return "Equal";
case "<":
return "Less";
case ">":
return "Greater";
default:
return;
}
}
private static parsePropertyName(nameStr: string): PropertyType | undefined {
switch (nameStr.toLowerCase().replace(/-_/g, "")) {
case "status":
return "Status";
case "filesize":
return "FileSize";
case "importedat":
case "importeddate":
case "importedtime":
return "ImportedTime";
case "changedat":
case "changeddate":
case "changedtime":
return "ChangedTime";
case "createdat":
case "createddate":
case "createdtime":
return "CreatedTime";
case "tagcount":
return "TagCount";
case "cd":
case "contentdescriptor":
return "Cd";
case "id":
case "fileid":
return "Id";
default:
return;
}
}
private static parsePropertyValue<T>(
valueStr: string,
parseFn: (valueStr: string) => T | undefined
): T[] | undefined {
const [firstValue, secondValue] = valueStr.split(" ");
if (secondValue != undefined) {
const firstValueParsed = parseFn(firstValue);
const secondValueParsed = parseFn(secondValue);
if (firstValueParsed && secondValueParsed) {
return [firstValueParsed, secondValueParsed];
}
} else {
const value = parseFn(firstValue);
return value != undefined ? [value] : undefined;
}
return;
}
}
function filterQuery(propertyQuery: PropertyQuery): FilterQuery {
return { Property: propertyQuery };
}
function valuesToCompareEnum<T>(min_value: T, comparator: Comparator, max_value?: T): ValueComparator<T> {
switch (comparator) {
case "Less":
return { Less: min_value };
case "Equal":
return { Equal: min_value };
case "Greater":
return { Greater: min_value };
case "Between":
return { Between: [min_value, max_value!] };
}
}
function parseNumber(value: string): number | undefined {
const num = Number(value);
return isNaN(num) ? undefined : num;
}
function parseDate(value: string): Date | undefined {
const date = Date.parse(value);
if (isNaN(date)) {
return undefined;
}
return new Date(date);
}
function parseStatus(value: string): FileStatus | undefined {
switch (value.toLowerCase()) {
case "imported":
return "Imported";
case "archived":
return "Archived";
case "deleted":
return "Deleted";
default:
return undefined;
}
}

@ -0,0 +1,75 @@
import {FilterExpression, FilterQuery} from "../api-types/files";
import * as deepEqual from "fast-deep-equal";
export class SearchFilters {
constructor(private filters: FilterExpression[]) {
}
public get length() {
return this.filters.length;
}
public getFilters(): FilterExpression[] {
return this.filters;
}
public hasFilter(expression: FilterExpression): boolean {
return !!this.filters.find(f => deepEqual(f, expression));
}
public addFilter(filter: FilterQuery, index: number) {
this.filters = [...this.filters.slice(
0,
index
), { Query: filter }, ...this.filters.slice(index)];
}
public appendFilter(filter: FilterQuery) {
this.filters.push({ Query: filter });
}
public removeFilter(filterToRemove: FilterExpression) {
this.filters = this.filters.filter(f => !deepEqual(f, filterToRemove));
}
public removeFilterAtIndex(index: number) {
this.filters.splice(index, 1);
}
public appendSubfilter(filter: FilterQuery, index: number) {
const expressionEntry = this.filters[index];
if (expressionEntry && "OrExpression" in expressionEntry) {
expressionEntry["OrExpression"]!.push(filter);
} else {
const otherQuery = expressionEntry["Query"]!;
let entry = expressionEntry as unknown as { OrExpression: FilterQuery[], Query: undefined };
entry["Query"] = undefined;
entry["OrExpression"] = [otherQuery, filter];
}
}
public removeSubfilter(queryToRemove: FilterQuery) {
let index = this.filters.findIndex(f => {
if ("Query" in f) {
return false;
} else {
f["OrExpression"] = f["OrExpression"]!.filter(q => !deepEqual(q, queryToRemove));
return (f["OrExpression"]!.length === 0);
}
});
this.filters.splice(index);
}
public removeSubfilterAtIndex(index: number, subindex: number) {
const filterEntry = this.filters[index];
if (filterEntry && "OrExpression" in filterEntry) {
filterEntry["OrExpression"]!.splice(subindex, 1);
if (filterEntry["OrExpression"]!.length === 0) {
this.removeFilterAtIndex(index);
}
}
}
}

@ -1,5 +1,5 @@
@use 'sass:map';
@use '~@angular/material' as mat;
@use '@angular/material' as mat;
@mixin color($theme) {
$color-config: mat.get-color-config($theme);

@ -1,22 +1,14 @@
import {AfterViewInit, Component, OnInit} from "@angular/core";
import {Repository} from "../../../../api/models/Repository";
import {
RepositoryService
} from "../../../services/repository/repository.service";
import {RepositoryService} from "../../../services/repository/repository.service";
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
import {
DownloadDaemonDialogComponent
} from "./download-daemon-dialog/download-daemon-dialog.component";
import {DownloadDaemonDialogComponent} from "./download-daemon-dialog/download-daemon-dialog.component";
import {
AddRepositoryDialogComponent
} from "../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
import {
ErrorBrokerService
} from "../../../services/error-broker/error-broker.service";
import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
import {BehaviorSubject} from "rxjs";
import {
BusyDialogComponent
} from "../../shared/app-common/busy-dialog/busy-dialog.component";
import {BusyDialogComponent} from "../../shared/app-common/busy-dialog/busy-dialog.component";
import {JobService} from "../../../services/job/job.service";
import {StateService} from "../../../services/state/state.service";
@ -83,7 +75,7 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
await this.repoService.loadRepositories();
await this.stateService.loadState();
dialogContext.dialog.close(true);
} catch (err) {
} catch (err: any) {
this.errorBroker.showError(err);
dialogContext.message.next(
"Failed to open repository: " + err.toString());
@ -92,6 +84,22 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
}
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
}
public async onOpenRepository(repository: Repository) {
if (!repository.local) {
await this.selectRepository(repository);
} else {
await this.startDaemonAndSelectRepository(repository);
}
}
private async forceCloseRepository() {
try {
await this.repoService.closeSelectedRepository();
@ -132,23 +140,17 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
}
});
return {message: dialogMessage, dialog};
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
return { message: dialogMessage, dialog };
}
private async checkAndPromptDaemonExecutable() {
if (!await this.repoService.checkDameonConfigured()) {
const result = await this.dialog.open(DownloadDaemonDialogComponent,
const result = await this.dialog.open(
DownloadDaemonDialogComponent,
{
disableClose: true,
}).afterClosed().toPromise();
}
).afterClosed().toPromise();
if (result) {
// recursion avoidance
setTimeout(
@ -156,12 +158,4 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
}
}
}
public async onOpenRepository(repository: Repository) {
if (!repository.local) {
await this.selectRepository(repository);
} else {
await this.startDaemonAndSelectRepository(repository);
}
}
}

@ -1,10 +1,6 @@
import {NgModule} from "@angular/core";
import {
ConfirmDialogComponent
} from "./confirm-dialog/confirm-dialog.component";
import {
BusyIndicatorComponent
} from "./busy-indicator/busy-indicator.component";
import {ConfirmDialogComponent} from "./confirm-dialog/confirm-dialog.component";
import {BusyIndicatorComponent} from "./busy-indicator/busy-indicator.component";
import {ContextMenuComponent} from "./context-menu/context-menu.component";
import {CommonModule} from "@angular/common";
import {NgIconsModule} from "@ng-icons/core";
@ -12,16 +8,10 @@ import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatButtonModule} from "@angular/material/button";
import {MatDialogModule} from "@angular/material/dialog";
import {MatMenuModule} from "@angular/material/menu";
import {
ContentAwareImageComponent
} from "./content-aware-image/content-aware-image.component";
import {
InputReceiverDirective
} from "./input-receiver/input-receiver.directive";
import {
MetadataEntryComponent
} from "./metadata-entry/metadata-entry.component";
import { BusyDialogComponent } from './busy-dialog/busy-dialog.component';
import {ContentAwareImageComponent} from "./content-aware-image/content-aware-image.component";
import {InputReceiverDirective} from "./input-receiver/input-receiver.directive";
import {MetadataEntryComponent} from "./metadata-entry/metadata-entry.component";
import {BusyDialogComponent} from "./busy-dialog/busy-dialog.component";
@NgModule({

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

@ -11,7 +11,7 @@ export class ContextMenuComponent {
public x: string = "0";
public y: string = "0";
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger;
constructor() {
}

@ -37,7 +37,7 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
partitionedGridEntries: Selectable<File>[][] = [];
private shiftClicked = false;
private ctrlClicked = false;
private gridEntries: Selectable<File>[] = []
private gridEntries: Selectable<File>[] = [];
constructor(
private tabService: TabService,
@ -102,6 +102,55 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
await this.fileService.deleteThumbnails(file);
}
public focus() {
this.inner.nativeElement.focus();
}
public handleKeydownEvent(event: KeyboardEvent) {
this.shiftClicked ||= event.shiftKey;
this.ctrlClicked ||= event.ctrlKey;
switch (event.key) {
case "ArrowRight":
this.handleArrowSelect("right");
break;
case "ArrowLeft":
this.handleArrowSelect("left");
break;
case "ArrowDown":
this.handleArrowSelect("down");
break;
case "ArrowUp":
this.handleArrowSelect("up");
break;
case "PageDown":
this.pageDown();
break;
case "PageUp":
this.pageUp();
break;
case "a":
case "A":
if (this.shiftClicked && this.ctrlClicked) {
this.selectNone();
} else if (this.ctrlClicked) {
event.preventDefault();
this.selectAll();
}
break;
case "Enter":
if (this.selectedEntries.length === 1) {
this.fileOpenEvent.emit(this.selectedEntries[0].data);
}
break;
}
}
public handleKeyupEvent(event: KeyboardEvent) {
this.shiftClicked = event.shiftKey ? false : this.shiftClicked;
this.ctrlClicked = event.ctrlKey ? false : this.ctrlClicked;
}
private setPartitionedGridEntries() {
this.partitionedGridEntries = [];
let scrollToIndex = -1;
@ -109,8 +158,10 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
for (let i = 0; i < (Math.ceil(
this.gridEntries.length / this.columns)); i++) {
const entries = this.gridEntries.slice(i * this.columns,
Math.min(this.gridEntries.length, (i + 1) * this.columns));
const entries = this.gridEntries.slice(
i * this.columns,
Math.min(this.gridEntries.length, (i + 1) * this.columns)
);
this.partitionedGridEntries.push(entries);
const preselectedEntry = entries.find(
@ -195,7 +246,7 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
selectedIndex += this.columns;
break;
case "left":
selectedIndex --;
selectedIndex--;
break;
case "right":
selectedIndex++;
@ -221,61 +272,12 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
offsetTop = this.virtualScroll.measureScrollOffset("top");
if (contentOffset < offsetTop + (viewportSize / 2)) {
this.virtualScroll.scrollToOffset((offsetTop + 130) - viewportSize/ 2);
this.virtualScroll.scrollToOffset((offsetTop + 130) - viewportSize / 2);
}
}
}
}
public focus() {
this.inner.nativeElement.focus();
}
public handleKeydownEvent(event: KeyboardEvent) {
this.shiftClicked ||= event.shiftKey;
this.ctrlClicked ||= event.ctrlKey;
switch (event.key) {
case "ArrowRight":
this.handleArrowSelect("right");
break;
case "ArrowLeft":
this.handleArrowSelect("left");
break;
case "ArrowDown":
this.handleArrowSelect("down");
break;
case "ArrowUp":
this.handleArrowSelect("up");
break;
case "PageDown":
this.pageDown();
break;
case "PageUp":
this.pageUp();
break;
case "a":
case "A":
if (this.shiftClicked && this.ctrlClicked) {
this.selectNone();
} else if (this.ctrlClicked) {
event.preventDefault();
this.selectAll();
}
break;
case "Enter":
if (this.selectedEntries.length === 1) {
this.fileOpenEvent.emit(this.selectedEntries[0].data);
}
break;
}
}
public handleKeyupEvent(event: KeyboardEvent) {
this.shiftClicked = event.shiftKey? false : this.shiftClicked;
this.ctrlClicked = event.ctrlKey? false : this.ctrlClicked;
}
private pageDown() {
if (this.virtualScroll) {
const offsetTop = this.virtualScroll.measureScrollOffset("top");

@ -1,10 +1,4 @@
import {
AfterViewInit,
Component,
Input,
OnChanges,
SimpleChanges
} from "@angular/core";
import {AfterViewInit, Component, Input, OnChanges, SimpleChanges} from "@angular/core";
import {File} from "../../../../../api/models/File";
import {FileService} from "../../../../services/file/file.service";
import {FileHelper} from "../../../../services/file/file.helper";
@ -21,9 +15,9 @@ export class FileThumbnailComponent implements OnChanges, AfterViewInit {
public thumbUrl: SafeResourceUrl | undefined;
private supportedThumbnailTypes = ["image", "video"]
private supportedThumbnailTypes = ["image", "video"];
constructor( private fileService: FileService) {
constructor(private fileService: FileService) {
}
public async ngAfterViewInit() {
@ -33,7 +27,8 @@ export class FileThumbnailComponent implements OnChanges, AfterViewInit {
public async ngOnChanges(changes: SimpleChanges) {
if (changes["file"]) {
this.thumbUrl = this.fileService.buildThumbnailUrl(this.file,
250, 250);
250, 250
);
}
}

@ -1,8 +1,6 @@
import {Component, EventEmitter, Output} from "@angular/core";
import {ImportService} from "../../../../../services/import/import.service";
import {
ErrorBrokerService
} from "../../../../../services/error-broker/error-broker.service";
import {ErrorBrokerService} from "../../../../../services/error-broker/error-broker.service";
import {AddFileOptions} from "../../../../../models/AddFileOptions";
import {File} from "../../../../../../api/models/File";
import {DialogFilter} from "@tauri-apps/api/dialog";
@ -26,12 +24,12 @@ export class FilesystemImportComponent {
name: "Images",
extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"]
},
{name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]},
{name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"]},
{name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]},
{name: "Text", extensions: ["txt", "md"]},
{name: "All", extensions: ["*"]}
]
{ name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"] },
{ name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"] },
{ name: "Documents", extensions: ["pdf", "doc", "docx", "odf"] },
{ name: "Text", extensions: ["txt", "md"] },
{ name: "All", extensions: ["*"] }
];
public resolving = false;
@ -61,8 +59,10 @@ export class FilesystemImportComponent {
for (const file of this.files) {
try {
const resultFile = await this.importService.addLocalFile(file,
this.importOptions);
const resultFile = await this.importService.addLocalFile(
file,
this.importOptions
);
this.fileImported.emit(resultFile);
} catch (err) {
console.log(err);

@ -4,8 +4,11 @@
<div class="tag-input-list-and-actions">
<div #tagInputList class="tag-input-list">
<div class="tag-input-list-inner">
<div (click)="removeFilterExpression(filter)" *ngFor="let filter of filters" class="tag-input-item"
mat-ripple>{{filter.getDisplayName()}}</div>
<div (click)="removeFilterExpression(filter)" *ngFor="let filter of filters.getFilters()"
class="tag-input-item"
mat-ripple>
<app-filter-expression-item [filter]="filter"></app-filter-expression-item>
</div>
</div>
</div>
@ -14,7 +17,9 @@
</button>
</div>
<app-tag-input [allowWildcards]="true" (tagAdded)="addSearchTag($event); searchForFiles()" [allowNegation]="true"
<app-tag-input (tagAdded)="addSearchQuery($event); searchForFiles()" [allowInvalid]="true"
[allowNegation]="true"
[allowWildcards]="true"
[availableTags]="getValidSearchTags()"
class="full-width">
<button (click)="openFilterDialog()" class="filter-dialog-button" mat-button>
@ -33,9 +38,9 @@
<div class="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill>
<app-busy-indicator [busy]="this.tagsLoading" [blurBackground]="true" [darkenBackground]="false">
<app-busy-indicator [blurBackground]="true" [busy]="this.tagsLoading" [darkenBackground]="false">
<cdk-virtual-scroll-viewport itemSize="50" maxBufferPx="4000" minBufferPx="500">
<div (click)="addSearchTag(tag.getNormalizedOutput())"
<div (click)="addSearchQuery(tag.getNormalizedOutput())"
(contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)"
*cdkVirtualFor="let tag of contextTags" class="selectable-tag">
<app-tag-item [tag]="tag"></app-tag-item>

@ -1,28 +1,15 @@
import {
AfterViewChecked,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} from "@angular/core";
import {TagQuery} from "../../../../models/TagQuery";
import {AfterViewChecked, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from "@angular/core";
import {SortKey} from "../../../../models/SortKey";
import {MatDialog} from "@angular/material/dialog";
import {SortDialogComponent} from "./sort-dialog/sort-dialog.component";
import {
ErrorBrokerService
} from "../../../../services/error-broker/error-broker.service";
import {
GenericFilter,
SingleFilterExpression
} from "../../../../models/GenericFilter";
import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service";
import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component";
import {Tag} from "../../../../../api/models/Tag";
import {clipboard} from "@tauri-apps/api";
import {TabState} from "../../../../models/TabState";
import {FilterQueryBuilder} from "../../../../../api/models/FilterQueryBuilder";
import {SearchFilters} from "../../../../../api/models/SearchFilters";
import {FilterExpression} from "../../../../../api/api-types/files";
@Component({
@ -32,7 +19,7 @@ import {TabState} from "../../../../models/TabState";
})
export class FileSearchComponent implements AfterViewChecked, OnInit {
public sortExpression: SortKey[] = [];
public filters: GenericFilter[] = [];
public filters: SearchFilters = new SearchFilters([]);
@Input() availableTags: Tag[] = [];
@Input() contextTags: Tag[] = [];
@ -73,40 +60,43 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
this.searchEndEvent.emit();
}
public addSearchTag(tag: string) {
this.filters.push(new SingleFilterExpression(TagQuery.fromString(tag)));
tag = tag.replace(/^-/g, "");
public addSearchQuery(queryStr: string) {
let filter = FilterQueryBuilder.buildFilterFromString(queryStr);
if (this.filters.filter(t => t.partiallyEq(tag)).length > 1) {
const index = this.filters.findIndex(t => t.partiallyEq(tag));
this.filters.splice(index, 1);
if (filter) {
this.filters.removeFilter({ Query: filter });
this.filters.appendFilter(filter);
}
this.state.setFilters(this.filters);
queryStr = queryStr.replace(/^-/g, "");
this.state.setTagFilters(this.filters);
}
public getValidSearchTags(): Tag[] {
return this.availableTags.filter(t => this.filters.findIndex(
f => f.partiallyEq(t.getNormalizedOutput())) < 0);
return this.availableTags.filter(t => !this.filters.hasFilter({
Query: FilterQueryBuilder.tag(
t.getNormalizedOutput(),
false
)
}));
}
public async removeAllSearchTags() {
this.filters = [];
this.state.setFilters([]);
this.filters = new SearchFilters([]);
this.state.setTagFilters(this.filters);
}
public async removeFilterExpression(expr: GenericFilter) {
const index = this.filters.indexOf(expr);
if (index >= 0) {
this.filters.splice(index, 1);
}
this.state.setFilters(this.filters);
public async removeFilterExpression(expr: FilterExpression) {
this.filters.removeFilter(expr);
this.state.setTagFilters(this.filters);
}
public openSortDialog() {
const sortEntries = this.sortExpression.map(
key => JSON.parse(JSON.stringify(key))).map(
key => new SortKey(key.sortType, key.sortDirection,
key.namespaceName));
key.namespaceName
));
const openedDialog = this.dialog.open(SortDialogComponent, {
minWidth: "40vw",
data: {
@ -123,7 +113,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
}
public openFilterDialog(): void {
const filterEntries = this.filters.map(f => f.clone());
const filterEntries = this.filters;
const filterDialog = this.dialog.open(FilterDialogComponent, {
minWidth: "25vw",
maxHeight: "80vh",
@ -136,7 +126,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
filterDialog.afterClosed().subscribe(async (filterExpression) => {
if (filterExpression !== undefined || filterExpression?.length > 0) {
this.filters = filterExpression;
this.state.setFilters(this.filters);
this.state.setTagFilters(this.filters);
}
});
}

@ -0,0 +1,14 @@
<span *ngIf="is('OrExpression')" class="or-expression">
<ng-container *ngFor="let query of this.orExpression().OrExpression">
<app-property-query-item *ngIf="this.queryIs(query, 'Property')"
[propertyQuery]="this.propertyQuery(query).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(query, 'Tag')"
[tagQuery]="this.tagQuery(query).Tag"></app-tag-query-item>
</ng-container>
</span>
<span *ngIf="is('Query')" class="query">
<app-property-query-item *ngIf="this.queryIs(this.query().Query, 'Property')"
[propertyQuery]="this.propertyQuery(this.query().Query).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(this.query().Query, 'Tag')"
[tagQuery]="this.tagQuery(this.query().Query).Tag"></app-tag-query-item>
</span>

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

@ -0,0 +1,47 @@
import {Component, Input} from "@angular/core";
import {
FilterExpression,
FilterExpressionOrExpression,
FilterExpressionQuery,
FilterQuery,
FilterQueryProperty,
FilterQueryTag
} from "../../../../../../api/api-types/files";
@Component({
selector: "app-filter-expression-item",
templateUrl: "./filter-expression-item.component.html",
styleUrls: ["./filter-expression-item.component.scss"]
})
export class FilterExpressionItemComponent {
@Input() filter!: FilterExpression;
constructor() {
}
public is(key: "OrExpression" | "Query"): boolean {
return key in this.filter;
}
public orExpression(): FilterExpressionOrExpression {
return this.filter as FilterExpressionOrExpression;
}
public query(): FilterExpressionQuery {
return this.filter as FilterExpressionQuery;
}
public queryIs(query: FilterQuery, key: "Property" | "Tag"): boolean {
return key in query;
}
public propertyQuery(query: FilterQuery): FilterQueryProperty {
return query as FilterQueryProperty;
}
public tagQuery(query: FilterQuery): FilterQueryTag {
return query as FilterQueryTag;
}
}

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

@ -0,0 +1,101 @@
import {Component, Input, OnChanges, OnInit, SimpleChanges} from "@angular/core";
import {PropertyQuery, ValueComparator} from "../../../../../../../api/api-types/files";
@Component({
selector: "app-property-query-item",
templateUrl: "./property-query-item.component.html",
styleUrls: ["./property-query-item.component.scss"]
})
export class PropertyQueryItemComponent implements OnInit, OnChanges {
@Input() propertyQuery!: PropertyQuery;
public stringExpression: string = "No Expression";
constructor() {
}
private static buildExpression(property: string, comparator: string, value: string): string {
return `.${property} ${comparator} ${value}`;
}
public ngOnInit(): void {
this.stringExpression = this.getStringExpression();
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes["propertyQuery"]) {
this.stringExpression = this.getStringExpression();
}
}
public getStringExpression(): string {
if ("Status" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression("Status", "is", this.propertyQuery.Status);
} else if ("FileSize" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"FileSize",
this.getComparator(this.propertyQuery.FileSize),
this.getValue(this.propertyQuery.FileSize).toString()
);
} else if ("ImportedTime" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"ImportedTime",
this.getComparator(this.propertyQuery.ImportedTime),
this.getValue(this.propertyQuery.ImportedTime).toISOString()
);
} else if ("ChangedTime" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"ChangedTime",
this.getComparator(this.propertyQuery.ChangedTime),
this.getValue(this.propertyQuery.ChangedTime).toISOString()
);
} else if ("CreatedTime" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"CreatedTime",
this.getComparator(this.propertyQuery.CreatedTime),
this.getValue(this.propertyQuery.CreatedTime).toISOString()
);
} else if ("TagCount" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"TagCount",
this.getComparator(this.propertyQuery.TagCount),
this.getValue(this.propertyQuery.TagCount).toString()
);
} else if ("Cd" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression("ContentDescriptor", "is", this.propertyQuery.Cd);
} else if ("Id" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression("FileId", "is", this.propertyQuery.Id.toString());
} else {
return "Invalid Expression";
}
}
public getComparator(value: ValueComparator<any>): "=" | "<" | ">" | "between" {
if ("Greater" in value) {
return ">";
} else if ("Equal" in value) {
return "=";
} else if ("Less" in value) {
return "<";
} else {
return "between";
}
}
public getValue<T>(value: ValueComparator<T>): T {
const singleValueKeys: ("Greater" | "Equal" | "Less")[] = ["Greater", "Equal", "Less"];
for (const key of singleValueKeys) {
if (key in value) {
//@ts-ignore
return value[key];
}
}
if ("Between" in value) {
return value.Between[0];
} else {
return "" as unknown as T; // unreachable
}
}
}

@ -0,0 +1 @@
<span><span *ngIf="tagQuery.negate" class="tag-negation">-</span>{{tagQuery.tag}}</span>

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

@ -0,0 +1,15 @@
import {Component, Input} from "@angular/core";
import {TagQuery} from "../../../../../../../api/api-types/files";
@Component({
selector: "app-tag-query-item",
templateUrl: "./tag-query-item.component.html",
styleUrls: ["./tag-query-item.component.scss"]
})
export class TagQueryItemComponent {
@Input() tagQuery!: TagQuery;
constructor() {
}
}

@ -12,7 +12,7 @@ import {TagService} from "../../../../../services/tag/tag.service";
})
export class SortDialogComponent {
public sortEntries: SortKey[] = []
public sortEntries: SortKey[] = [];
public suggestedNamespaces: Namespace[] = [];
private namespaces: Namespace[] = [];
@ -24,6 +24,20 @@ export class SortDialogComponent {
namespaces => this.namespaces = namespaces);
}
private static compareSuggestionNamespaces(query: string, l: string, r: string): number {
if (l.startsWith(query) && !r.startsWith(query)) {
return -1;
} else if (!l.startsWith(query) && r.startsWith(query)) {
return 1;
} else if (l.length < r.length) {
return -1;
} else if (l.length > r.length) {
return 1;
} else {
return l.localeCompare(r);
}
}
addNewSortKey() {
const sortKey = new SortKey("FileName", "Ascending", undefined);
this.sortEntries.push(sortKey);
@ -44,7 +58,8 @@ export class SortDialogComponent {
public onSortEntryDrop(event: CdkDragDrop<SortKey[]>): void {
moveItemInArray(this.sortEntries, event.previousIndex,
event.currentIndex);
event.currentIndex
);
}
public updateAutocompleteSuggestions(value: string): void {
@ -52,18 +67,4 @@ export class SortDialogComponent {
(a, b) => SortDialogComponent.compareSuggestionNamespaces(value, a.name, b.name))
.slice(0, 50);
}
private static compareSuggestionNamespaces(query: string, l: string, r: string): number {
if (l.startsWith(query) && !r.startsWith(query)) {
return -1;
} else if (!l.startsWith(query) && r.startsWith(query)) {
return 1;
} else if (l.length < r.length) {
return -1;
} else if (l.length > r.length) {
return 1;
} else {
return l.localeCompare(r);
}
}
}

@ -23,21 +23,15 @@ import {MatInputModule} from "@angular/material/input";
import {
TagFilterListItemComponent
} from "./file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component";
import {
SortDialogComponent
} from "./file-search/sort-dialog/sort-dialog.component";
import {
FilterDialogComponent
} from "./file-search/filter-dialog/filter-dialog.component";
import {SortDialogComponent} from "./file-search/sort-dialog/sort-dialog.component";
import {FilterDialogComponent} from "./file-search/filter-dialog/filter-dialog.component";
import {MatListModule} from "@angular/material/list";
import {MatDialogModule} from "@angular/material/dialog";
import {AppCommonModule} from "../app-common/app-common.module";
import {DragDropModule} from "@angular/cdk/drag-drop";
import {TagModule} from "../tag/tag.module";
import {FileImportComponent} from "./file-import/file-import.component";
import {
FilesystemImportComponent
} from "./file-import/filesystem-import/filesystem-import.component";
import {FilesystemImportComponent} from "./file-import/filesystem-import/filesystem-import.component";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatMenuModule} from "@angular/material/menu";
@ -47,6 +41,11 @@ import {
} from "./file-metadata/editable-metadata-entry/editable-metadata-entry.component";
import {ReactiveFormsModule} from "@angular/forms";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {FilterExpressionItemComponent} from "./file-search/filter-expression-item/filter-expression-item.component";
import {TagQueryItemComponent} from "./file-search/filter-expression-item/tag-query-item/tag-query-item.component";
import {
PropertyQueryItemComponent
} from "./file-search/filter-expression-item/property-query-item/property-query-item.component";
@NgModule({
@ -60,6 +59,9 @@ import {MatAutocompleteModule} from "@angular/material/autocomplete";
FilesystemImportComponent,
FileMetadataComponent,
EditableMetadataEntryComponent,
FilterExpressionItemComponent,
TagQueryItemComponent,
PropertyQueryItemComponent,
],
exports: [
TagEditComponent,

@ -5,30 +5,16 @@ import {TabCategory} from "./TabCategory";
export class AppState {
private tabIdCounter = 0;
public tabs = new BehaviorSubject<TabState[]>([]);
public selectedTab = new BehaviorSubject<number | undefined>(undefined);
public repoName: string | undefined;
private readonly fileService: FileService
private tabIdCounter = 0;
private readonly fileService: FileService;
constructor(fileService: FileService) {
this.fileService = fileService;
}
public addTab(category: TabCategory): TabState {
const state = new TabState(this.tabIdCounter++, category, this.fileService);
this.tabs.next([...this.tabs.value, state]);
return state;
}
public async closeTab(uuid: number) {
const index = this.tabs.value.findIndex(t => t.uuid === uuid);
const tabs = this.tabs.value;
tabs.splice(index, 1);
this.tabs.next(tabs);
}
public static deserializeJson(stateString: string, fileService: FileService): AppState {
let state = JSON.parse(stateString);
let appState = new AppState(fileService);
@ -42,6 +28,19 @@ export class AppState {
return appState;
}
public addTab(category: TabCategory): TabState {
const state = new TabState(this.tabIdCounter++, category, this.fileService);
this.tabs.next([...this.tabs.value, state]);
return state;
}
public async closeTab(uuid: number) {
const index = this.tabs.value.findIndex(t => t.uuid === uuid);
const tabs = this.tabs.value;
tabs.splice(index, 1);
this.tabs.next(tabs);
}
public serializeJson(): string {
const tabDTOs = this.tabs.value.map(tab => tab.getDTO());
return JSON.stringify({

@ -66,7 +66,7 @@ export class OrFilterExpression implements GenericFilter {
}
public toBackendType(): FilterExpression {
return createRustEnum(this.filter_type, this.filter) as unknown as FilterExpression;
return createRustEnum(this.filter_type, createRustEnum("Tag", this.filter)) as unknown as FilterExpression;
}
}
@ -100,6 +100,6 @@ export class SingleFilterExpression implements GenericFilter {
}
public toBackendType(): FilterExpression {
return createRustEnum(this.filter_type, this.filter) as unknown as FilterExpression;
return createRustEnum(this.filter_type, createRustEnum("Tag", this.filter)) as unknown as FilterExpression;
}
}

@ -2,15 +2,10 @@ import {BehaviorSubject} from "rxjs";
import {TabCategory} from "./TabCategory";
import {FileService} from "../services/file/file.service";
import {File} from "../../api/models/File";
import {
GenericFilter,
OrFilterExpression,
SingleFilterExpression
} from "./GenericFilter";
import {SortKey} from "./SortKey";
import {TagQuery} from "./TagQuery";
import {debounceTime} from "rxjs/operators";
import {mapNew} from "../../api/models/adaptors";
import {SearchFilters} from "../../api/models/SearchFilters";
export class TabState {
public uuid: number;
@ -20,14 +15,21 @@ export class TabState {
public loading = new BehaviorSubject<boolean>(false);
public files = new BehaviorSubject<File[]>([]);
public filters = new BehaviorSubject<GenericFilter[]>([]);
public filters = new BehaviorSubject<SearchFilters>(new SearchFilters([]));
public sortKeys = new BehaviorSubject<SortKey[]>(
[new SortKey("FileImportedTime",
"Ascending", undefined)]);
[new SortKey(
"FileImportedTime",
"Ascending",
undefined
)]);
private fileService: FileService;
constructor(uuid: number, category: TabCategory, fileService: FileService) {
constructor(
uuid: number,
category: TabCategory,
fileService: FileService
) {
this.category = category;
this.uuid = uuid;
this.fileService = fileService;
@ -39,15 +41,43 @@ export class TabState {
}
}
public static fromDTO(
dto: any,
fileService: FileService
): TabState {
const state = new TabState(
dto.uuid,
dto.category,
fileService
);
const sortKeys = dto.sortKeys.map(
(s: { sortType: any, sortDirection: any, namespaceName: any }) =>
new SortKey(
s.sortType,
s.sortDirection,
s.namespaceName
)
);
state.filters.next(new SearchFilters(dto.filters ?? []));
state.sortKeys.next(sortKeys);
state.mode.next(dto.mode ?? "grid");
state.selectedCD.next(dto.selectedFileHash);
state.files.next((dto.files ?? []).map(mapNew(File)));
return state;
}
public async findFiles() {
this.loading.next(true);
const files = await this.fileService.findFiles(this.filters.value,
this.sortKeys.value);
const files = await this.fileService.findFiles(
this.filters.value,
this.sortKeys.value
);
this.files.next(files);
this.loading.next(false);
}
public setFilters(filters: GenericFilter[]) {
public setTagFilters(filters: SearchFilters) {
this.filters.next(filters);
}
@ -55,36 +85,16 @@ export class TabState {
this.sortKeys.next(keys);
}
public static fromDTO(dto: any, fileService: FileService): TabState {
const state = new TabState(dto.uuid, dto.category, fileService);
const filters = dto.filters.map((f: {filter: any, filter_type: any}) => {
if (f.filter_type === "OrExpression") {
return new OrFilterExpression(f.filter.map((f: any) => new TagQuery(f.tag, f.negate)));
} else {
return new SingleFilterExpression(new TagQuery(f.filter.tag, f.filter.negate));
}
});
const sortKeys = dto.sortKeys.map((s: {sortType: any, sortDirection: any, namespaceName: any}) =>
new SortKey(s.sortType, s.sortDirection, s.namespaceName)
);
state.filters.next(filters);
state.sortKeys.next(sortKeys);
state.mode.next(dto.mode ?? "grid");
state.selectedCD.next(dto.selectedFileHash);
state.files.next((dto.files ?? []).map(mapNew(File)));
return state;
}
public getDTO(): any {
return {
uuid: this.uuid,
category: this.category,
filters: this.filters.value,
filters: this.filters.value.getFilters(),
sortKeys: this.sortKeys.value,
mode: this.mode.value,
selectedFileHash: this.selectedCD.value,
files: this.category === TabCategory.Import? this.files.value.map(f => f.rawData) : [],
files: this.category === TabCategory.Import ? this.files.value.map(
f => f.rawData) : [],
};
}
}

@ -2,9 +2,9 @@ export type RustEnum<VariantData> = {
[key: string]: VariantData
};
export function createRustEnum<VariantData>(variant: string, data: VariantData): RustEnum<VariantData> {
export function createRustEnum<T, VariantData>(variant: string, data: VariantData): T {
let enumInstance: RustEnum<VariantData> = {};
enumInstance[variant] = data;
return enumInstance;
return enumInstance as unknown as T;
}

@ -31,13 +31,13 @@ export class ErrorBrokerService {
}
}
showError(error: { message: string }) {
showError(error: { message: string } | any) {
console.error(error);
if (this.errorCb) {
if (!error.message) {
this.errorCb({message: error});
this.errorCb({ message: error });
} else {
this.errorCb({...error});
this.errorCb({ ...error });
}
}
}

@ -2,10 +2,10 @@ import {Inject, Injectable} from "@angular/core";
import {File} from "../../../api/models/File";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
import {SortKey} from "../../models/SortKey";
import {GenericFilter} from "../../models/GenericFilter";
import {MediarepoApi} from "../../../api/Api";
import {mapMany, mapNew} from "../../../api/models/adaptors";
import {FileMetadata} from "../../../api/api-types/files";
import {SearchFilters} from "../../../api/models/SearchFilters";
@Injectable({
@ -22,17 +22,21 @@ export class FileService {
return MediarepoApi.getAllFiles().then(mapMany(mapNew(File)));
}
public async findFiles(filters: GenericFilter[], sortBy: SortKey[]): Promise<File[]> {
let backendFilters = filters.map(f => f.toBackendType());
return MediarepoApi.findFiles({filters: backendFilters, sortBy: sortBy.map(k => k.toBackendType())}).then(mapMany(mapNew(File)));
public async findFiles(filters: SearchFilters, sortBy: SortKey[]): Promise<File[]> {
return MediarepoApi.findFiles(
{
filters: filters.getFilters(),
sortBy: sortBy.map(k => k.toBackendType())
})
.then(mapMany(mapNew(File)));
}
public async getFileMetadata(id: number): Promise<FileMetadata> {
return MediarepoApi.getFileMetadata({id});
return MediarepoApi.getFileMetadata({ id });
}
public async updateFileName(id: number, name: string): Promise<FileMetadata> {
return MediarepoApi.updateFileName({id, name});
return MediarepoApi.updateFileName({ id, name });
}
/**
@ -64,7 +68,7 @@ export class FileService {
* @returns {Promise<void>}
*/
public async saveFile(file: File, targetPath: string) {
await MediarepoApi.saveFileLocally({id: file.id, path: targetPath});
await MediarepoApi.saveFileLocally({ id: file.id, path: targetPath });
}
/**
@ -73,7 +77,7 @@ export class FileService {
* @returns {Promise<void>}
*/
public async deleteThumbnails(file: File) {
await MediarepoApi.deleteThumbnails({id: file.id});
await MediarepoApi.deleteThumbnails({ id: file.id });
}
/**
@ -82,8 +86,9 @@ export class FileService {
* @returns {Promise<SafeResourceUrl>}
*/
public async readFile(file: File): Promise<SafeResourceUrl> {
const data = await MediarepoApi.readFile({mimeType: file.mimeType, hash: file.cd});
const blob = new Blob([new Uint8Array(data)], {type: file.mimeType});
const data = await MediarepoApi.readFile(
{ mimeType: file.mimeType, hash: file.cd });
const blob = new Blob([new Uint8Array(data)], { type: file.mimeType });
const url = URL?.createObjectURL(blob);
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}

@ -1,16 +1,16 @@
import { TestBed } from '@angular/core/testing';
import {TestBed} from "@angular/core/testing";
import { JobService } from './job.service';
import {JobService} from "./job.service";
describe('JobService', () => {
let service: JobService;
describe("JobService", () => {
let service: JobService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(JobService);
});
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(JobService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it("should be created", () => {
expect(service).toBeTruthy();
});
});

@ -5,7 +5,7 @@ import {Injectable} from "@angular/core";
})
export class SchedulingService {
private workQueue: { [key: string]: {id: number, cancelled: boolean, cb: Function}[] } = {}
private workQueue: { [key: string]: { id: number, cancelled: boolean, cb: Function }[] } = {};
private lastWorkId = 0;
constructor() {
@ -17,7 +17,7 @@ export class SchedulingService {
setTimeout(() => this.startWork(key), 0); // start in the next tick
}
const id = this.lastWorkId++;
this.workQueue[key].push({id, cb, cancelled: false});
this.workQueue[key].push({ id, cb, cancelled: false });
return id;
}
@ -28,6 +28,12 @@ export class SchedulingService {
}
}
public async delay(time: number) {
return new Promise((res) => {
setTimeout(res, time);
});
}
private async startWork(key: string) {
while (true) {
if (this.workQueue[key]?.length > 0) {
@ -47,10 +53,4 @@ export class SchedulingService {
await this.delay(1);
}
}
public async delay(time: number) {
return new Promise((res) => {
setTimeout(res, time);
});
}
}

@ -1,5 +1,5 @@
@use 'sass:map';
@use "~@angular/material" as mat;
@use "@angular/material" as mat;
@use 'src/app/app.component-theme' as app;
@use 'app/components/shared/file/file-card/file-card.component-theme' as file-card;
@use 'app/components/shared/sidebar/file-search/file-search.component-theme' as file-search;
@ -7,15 +7,15 @@
@include mat.core();
$theme: mat.define-dark-theme((
color: (
primary: mat.define-palette(mat.$purple-palette, 700, 500, 800),
accent: mat.define-palette(mat.$green-palette, A200, A100, A400),
warn: mat.define-palette(mat.$red-palette),
background: mat.define-palette(mat.$blue-grey-palette)
),
typography: mat.define-typography-config(
$font-family: 'Noto Sans',
)
color: (
primary: mat.define-palette(mat.$purple-palette, 700, 500, 800),
accent: mat.define-palette(mat.$green-palette, A200, A100, A400),
warn: mat.define-palette(mat.$red-palette),
background: mat.define-palette(mat.$blue-grey-palette)
),
typography: mat.define-typography-config(
$font-family: 'Noto Sans',
)
));
$color-config: mat.get-color-config($theme);

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save