Merge pull request #4 from Trivernis/feature/filesystem-imports
Feature/filesystem importspull/4/head
commit
dcd20fd758
@ -0,0 +1,17 @@
|
||||
<div class="file-select-inner" fxLayout="row">
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{label}}</mat-label>
|
||||
<input #filesInput matInput [value]="files.join(', ')" (change)="this.setFiles(filesInput.value)" class="file-input">
|
||||
|
||||
<div class="buttons-native-select">
|
||||
<button *ngIf="mode === 'files'" (click)="openNativeFileSelectDialog(false)" mat-button>
|
||||
<mat-icon>insert_drive_file</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="mode === 'folders'" (click)="openNativeFileSelectDialog(true)" mat-button>
|
||||
<mat-icon>folder</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</mat-form-field>
|
||||
</div>
|
@ -0,0 +1,22 @@
|
||||
.file-select-inner, mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.file-input {
|
||||
width: calc(100% - 3em);
|
||||
float: left;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.buttons-native-select {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: -19px;
|
||||
|
||||
button {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NativeFileSelectComponent } from './native-file-select.component';
|
||||
|
||||
describe('NativeFileSelectComponent', () => {
|
||||
let component: NativeFileSelectComponent;
|
||||
let fixture: ComponentFixture<NativeFileSelectComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ NativeFileSelectComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NativeFileSelectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,83 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import {FormControl} from "@angular/forms";
|
||||
import {dialog} from "@tauri-apps/api";
|
||||
import {DialogFilter} from "@tauri-apps/api/dialog";
|
||||
|
||||
@Component({
|
||||
selector: 'app-native-file-select',
|
||||
templateUrl: './native-file-select.component.html',
|
||||
styleUrls: ['./native-file-select.component.scss']
|
||||
})
|
||||
export class NativeFileSelectComponent implements OnInit, OnChanges {
|
||||
|
||||
|
||||
@Input() mode: "files" | "folders" = "files";
|
||||
@Input() formControlName: string | undefined;
|
||||
@Input() formControl: FormControl | undefined;
|
||||
@Input() startPath: string | undefined;
|
||||
@Input() multiSelect: boolean = true;
|
||||
@Input() filters: DialogFilter[] = [];
|
||||
|
||||
@Output() fileSelect = new EventEmitter<string[]>();
|
||||
|
||||
public files: string[] = [];
|
||||
public label: string | undefined;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.setLabel();
|
||||
}
|
||||
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["mode"]) {
|
||||
this.setLabel();
|
||||
}
|
||||
}
|
||||
|
||||
public setFiles(filesExpr: string) {
|
||||
this.files = filesExpr.split(",");
|
||||
this.fileSelect.emit(this.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the native dialog to select files or folders
|
||||
* @param {boolean} folders
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async openNativeFileSelectDialog(folders: boolean) {
|
||||
const files = await dialog.open({
|
||||
multiple: this.multiSelect,
|
||||
directory: folders,
|
||||
defaultPath: this.startPath,
|
||||
filters: this.filters,
|
||||
});
|
||||
if (files instanceof Array) {
|
||||
this.files = files;
|
||||
} else if (files) {
|
||||
this.files = [files];
|
||||
}
|
||||
this.fileSelect.emit(this.files);
|
||||
}
|
||||
|
||||
private setLabel(): void {
|
||||
switch (this.mode) {
|
||||
case "files":
|
||||
this.label = "Select Files";
|
||||
break;
|
||||
case "folders":
|
||||
this.label = "Select a folder";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
export class AddFileOptions {
|
||||
public read_tags_from_txt = true;
|
||||
public delete_after_import = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export type FileOsMetadata = {
|
||||
name: string,
|
||||
path: string,
|
||||
mime_type: string,
|
||||
created_at: Date,
|
||||
modified_at: Date,
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
<div id="content">
|
||||
<mat-tab-group #tabGroup>
|
||||
<mat-tab-group #tabGroup (selectedTabChange)="this.onTabSelectionChange($event)">
|
||||
<mat-tab label="Repositories">
|
||||
<app-repositories-tab></app-repositories-tab>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="this.selectedRepository" label="Files">
|
||||
<app-files-tab></app-files-tab>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="this.selectedRepository" label="Import">
|
||||
<app-import-tab></app-import-tab>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
@ -0,0 +1,30 @@
|
||||
<mat-form-field>
|
||||
<mat-label>Selection Type</mat-label>
|
||||
<mat-select #selectionType value="files">
|
||||
<mat-option value="folders">Folders</mat-option>
|
||||
<mat-option value="files">Files</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<app-native-file-select [mode]="selectionType.value" (fileSelect)="this.setSelectedPaths($event)"></app-native-file-select>
|
||||
<button mat-flat-button>
|
||||
{{resolving? "Searching for files..." : this.fileCount + " files found"}}
|
||||
<mat-progress-bar *ngIf="resolving" mode="indeterminate" color="primary"></mat-progress-bar>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<section class="binary-import-options">
|
||||
<mat-checkbox [checked]="this.importOptions.read_tags_from_txt" (change)="this.importOptions.read_tags_from_txt = $event.checked">Import tags from
|
||||
adjacent .txt tag files
|
||||
</mat-checkbox>
|
||||
<mat-checkbox [checked]="this.importOptions.delete_after_import" (change)="this.importOptions.delete_after_import = $event.checked" color="warn">
|
||||
Delete files from original location after import
|
||||
</mat-checkbox>
|
||||
</section>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<button mat-flat-button color="primary" class="import-button" [disabled]="importing || this.fileCount === 0" (click)="import()">
|
||||
{{importing? "Importing..." : "Import"}}
|
||||
</button>
|
||||
<mat-progress-bar *ngIf="importing" mode="determinate" color="primary" [value]="this.importingProgress"></mat-progress-bar>
|
@ -0,0 +1,25 @@
|
||||
app-native-file-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.filled-button {
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
.binary-import-options {
|
||||
margin-top: 1em;
|
||||
|
||||
mat-checkbox {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilesystemImportComponent } from './filesystem-import.component';
|
||||
|
||||
describe('FilesystemImportComponent', () => {
|
||||
let component: FilesystemImportComponent;
|
||||
let fixture: ComponentFixture<FilesystemImportComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilesystemImportComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilesystemImportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
import {Component, EventEmitter, Output} from '@angular/core';
|
||||
import {FileOsMetadata} from "../../../../../models/FileOsMetadata";
|
||||
import {ImportService} from "../../../../../services/import/import.service";
|
||||
import {ErrorBrokerService} from "../../../../../services/error-broker/error-broker.service";
|
||||
import {AddFileOptions} from "../../../../../models/AddFileOptions";
|
||||
import {File} from "../../../../../models/File";
|
||||
|
||||
@Component({
|
||||
selector: 'app-filesystem-import',
|
||||
templateUrl: './filesystem-import.component.html',
|
||||
styleUrls: ['./filesystem-import.component.scss']
|
||||
})
|
||||
export class FilesystemImportComponent {
|
||||
|
||||
@Output() fileImported = new EventEmitter<File>();
|
||||
@Output() importFinished = new EventEmitter<void>();
|
||||
|
||||
public fileCount: number = 0;
|
||||
public files: FileOsMetadata[] = [];
|
||||
public importOptions = new AddFileOptions();
|
||||
|
||||
public resolving = false;
|
||||
public importing = false;
|
||||
public importingProgress = 0;
|
||||
|
||||
constructor(private errorBroker: ErrorBrokerService, private importService: ImportService) {
|
||||
}
|
||||
|
||||
public async setSelectedPaths(paths: string[]) {
|
||||
this.resolving = true;
|
||||
try {
|
||||
this.files = await this.importService.resolvePathsToFiles(paths);
|
||||
this.fileCount = this.files.length;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
this.errorBroker.showError(err);
|
||||
}
|
||||
this.resolving = false;
|
||||
}
|
||||
|
||||
public async import() {
|
||||
this.importing = true;
|
||||
|
||||
this.importingProgress = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const file of this.files) {
|
||||
try {
|
||||
const resultFile = await this.importService.addLocalFile(file,
|
||||
this.importOptions);
|
||||
this.fileImported.emit(resultFile);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
this.errorBroker.showError(err);
|
||||
}
|
||||
count++;
|
||||
this.importingProgress = (count / this.fileCount) * 100;
|
||||
}
|
||||
|
||||
this.importing = false;
|
||||
this.importFinished.emit();
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<div class="import-tab-inner">
|
||||
<mat-tab-group headerPosition="below">
|
||||
<mat-tab label="Import">
|
||||
<div class="import-sidebar-tab-inner" fxLayout="column">
|
||||
<div class="import-type-select-wrapper" fxFlex="6em">
|
||||
<mat-form-field class="import-type-select">
|
||||
<mat-label>Import Type</mat-label>
|
||||
<mat-select value="filesystem">
|
||||
<mat-option value="filesystem">Filesystem</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
<div class="import-configuration" fxFlex fxFlexFill>
|
||||
<app-filesystem-import (fileImported)="this.fileImported.emit($event)" (importFinished)="importFinished.emit()"></app-filesystem-import>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
@ -0,0 +1,34 @@
|
||||
mat-tab-group, mat-tab {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.import-tab-inner {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.import-type-select-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.import-type-select {
|
||||
width: calc(100% - 2em);
|
||||
height: calc(100% - 2em);
|
||||
margin: 1em;
|
||||
mat-select {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-sidebar-tab-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.import-configuration {
|
||||
padding: 1em;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImportTabSidebarComponent } from './import-tab-sidebar.component';
|
||||
|
||||
describe('ImportTabSidebarComponent', () => {
|
||||
let component: ImportTabSidebarComponent;
|
||||
let fixture: ComponentFixture<ImportTabSidebarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ImportTabSidebarComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImportTabSidebarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {MatTabGroup} from "@angular/material/tabs";
|
||||
import {File} from "../../../../models/File";
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-tab-sidebar',
|
||||
templateUrl: './import-tab-sidebar.component.html',
|
||||
styleUrls: ['./import-tab-sidebar.component.scss']
|
||||
})
|
||||
export class ImportTabSidebarComponent {
|
||||
|
||||
@Output() fileImported = new EventEmitter<File>();
|
||||
@Output() importFinished = new EventEmitter<void>();
|
||||
|
||||
constructor() { }
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<mat-drawer-container autosize>
|
||||
<mat-drawer disableClose="true" mode="side" opened>
|
||||
<app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)" (importFinished)="this.refreshFileView()"></app-import-tab-sidebar>
|
||||
</mat-drawer>
|
||||
<mat-drawer-content>
|
||||
<app-file-grid #fileGrid [files]="this.files" ></app-file-grid>
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
@ -0,0 +1,27 @@
|
||||
mat-drawer-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
mat-drawer-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
mat-drawer {
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
app-import-tab-sidebar, app-file-grid {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
app-file-grid {
|
||||
display: block;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImportTabComponent } from './import-tab.component';
|
||||
|
||||
describe('ImportTabComponent', () => {
|
||||
let component: ImportTabComponent;
|
||||
let fixture: ComponentFixture<ImportTabComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ImportTabComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImportTabComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {File} from "../../../models/File";
|
||||
import {FileService} from "../../../services/file/file.service";
|
||||
import {FileGridComponent} from "../../../components/file-grid/file-grid.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-tab',
|
||||
templateUrl: './import-tab.component.html',
|
||||
styleUrls: ['./import-tab.component.scss']
|
||||
})
|
||||
export class ImportTabComponent {
|
||||
|
||||
public files: File[] = [];
|
||||
|
||||
@ViewChild("fileGrid") fileGrid!: FileGridComponent;
|
||||
|
||||
constructor(private fileService: FileService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an imported file to the list of imported files
|
||||
* @param {File} file
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async addFileFromImport(file: File) {
|
||||
this.files.push(file);
|
||||
if (this.files.length % 50 === 0) { // refresh every 50 pictures
|
||||
this.refreshFileView();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the file view
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public refreshFileView() {
|
||||
this.files = [...this.files];
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImportService } from './import.service';
|
||||
|
||||
describe('ImportService', () => {
|
||||
let service: ImportService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ImportService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {FileOsMetadata} from "../../models/FileOsMetadata";
|
||||
import {invoke} from "@tauri-apps/api/tauri";
|
||||
import {AddFileOptions} from "../../models/AddFileOptions";
|
||||
import {File} from "../../models/File";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ImportService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Resolves paths from the local file system into a list of files that can be imported
|
||||
* @param {string[]} paths
|
||||
* @returns {Promise<FileOsMetadata[]>}
|
||||
*/
|
||||
public async resolvePathsToFiles(paths: string[]): Promise<FileOsMetadata[]> {
|
||||
return await invoke<FileOsMetadata[]>("plugin:mediarepo|resolve_paths_to_files", {paths});
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a file from the local file system
|
||||
* @param {FileOsMetadata} metadata
|
||||
* @param {AddFileOptions} options
|
||||
* @returns {Promise<File>}
|
||||
*/
|
||||
public async addLocalFile(metadata: FileOsMetadata, options: AddFileOptions): Promise<File> {
|
||||
return await invoke<File>("plugin:mediarepo|add_local_file", {metadata, options});
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TabService } from './tab.service';
|
||||
|
||||
describe('TabService', () => {
|
||||
let service: TabService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(TabService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {BehaviorSubject} from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TabService {
|
||||
|
||||
public selectedTab = new BehaviorSubject<number>(0);
|
||||
constructor() { }
|
||||
|
||||
public setSelectedTab(index: number) {
|
||||
this.selectedTab.next(index);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue