diff --git a/mediarepo-ui/src/app/components/core/core.component.ts b/mediarepo-ui/src/app/components/core/core.component.ts index 4379138..01c6f89 100644 --- a/mediarepo-ui/src/app/components/core/core.component.ts +++ b/mediarepo-ui/src/app/components/core/core.component.ts @@ -50,6 +50,11 @@ export class CoreComponent { } state.tabs.subscribe(tabs => { this.tabs = tabs; + const selectedIndex = state.selectedTab.value; + + if (selectedIndex) { + this.tabGroup.selectedIndex = selectedIndex; + } if (this.tabs.length === 0) { this.addTab(); @@ -64,6 +69,9 @@ export class CoreComponent { public onTabSelectionChange(event: MatTabChangeEvent): void { this.tabService.setSelectedTab(event.index); + if (event.index > 0 && event.index <= this.tabs.length) { + this.appState.selectedTab.next(event.index); + } } public addFilesTab(): void { diff --git a/mediarepo-ui/src/app/components/core/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts b/mediarepo-ui/src/app/components/core/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts index c9577ec..10ca564 100644 --- a/mediarepo-ui/src/app/components/core/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts +++ b/mediarepo-ui/src/app/components/core/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts @@ -57,7 +57,7 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges { if (this.fileSearch) { await this.fileSearch.searchForFiles(); } - if (this.tags.length === 0) { + if (this.tags.length === 0 && this.selectedFiles.length === 0) { this.tags = this.tagsOfFiles; } } @@ -102,7 +102,7 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges { } private showAllTagsFallback() { - if (this.tags.length === 0) { + if (this.tags.length === 0 && this.selectedFiles.length === 0) { this.tags = this.tagsOfFiles.sort( (a, b) => a.getNormalizedOutput() .localeCompare(b.getNormalizedOutput())); diff --git a/mediarepo-ui/src/app/components/shared/file/file-card/file-card.component.ts b/mediarepo-ui/src/app/components/shared/file/file-card/file-card.component.ts index 0c5c289..f48a30e 100644 --- a/mediarepo-ui/src/app/components/shared/file/file-card/file-card.component.ts +++ b/mediarepo-ui/src/app/components/shared/file/file-card/file-card.component.ts @@ -4,6 +4,7 @@ import { EventEmitter, Input, OnChanges, + OnDestroy, OnInit, Output, SimpleChanges, @@ -11,24 +12,28 @@ import { } from "@angular/core"; import {File} from "../../../../models/File"; import {Selectable} from "../../../../models/Selectable"; +import { + SchedulingService +} from "../../../../services/scheduling/scheduling.service"; + +const LOADING_WORK_KEY = "FILE_THUMBNAIL_LOADING"; @Component({ selector: "app-file-card", templateUrl: "./file-card.component.html", styleUrls: ["./file-card.component.scss"] }) -export class FileCardComponent implements OnInit, OnChanges { - +export class FileCardComponent implements OnInit, OnChanges, OnDestroy { @ViewChild("card") card!: ElementRef; @Input() public entry!: Selectable; @Output() clickEvent = new EventEmitter(); @Output() dblClickEvent = new EventEmitter(); private cachedId: number | undefined; - private urlSetTimeout: number | undefined; + private workId: number | undefined; public loading = false; - constructor() { + constructor(private schedulingService: SchedulingService) { } async ngOnInit() { @@ -43,10 +48,21 @@ export class FileCardComponent implements OnInit, OnChanges { } } + public ngOnDestroy(): void { + if (this.workId) { + this.schedulingService.cancelWork(LOADING_WORK_KEY, this.workId); + } + } + private setImageDelayed() { + if (this.workId) { + this.schedulingService.cancelWork(LOADING_WORK_KEY, this.workId); + } this.loading = true; - clearTimeout(this.urlSetTimeout); - this.urlSetTimeout = setTimeout( - () => this.loading = false, 200); + this.workId = this.schedulingService.addWork(LOADING_WORK_KEY, + async () => { + await this.schedulingService.delay(1); + this.loading = false + }); } } diff --git a/mediarepo-ui/src/app/components/shared/file/file-multiview/file-multiview.component.ts b/mediarepo-ui/src/app/components/shared/file/file-multiview/file-multiview.component.ts index c5f65e2..908391b 100644 --- a/mediarepo-ui/src/app/components/shared/file/file-multiview/file-multiview.component.ts +++ b/mediarepo-ui/src/app/components/shared/file/file-multiview/file-multiview.component.ts @@ -1,4 +1,5 @@ import { + AfterViewChecked, AfterViewInit, Component, ElementRef, EventEmitter, @@ -15,7 +16,7 @@ import {FileGridComponent} from "./file-grid/file-grid.component"; templateUrl: "./file-multiview.component.html", styleUrls: ["./file-multiview.component.scss"] }) -export class FileMultiviewComponent { +export class FileMultiviewComponent implements AfterViewInit { @Input() files!: File[]; @Input() mode: "grid" | "gallery" = "grid"; @@ -33,6 +34,13 @@ export class FileMultiviewComponent { constructor() { } + public ngAfterViewInit(): void { + if (this.preselectedFile) { + this.fileSelectEvent.emit([this.preselectedFile]) + this.selectedFiles = [this.preselectedFile]; + } + } + public onFileSelect(files: File[]): void { this.selectedFiles = files; this.preselectedFile = files[0]; diff --git a/mediarepo-ui/src/app/components/shared/file/file-thumbnail/file-thumbnail.component.ts b/mediarepo-ui/src/app/components/shared/file/file-thumbnail/file-thumbnail.component.ts index 9eb62bb..e17da1e 100644 --- a/mediarepo-ui/src/app/components/shared/file/file-thumbnail/file-thumbnail.component.ts +++ b/mediarepo-ui/src/app/components/shared/file/file-thumbnail/file-thumbnail.component.ts @@ -1,21 +1,24 @@ import { + AfterViewInit, Component, Input, OnChanges, - OnInit, SimpleChanges } from "@angular/core"; import {File} from "../../../../models/File"; import {FileService} from "../../../../services/file/file.service"; import {FileHelper} from "../../../../services/file/file.helper"; import {SafeResourceUrl} from "@angular/platform-browser"; +import { + SchedulingService +} from "../../../../services/scheduling/scheduling.service"; @Component({ selector: "app-file-thumbnail", templateUrl: "./file-thumbnail.component.html", styleUrls: ["./file-thumbnail.component.scss"] }) -export class FileThumbnailComponent implements OnInit, OnChanges { +export class FileThumbnailComponent implements OnChanges, AfterViewInit { @Input() file!: File; @@ -23,17 +26,17 @@ export class FileThumbnailComponent implements OnInit, OnChanges { private supportedThumbnailTypes = ["image", "video"] - constructor(private fileService: FileService) { + constructor( private fileService: FileService) { } - public ngOnInit(): void { + public async ngAfterViewInit() { this.thumbUrl = this.fileService.buildThumbnailUrl(this.file, 250, 250); } - public ngOnChanges(changes: SimpleChanges): void { + public async ngOnChanges(changes: SimpleChanges) { if (changes["file"]) { - this.thumbUrl = this.fileService.buildThumbnailUrl(this.file, 250, - 250) + this.thumbUrl = this.fileService.buildThumbnailUrl(this.file, + 250, 250); } } diff --git a/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.html b/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.html index b14f90d..5c77125 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.html +++ b/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.html @@ -33,4 +33,5 @@ + diff --git a/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.scss b/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.scss index 76a915c..b3454e0 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.scss +++ b/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.scss @@ -74,3 +74,12 @@ cdk-virtual-scroll-viewport { mat-divider { width: 100%; } + +app-busy-indicator { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 99; +} diff --git a/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.ts b/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.ts index d40058e..9b606a7 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.ts +++ b/mediarepo-ui/src/app/components/shared/sidebar/tag-edit/tag-edit.component.ts @@ -10,6 +10,7 @@ import {File} from "../../../../models/File"; import {Tag} from "../../../../models/Tag"; import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling"; import {TagService} from "../../../../services/tag/tag.service"; +import {delay} from "rxjs/operators"; @Component({ selector: "app-tag-edit", @@ -26,6 +27,8 @@ export class TagEditComponent implements OnInit, OnChanges { @ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport; private fileTags: { [key: number]: Tag[] } = {}; + public loading = false; + constructor( private tagService: TagService, ) { @@ -44,6 +47,7 @@ export class TagEditComponent implements OnInit, OnChanges { } public async editTag(tag: string): Promise { + this.loading = true; if (tag.length > 0) { let tagInstance = this.allTags.find( t => t.getNormalizedOutput() === tag); @@ -64,6 +68,7 @@ export class TagEditComponent implements OnInit, OnChanges { break; } } + this.loading = false; } async toggleTag(tag: Tag) { @@ -87,7 +92,7 @@ export class TagEditComponent implements OnInit, OnChanges { async addTag(tag: Tag) { for (const file of this.files) { - if (this.fileTags[file.id].findIndex(t => t.id === tag.id) < 0) { + if ((this.fileTags[file.id] ?? []).findIndex(t => t.id === tag.id) < 0) { this.fileTags[file.id] = await this.tagService.changeFileTags( file.id, [tag.id], []); @@ -99,6 +104,7 @@ export class TagEditComponent implements OnInit, OnChanges { } public async removeTag(tag: Tag) { + this.loading = true; for (const file of this.files) { if (this.fileTags[file.id].findIndex(t => t.id === tag.id) >= 0) { this.fileTags[file.id] = await this.tagService.changeFileTags( @@ -107,14 +113,23 @@ export class TagEditComponent implements OnInit, OnChanges { } } this.mapFileTagsToTagList(); + this.loading = false; } private async loadFileTags() { - for (const file of this.files) { + this.loading = true; + const promises = []; + const loadFn = async (file: File) => { this.fileTags[file.id] = await this.tagService.getTagsForFiles( [file.hash]); } + for (const file of this.files) { + promises.push(loadFn(file)); + } + + await Promise.all(promises); this.mapFileTagsToTagList(); + this.loading = false; } private mapFileTagsToTagList() { diff --git a/mediarepo-ui/src/app/models/AppState.ts b/mediarepo-ui/src/app/models/AppState.ts index a9d3fd7..8e594e9 100644 --- a/mediarepo-ui/src/app/models/AppState.ts +++ b/mediarepo-ui/src/app/models/AppState.ts @@ -7,6 +7,7 @@ export class AppState { private tabIdCounter = 0; public tabs = new BehaviorSubject([]); + public selectedTab = new BehaviorSubject(undefined); private readonly fileService: FileService @@ -34,6 +35,7 @@ export class AppState { appState.tabs.next(tabs); appState.tabIdCounter = state.tabIdCounter; + appState.selectedTab.next(state.selectedTab); return appState } @@ -43,6 +45,7 @@ export class AppState { return JSON.stringify({ tabs: tabDTOs, tabIdCounter: this.tabIdCounter, + selectedTab: this.selectedTab.value, }); } } diff --git a/mediarepo-ui/src/app/services/repository/repository.service.ts b/mediarepo-ui/src/app/services/repository/repository.service.ts index 8681d69..62ea3ea 100644 --- a/mediarepo-ui/src/app/services/repository/repository.service.ts +++ b/mediarepo-ui/src/app/services/repository/repository.service.ts @@ -59,6 +59,14 @@ export class RepositoryService { } else { await this.disconnectSelectedRepository(); } + } else { + try { + // just to make sure because sometimes there's some weird issues + await this.disconnectSelectedRepository(); + } catch (err) { + console.warn(err); + } + } await invoke("plugin:mediarepo|select_repository", {name: repo.name}); await this.loadRepositories(); diff --git a/mediarepo-ui/src/app/services/scheduling/scheduling.service.spec.ts b/mediarepo-ui/src/app/services/scheduling/scheduling.service.spec.ts new file mode 100644 index 0000000..81eb18e --- /dev/null +++ b/mediarepo-ui/src/app/services/scheduling/scheduling.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SchedulingService } from './scheduling.service'; + +describe('SchedulingService', () => { + let service: SchedulingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SchedulingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/services/scheduling/scheduling.service.ts b/mediarepo-ui/src/app/services/scheduling/scheduling.service.ts new file mode 100644 index 0000000..060ca39 --- /dev/null +++ b/mediarepo-ui/src/app/services/scheduling/scheduling.service.ts @@ -0,0 +1,58 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from "rxjs"; +import {filter} from "rxjs/operators"; + +@Injectable({ + providedIn: 'root' +}) +export class SchedulingService { + + private workQueue: { [key: string]: {id: number, cancelled: boolean, cb: Function}[] } = {} + private lastWorkId = 0; + + constructor() { + } + + public addWork(key: string, cb: Function): number { + if (!this.workQueue[key]) { + this.workQueue[key] = []; + setTimeout(() => this.startWork(key), 0); // start in the next tick + } + const id = this.lastWorkId++; + this.workQueue[key].push({id, cb, cancelled: false}); + return id; + } + + public cancelWork(key: string, id: number) { + const work = this.workQueue[key]?.find(w => w.id === id); + if (work) { + work.cancelled = true; + } + } + + private async startWork(key: string) { + while (true) { + if (this.workQueue[key]?.length > 0) { + let work = this.workQueue[key].shift(); + let count = 0; + while (work?.cancelled && count++ < 100) { + work = this.workQueue[key].shift(); + } + if (work) { + try { + await work.cb(); + } catch (err) { + console.error(err); + } + } + } + await this.delay(1); + } + } + + public async delay(time: number) { + return new Promise((res, rej) => { + setTimeout(res, time); + }) + } +}