Add multiple tabs of one type

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 481114052e
commit 20d527f195

@ -1,13 +1,33 @@
<div id="content"> <div id="content">
<mat-tab-group #tabGroup (selectedTabChange)="this.onTabSelectionChange($event)"> <mat-tab-group #tabGroup (selectedTabChange)="this.onTabSelectionChange($event)" class="main-tab-group">
<mat-tab label="Repositories"> <mat-tab label="Repositories">
<app-repositories-tab></app-repositories-tab> <app-repositories-tab></app-repositories-tab>
</mat-tab> </mat-tab>
<mat-tab *ngIf="this.selectedRepository" label="Files"> <mat-tab *ngFor="let tab of tabs">
<app-files-tab></app-files-tab> <ng-template mat-tab-label>
<div class="tab-label-div" (click)="this.onMouseClickTabLabel(tab, $event)">
{{tab.category}}
<button class="close-tab-button" mat-icon-button (click)="this.closeTab(tab)">
<ng-icon name="mat-close"></ng-icon>
</button>
</div>
</ng-template>
<app-files-tab *ngIf="tab.category === 'Files'" [state]="tab"></app-files-tab>
<app-import-tab *ngIf="tab.category === 'Import'" [state]="tab"></app-import-tab>
</mat-tab>
<mat-tab *ngIf="this.newTab" label="New Tab">
<div class="new-tab-content">
Select the tab type
<button mat-flat-button (click)="this.addFilesTab()" color="primary">Files</button>
<button mat-flat-button (click)="this.addImportTab()" color="primary">Import</button>
</div>
</mat-tab> </mat-tab>
<mat-tab *ngIf="this.selectedRepository" label="Import"> <mat-tab disabled>
<app-import-tab></app-import-tab> <ng-template mat-tab-label>
<button class="new-tab-button" mat-icon-button (click)="this.addTab()">
<ng-icon name="mat-plus"></ng-icon>
</button>
</ng-template>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>

