diff --git a/mediarepo-ui/src-tauri/Cargo.lock b/mediarepo-ui/src-tauri/Cargo.lock index a74a66b..4cfd9fe 100644 --- a/mediarepo-ui/src-tauri/Cargo.lock +++ b/mediarepo-ui/src-tauri/Cargo.lock @@ -1499,8 +1499,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "mediarepo-api" -version = "0.26.0" -source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=bd21f48f41aa2943f76b21addf137b2e58d492ca#bd21f48f41aa2943f76b21addf137b2e58d492ca" +version = "0.27.0" +source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=57d85eb3a668a8d4d344b7fbe2403731a16625a7#57d85eb3a668a8d4d344b7fbe2403731a16625a7" dependencies = [ "async-trait", "bromine", diff --git a/mediarepo-ui/src-tauri/Cargo.toml b/mediarepo-ui/src-tauri/Cargo.toml index aab1966..24db0fb 100644 --- a/mediarepo-ui/src-tauri/Cargo.toml +++ b/mediarepo-ui/src-tauri/Cargo.toml @@ -28,7 +28,7 @@ features = ["env-filter"] [dependencies.mediarepo-api] git = "https://github.com/Trivernis/mediarepo-api.git" -rev = "bd21f48f41aa2943f76b21addf137b2e58d492ca" +rev = "57d85eb3a668a8d4d344b7fbe2403731a16625a7" features = ["tauri-plugin"] [features] diff --git a/mediarepo-ui/src/api/Api.ts b/mediarepo-ui/src/api/Api.ts index 817a193..6faeec7 100644 --- a/mediarepo-ui/src/api/Api.ts +++ b/mediarepo-ui/src/api/Api.ts @@ -8,6 +8,7 @@ import { CheckDaemonRunningRequest, CheckLocalRepositoryExistsRequest, CreateTagsRequest, + DeleteFileRequest, DeleteRepositoryRequest, DeleteThumbnailsRequest, FindFilesRequest, @@ -23,7 +24,8 @@ import { SelectRepositoryRequest, SetFrontendStateRequest, StartDaemonRequest, - UpdateFileNameRequest + UpdateFileNameRequest, + UpdateFileStatusRequest } from "./api-types/requests"; import {RepositoryData, RepositoryMetadata, SizeMetadata} from "./api-types/repo"; import {NamespaceData, TagData} from "./api-types/tags"; @@ -107,6 +109,10 @@ export class MediarepoApi { return this.invokePlugin(ApiFunction.UpdateFileName, request); } + public static async updateFileStatus(request: UpdateFileStatusRequest): Promise { + return this.invokePlugin(ApiFunction.UpdateFileStatus, request); + } + public static async saveFileLocally(request: SaveFileRequest): Promise { return this.invokePlugin(ApiFunction.SaveFileLocally, request); } @@ -119,6 +125,10 @@ export class MediarepoApi { return this.invokePlugin(ApiFunction.ReadFile, request); } + public static async deleteFile(request: DeleteFileRequest): Promise { + return this.invokePlugin(ApiFunction.DeleteFile, request); + } + public static async getAllTags(): Promise { return ShortCache.cached("all-tags", () => this.invokePlugin(ApiFunction.GetAllTags), 2000); } diff --git a/mediarepo-ui/src/api/api-types/functions.ts b/mediarepo-ui/src/api/api-types/functions.ts index 3884d6c..33a5140 100644 --- a/mediarepo-ui/src/api/api-types/functions.ts +++ b/mediarepo-ui/src/api/api-types/functions.ts @@ -20,9 +20,11 @@ export enum ApiFunction { FindFiles = "find_files", GetFileMetadata = "get_file_metadata", UpdateFileName = "update_file_name", + UpdateFileStatus = "update_file_status", SaveFileLocally = "save_file_locally", DeleteThumbnails = "delete_thumbnails", ReadFile = "read_file", + DeleteFile = "delete_file", // tags GetAllTags = "get_all_tags", GetAllNamespace = "get_all_namespaces", diff --git a/mediarepo-ui/src/api/api-types/requests.ts b/mediarepo-ui/src/api/api-types/requests.ts index 8c64a91..5a261b5 100644 --- a/mediarepo-ui/src/api/api-types/requests.ts +++ b/mediarepo-ui/src/api/api-types/requests.ts @@ -1,4 +1,4 @@ -import {FileOsMetadata, FilterExpression, SortKey} from "./files"; +import {FileOsMetadata, FileStatus, FilterExpression, SortKey} from "./files"; import {RepositoryData, SizeType} from "./repo"; import {JobType} from "./job"; @@ -60,8 +60,15 @@ export type ReadFileRequest = { mimeType: string, }; +export type DeleteFileRequest = IdIdentifierRequest; + export type GetFileMetadataRequest = IdIdentifierRequest; +export type UpdateFileStatusRequest = { + id: number, + status: FileStatus +}; + export type GetTagsForFilesRequest = { cds: string[] }; diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts index bbaf6cc..06a6afb 100644 --- a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts +++ b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts @@ -123,7 +123,7 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit { } private openStartupDialog(repository: Repository): BusyDialogContext { - let dialogMessage = new BehaviorSubject( + const dialogMessage = new BehaviorSubject( "Opening repository..."); let dialog = this.dialog.open(BusyDialogComponent, { data: { diff --git a/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts b/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts index 0f8a603..822cbe0 100644 --- a/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts +++ b/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts @@ -13,6 +13,7 @@ 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 {SelectableComponent} from "./selectable/selectable.component"; +import {MatProgressBarModule} from "@angular/material/progress-bar"; @NgModule({ @@ -41,7 +42,8 @@ import {SelectableComponent} from "./selectable/selectable.component"; MatProgressSpinnerModule, MatButtonModule, MatDialogModule, - MatMenuModule + MatMenuModule, + MatProgressBarModule ] }) export class AppCommonModule { diff --git a/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.html b/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.html index af11caf..e0a5027 100644 --- a/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.html +++ b/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.html @@ -1,12 +1,12 @@ -

