From 4a0b1eb2ae441b13e97df4715117f82c3b633ceb Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 21 Nov 2021 17:30:23 +0100 Subject: [PATCH] Add support for videos and audios in gallery view Signed-off-by: trivernis --- mediarepo-ui/src-tauri/Cargo.lock | 8 +- mediarepo-ui/src-tauri/Cargo.toml | 2 +- mediarepo-ui/src-tauri/tauri.conf.json | 6 +- mediarepo-ui/src/app/app.module.ts | 104 ++++++++++-------- .../busy-indicator.component.html | 4 + .../busy-indicator.component.scss | 31 ++++++ .../busy-indicator.component.spec.ts | 25 +++++ .../busy-indicator.component.ts | 48 ++++++++ .../file-context-menu.component.ts | 33 +----- .../audio-viewer/audio-viewer.component.html | 4 + .../audio-viewer/audio-viewer.component.scss | 8 ++ .../audio-viewer.component.spec.ts | 25 +++++ .../audio-viewer/audio-viewer.component.ts | 14 +++ .../content-viewer.component.html | 10 +- .../content-viewer.component.scss | 17 ++- .../content-viewer.component.ts | 93 ++++++++++++++-- .../video-viewer/video-viewer.component.html | 3 + .../video-viewer/video-viewer.component.scss | 4 + .../video-viewer.component.spec.ts | 25 +++++ .../video-viewer/video-viewer.component.ts | 14 +++ .../file-gallery/file-gallery.component.html | 6 +- .../file-gallery/file-gallery.component.scss | 3 +- .../home/files-tab/files-tab.component.html | 23 ++-- .../filesystem-import.component.ts | 1 + .../repositories-tab.component.scss | 4 + .../repository-card.component.html | 68 ++++++------ .../repository-card.component.ts | 6 +- .../src/app/services/file/file.helper.ts | 49 +++++++++ .../src/app/services/file/file.service.ts | 21 +++- 29 files changed, 511 insertions(+), 148 deletions(-) create mode 100644 mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.html create mode 100644 mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.scss create mode 100644 mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.ts create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.html create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.scss create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.ts create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.html create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.scss create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.ts create mode 100644 mediarepo-ui/src/app/services/file/file.helper.ts diff --git a/mediarepo-ui/src-tauri/Cargo.lock b/mediarepo-ui/src-tauri/Cargo.lock index ef10d76..ae23ff6 100644 --- a/mediarepo-ui/src-tauri/Cargo.lock +++ b/mediarepo-ui/src-tauri/Cargo.lock @@ -1473,8 +1473,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "mediarepo-api" -version = "0.11.1" -source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=d56e2c7406e9b8b15b22bc2714eebff00f63d778#d56e2c7406e9b8b15b22bc2714eebff00f63d778" +version = "0.12.0" +source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=16b892eab0b3446198601a8f5829d0bd54d2efdf#16b892eab0b3446198601a8f5829d0bd54d2efdf" dependencies = [ "async-trait", "chrono", @@ -2309,9 +2309,9 @@ dependencies = [ [[package]] name = "rmp-ipc" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc14d76d6718bdbdf12596ed68f5db46f4fa46fca0bd3acf85f7e347788df3c2" +checksum = "573eb3e2e1008f550b7b5a53053d5ed8378fbda57731fa3739c3cfb18ad667f6" dependencies = [ "async-trait", "byteorder", diff --git a/mediarepo-ui/src-tauri/Cargo.toml b/mediarepo-ui/src-tauri/Cargo.toml index 6e040b8..6f78e24 100644 --- a/mediarepo-ui/src-tauri/Cargo.toml +++ b/mediarepo-ui/src-tauri/Cargo.toml @@ -30,7 +30,7 @@ features = ["env-filter"] [dependencies.mediarepo-api] git = "https://github.com/Trivernis/mediarepo-api.git" -rev = "d56e2c7406e9b8b15b22bc2714eebff00f63d778" +rev = "16b892eab0b3446198601a8f5829d0bd54d2efdf" features = ["tauri-plugin"] [features] diff --git a/mediarepo-ui/src-tauri/tauri.conf.json b/mediarepo-ui/src-tauri/tauri.conf.json index 66e8a82..f98a965 100644 --- a/mediarepo-ui/src-tauri/tauri.conf.json +++ b/mediarepo-ui/src-tauri/tauri.conf.json @@ -13,7 +13,7 @@ "bundle": { "active": true, "targets": "all", - "identifier": "com.tauri.dev", + "identifier": "net.trivernis.mediarepo", "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -24,8 +24,8 @@ "resources": [], "externalBin": [], "copyright": "", - "category": "DeveloperTool", - "shortDescription": "", + "category": "Productivity", + "shortDescription": "A media mangagement tool", "longDescription": "", "deb": { "depends": [], diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts index a4cadde..e5a0467 100644 --- a/mediarepo-ui/src/app/app.module.ts +++ b/mediarepo-ui/src/app/app.module.ts @@ -48,19 +48,23 @@ import {ConfirmDialogComponent} from './components/confirm-dialog/confirm-dialog import {FilesTabSidebarComponent} from './pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component'; import {MatExpansionModule} from "@angular/material/expansion"; import {TagItemComponent} from './components/tag-item/tag-item.component'; -import { FileEditComponent } from './components/file-edit/file-edit.component'; -import { ImportTabComponent } from './pages/home/import-tab/import-tab.component'; -import { ImportTabSidebarComponent } from './pages/home/import-tab/import-tab-sidebar/import-tab-sidebar.component'; -import { NativeFileSelectComponent } from './components/inputs/native-file-select/native-file-select.component'; -import { FilesystemImportComponent } from './pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component'; +import {FileEditComponent} from './components/file-edit/file-edit.component'; +import {ImportTabComponent} from './pages/home/import-tab/import-tab.component'; +import {ImportTabSidebarComponent} from './pages/home/import-tab/import-tab-sidebar/import-tab-sidebar.component'; +import {NativeFileSelectComponent} from './components/inputs/native-file-select/native-file-select.component'; +import {FilesystemImportComponent} from './pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component'; import {MatCheckboxModule} from "@angular/material/checkbox"; -import { FilterDialogComponent } from './components/file-search/filter-dialog/filter-dialog.component'; -import { TagFilterListItemComponent } from './components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component'; -import { TagInputComponent } from './components/inputs/tag-input/tag-input.component'; -import { ContextMenuComponent } from './components/context-menu/context-menu.component'; -import { FileContextMenuComponent } from './components/context-menu/file-context-menu/file-context-menu.component'; -import { ContentViewerComponent } from './components/file-gallery/content-viewer/content-viewer.component'; -import { ImageViewerComponent } from './components/file-gallery/content-viewer/image-viewer/image-viewer.component'; +import {FilterDialogComponent} from './components/file-search/filter-dialog/filter-dialog.component'; +import {TagFilterListItemComponent} from './components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component'; +import {TagInputComponent} from './components/inputs/tag-input/tag-input.component'; +import {ContextMenuComponent} from './components/context-menu/context-menu.component'; +import {FileContextMenuComponent} from './components/context-menu/file-context-menu/file-context-menu.component'; +import {ContentViewerComponent} from './components/file-gallery/content-viewer/content-viewer.component'; +import {ImageViewerComponent} from './components/file-gallery/content-viewer/image-viewer/image-viewer.component'; +import {VideoViewerComponent} from './components/file-gallery/content-viewer/video-viewer/video-viewer.component'; +import {HttpClientModule} from "@angular/common/http"; +import { AudioViewerComponent } from './components/file-gallery/content-viewer/audio-viewer/audio-viewer.component'; +import { BusyIndicatorComponent } from './components/busy-indicator/busy-indicator.component'; @NgModule({ declarations: [ @@ -92,43 +96,47 @@ import { ImageViewerComponent } from './components/file-gallery/content-viewer/i FileContextMenuComponent, ContentViewerComponent, ImageViewerComponent, + VideoViewerComponent, + AudioViewerComponent, + BusyIndicatorComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatCardModule, + MatListModule, + MatButtonModule, + MatToolbarModule, + MatSnackBarModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatSidenavModule, + MatGridListModule, + MatProgressBarModule, + MatPaginatorModule, + ScrollingModule, + MatChipsModule, + MatIconModule, + MatAutocompleteModule, + MatTabsModule, + FlexModule, + GridModule, + MatRippleModule, + MatDialogModule, + MatSelectModule, + MatProgressSpinnerModule, + BlockUIModule, + PanelModule, + DragDropModule, + MatSliderModule, + MatTooltipModule, + MatMenuModule, + MatExpansionModule, + MatCheckboxModule, + HttpClientModule, ], - imports: [ - BrowserModule, - AppRoutingModule, - BrowserAnimationsModule, - MatCardModule, - MatListModule, - MatButtonModule, - MatToolbarModule, - MatSnackBarModule, - MatFormFieldModule, - MatInputModule, - ReactiveFormsModule, - MatSidenavModule, - MatGridListModule, - MatProgressBarModule, - MatPaginatorModule, - ScrollingModule, - MatChipsModule, - MatIconModule, - MatAutocompleteModule, - MatTabsModule, - FlexModule, - GridModule, - MatRippleModule, - MatDialogModule, - MatSelectModule, - MatProgressSpinnerModule, - BlockUIModule, - PanelModule, - DragDropModule, - MatSliderModule, - MatTooltipModule, - MatMenuModule, - MatExpansionModule, - MatCheckboxModule, - ], providers: [], bootstrap: [AppComponent] }) diff --git a/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.html b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.html new file mode 100644 index 0000000..36c45b3 --- /dev/null +++ b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.html @@ -0,0 +1,4 @@ + +
+ +
diff --git a/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.scss b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.scss new file mode 100644 index 0000000..93d7771 --- /dev/null +++ b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.scss @@ -0,0 +1,31 @@ +.busy-indicator-overlay { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + overflow: hidden; + display: flex; + z-index: 998; + + mat-progress-spinner { + z-index: 999; + margin: auto; + } +} + +.busy-indicator-overlay.blur { + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); +} + +.busy-indicator-overlay.darken { + background-color: rgba(0, 0, 0, 0.2); +} + +::ng-deep app-busy-indicator { + width: 100%; + height: 100%; + position: relative; + display: block; +} diff --git a/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.spec.ts b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.spec.ts new file mode 100644 index 0000000..198dbe9 --- /dev/null +++ b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BusyIndicatorComponent } from './busy-indicator.component'; + +describe('BusyIndicatorComponent', () => { + let component: BusyIndicatorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BusyIndicatorComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BusyIndicatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.ts b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.ts new file mode 100644 index 0000000..90f3be2 --- /dev/null +++ b/mediarepo-ui/src/app/components/busy-indicator/busy-indicator.component.ts @@ -0,0 +1,48 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {ProgressSpinnerMode} from "@angular/material/progress-spinner"; + +@Component({ + selector: 'app-busy-indicator', + templateUrl: './busy-indicator.component.html', + styleUrls: ['./busy-indicator.component.scss'] +}) +export class BusyIndicatorComponent { + + @Input() busy: boolean = false; + @Input() blurBackground: boolean = false; + @Input() darkenBackground: boolean = false; + @Input() mode: ProgressSpinnerMode = "indeterminate"; + @Input() value: number | undefined; + + constructor() { } + + public setBusy(busy: boolean) { + this.busy = busy; + } + + public wrapOperation(operation: Function): T | undefined { + this.setBusy(true) + try { + const result = operation(); + this.setBusy(false); + return result; + } catch { + return undefined; + } finally { + this.setBusy(false); + } + } + + public async wrapAsyncOperation(operation: Function): Promise { + this.setBusy(true) + try { + const result = await operation(); + this.setBusy(false); + return result; + } catch { + return undefined; + } finally { + this.setBusy(false); + } + } +} diff --git a/mediarepo-ui/src/app/components/context-menu/file-context-menu/file-context-menu.component.ts b/mediarepo-ui/src/app/components/context-menu/file-context-menu/file-context-menu.component.ts index 0e349b0..0aa41d3 100644 --- a/mediarepo-ui/src/app/components/context-menu/file-context-menu/file-context-menu.component.ts +++ b/mediarepo-ui/src/app/components/context-menu/file-context-menu/file-context-menu.component.ts @@ -5,6 +5,7 @@ import {clipboard, dialog} from "@tauri-apps/api"; import {FileService} from "../../../services/file/file.service"; import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service"; import {downloadDir} from "@tauri-apps/api/path"; +import {FileHelper} from "../../../services/file/file.helper"; @Component({ selector: 'app-file-context-menu', @@ -29,17 +30,8 @@ export class FileContextMenuComponent { } public async exportFile(): Promise { - let extension; - if (this.file.mime_type) { - extension = FileContextMenuComponent.getExtensionForMime(this.file.mime_type); - } - const downloadDirectory = await downloadDir(); - const suggestionPath = downloadDirectory + this.file.hash + "." + extension; + const path = await FileHelper.getFileDownloadLocation(this.file) - const path = await dialog.save({ - defaultPath: suggestionPath, - filters: [{name: this.file.mime_type ?? "All", extensions: [extension ?? "*"]}, {name: "All", extensions: ["*"]}] - }); if (path) { try { await this.fileService.saveFile(this.file, path); @@ -48,25 +40,4 @@ export class FileContextMenuComponent { } } } - - /** - * Returns the extension for a mime type - * @param {string} mime - * @returns {string | undefined} - * @private - */ - private static getExtensionForMime(mime: string): string | undefined { - let parts = mime.split("/"); - - if (parts.length === 2) { - const type = parts[0]; - const subtype = parts[1]; - return FileContextMenuComponent.convertMimeSubtypeToExtension(subtype); - } - return undefined; - } - - private static convertMimeSubtypeToExtension(subtype: string): string { - return subtype; - } } diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.html b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.html new file mode 100644 index 0000000..1172d35 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.html @@ -0,0 +1,4 @@ +
+ +
diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.scss b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.scss new file mode 100644 index 0000000..52ae794 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.scss @@ -0,0 +1,8 @@ +.audio-container { + height: 100%; + width: 100%; + display: flex; + audio { + margin: auto; + } +} diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.spec.ts b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.spec.ts new file mode 100644 index 0000000..1ef6ca2 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AudioViewerComponent } from './audio-viewer.component'; + +describe('AudioViewerComponent', () => { + let component: AudioViewerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AudioViewerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AudioViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.ts b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.ts new file mode 100644 index 0000000..e3ce219 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/audio-viewer/audio-viewer.component.ts @@ -0,0 +1,14 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {SafeResourceUrl} from "@angular/platform-browser"; + +@Component({ + selector: 'app-audio-viewer', + templateUrl: './audio-viewer.component.html', + styleUrls: ['./audio-viewer.component.scss'] +}) +export class AudioViewerComponent { + + @Input() blobUrl!: SafeResourceUrl; + + constructor() { } +} diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.html b/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.html index 14cb1f4..c64a703 100644 --- a/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.html +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.html @@ -1 +1,9 @@ - + + + + +
+ Unsupported content type {{this.file.mime_type}} + +
+ diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.scss b/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.scss index aa664cb..d9b4e79 100644 --- a/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.scss +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.scss @@ -1,4 +1,19 @@ -app-image-viewer { +app-image-viewer, app-video-viewer, app-audio-viewer { width: 100%; height: 100%; } + +.download-prompt { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + button { + margin: 1em 0 auto; + align-self: center; + } + span { + margin: auto 0 0; + align-self: center; + } +} diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.ts b/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.ts index 0b966f2..af22e9c 100644 --- a/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.ts +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/content-viewer.component.ts @@ -1,25 +1,68 @@ -import {Component, Input, OnInit} from '@angular/core'; +import { + AfterContentInit, AfterViewInit, + Component, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, ViewChild +} from '@angular/core'; import {SafeResourceUrl} from "@angular/platform-browser"; +import {File} from "../../../models/File"; +import {FileService} from "../../../services/file/file.service"; +import {FileHelper} from "../../../services/file/file.helper"; +import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service"; +import {BusyIndicatorComponent} from "../../busy-indicator/busy-indicator.component"; -type ContentType = "image" | "video" | "text" | "other"; +type ContentType = "image" | "video" | "audio" | "other"; @Component({ selector: 'app-content-viewer', templateUrl: './content-viewer.component.html', styleUrls: ['./content-viewer.component.scss'] }) -export class ContentViewerComponent { +export class ContentViewerComponent implements AfterViewInit, OnChanges, OnDestroy { + @Input() file!: File; - @Input() contentUrl!: SafeResourceUrl | string; - @Input() mimeType: string | undefined; + public contentUrl: SafeResourceUrl | undefined; + public blobUrl: SafeResourceUrl | undefined; - constructor() { } + @ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent; + + constructor( + private errorBroker: ErrorBrokerService, + private fileService: FileService + ) { + } + + public async ngAfterViewInit() { + if (["audio", "video"].includes(this.getContentType())) { + await this.loadBlobUrl(); + } else { + this.contentUrl = this.fileService.buildContentUrl(this.file); + } + } + + public async ngOnChanges(changes:SimpleChanges) { + if (changes["file"]) { + if (["audio", "video"].includes(this.getContentType()) && this.busyIndicator) { + await this.loadBlobUrl(); + } else { + this.contentUrl = this.fileService.buildContentUrl(this.file); + this.unloadBlobUrl(); + } + } + } + + public ngOnDestroy(): void { + this.unloadBlobUrl(); + } public getContentType(): ContentType { - if (!this.mimeType) { + if (!this.file.mime_type) { return "other"; } - let mimeParts = this.mimeType.split("/"); + let mimeParts = this.file.mime_type.split("/"); const type = mimeParts.shift() ?? "other"; const subtype = mimeParts.shift() ?? "*"; @@ -28,10 +71,40 @@ export class ContentViewerComponent { return "image"; case "video": return "video"; - case "text": - return "text"; + case "audio": + return "audio"; default: return "other"; } } + + public async downloadContent() { + const path = await FileHelper.getFileDownloadLocation(this.file) + + if (path) { + try { + await this.fileService.saveFile(this.file, path); + } catch (err) { + this.errorBroker.showError(err); + } + } + } + + public async loadBlobUrl(): Promise { + await this.busyIndicator.wrapAsyncOperation(async () => { + const startId = this.file.id; + this.unloadBlobUrl(); + const url = await this.fileService.readFile(this.file); + if (startId === this.file.id) { + this.blobUrl = url; + } + }); + } + + private unloadBlobUrl() { + if (this.blobUrl) { + URL?.revokeObjectURL(this.blobUrl as string); + this.blobUrl = undefined; + } + } } diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.html b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.html new file mode 100644 index 0000000..ca7c555 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.html @@ -0,0 +1,3 @@ + diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.scss b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.scss new file mode 100644 index 0000000..b1f16a5 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.scss @@ -0,0 +1,4 @@ +video { + height: 100%; + width: 100%; +} diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.spec.ts b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.spec.ts new file mode 100644 index 0000000..cccd6c0 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VideoViewerComponent } from './video-viewer.component'; + +describe('VideoViewerComponent', () => { + let component: VideoViewerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ VideoViewerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VideoViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.ts b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.ts new file mode 100644 index 0000000..a667dd8 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-gallery/content-viewer/video-viewer/video-viewer.component.ts @@ -0,0 +1,14 @@ +import { + Component, + Input, +} from '@angular/core'; +import {SafeResourceUrl} from "@angular/platform-browser"; + +@Component({ + selector: 'app-video-viewer', + templateUrl: './video-viewer.component.html', + styleUrls: ['./video-viewer.component.scss'] +}) +export class VideoViewerComponent { + @Input() blobUrl!: SafeResourceUrl; +} diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html index b149aa6..d7aaba4 100644 --- a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html +++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html @@ -4,10 +4,8 @@
-
- -
- +
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss index 83fccf1..30dd6d9 100644 --- a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss +++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss @@ -32,9 +32,10 @@ app-file-gallery-entry { overflow: hidden; } -app-content-aware-image { +app-content-viewer{ height: 100%; width: 100%; + display: block; } .close-button { diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab.component.html b/mediarepo-ui/src/app/pages/home/files-tab/files-tab.component.html index 2846292..19d1273 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab.component.html +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab.component.html @@ -5,16 +5,17 @@ [selectedFiles]="this.selectedFiles"> -
- -
- - + + + +
diff --git a/mediarepo-ui/src/app/pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component.ts b/mediarepo-ui/src/app/pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component.ts index a64365c..77dcfbe 100644 --- a/mediarepo-ui/src/app/pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component.ts +++ b/mediarepo-ui/src/app/pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component.ts @@ -22,6 +22,7 @@ export class FilesystemImportComponent { public filters: DialogFilter[] = [ {name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "bmp"]}, {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: ["*"]} diff --git a/mediarepo-ui/src/app/pages/home/repositories-tab/repositories-tab.component.scss b/mediarepo-ui/src/app/pages/home/repositories-tab/repositories-tab.component.scss index 0091879..61c3481 100644 --- a/mediarepo-ui/src/app/pages/home/repositories-tab/repositories-tab.component.scss +++ b/mediarepo-ui/src/app/pages/home/repositories-tab/repositories-tab.component.scss @@ -26,3 +26,7 @@ overflow-y: auto; height: calc(100% - 5em); } + +app-repository-card{ + position: relative; +} diff --git a/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.html b/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.html index 30e43a3..0f1c07b 100644 --- a/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.html +++ b/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.html @@ -1,31 +1,37 @@ - - {{repository.name}} -
-

{{this.getDaemonStatusText()}}

-
- -

{{repository.path!}}

-

{{repository.address}}

-
- - - - - - - - - - - -
+ + + {{repository.name}} +
+

{{this.getDaemonStatusText()}}

+
+ +

{{repository.path!}}

+

{{repository.address}}

+
+ + + + + + + + + + + +
+
diff --git a/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.ts b/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.ts index 7623bfb..4544ffe 100644 --- a/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.ts +++ b/mediarepo-ui/src/app/pages/home/repositories-tab/repository-card/repository-card.component.ts @@ -1,10 +1,11 @@ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Repository} from "../../../../models/Repository"; import {RepositoryService} from "../../../../services/repository/repository.service"; import {Router} from "@angular/router"; import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service"; import {MatDialog} from "@angular/material/dialog"; import {ConfirmDialogComponent} from "../../../../components/confirm-dialog/confirm-dialog.component"; +import {BusyIndicatorComponent} from "../../../../components/busy-indicator/busy-indicator.component"; @Component({ selector: 'app-repository-card', @@ -14,6 +15,7 @@ import {ConfirmDialogComponent} from "../../../../components/confirm-dialog/conf export class RepositoryCardComponent implements OnInit, OnDestroy { @Input() repository!: Repository; + @ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent; public daemonRunning: boolean = false; @@ -95,11 +97,13 @@ export class RepositoryCardComponent implements OnInit, OnDestroy { } public async selectRepository() { + this.busyIndicator.setBusy(true); try { await this.repoService.setRepository(this.repository); } catch (err) { this.errorBroker.showError(err); } + this.busyIndicator.setBusy(false); } async checkRemoteRepositoryStatus() { diff --git a/mediarepo-ui/src/app/services/file/file.helper.ts b/mediarepo-ui/src/app/services/file/file.helper.ts new file mode 100644 index 0000000..f99e72a --- /dev/null +++ b/mediarepo-ui/src/app/services/file/file.helper.ts @@ -0,0 +1,49 @@ +import {downloadDir} from "@tauri-apps/api/path"; +import {dialog} from "@tauri-apps/api"; +import {File} from "../../models/File"; + +export class FileHelper { + + /** + * Opens a dialog to get a download location for the given file + * @param {File} file + */ + public static async getFileDownloadLocation(file: File): Promise { + let extension; + + if (file.mime_type) { + extension = FileHelper.getExtensionForMime(file.mime_type); + } + const downloadDirectory = await downloadDir(); + const suggestionPath = downloadDirectory + file.hash + "." + extension; + + return await dialog.save({ + defaultPath: suggestionPath, + filters: [{ + name: file.mime_type ?? "All", + extensions: [extension ?? "*"] + }, {name: "All", extensions: ["*"]}] + }) + } + + /** + * Returns the extension for a mime type + * @param {string} mime + * @returns {string | undefined} + * @private + */ + public static getExtensionForMime(mime: string): string | undefined { + let parts = mime.split("/"); + + if (parts.length === 2) { + const type = parts[0]; + const subtype = parts[1]; + return FileHelper.convertMimeSubtypeToExtension(subtype); + } + return undefined; + } + + private static convertMimeSubtypeToExtension(subtype: string): string { + return subtype; + } +} diff --git a/mediarepo-ui/src/app/services/file/file.service.ts b/mediarepo-ui/src/app/services/file/file.service.ts index 274b6d4..8537a97 100644 --- a/mediarepo-ui/src/app/services/file/file.service.ts +++ b/mediarepo-ui/src/app/services/file/file.service.ts @@ -8,6 +8,9 @@ import {TagQuery} from "../../models/TagQuery"; import {SortKey} from "../../models/SortKey"; import {RepositoryService} from "../repository/repository.service"; import {FilterExpression} from "../../models/FilterExpression"; +import {HttpClient} from "@angular/common/http"; +import {map} from "rxjs/operators"; +import {http} from "@tauri-apps/api"; @Injectable({ providedIn: 'root' @@ -17,7 +20,11 @@ export class FileService { displayedFiles = new BehaviorSubject([]); thumbnailCache: {[key: number]: Thumbnail[]} = {}; - constructor(@Inject(DomSanitizer) private sanitizer: DomSanitizer, private repoService: RepositoryService) { + constructor( + @Inject(DomSanitizer) private sanitizer: DomSanitizer, + private repoService: RepositoryService, + private http: HttpClient, + ) { repoService.selectedRepository.subscribe(_ => this.clearCache()); } @@ -79,4 +86,16 @@ export class FileService { public async deleteThumbnails(file: File) { await invoke("plugin:mediarepo|delete_thumbnails", {id: file.id}); } + + /** + * Reads the contents of a file and returns the object url for it + * @param {File} file + * @returns {Promise} + */ + public async readFile(file: File): Promise { + const data = await invoke("plugin:mediarepo|read_file", {hash: file.hash, mimeType: file.mime_type}); + const blob = new Blob([new Uint8Array(data)], {type: file.mime_type}); + const url = URL?.createObjectURL(blob); + return this.sanitizer.bypassSecurityTrustResourceUrl(url); + } }