mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08: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 {Config} from "../../config/Config";
|
||||||
import {Utils} from "../../../../common/Utils";
|
import {Utils} from "../../../../common/Utils";
|
||||||
export class GridPhoto {
|
export class GridPhoto {
|
||||||
|
|
||||||
|
private replacementSizeCache:boolean|number = false;
|
||||||
|
|
||||||
constructor(public photo:Photo, public renderWidth:number, public renderHeight:number) {
|
constructor(public photo:Photo, public renderWidth:number, public renderHeight:number) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -19,19 +22,25 @@ export class GridPhoto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getReplacementThumbnailSize() {
|
getReplacementThumbnailSize() {
|
||||||
let size = this.getThumbnailSize();
|
|
||||||
for (let i = 0; i < this.photo.readyThumbnails.length; i++) {
|
if (this.replacementSizeCache === false) {
|
||||||
if (this.photo.readyThumbnails[i] < size) {
|
this.replacementSizeCache = null;
|
||||||
return this.photo.readyThumbnails[i];
|
|
||||||
|
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() {
|
isReplacementThumbnailAvailable() {
|
||||||
return this.getReplacementThumbnailSize() !== null;
|
return this.getReplacementThumbnailSize() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isThumbnailAvailable() {
|
isThumbnailAvailable() {
|
||||||
return this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) != -1;
|
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());
|
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailPath() {
|
getThumbnailPath() {
|
||||||
let size = this.getThumbnailSize();
|
let size = this.getThumbnailSize();
|
||||||
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
|
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">
|
<img #img [src]="image.src" [hidden]="!image.show">
|
||||||
|
|
||||||
<gallery-grid-photo-loading [animate]="loading.animate" *ngIf="loading.show">
|
<gallery-grid-photo-loading [animate]="loading.animate" *ngIf="loading.show">
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
///<reference path="../../../../browser.d.ts"/>
|
///<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 {IRenderable, Dimension} from "../../../model/IRenderable";
|
||||||
import {GridPhoto} from "../GridPhoto";
|
import {GridPhoto} from "../GridPhoto";
|
||||||
import {SearchTypes} from "../../../../../common/entities/AutoCompleteItem";
|
import {SearchTypes} from "../../../../../common/entities/AutoCompleteItem";
|
||||||
import {RouterLink} from "@angular/router-deprecated";
|
import {RouterLink} from "@angular/router-deprecated";
|
||||||
import {Config} from "../../../config/Config";
|
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";
|
import {GalleryPhotoLoadingComponent} from "./loading/loading.photo.grid.gallery.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -15,10 +20,11 @@ import {GalleryPhotoLoadingComponent} from "./loading/loading.photo.grid.gallery
|
|||||||
styleUrls: ['app/gallery/grid/photo/photo.grid.gallery.component.css'],
|
styleUrls: ['app/gallery/grid/photo/photo.grid.gallery.component.css'],
|
||||||
directives: [RouterLink, GalleryPhotoLoadingComponent],
|
directives: [RouterLink, GalleryPhotoLoadingComponent],
|
||||||
})
|
})
|
||||||
export class GalleryPhotoComponent implements IRenderable, AfterViewInit {
|
export class GalleryPhotoComponent implements IRenderable, OnInit, AfterViewInit, OnDestroy {
|
||||||
@Input() gridPhoto:GridPhoto;
|
@Input() gridPhoto:GridPhoto;
|
||||||
@ViewChild("img") imageRef:ElementRef;
|
@ViewChild("img") imageRef:ElementRef;
|
||||||
@ViewChild("info") infoDiv:ElementRef;
|
@ViewChild("info") infoDiv:ElementRef;
|
||||||
|
@ViewChild("photoContainer") container:ElementRef;
|
||||||
|
|
||||||
|
|
||||||
image = {
|
image = {
|
||||||
@ -30,7 +36,9 @@ export class GalleryPhotoComponent implements IRenderable, AfterViewInit {
|
|||||||
animate: false,
|
animate: false,
|
||||||
show: false
|
show: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
thumbnailTask:ThumbnailTaskEntity = null;
|
||||||
|
|
||||||
infoStyle = {
|
infoStyle = {
|
||||||
height: 0,
|
height: 0,
|
||||||
background: "rgba(0,0,0,0.0)"
|
background: "rgba(0,0,0,0.0)"
|
||||||
@ -44,49 +52,89 @@ export class GalleryPhotoComponent implements IRenderable, AfterViewInit {
|
|||||||
this.searchEnabled = Config.Client.Search.searchEnabled;
|
this.searchEnabled = Config.Client.Search.searchEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngOnInit() {
|
||||||
//schedule change after Angular checks the model
|
//set up befoar adding task to thumbnail generator
|
||||||
setImmediate(() => {
|
if (this.gridPhoto.isThumbnailAvailable()) {
|
||||||
if (this.gridPhoto.isThumbnailAvailable()) {
|
this.image.src = this.gridPhoto.getThumbnailPath();
|
||||||
this.image.src = this.gridPhoto.getThumbnailPath();
|
this.image.show = true;
|
||||||
this.image.show = true;
|
this.loading.show = false;
|
||||||
this.loading.show = false;
|
|
||||||
} else if (this.gridPhoto.isReplacementThumbnailAvailable()) {
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (this.gridPhoto.isReplacementThumbnailAvailable()) {
|
||||||
this.image.src = this.gridPhoto.getReplacementThumbnailPath();
|
this.image.src = this.gridPhoto.getReplacementThumbnailPath();
|
||||||
this.image.show = true;
|
this.image.show = true;
|
||||||
this.loading.show = false;
|
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 {
|
} else {
|
||||||
this.loading.show = true;
|
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
getPositionText():string {
|
||||||
if (!this.gridPhoto) {
|
if (!this.gridPhoto) {
|
||||||
|
@ -4,6 +4,10 @@ import {Injectable} from "@angular/core";
|
|||||||
import {GridPhoto} from "./GridPhoto";
|
import {GridPhoto} from "./GridPhoto";
|
||||||
import {Config} from "../../config/Config";
|
import {Config} from "../../config/Config";
|
||||||
|
|
||||||
|
export enum ThumbnailLoadingPriority{
|
||||||
|
high, medium, low
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ThumbnailLoaderService {
|
export class ThumbnailLoaderService {
|
||||||
|
|
||||||
@ -17,7 +21,24 @@ export class ThumbnailLoaderService {
|
|||||||
this.que = [];
|
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;
|
let tmp:ThumbnailTask = null;
|
||||||
//is image already qued?
|
//is image already qued?
|
||||||
for (let i = 0; i < this.que.length; i++) {
|
for (let i = 0; i < this.que.length; i++) {
|
||||||
@ -26,13 +47,13 @@ export class ThumbnailLoaderService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let thumbnailTaskEntity = {priority: priority, listener: listener};
|
||||||
//add to previous
|
//add to previous
|
||||||
if (tmp != null) {
|
if (tmp != null) {
|
||||||
tmp.onStartedLoading.push(onStartedLoading);
|
tmp.taskEntities.push(thumbnailTaskEntity);
|
||||||
tmp.onLoad.push(onLoad);
|
|
||||||
tmp.onError.push(onError);
|
|
||||||
if (tmp.inProgress == true) {
|
if (tmp.inProgress == true) {
|
||||||
onStartedLoading();
|
listener.onStartedLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -40,54 +61,105 @@ export class ThumbnailLoaderService {
|
|||||||
this.que.push({
|
this.que.push({
|
||||||
gridPhoto: gridPhoto,
|
gridPhoto: gridPhoto,
|
||||||
inProgress: false,
|
inProgress: false,
|
||||||
onStartedLoading: [onStartedLoading],
|
taskEntities: [thumbnailTaskEntity]
|
||||||
onLoad: [onLoad],
|
|
||||||
onError: [onError]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
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) {
|
if (this.que.length === 0 || this.runningRequests >= Config.Client.concurrentThumbnailGenerations) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let task = this.getNextTask();
|
||||||
|
|
||||||
|
if (task === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.runningRequests++;
|
this.runningRequests++;
|
||||||
let task = this.que[0];
|
task.taskEntities.forEach(te=>te.listener.onStartedLoading());
|
||||||
task.onStartedLoading.forEach(cb=>cb());
|
|
||||||
task.inProgress = true;
|
task.inProgress = true;
|
||||||
|
|
||||||
let curImg = new Image();
|
let curImg = new Image();
|
||||||
curImg.src = task.gridPhoto.getThumbnailPath();
|
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.runningRequests--;
|
||||||
this.run();
|
this.run();
|
||||||
};
|
};
|
||||||
|
|
||||||
curImg.onerror = (error) => {
|
curImg.onerror = (error) => {
|
||||||
|
task.taskEntities.forEach(te=>te.listener.onError(error));
|
||||||
task.onLoad.forEach(cb=>cb(error));
|
|
||||||
|
|
||||||
this.que.shift();
|
this.taskReady(task);
|
||||||
this.runningRequests--;
|
this.runningRequests--;
|
||||||
this.run();
|
this.run();
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ThumbnailLoadingListener {
|
||||||
|
onStartedLoading:()=>void;
|
||||||
|
onLoad:()=>void;
|
||||||
|
onError:(error)=>void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ThumbnailTaskEntity {
|
||||||
|
|
||||||
|
priority:ThumbnailLoadingPriority;
|
||||||
|
listener:ThumbnailLoadingListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThumbnailTask {
|
interface ThumbnailTask {
|
||||||
gridPhoto:GridPhoto;
|
gridPhoto:GridPhoto;
|
||||||
inProgress:boolean;
|
inProgress:boolean;
|
||||||
onStartedLoading:Array<Function>;
|
taskEntities:Array<ThumbnailTaskEntity>;
|
||||||
onLoad:Array<Function>;
|
|
||||||
onError:Array<Function>;
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user