feat: caching pages

This commit is contained in:
2024-07-07 00:48:54 +05:00
parent 6bad651312
commit 8f5f12ad30
3 changed files with 87 additions and 35 deletions

View File

@@ -1,11 +1,11 @@
<h1>It's reader page</h1> <h1>It's reader page</h1>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
@if(pages.length > 0){ @if(pages.length > 0){
<img [src]="cachedPages[currentPageIndex].url" [alt]="cachedPages[currentPageIndex].slug" /> <img #mangaImage [alt]="cachedPages[currentPageIndex]?.slug" />
<div class="flex items-center justify-center space-x-4 mb-10"> <div class="flex items-center justify-center space-x-4 mb-10">
<button (click)="prevPage()" [disabled]="currentPageIndex === 0" class="p-3 text-white bg-slate-600 w-[100px] mt-5 rounded-lg"></button> <button (click)="prevPage()" [disabled]="currentPageIndex === 0" class="p-3 text-white bg-slate-600 w-[100px] mt-5 rounded-lg"></button>
<p>{{pages[currentPageIndex].slug}} / {{pages.length}}</p> <p>{{pages[currentPageIndex]?.slug}} / {{pages.length}}</p>
<button (click)="nextPage()" [disabled]="currentPageIndex === pages.length - 1" class="p-3 text-white bg-slate-600 w-[100px] mt-5 rounded-lg"></button> <button (click)="nextPage()" [disabled]="currentPageIndex === pages.length - 1" class="p-3 text-white bg-slate-600 w-[100px] mt-5 rounded-lg"></button>
</div> </div>
} }
</div> </div>

View File

@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { AfterViewInit, Component } from "@angular/core"; import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Observable, map } from "rxjs";
import { Page } from "../../services/parsers/rulib/rulib.chapter.dto"; import { Page } from "../../services/parsers/rulib/rulib.chapter.dto";
import { SearchService } from "../../services/search.service"; import { SearchService } from "../../services/search.service";
@@ -14,7 +15,8 @@ import { SearchService } from "../../services/search.service";
export class ReaderComponent implements AfterViewInit { export class ReaderComponent implements AfterViewInit {
pages: Page[] = []; pages: Page[] = [];
currentPageIndex = 0; currentPageIndex = 0;
cachedPages: { [key: number]: Page } = {}; cachedPages: { [key: number]: Page & { imageData?: Uint8Array } } = {};
@ViewChild('mangaImage') mangaImage!: ElementRef<HTMLImageElement>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -38,44 +40,81 @@ export class ReaderComponent implements AfterViewInit {
loadChapter(url: string, chapter: string, volume: string) { loadChapter(url: string, chapter: string, volume: string) {
this.searchService.getChapter(url, chapter, volume).subscribe((data) => { this.searchService.getChapter(url, chapter, volume).subscribe((data) => {
this.pages = data.data.pages; this.pages = data.data.pages;
this.loadPage(0); this.loadPage(0); // Загрузить первую страницу при открытии главы
}); });
} }
loadPage(index: number) { loadPage(index: number) {
if (index >= 0 && index < this.pages.length) { if (index >= 0 && index < this.pages.length) {
this.currentPageIndex = index; this.currentPageIndex = index;
this.cachePages(index); this.cachePage(index); // Кэшируем текущую и соседние страницы
this.unloadCachedPages(index); // Сгружаем ненужные страницы из кэша
if (!this.cachedPages[index]) { if (!this.cachedPages[index]?.imageData) {
this.cachedPages[index] = { // Если страница не закэширована, загружаем её
...this.pages[index], this.fetchAndCachePage(index).subscribe(() => {
url: this.searchService.getImageServer() + this.pages[index].url, this.updateImage();
}; });
} else {
// Если страница уже в кэше, просто обновляем изображение
this.updateImage();
} }
} }
} }
cachePages(currentIndex: number) { // Кэширование текущей страницы и несколько соседних для ускорения процесса навигации по страницам
for (let i = 0; i <= currentIndex && i < this.pages.length; i++) { private cachePage(index: number) {
if (!this.cachedPages[i]) { const startIndex = Math.max(0, index - 2);
this.cachedPages[i] = { const endIndex = Math.min(this.pages.length - 1, index + 2);
...this.pages[i],
url: this.searchService.getImageServer() + this.pages[i].url, for (let i = startIndex; i <= endIndex; i++) {
}; if (!this.isPageCached(i)) {
this.fetchAndCachePage(i).subscribe(() => {
if (i === this.currentPageIndex) {
this.updateImage();
}
});
} }
} }
}
for (let i = currentIndex + 1; i <= currentIndex + 3 && i < this.pages.length; i++) { private isPageCached(index: number): boolean {
if (!this.cachedPages[i]) { return !!this.cachedPages[index]?.imageData;
this.cachedPages[i] = { }
...this.pages[i],
url: this.searchService.getImageServer() + this.pages[i].url, // Загрузка и сохранение изображения в кэш
}; private fetchAndCachePage(index: number): Observable<void> {
return this.searchService
.getImageData(this.searchService.getImageServer() + this.pages[index].url)
.pipe(
map((imageData) => {
this.cachedPages[index] = {
...this.pages[index],
imageData,
};
})
);
}
// Выгрузка из кэша старых страниц
private unloadCachedPages(index: number) {
for (const key in this.cachedPages) {
const pageIndex = parseInt(key, 10);
if (index - pageIndex > 2) {
delete this.cachedPages[pageIndex];
} }
} }
} }
// Обновляем изображение на странице
private updateImage() {
const currentPage = this.cachedPages[this.currentPageIndex];
if (this.mangaImage?.nativeElement && currentPage?.imageData) {
const blob = new Blob([currentPage.imageData], { type: 'image/jpeg' });
const urlCreator = window.URL || window.webkitURL;
this.mangaImage.nativeElement.src = urlCreator.createObjectURL(blob);
}
}
nextPage() { nextPage() {
this.loadPage(this.currentPageIndex + 1); this.loadPage(this.currentPageIndex + 1);
} }

View File

@@ -5,12 +5,16 @@ import { IRulibChapterResult } from "./parsers/rulib/rulib.chapter.dto";
import { IRulibChaptersResult } from "./parsers/rulib/rulib.chapters.dto"; import { IRulibChaptersResult } from "./parsers/rulib/rulib.chapters.dto";
import { IRulibDetailResult } from "./parsers/rulib/rulib.detail.dto"; import { IRulibDetailResult } from "./parsers/rulib/rulib.detail.dto";
import { Datum } from "./parsers/rulib/rulib.search.dto"; import { Datum } from "./parsers/rulib/rulib.search.dto";
import { HttpClient } from "@angular/common/http";
@Injectable({ providedIn: "root" }) @Injectable({ providedIn: "root" })
export class SearchService { export class SearchService {
private itemsTerm = new BehaviorSubject<Datum[]>([]); private itemsTerm = new BehaviorSubject<Datum[]>([]);
currentItemsTerm = this.itemsTerm.asObservable(); currentItemsTerm = this.itemsTerm.asObservable();
constructor(private parser: LibSocialParserService) {} constructor(
private parser: LibSocialParserService,
private http: HttpClient,
) {}
search(query: string) { search(query: string) {
this.parser.searchManga(query).subscribe((data) => { this.parser.searchManga(query).subscribe((data) => {
@@ -45,4 +49,13 @@ export class SearchService {
getImageServer() { getImageServer() {
return this.parser.imageServer; return this.parser.imageServer;
} }
getImageData(imageUrl: string): Observable<Uint8Array>{
return this.http.get(imageUrl, {responseType: 'arraybuffer'}).pipe(
map((arrayBuffer: ArrayBuffer) => {
return new Uint8Array(arrayBuffer);
})
);
}
} }