Add support for videos and audios in gallery view

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 47738b392d
commit 4a0b1eb2ae

@ -1473,8 +1473,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mediarepo-api" name = "mediarepo-api"
version = "0.11.1" version = "0.12.0"
source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=d56e2c7406e9b8b15b22bc2714eebff00f63d778#d56e2c7406e9b8b15b22bc2714eebff00f63d778" source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=16b892eab0b3446198601a8f5829d0bd54d2efdf#16b892eab0b3446198601a8f5829d0bd54d2efdf"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@ -2309,9 +2309,9 @@ dependencies = [
[[package]] [[package]]
name = "rmp-ipc" name = "rmp-ipc"
version = "0.10.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc14d76d6718bdbdf12596ed68f5db46f4fa46fca0bd3acf85f7e347788df3c2" checksum = "573eb3e2e1008f550b7b5a53053d5ed8378fbda57731fa3739c3cfb18ad667f6"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",

@ -30,7 +30,7 @@ features = ["env-filter"]
[dependencies.mediarepo-api] [dependencies.mediarepo-api]
git = "https://github.com/Trivernis/mediarepo-api.git" git = "https://github.com/Trivernis/mediarepo-api.git"
rev = "d56e2c7406e9b8b15b22bc2714eebff00f63d778" rev = "16b892eab0b3446198601a8f5829d0bd54d2efdf"
features = ["tauri-plugin"] features = ["tauri-plugin"]
[features] [features]

