Add support for videos and audios in gallery view
Signed-off-by: trivernis <trivernis@protonmail.com>pull/4/head
parent
47738b392d
commit
4a0b1eb2ae
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<video controls [src]="this.blobUrl">
|
||||||
|
Unsupported video type
|
||||||
|
</video>
|
@ -0,0 +1,4 @@
|
|||||||
|
video {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue