mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-12-31 02:29:51 +02:00
implementing advanced thumbnail loading scheduler
This commit is contained in:
parent
afd8ee760e
commit
54636ac290
@ -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());
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="photo-container" (mouseover)="hover()" (mouseout)="mouseOut()">
|
||||
<div #photoContainer class="photo-container" (mouseover)="hover()" (mouseout)="mouseOut()">
|
||||
<img #img [src]="image.src" [hidden]="!image.show">
|
||||
|
||||
<gallery-grid-photo-loading [animate]="loading.animate" *ngIf="loading.show">
|
||||
|
@ -1,12 +1,17 @@
|
||||
///<reference path="../../../../browser.d.ts"/>
|
||||
|
||||
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) {
|
||||
|
@ -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<Function>;
|
||||
onLoad:Array<Function>;
|
||||
onError:Array<Function>;
|
||||
taskEntities:Array<ThumbnailTaskEntity>;
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user