diff --git a/frontend/app/gallery/grid/GridPhoto.ts b/frontend/app/gallery/grid/GridPhoto.ts index d4c56b22..5dd25e30 100644 --- a/frontend/app/gallery/grid/GridPhoto.ts +++ b/frontend/app/gallery/grid/GridPhoto.ts @@ -2,6 +2,9 @@ import {Photo} from "../../../../common/entities/Photo"; import {Config} from "../../config/Config"; import {Utils} from "../../../../common/Utils"; export class GridPhoto { + + private replacementSizeCache:boolean|number = false; + constructor(public photo:Photo, public renderWidth:number, public renderHeight:number) { } @@ -19,19 +22,25 @@ export class GridPhoto { } getReplacementThumbnailSize() { - let size = this.getThumbnailSize(); - for (let i = 0; i < this.photo.readyThumbnails.length; i++) { - if (this.photo.readyThumbnails[i] < size) { - return this.photo.readyThumbnails[i]; + + if (this.replacementSizeCache === false) { + this.replacementSizeCache = null; + + let size = this.getThumbnailSize(); + for (let i = 0; i < this.photo.readyThumbnails.length; i++) { + if (this.photo.readyThumbnails[i] < size) { + this.replacementSizeCache = this.photo.readyThumbnails[i]; + break; + } } } - return null; + return this.replacementSizeCache; } isReplacementThumbnailAvailable() { return this.getReplacementThumbnailSize() !== null; } - + isThumbnailAvailable() { return this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) != -1; } @@ -41,7 +50,7 @@ export class GridPhoto { return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString()); } - + getThumbnailPath() { let size = this.getThumbnailSize(); return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString()); diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html index cdfc2a41..58c88ad3 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts index 418875ec..859505e7 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts @@ -1,12 +1,17 @@ /// -import {Component, Input, ElementRef, ViewChild, AfterViewInit} from "@angular/core"; +import {Component, Input, ElementRef, ViewChild, OnInit, AfterViewInit, OnDestroy, HostListener} from "@angular/core"; import {IRenderable, Dimension} from "../../../model/IRenderable"; import {GridPhoto} from "../GridPhoto"; import {SearchTypes} from "../../../../../common/entities/AutoCompleteItem"; import {RouterLink} from "@angular/router-deprecated"; import {Config} from "../../../config/Config"; -import {ThumbnailLoaderService} from "../thumnailLoader.service"; +import { + ThumbnailLoaderService, + ThumbnailTaskEntity, + ThumbnailLoadingListener, + ThumbnailLoadingPriority +} from "../thumnailLoader.service"; import {GalleryPhotoLoadingComponent} from "./loading/loading.photo.grid.gallery.component"; @Component({ @@ -15,10 +20,11 @@ import {GalleryPhotoLoadingComponent} from "./loading/loading.photo.grid.gallery styleUrls: ['app/gallery/grid/photo/photo.grid.gallery.component.css'], directives: [RouterLink, GalleryPhotoLoadingComponent], }) -export class GalleryPhotoComponent implements IRenderable, AfterViewInit { +export class GalleryPhotoComponent implements IRenderable, OnInit, AfterViewInit, OnDestroy { @Input() gridPhoto:GridPhoto; @ViewChild("img") imageRef:ElementRef; @ViewChild("info") infoDiv:ElementRef; + @ViewChild("photoContainer") container:ElementRef; image = { @@ -30,7 +36,9 @@ export class GalleryPhotoComponent implements IRenderable, AfterViewInit { animate: false, show: false }; - + + thumbnailTask:ThumbnailTaskEntity = null; + infoStyle = { height: 0, background: "rgba(0,0,0,0.0)" @@ -44,49 +52,89 @@ export class GalleryPhotoComponent implements IRenderable, AfterViewInit { this.searchEnabled = Config.Client.Search.searchEnabled; } - ngAfterViewInit() { - //schedule change after Angular checks the model - setImmediate(() => { - if (this.gridPhoto.isThumbnailAvailable()) { - this.image.src = this.gridPhoto.getThumbnailPath(); - this.image.show = true; - this.loading.show = false; - } else if (this.gridPhoto.isReplacementThumbnailAvailable()) { + ngOnInit() { + //set up befoar adding task to thumbnail generator + if (this.gridPhoto.isThumbnailAvailable()) { + this.image.src = this.gridPhoto.getThumbnailPath(); + this.image.show = true; + this.loading.show = false; + } else { + if (this.gridPhoto.isReplacementThumbnailAvailable()) { this.image.src = this.gridPhoto.getReplacementThumbnailPath(); this.image.show = true; this.loading.show = false; - this.thumbnailService.loadImage(this.gridPhoto, - ()=> { //onLoadStarted - }, - ()=> {//onLoaded - this.image.src = this.gridPhoto.getThumbnailPath(); - }, - (error)=> {//onError - //TODO: handle error - console.error("something bad happened"); - console.error(error); - }); } else { - this.loading.show = true; - this.thumbnailService.loadImage(this.gridPhoto, - ()=> { //onLoadStarted - this.loading.animate = true; - }, - ()=> {//onLoaded - this.image.src = this.gridPhoto.getThumbnailPath(); - this.image.show = true; - this.loading.show = false; - }, - (error)=> {//onError - //TODO: handle error - console.error("something bad happened"); - console.error(error); - }); + this.loading.show = true; } - }); + } } + ngAfterViewInit() { + //schedule change after Angular checks the model + if (!this.gridPhoto.isThumbnailAvailable()) { + setImmediate(() => { + + let listener:ThumbnailLoadingListener = { + onStartedLoading: ()=> { //onLoadStarted + this.loading.animate = true; + }, + onLoad: ()=> {//onLoaded + this.image.src = this.gridPhoto.getThumbnailPath(); + this.image.show = true; + this.loading.show = false; + this.thumbnailTask = null; + }, + onError: (error)=> {//onError + this.thumbnailTask = null; + //TODO: handle error + console.error("something bad happened"); + console.error(error); + } + }; + + if (this.gridPhoto.isReplacementThumbnailAvailable()) { + this.thumbnailTask = this.thumbnailService.loadImage(this.gridPhoto, ThumbnailLoadingPriority.medium, listener); + } else { + this.thumbnailTask = this.thumbnailService.loadImage(this.gridPhoto, ThumbnailLoadingPriority.high, listener); + } + + + }); + } + } + + ngOnDestroy() { + if (this.thumbnailTask != null) { + this.thumbnailService.removeTask(this.thumbnailTask); + this.thumbnailTask = null; + } + } + + + isInView():boolean { + return document.body.scrollTop < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight + && document.body.scrollTop + window.innerHeight > this.container.nativeElement.offsetTop; + } + + @HostListener('window:scroll') + onScroll() { + if (this.thumbnailTask != null) { + if (this.isInView() == true) { + if (this.gridPhoto.isReplacementThumbnailAvailable()) { + this.thumbnailTask.priority = ThumbnailLoadingPriority.medium; + } else { + this.thumbnailTask.priority = ThumbnailLoadingPriority.high; + } + } else { + if (this.gridPhoto.isReplacementThumbnailAvailable()) { + this.thumbnailTask.priority = ThumbnailLoadingPriority.low; + } else { + this.thumbnailTask.priority = ThumbnailLoadingPriority.medium; + } + } + } + } getPositionText():string { if (!this.gridPhoto) { diff --git a/frontend/app/gallery/grid/thumnailLoader.service.ts b/frontend/app/gallery/grid/thumnailLoader.service.ts index b6b5d3ce..343af54f 100644 --- a/frontend/app/gallery/grid/thumnailLoader.service.ts +++ b/frontend/app/gallery/grid/thumnailLoader.service.ts @@ -4,6 +4,10 @@ import {Injectable} from "@angular/core"; import {GridPhoto} from "./GridPhoto"; import {Config} from "../../config/Config"; +export enum ThumbnailLoadingPriority{ + high, medium, low +} + @Injectable() export class ThumbnailLoaderService { @@ -17,7 +21,24 @@ export class ThumbnailLoaderService { this.que = []; } - loadImage(gridPhoto:GridPhoto, onStartedLoading:()=>void, onLoad:()=>void, onError:(error)=>void):void { + removeTask(taskEntry:ThumbnailTaskEntity) { + + for (let i = 0; i < this.que.length; i++) { + let index = this.que[i].taskEntities.indexOf(taskEntry); + if (index == -1) { + this.que[i].taskEntities.splice(index, 1); + if (this.que[i].taskEntities.length == 0) { + this.que.splice(i, 1); + + } + return; + } + } + + } + + loadImage(gridPhoto:GridPhoto, priority:ThumbnailLoadingPriority, listener:ThumbnailLoadingListener):ThumbnailTaskEntity { + let tmp:ThumbnailTask = null; //is image already qued? for (let i = 0; i < this.que.length; i++) { @@ -26,13 +47,13 @@ export class ThumbnailLoaderService { break; } } + + let thumbnailTaskEntity = {priority: priority, listener: listener}; //add to previous if (tmp != null) { - tmp.onStartedLoading.push(onStartedLoading); - tmp.onLoad.push(onLoad); - tmp.onError.push(onError); + tmp.taskEntities.push(thumbnailTaskEntity); if (tmp.inProgress == true) { - onStartedLoading(); + listener.onStartedLoading(); } @@ -40,54 +61,105 @@ export class ThumbnailLoaderService { this.que.push({ gridPhoto: gridPhoto, inProgress: false, - onStartedLoading: [onStartedLoading], - onLoad: [onLoad], - onError: [onError] + taskEntities: [thumbnailTaskEntity] }); } - this.run(); + setImmediate(this.run); + return thumbnailTaskEntity; } - run() { + private getNextTask():ThumbnailTask { + if (this.que.length === 0) { + return null; + } + + for (let i = 0; i < this.que.length; i++) { + for (let j = 0; j < this.que[i].taskEntities.length; j++) { + if (this.que[i].taskEntities[j].priority === ThumbnailLoadingPriority.high) { + return this.que[i]; + } + } + } + + for (let i = 0; i < this.que.length; i++) { + for (let j = 0; j < this.que[i].taskEntities.length; j++) { + if (this.que[i].taskEntities[j].priority === ThumbnailLoadingPriority.medium) { + return this.que[i]; + } + } + } + + return this.que[0]; + } + + private taskReady(task:ThumbnailTask) { + let i = this.que.indexOf(task); + if (i == -1) { + if (task.taskEntities.length !== 0) { + console.error("ThumbnailLoader: can't find task to remove"); + } + return; + } + this.que.splice(i, 1); + } + + + run = () => { if (this.que.length === 0 || this.runningRequests >= Config.Client.concurrentThumbnailGenerations) { return; } + let task = this.getNextTask(); + + if (task === null) { + return; + } + this.runningRequests++; - let task = this.que[0]; - task.onStartedLoading.forEach(cb=>cb()); + task.taskEntities.forEach(te=>te.listener.onStartedLoading()); task.inProgress = true; - + let curImg = new Image(); curImg.src = task.gridPhoto.getThumbnailPath(); - - curImg.onload = () => { - - task.gridPhoto.thumbnailLoaded(); - task.onLoad.forEach(cb=>cb()); - this.que.shift(); + curImg.onload = () => { + + task.gridPhoto.thumbnailLoaded(); + task.taskEntities.forEach(te=>te.listener.onLoad()); + + this.taskReady(task); this.runningRequests--; this.run(); }; curImg.onerror = (error) => { - - task.onLoad.forEach(cb=>cb(error)); + task.taskEntities.forEach(te=>te.listener.onError(error)); - this.que.shift(); + this.taskReady(task); this.runningRequests--; this.run(); }; - } + }; +} + +export interface ThumbnailLoadingListener { + onStartedLoading:()=>void; + onLoad:()=>void; + onError:(error)=>void; +} + + +export interface ThumbnailTaskEntity { + + priority:ThumbnailLoadingPriority; + listener:ThumbnailLoadingListener; } interface ThumbnailTask { gridPhoto:GridPhoto; inProgress:boolean; - onStartedLoading:Array; - onLoad:Array; - onError:Array; + taskEntities:Array; + }