@ -13,7 +13,7 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"identifier": "com.tauri.dev", "identifier": "net.trivernis.mediarepo",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@ -24,8 +24,8 @@
"resources": [], "resources": [],
"externalBin": [], "externalBin": [],
"copyright": "", "copyright": "",
"category": "DeveloperTool", "category": "Productivity",
"shortDescription": "", "shortDescription": "A media mangagement tool",
"longDescription": "", "longDescription": "",
"deb": { "deb": {
"depends": [], "depends": [],

@ -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 {FilesTabSidebarComponent} from './pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component';
import {MatExpansionModule} from "@angular/material/expansion"; import {MatExpansionModule} from "@angular/material/expansion";
import {TagItemComponent} from './components/tag-item/tag-item.component'; import {TagItemComponent} from './components/tag-item/tag-item.component';
import { FileEditComponent } from './components/file-edit/file-edit.component'; import {FileEditComponent} from './components/file-edit/file-edit.component';
import { ImportTabComponent } from './pages/home/import-tab/import-tab.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 {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 {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 {FilesystemImportComponent} from './pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component';
import {MatCheckboxModule} from "@angular/material/checkbox"; import {MatCheckboxModule} from "@angular/material/checkbox";
import { FilterDialogComponent } from './components/file-search/filter-dialog/filter-dialog.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 {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 {TagInputComponent} from './components/inputs/tag-input/tag-input.component';
import { ContextMenuComponent } from './components/context-menu/context-menu.component'; import {ContextMenuComponent} from './components/context-menu/context-menu.component';
import { FileContextMenuComponent } from './components/context-menu/file-context-menu/file-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 {ContentViewerComponent} from './components/file-gallery/content-viewer/content-viewer.component';
import { ImageViewerComponent } from './components/file-gallery/content-viewer/image-viewer/image-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({ @NgModule({
declarations: [ declarations: [
@ -92,6 +96,9 @@ import { ImageViewerComponent } from './components/file-gallery/content-viewer/i
FileContextMenuComponent, FileContextMenuComponent,
ContentViewerComponent, ContentViewerComponent,
ImageViewerComponent, ImageViewerComponent,
VideoViewerComponent,
AudioViewerComponent,
BusyIndicatorComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -128,6 +135,7 @@ import { ImageViewerComponent } from './components/file-gallery/content-viewer/i
MatMenuModule, MatMenuModule,
MatExpansionModule, MatExpansionModule,
MatCheckboxModule, MatCheckboxModule,
HttpClientModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

@ -0,0 +1,4 @@
<ng-content></ng-content>
<div *ngIf="this.busy" class="busy-indicator-overlay" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground">
<mat-progress-spinner color="primary" [mode]="mode" [value]="value"></mat-progress-spinner>
</div>

@ -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;
}

@ -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<BusyIndicatorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BusyIndicatorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BusyIndicatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<T>(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<T>(operation: Function): Promise<T | undefined> {
this.setBusy(true)
try {
const result = await operation();
this.setBusy(false);
return result;
} catch {
return undefined;
} finally {
this.setBusy(false);
}
}
}

@ -5,6 +5,7 @@ import {clipboard, dialog} from "@tauri-apps/api";
import {FileService} from "../../../services/file/file.service"; 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 {downloadDir} from "@tauri-apps/api/path"; import {downloadDir} from "@tauri-apps/api/path";
import {FileHelper} from "../../../services/file/file.helper";
@Component({ @Component({
selector: 'app-file-context-menu', selector: 'app-file-context-menu',
@ -29,17 +30,8 @@ export class FileContextMenuComponent {
} }
public async exportFile(): Promise<void> { public async exportFile(): Promise<void> {
let extension; const path = await FileHelper.getFileDownloadLocation(this.file)
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 dialog.save({
defaultPath: suggestionPath,
filters: [{name: this.file.mime_type ?? "All", extensions: [extension ?? "*"]}, {name: "All", extensions: ["*"]}]
});
if (path) { if (path) {
try { try {
await this.fileService.saveFile(this.file, path); 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;
}
} }

@ -0,0 +1,4 @@
<div class="audio-container">
<audio controls [src]="this.blobUrl">
</audio>
</div>

@ -0,0 +1,8 @@
.audio-container {
height: 100%;
width: 100%;
display: flex;
audio {
margin: auto;
}
}

@ -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<AudioViewerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AudioViewerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AudioViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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() { }
}

@ -1 +1,9 @@
<app-image-viewer *ngIf="getContentType() === 'image'" [imageUrl]="contentUrl"></app-image-viewer> <app-image-viewer *ngIf="getContentType() === 'image' && contentUrl" [imageUrl]="contentUrl"></app-image-viewer>
<app-video-viewer *ngIf="getContentType() === 'video' && this.blobUrl" [blobUrl]="this.blobUrl!"></app-video-viewer>
<app-audio-viewer *ngIf="getContentType() === 'audio' && this.blobUrl" [blobUrl]="this.blobUrl!"></app-audio-viewer>
<div *ngIf="getContentType() === 'other'" class="download-prompt">
<span>Unsupported content type <b>{{this.file.mime_type}}</b></span>
<button mat-flat-button color="primary" (click)="this.downloadContent()">Download</button>
</div>
<app-busy-indicator></app-busy-indicator>

@ -1,4 +1,19 @@
app-image-viewer { app-image-viewer, app-video-viewer, app-audio-viewer {
width: 100%; width: 100%;
height: 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;
}
}

@ -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 {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({ @Component({
selector: 'app-content-viewer', selector: 'app-content-viewer',
templateUrl: './content-viewer.component.html', templateUrl: './content-viewer.component.html',
styleUrls: ['./content-viewer.component.scss'] styleUrls: ['./content-viewer.component.scss']
}) })
export class ContentViewerComponent { export class ContentViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() file!: File;
@Input() contentUrl!: SafeResourceUrl | string; public contentUrl: SafeResourceUrl | undefined;
@Input() mimeType: string | 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 { public getContentType(): ContentType {
if (!this.mimeType) { if (!this.file.mime_type) {
return "other"; return "other";
} }
let mimeParts = this.mimeType.split("/"); let mimeParts = this.file.mime_type.split("/");
const type = mimeParts.shift() ?? "other"; const type = mimeParts.shift() ?? "other";
const subtype = mimeParts.shift() ?? "*"; const subtype = mimeParts.shift() ?? "*";
@ -28,10 +71,40 @@ export class ContentViewerComponent {
return "image"; return "image";
case "video": case "video":
return "video"; return "video";
case "text": case "audio":
return "text"; return "audio";
default: default:
return "other"; 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<void> {
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;
}
}
} }

@ -0,0 +1,3 @@
<video controls [src]="this.blobUrl">
Unsupported video type
</video>

@ -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<VideoViewerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ VideoViewerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(VideoViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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;
}

@ -4,10 +4,8 @@
</button> </button>
<div (dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null" class="file-full-view" <div (dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null" class="file-full-view"
fxFlex="80%"> fxFlex="80%">
<div *ngIf="!this.fileContentUrl" class="url-loading-backdrop"> <app-content-viewer
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner> (contextmenu)="this.selectedFile && fileContextMenu.onContextMenu($event, this.selectedFile!.data)" [file]="this.selectedFile!.data"></app-content-viewer>
</div>
<app-content-viewer *ngIf="this.fileContentUrl" [contentUrl]="this.fileContentUrl" [mimeType]="this.selectedFile!.data.mime_type"></app-content-viewer>
</div> </div>
<mat-divider fxFlex></mat-divider> <mat-divider fxFlex></mat-divider>
<div class="file-scroll-view" fxFlex="20%"> <div class="file-scroll-view" fxFlex="20%">

@ -32,9 +32,10 @@ app-file-gallery-entry {
overflow: hidden; overflow: hidden;
} }
app-content-aware-image { app-content-viewer{
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block;
} }
.close-button { .close-button {

@ -5,16 +5,17 @@
[selectedFiles]="this.selectedFiles"></app-files-tab-sidebar> [selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<div *ngIf="contentLoading" class="spinner-overlay"> <app-busy-indicator [busy]="contentLoading" [blurBackground]="true">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner> <app-file-grid (fileDblClickEvent)="openGallery($event)" (fileMultiselectEvent)="onFileMultiSelect($event)"
</div> (fileSelectEvent)="onFileSelect($event)"
<app-file-grid (fileDblClickEvent)="openGallery($event)" (fileMultiselectEvent)="onFileMultiSelect($event)" (fileSelectEvent)="onFileSelect($event)"
*ngIf="!this.showGallery" *ngIf="!this.showGallery"
[files]="files" [files]="files"
[preselectedFile]="this.preselectedFile" [preselectedFile]="this.preselectedFile"
></app-file-grid> ></app-file-grid>
<app-file-gallery (closeEvent)="this.closeGallery($event.selectedFile?.data)" (fileSelectEvent)="onFileSelect($event)" *ngIf="this.showGallery" <app-file-gallery (closeEvent)="this.closeGallery($event.selectedFile?.data)"
(fileSelectEvent)="onFileSelect($event)" *ngIf="this.showGallery"
[files]="files" [files]="files"
[preselectedFile]="this.preselectedFile"></app-file-gallery> [preselectedFile]="this.preselectedFile"></app-file-gallery>
</app-busy-indicator>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>

@ -22,6 +22,7 @@ export class FilesystemImportComponent {
public filters: DialogFilter[] = [ public filters: DialogFilter[] = [
{name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "bmp"]}, {name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "bmp"]},
{name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]}, {name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]},
{name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"]},
{name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]}, {name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]},
{name: "Text", extensions: ["txt", "md"]}, {name: "Text", extensions: ["txt", "md"]},
{name: "All", extensions: ["*"]} {name: "All", extensions: ["*"]}

@ -26,3 +26,7 @@
overflow-y: auto; overflow-y: auto;
height: calc(100% - 5em); height: calc(100% - 5em);
} }
app-repository-card{
position: relative;
}

@ -1,4 +1,5 @@
<mat-card> <app-busy-indicator [darkenBackground]="true">
<mat-card>
<mat-card-title>{{repository.name}}</mat-card-title> <mat-card-title>{{repository.name}}</mat-card-title>
<div [class]="'repository-status ' + this.getDaemonStatusClass()"> <div [class]="'repository-status ' + this.getDaemonStatusClass()">
<p>{{this.getDaemonStatusText()}}</p> <p>{{this.getDaemonStatusText()}}</p>
@ -8,16 +9,20 @@
<p *ngIf="!repository.local" class="repository-address">{{repository.address}}</p> <p *ngIf="!repository.local" class="repository-address">{{repository.address}}</p>
</mat-card-content> </mat-card-content>
<mat-action-list> <mat-action-list>
<button (click)="startDaemonAndSelectRepository()" *ngIf="!this.isSelectedRepository() && repository.local" color="primary" <button (click)="startDaemonAndSelectRepository()" *ngIf="!this.isSelectedRepository() && repository.local"
color="primary"
mat-flat-button>Open mat-flat-button>Open
</button> </button>
<button (click)="selectRepository()" *ngIf="!this.isSelectedRepository() && !repository.local" [disabled]="!this.daemonRunning" <button (click)="selectRepository()" *ngIf="!this.isSelectedRepository() && !repository.local"
[disabled]="!this.daemonRunning"
color="primary" mat-flat-button>Connect color="primary" mat-flat-button>Connect
</button> </button>
<button (click)="this.repoService.closeSelectedRepository()" *ngIf="this.isSelectedRepository() && repository.local" color="primary" <button (click)="this.repoService.closeSelectedRepository()"
*ngIf="this.isSelectedRepository() && repository.local" color="primary"
mat-flat-button>Close mat-flat-button>Close
</button> </button>
<button (click)="this.repoService.disconnectSelectedRepository()" *ngIf="this.isSelectedRepository() && !repository.local" color="primary" <button (click)="this.repoService.disconnectSelectedRepository()"
*ngIf="this.isSelectedRepository() && !repository.local" color="primary"
mat-flat-button>Disconnect mat-flat-button>Disconnect
</button> </button>
<button [mat-menu-trigger-for]="menu" class="menu-button" mat-button> <button [mat-menu-trigger-for]="menu" class="menu-button" mat-button>
@ -28,4 +33,5 @@
<button (click)="removeRepository()" *ngIf="!repository.local" mat-menu-item>Remove</button> <button (click)="removeRepository()" *ngIf="!repository.local" mat-menu-item>Remove</button>
</mat-menu> </mat-menu>
</mat-action-list> </mat-action-list>
</mat-card> </mat-card>
</app-busy-indicator>

@ -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 {Repository} from "../../../../models/Repository";
import {RepositoryService} from "../../../../services/repository/repository.service"; import {RepositoryService} from "../../../../services/repository/repository.service";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service"; import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {ConfirmDialogComponent} from "../../../../components/confirm-dialog/confirm-dialog.component"; import {ConfirmDialogComponent} from "../../../../components/confirm-dialog/confirm-dialog.component";
import {BusyIndicatorComponent} from "../../../../components/busy-indicator/busy-indicator.component";
@Component({ @Component({
selector: 'app-repository-card', selector: 'app-repository-card',
@ -14,6 +15,7 @@ import {ConfirmDialogComponent} from "../../../../components/confirm-dialog/conf
export class RepositoryCardComponent implements OnInit, OnDestroy { export class RepositoryCardComponent implements OnInit, OnDestroy {
@Input() repository!: Repository; @Input() repository!: Repository;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
public daemonRunning: boolean = false; public daemonRunning: boolean = false;
@ -95,11 +97,13 @@ export class RepositoryCardComponent implements OnInit, OnDestroy {
} }
public async selectRepository() { public async selectRepository() {
this.busyIndicator.setBusy(true);
try { try {
await this.repoService.setRepository(this.repository); await this.repoService.setRepository(this.repository);
} catch (err) { } catch (err) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
} }
this.busyIndicator.setBusy(false);
} }
async checkRemoteRepositoryStatus() { async checkRemoteRepositoryStatus() {

@ -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<string | undefined> {
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;
}
}

@ -8,6 +8,9 @@ import {TagQuery} from "../../models/TagQuery";
import {SortKey} from "../../models/SortKey"; import {SortKey} from "../../models/SortKey";
import {RepositoryService} from "../repository/repository.service"; import {RepositoryService} from "../repository/repository.service";
import {FilterExpression} from "../../models/FilterExpression"; import {FilterExpression} from "../../models/FilterExpression";
import {HttpClient} from "@angular/common/http";
import {map} from "rxjs/operators";
import {http} from "@tauri-apps/api";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -17,7 +20,11 @@ export class FileService {
displayedFiles = new BehaviorSubject<File[]>([]); displayedFiles = new BehaviorSubject<File[]>([]);
thumbnailCache: {[key: number]: Thumbnail[]} = {}; 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()); repoService.selectedRepository.subscribe(_ => this.clearCache());
} }
@ -79,4 +86,16 @@ export class FileService {
public async deleteThumbnails(file: File) { public async deleteThumbnails(file: File) {
await invoke("plugin:mediarepo|delete_thumbnails", {id: file.id}); 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<SafeResourceUrl>}
*/
public async readFile(file: File): Promise<SafeResourceUrl> {
const data = await invoke<number[]>("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);
}
} }

Loading…
Cancel
Save