+

{{title}}

- + {{message}}
-
-
diff --git a/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.ts b/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.ts index 03fffc4..2e330b4 100644 --- a/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.ts +++ b/mediarepo-ui/src/app/components/shared/app-common/busy-dialog/busy-dialog.component.ts @@ -1,10 +1,12 @@ import {Component, Inject} from "@angular/core"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {BehaviorSubject} from "rxjs"; +import {ProgressBarMode} from "@angular/material/progress-bar"; export type BusyDialogData = { title: string, - message: BehaviorSubject, + message?: BehaviorSubject, + progress?: BehaviorSubject, allowCancel?: boolean, } @@ -18,10 +20,18 @@ export class BusyDialogComponent { public title: string; public message?: string; public allowCancel: boolean; + public progress = 0; + public mode: ProgressBarMode = "indeterminate"; constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) data: BusyDialogData) { this.title = data.title; - data.message.subscribe(m => this.message = m); + if (data.message) { + data.message.subscribe(m => this.message = m); + } + if (data.progress) { + data.progress.subscribe(p => this.progress = p); + this.mode = "determinate"; + } this.allowCancel = data.allowCancel ?? false; } } diff --git a/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.html b/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.html index 5726e98..0001e15 100644 --- a/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.html +++ b/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.html @@ -1,6 +1,24 @@ - - + + + + + + + + + + + + + diff --git a/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.ts b/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.ts index b35ac01..b65e4d3 100644 --- a/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.ts +++ b/mediarepo-ui/src/app/components/shared/file/file-context-menu/file-context-menu.component.ts @@ -1,14 +1,20 @@ -import {Component, ViewChild} from "@angular/core"; +import {Component, EventEmitter, Output, ViewChild} from "@angular/core"; import {File} from "../../../../../api/models/File"; -import { - ContextMenuComponent -} from "../../app-common/context-menu/context-menu.component"; +import {ContextMenuComponent} from "../../app-common/context-menu/context-menu.component"; import {clipboard} from "@tauri-apps/api"; import {FileService} from "../../../../services/file/file.service"; -import { - ErrorBrokerService -} from "../../../../services/error-broker/error-broker.service"; +import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service"; import {FileHelper} from "../../../../services/file/file.helper"; +import {FileStatus} from "../../../../../api/api-types/files"; +import {MatDialog, MatDialogRef} from "@angular/material/dialog"; +import {BusyDialogComponent} from "../../app-common/busy-dialog/busy-dialog.component"; +import {BehaviorSubject} from "rxjs"; + +type ProgressDialogContext = { + dialog: MatDialogRef, + progress: BehaviorSubject, + message: BehaviorSubject, +}; @Component({ selector: "app-file-context-menu", @@ -17,31 +23,115 @@ import {FileHelper} from "../../../../services/file/file.helper"; }) export class FileContextMenuComponent { - public file!: File; + public files: File[] = []; + + public actionImported = false; + public actionArchive = false; + public actionRestore = false; + public actionDelete = false; + public actionDeletePermantently = false; @ViewChild("contextMenu") contextMenu!: ContextMenuComponent; + @Output() fileUpdate = new EventEmitter(); - constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { + constructor(private fileService: FileService, private errorBroker: ErrorBrokerService, private dialog: MatDialog) { } - public onContextMenu(event: MouseEvent, file: File) { - this.file = file; + public onContextMenu(event: MouseEvent, files: File[]) { + this.files = files; + this.applyStatus(); this.contextMenu.onContextMenu(event); } public async copyFileHash(): Promise { - await clipboard.writeText(this.file.cd); + await clipboard.writeText(this.files[0].cd); } public async exportFile(): Promise { - const path = await FileHelper.getFileDownloadLocation(this.file); + const path = await FileHelper.getFileDownloadLocation(this.files[0]); if (path) { try { - await this.fileService.saveFile(this.file, path); + await this.fileService.saveFile(this.files[0], path); } catch (err) { this.errorBroker.showError(err); } } } + + public async updateStatus(status: FileStatus) { + if (this.files.length === 1) { + const newFile = await this.fileService.updateFileStatus(this.files[0].id, status); + this.files[0].status = newFile.status; + } else { + await this.iterateWithProgress( + `Updating file status to '${status}'`, + this.files, + async (file) => { + const newFile = await this.fileService.updateFileStatus(file.id, status); + file.status = newFile.status; + } + ); + } + this.fileUpdate.emit(); + } + + public async deletePermanently() { + if (this.files.length === 1) { + await this.fileService.deleteFile(this.files[0].id); + } else { + await this.iterateWithProgress( + "Deleting files", + this.files, + async (file) => this.fileService.deleteFile(file.id) + ); + } + this.fileUpdate.emit(); + } + + private applyStatus() { + for (const file of this.files) { + this.actionDeletePermantently &&= file.status === "Deleted"; + this.actionDelete ||= file.status !== "Deleted"; + this.actionArchive ||= file.status !== "Archived"; + this.actionImported ||= file.status !== "Imported"; + this.actionRestore ||= file.status === "Deleted"; + } + } + + private async iterateWithProgress(title: string, items: T[], action: (arg: T) => Promise): Promise { + const totalCount = items.length; + const dialogCtx = this.openProgressDialog(title, `0/${totalCount}`); + let count = 0; + + for (const item of items) { + await action(item); + dialogCtx.message.next(`${++count}/${totalCount}`); + dialogCtx.progress.next(count / totalCount); + } + dialogCtx.dialog.close(true); + } + + private openProgressDialog(title: string, message: string): ProgressDialogContext { + const dialogMessage = new BehaviorSubject(message); + const dialogProgress = new BehaviorSubject(0); + + const dialog = this.dialog.open(BusyDialogComponent, { + data: { + message: dialogMessage, + progress: dialogProgress, + title, + allowCancel: false, + }, + disableClose: true, + minWidth: "30%", + minHeight: "30%", + }); + + return { + dialog, + message: dialogMessage, + progress: dialogProgress, + }; + } } diff --git a/mediarepo-ui/src/app/components/shared/file/file-multiview/file-gallery/file-gallery.component.html b/mediarepo-ui/src/app/components/shared/file/file-multiview/file-gallery/file-gallery.component.html index ca0ec50..1ede790 100644 --- a/mediarepo-ui/src/app/components/shared/file/file-multiview/file-gallery/file-gallery.component.html +++ b/mediarepo-ui/src/app/components/shared/file/file-multiview/file-gallery/file-gallery.component.html @@ -1,13 +1,13 @@ -