@ -17,3 +17,43 @@ mat-tab-group {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
::ng-deep .main-tab-group .mat-tab-label.mat-tab-disabled {
width: 2em;
min-width: 0;
color: white;
}
::ng-deep .main-tab-group .mat-tab-label {
ng-icon {
margin-top: -0.5em;
--ng-icon__size: 1em;
}
.close-tab-button {
float: right;
position: absolute;
right: 0;
top: 4px;
ng-icon {
font-size: 1.5em;
margin-top: -1.1em;
--ng-icon__size: 0.4em;
}
}
}
.new-tab-content {
display: block;
width: 100%;
text-align: center;
button {
display: flex;
margin: 1em auto auto;
}
}
.tab-label-div {
display: flex;
}

@ -4,6 +4,8 @@ import {RepositoryService} from "../../services/repository/repository.service";
import {MatTabChangeEvent, MatTabGroup} from "@angular/material/tabs"; import {MatTabChangeEvent, MatTabGroup} from "@angular/material/tabs";
import {TagService} from "../../services/tag/tag.service"; import {TagService} from "../../services/tag/tag.service";
import {TabService} from "../../services/tab/tab.service"; import {TabService} from "../../services/tab/tab.service";
import {TabCategory} from "../../models/TabCategory";
import {TabState} from "../../models/TabState.rs";
@Component({ @Component({
selector: "app-core", selector: "app-core",
@ -13,9 +15,10 @@ import {TabService} from "../../services/tab/tab.service";
export class CoreComponent implements OnInit { export class CoreComponent implements OnInit {
public selectedRepository: Repository | undefined; public selectedRepository: Repository | undefined;
public tabs: TabState[] = [];
@ViewChild("tabGroup") tabGroup!: MatTabGroup; @ViewChild("tabGroup") tabGroup!: MatTabGroup;
public newTab = false;
constructor( constructor(
private tabService: TabService, private tabService: TabService,
@ -27,20 +30,11 @@ export class CoreComponent implements OnInit {
this.selectedRepository = this.repoService.selectedRepository.getValue(); this.selectedRepository = this.repoService.selectedRepository.getValue();
this.repoService.selectedRepository.subscribe(async (selected) => { this.repoService.selectedRepository.subscribe(async (selected) => {
this.selectedRepository = selected; this.selectedRepository = selected;
this.updateSelectedTab();
await this.loadRepoData(); await this.loadRepoData();
}); });
} this.tabService.tabs.subscribe(tabs => {
this.tabs = tabs;
public updateSelectedTab() { });
if (!this.tabGroup) {
return;
}
if (!this.selectedRepository) {
this.tabGroup.selectedIndex = 0;
} else if (this.tabGroup.selectedIndex === 0) {
this.tabGroup.selectedIndex = 1;
}
} }
async loadRepoData() { async loadRepoData() {
@ -50,4 +44,39 @@ export class CoreComponent implements OnInit {
public onTabSelectionChange(event: MatTabChangeEvent): void { public onTabSelectionChange(event: MatTabChangeEvent): void {
this.tabService.setSelectedTab(event.index); this.tabService.setSelectedTab(event.index);
} }
public addFilesTab(): void {
this.tabService.addTab(TabCategory.Files);
this.tabGroup.selectedIndex = this.tabs.length;
this.newTab = false;
}
public addImportTab(): void {
this.tabService.addTab(TabCategory.Import);
this.tabGroup.selectedIndex = this.tabs.length;
this.newTab = false;
}
public addTab(): void {
this.newTab = true;
this.tabGroup.selectedIndex = this.tabs.length + 1;
}
public closeTab(tab: TabState): void {
const previousIndex = this.tabGroup.selectedIndex;
this.tabService.closeTab(tab.uuid);
if (previousIndex) {
this.tabGroup.selectedIndex = previousIndex - 1;
} else {
this.tabGroup.selectedIndex = 0;
}
}
public onMouseClickTabLabel(tab: TabState, event: MouseEvent): void {
console.log(event);
if (event.button === 1) { // middle mouse button
this.closeTab(tab);
}
}
} }

@ -1,9 +1,9 @@
<div class="sidebar-inner"> <div class="sidebar-inner">
<mat-tab-group headerPosition="below"> <mat-tab-group headerPosition="below">
<mat-tab label="Search"> <mat-tab label="Search">
<app-file-search [availableTags]="this.allTags" [contextTags]="this.tags" <app-file-search [state]="this.state" [availableTags]="this.allTags" [contextTags]="this.tags"
(searchStartEvent)="this.searchStartEvent.emit($event)" (searchStartEvent)="this.searchStartEvent.emit($event)"
(searchEndEvent)="this.searchEndEvent.emit($event)"></app-file-search> (searchEndEvent)="this.onDisplayedFilesChange(); this.searchEndEvent.emit($event);"></app-file-search>
</mat-tab> </mat-tab>
<mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit Tags"> <mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit Tags">
<app-tag-edit #fileedit [files]="this.selectedFiles"></app-tag-edit> <app-tag-edit #fileedit [files]="this.selectedFiles"></app-tag-edit>

@ -10,12 +10,17 @@ import {
} from "@angular/core"; } from "@angular/core";
import {Tag} from "../../../../models/Tag"; import {Tag} from "../../../../models/Tag";
import {TagService} from "../../../../services/tag/tag.service"; import {TagService} from "../../../../services/tag/tag.service";
import {FileService} from "../../../../services/file/file.service";
import {File} from "../../../../models/File"; import {File} from "../../../../models/File";
import {FileSearchComponent} from "../../../shared/sidebar/file-search/file-search.component"; import {
import {RepositoryService} from "../../../../services/repository/repository.service"; FileSearchComponent
import {TagEditComponent} from "../../../shared/sidebar/tag-edit/tag-edit.component"; } from "../../../shared/sidebar/file-search/file-search.component";
import {clipboard} from "@tauri-apps/api"; import {
RepositoryService
} from "../../../../services/repository/repository.service";
import {
TagEditComponent
} from "../../../shared/sidebar/tag-edit/tag-edit.component";
import {TabState} from "../../../../models/TabState.rs";
@Component({ @Component({
selector: "app-files-tab-sidebar", selector: "app-files-tab-sidebar",
@ -24,6 +29,7 @@ import {clipboard} from "@tauri-apps/api";
}) })
export class FilesTabSidebarComponent implements OnInit, OnChanges { export class FilesTabSidebarComponent implements OnInit, OnChanges {
@Input() state!: TabState;
@Input() selectedFiles: File[] = []; @Input() selectedFiles: File[] = [];
@Output() searchStartEvent = new EventEmitter<void>(); @Output() searchStartEvent = new EventEmitter<void>();
@Output() searchEndEvent = new EventEmitter<void>(); @Output() searchEndEvent = new EventEmitter<void>();
@ -37,19 +43,20 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
public files: File[] = []; public files: File[] = [];
public tagsOfSelection: Tag[] = []; public tagsOfSelection: Tag[] = [];
constructor(private repoService: RepositoryService, private tagService: TagService, private fileService: FileService) { constructor(private repoService: RepositoryService, private tagService: TagService) {
this.fileService.displayedFiles.subscribe(async files => {
this.files = files;
await this.loadTagsForDisplayedFiles();
await this.refreshFileSelection();
});
this.repoService.selectedRepository.subscribe( this.repoService.selectedRepository.subscribe(
async (repo) => repo && this.fileSearch && await this.fileSearch.searchForFiles()); async (repo) => repo && this.fileSearch && await this.fileSearch.searchForFiles());
this.tagService.tags.subscribe(t => this.allTags = t); this.tagService.tags.subscribe(t => this.allTags = t);
} }
async ngOnInit() { async ngOnInit() {
this.fileSearch && await this.fileSearch.searchForFiles(); this.state.files.subscribe(async (files) => {
this.files = files;
await this.onDisplayedFilesChange();
})
if (this.fileSearch) {
await this.fileSearch.searchForFiles();
}
if (this.tags.length === 0) { if (this.tags.length === 0) {
this.tags = this.tagsOfFiles; this.tags = this.tagsOfFiles;
} }
@ -62,6 +69,11 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
} }
} }
public async onDisplayedFilesChange() {
await this.loadTagsForDisplayedFiles();
await this.refreshFileSelection();
}
async loadTagsForDisplayedFiles() { async loadTagsForDisplayedFiles() {
this.tagsOfFiles = await this.tagService.getTagsForFiles( this.tagsOfFiles = await this.tagService.getTagsForFiles(
this.files.map(f => f.hash)); this.files.map(f => f.hash));

@ -1,7 +1,7 @@
<mat-drawer-container autosize> <mat-drawer-container autosize>
<mat-drawer disableClose mode="side" opened> <mat-drawer disableClose mode="side" opened>
<app-files-tab-sidebar (searchEndEvent)="this.contentLoading = false" <app-files-tab-sidebar [state]="this.state" (searchEndEvent)="this.contentLoading = false;"
(searchStartEvent)="this.contentLoading = true" (searchStartEvent)="this.contentLoading = true;"
[selectedFiles]="this.selectedFiles"></app-files-tab-sidebar> [selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>

@ -1,8 +1,6 @@
import {Component, OnInit} from "@angular/core"; import {Component, Input, OnInit} from "@angular/core";
import {File} from "../../../models/File"; import {File} from "../../../models/File";
import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service"; import {TabState} from "../../../models/TabState.rs";
import {FileService} from "../../../services/file/file.service";
import {RepositoryService} from "../../../services/repository/repository.service";
@Component({ @Component({
selector: "app-files-tab", selector: "app-files-tab",
@ -11,21 +9,17 @@ import {RepositoryService} from "../../../services/repository/repository.service
}) })
export class FilesTabComponent implements OnInit { export class FilesTabComponent implements OnInit {
@Input() state!: TabState;
files: File[] = []; files: File[] = [];
contentLoading = false; contentLoading = false;
selectedFiles: File[] = []; selectedFiles: File[] = [];
constructor( constructor() {
private errorBroker: ErrorBrokerService,
private repoService: RepositoryService,
private fileService: FileService,) {
} }
async ngOnInit() { async ngOnInit() {
this.fileService.displayedFiles.subscribe(async (files) => { this.state.files.subscribe(files => this.files = files);
this.files = files;
});
} }
async onFileSelect(files: File[]) { async onFileSelect(files: File[]) {

@ -1,5 +1,6 @@
import {Component} from "@angular/core"; import {Component, Input} from "@angular/core";
import {File} from "../../../models/File"; import {File} from "../../../models/File";
import {TabState} from "../../../models/TabState.rs";
@Component({ @Component({
selector: "app-import-tab", selector: "app-import-tab",
@ -8,6 +9,8 @@ import {File} from "../../../models/File";
}) })
export class ImportTabComponent { export class ImportTabComponent {
@Input() state!: TabState;
public files: File[] = []; public files: File[] = [];
public selectedFiles: File[] = []; public selectedFiles: File[] = [];

@ -3,17 +3,18 @@ import {
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, OnChanges, Input,
OnInit, OnInit,
Output, SimpleChanges, Output,
ViewChild ViewChild
} from "@angular/core"; } from "@angular/core";
import {FileService} from "../../../../services/file/file.service";
import {TagQuery} from "../../../../models/TagQuery"; import {TagQuery} from "../../../../models/TagQuery";
import {SortKey} from "../../../../models/SortKey"; import {SortKey} from "../../../../models/SortKey";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {SortDialogComponent} from "./sort-dialog/sort-dialog.component"; import {SortDialogComponent} from "./sort-dialog/sort-dialog.component";
import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service"; import {
ErrorBrokerService
} from "../../../../services/error-broker/error-broker.service";
import { import {
FilterExpression, FilterExpression,
SingleFilterExpression SingleFilterExpression
@ -21,6 +22,7 @@ import {
import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component"; import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component";
import {Tag} from "../../../../models/Tag"; import {Tag} from "../../../../models/Tag";
import {clipboard} from "@tauri-apps/api"; import {clipboard} from "@tauri-apps/api";
import {TabState} from "../../../../models/TabState.rs";
@Component({ @Component({
@ -35,6 +37,8 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
@Input() availableTags: Tag[] = []; @Input() availableTags: Tag[] = [];
@Input() contextTags: Tag[] = []; @Input() contextTags: Tag[] = [];
@Input() state!: TabState;
@Output() searchStartEvent = new EventEmitter<void>(); @Output() searchStartEvent = new EventEmitter<void>();
@Output() searchEndEvent = new EventEmitter<void>(); @Output() searchEndEvent = new EventEmitter<void>();
@ -45,7 +49,6 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
constructor( constructor(
private errorBroker: ErrorBrokerService, private errorBroker: ErrorBrokerService,
private fileService: FileService,
public dialog: MatDialog public dialog: MatDialog
) { ) {
} }
@ -61,7 +64,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
public async searchForFiles() { public async searchForFiles() {
this.searchStartEvent.emit(); this.searchStartEvent.emit();
try { try {
await this.fileService.findFiles(this.filters, this.sortExpression); await this.state.findFiles(this.filters, this.sortExpression);
} catch (err) { } catch (err) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
} }

@ -0,0 +1,4 @@
export enum TabCategory {
Files = "Files",
Import = "Import",
}

@ -0,0 +1,30 @@
import {BehaviorSubject} from "rxjs";
import {TabCategory} from "./TabCategory";
import {FileService} from "../services/file/file.service";
import {File} from "./File";
import {FilterExpression} from "./FilterExpression";
import {SortKey} from "./SortKey";
export class TabState {
public uuid: number;
public category: TabCategory;
public files = new BehaviorSubject<File[]>([]);
private fileService: FileService;
constructor(uuid: number, category: TabCategory, fileService: FileService) {
this.category = category;
this.uuid = uuid;
this.fileService = fileService;
}
public async loadAllFiles() {
const files = await this.fileService.getAllFiles();
this.files.next(files);
}
public async findFiles(filters: FilterExpression[], sortBy: SortKey[]) {
const files = await this.fileService.findFiles(filters, sortBy);
this.files.next(files);
}
}

@ -1,5 +1,4 @@
import {Inject, Injectable} from "@angular/core"; import {Inject, Injectable} from "@angular/core";
import {BehaviorSubject} from "rxjs";
import {File} from "../../models/File"; import {File} from "../../models/File";
import {invoke} from "@tauri-apps/api/tauri"; import {invoke} from "@tauri-apps/api/tauri";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
@ -14,7 +13,6 @@ import {FilterExpression} from "../../models/FilterExpression";
}) })
export class FileService { export class FileService {
displayedFiles = new BehaviorSubject<File[]>([]);
thumbnailCache: { [key: number]: Thumbnail[] } = {}; thumbnailCache: { [key: number]: Thumbnail[] } = {};
constructor( constructor(
@ -28,17 +26,18 @@ export class FileService {
this.thumbnailCache = {}; this.thumbnailCache = {};
} }
public async getFiles() { public async getAllFiles(): Promise<File[]> {
let all_files = await invoke<File[]>("plugin:mediarepo|get_all_files"); return await invoke<File[]>("plugin:mediarepo|get_all_files");
this.displayedFiles.next(all_files);
} }
public async findFiles(filters: FilterExpression[], sortBy: SortKey[]) { public async findFiles(filters: FilterExpression[], sortBy: SortKey[]): Promise<File[]> {
console.log(filters); console.log(filters);
let backendFilters = filters.map(f => f.toBackendType()); let backendFilters = filters.map(f => f.toBackendType());
let files = await invoke<File[]>("plugin:mediarepo|find_files", return await invoke<File[]>("plugin:mediarepo|find_files",
{filters: backendFilters, sortBy: sortBy.map(k => k.toBackendType())}); {
this.displayedFiles.next(files); filters: backendFilters,
sortBy: sortBy.map(k => k.toBackendType())
});
} }
public async updateFileName(file: File, name: string): Promise<File> { public async updateFileName(file: File, name: string): Promise<File> {

@ -1,17 +1,35 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import {BehaviorSubject} from "rxjs"; import {BehaviorSubject} from "rxjs";
import {TabState} from "../../models/TabState.rs";
import {TabCategory} from "../../models/TabCategory";
import {FileService} from "../file/file.service";
@Injectable({ @Injectable({
providedIn: "root" providedIn: "root"
}) })
export class TabService { export class TabService {
private tabIdCounter = 0;
public selectedTab = new BehaviorSubject<number>(0); public selectedTab = new BehaviorSubject<number>(0);
public tabs = new BehaviorSubject<TabState[]>([]);
constructor() { constructor(private fileService: FileService) {
} }
public setSelectedTab(index: number) { public setSelectedTab(index: number) {
this.selectedTab.next(index); this.selectedTab.next(index);
} }
public addTab(category: TabCategory): TabState {
const state = new TabState(this.tabIdCounter++, category, this.fileService);
this.tabs.next([...this.tabs.value, state]);
return state;
}
public closeTab(uuid: number) {
const index = this.tabs.value.findIndex(t => t.uuid === uuid);
const tabs = this.tabs.value;
tabs.splice(index, 1)
this.tabs.next(tabs);
}
} }

Loading…
Cancel
Save