diff --git a/apps/NwaifuAnime/src/app/components/detail/detail.component.html b/apps/NwaifuAnime/src/app/components/detail/detail.component.html index 0c401c0..bc337dc 100644 --- a/apps/NwaifuAnime/src/app/components/detail/detail.component.html +++ b/apps/NwaifuAnime/src/app/components/detail/detail.component.html @@ -3,11 +3,25 @@

{{ detail_item.name }}

{{ detail_item.rus_name }}

- @for (chapter of chapters.data; track $index) { -

- {{ chapter.number }}. {{ chapter.name || "Нет названия" }} -

- } + +
+ +

Главы

+
+
+ @for (chapter of chapters.data; track $index) { + + } +
+
diff --git a/apps/NwaifuAnime/src/app/components/detail/detail.component.ts b/apps/NwaifuAnime/src/app/components/detail/detail.component.ts index a831ed3..f0df833 100644 --- a/apps/NwaifuAnime/src/app/components/detail/detail.component.ts +++ b/apps/NwaifuAnime/src/app/components/detail/detail.component.ts @@ -27,6 +27,7 @@ export class DetailComponent implements AfterViewInit { }); this.searchService.getChapters(url).subscribe((data) => { this.chapters = data; + console.log(data); }); } @@ -41,13 +42,13 @@ export class DetailComponent implements AfterViewInit { }); } - goToReader() { - //TODO: Not only first chapter + goToReader(chapter: string = "1", volume: string = "1") { + console.log(chapter, volume); this.router.navigate(["/", "reader"], { queryParams: { url: this.detail_item?.slug_url, - chapter: this.chapters.data[0].number, - volume: this.chapters.data[0].volume, + chapter: +chapter, + volume: +volume, }, }); } diff --git a/apps/NwaifuAnime/src/app/components/header/header.component.html b/apps/NwaifuAnime/src/app/components/header/header.component.html index 4740388..09c258e 100644 --- a/apps/NwaifuAnime/src/app/components/header/header.component.html +++ b/apps/NwaifuAnime/src/app/components/header/header.component.html @@ -1,5 +1,5 @@

NwaifuAnime

diff --git a/apps/NwaifuAnime/src/app/components/reader/reader.component.html b/apps/NwaifuAnime/src/app/components/reader/reader.component.html index d117c31..17b1b07 100644 --- a/apps/NwaifuAnime/src/app/components/reader/reader.component.html +++ b/apps/NwaifuAnime/src/app/components/reader/reader.component.html @@ -1,11 +1,22 @@ -

It's reader page

+
+ +

{{ currentChapterInfo?.number }}. {{ currentChapterInfo?.name }}

+
- @if(pages.length > 0){ - -
- -

{{pages[currentPageIndex]?.slug}} / {{pages.length}}

- -
-} -
\ No newline at end of file + @if (pages.length > 0 && cachedPages[currentPageIndex]) { +
+ +
+
+ +

{{ pages[currentPageIndex].slug }} / {{ pages.length }}

+ +
+ } +
diff --git a/apps/NwaifuAnime/src/app/components/reader/reader.component.ts b/apps/NwaifuAnime/src/app/components/reader/reader.component.ts index a5d69dd..8a43759 100644 --- a/apps/NwaifuAnime/src/app/components/reader/reader.component.ts +++ b/apps/NwaifuAnime/src/app/components/reader/reader.component.ts @@ -1,22 +1,31 @@ import { CommonModule } from "@angular/common"; -import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core"; +import { AfterViewInit, Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { Observable, map } from "rxjs"; -import { Page } from "../../services/parsers/rulib/rulib.chapter.dto"; +import { Chapter, Page } from "../../services/parsers/rulib/rulib.chapter.dto"; +import { IRulibChapter } from "../../services/parsers/rulib/rulib.chapters.dto"; import { SearchService } from "../../services/search.service"; +import { ScaleImageComponent } from "../scale-image/scale-image.component"; +import { CachedPages } from "./reader.dto"; @Component({ selector: "app-reader", templateUrl: "./reader.component.html", styleUrls: ["./reader.component.less"], standalone: true, - imports: [CommonModule], + imports: [CommonModule, ScaleImageComponent], }) export class ReaderComponent implements AfterViewInit { pages: Page[] = []; currentPageIndex = 0; - cachedPages: { [key: number]: Page & { imageData?: Uint8Array } } = {}; - @ViewChild('mangaImage') mangaImage!: ElementRef; + cachedPages: CachedPages = {}; + imageUrl: string = ""; + private chaptersInfo: IRulibChapter[] = []; + currentChapterInfo: Chapter | null = null; + private chapterNum: number = 0; + private chapterVol: number = 0; + private url = ""; + private fromTowards = false; constructor( private route: ActivatedRoute, @@ -29,18 +38,36 @@ export class ReaderComponent implements AfterViewInit { const url = params["url"]; const chapter = params["chapter"]; const volume = params["volume"]; + const fromTowards = params["from_towards"]; if (url && chapter && volume) { + if (fromTowards) this.fromTowards = Boolean(+fromTowards); + else this.fromTowards = false; + this.chapterNum = +chapter; + this.chapterVol = +volume; + this.url = url; this.loadChapter(url, chapter, volume); + this.searchService.getChapters(url).subscribe((data) => { + this.chaptersInfo = data.data; + }); } else { this.router.navigate(["/"]); } }); } + backToTitle() { + this.router.navigate(["/", "detail"], { queryParams: { url: this.url } }); + } + loadChapter(url: string, chapter: string, volume: string) { this.searchService.getChapter(url, chapter, volume).subscribe((data) => { + this.currentChapterInfo = data.data; + console.log(this.currentChapterInfo); this.pages = data.data.pages; - this.loadPage(0); // Загрузить первую страницу при открытии главы + if (this.fromTowards) { + this.currentPageIndex = this.pages.length - 1; + this.loadPage(this.pages.length - 1); // Загрузить последнюю страницу при открытии главы + } else this.loadPage(0); // Загрузить первую страницу при открытии главы }); } @@ -51,13 +78,46 @@ export class ReaderComponent implements AfterViewInit { this.unloadCachedPages(index); // Сгружаем ненужные страницы из кэша if (!this.cachedPages[index]?.imageData) { // Если страница не закэширована, загружаем её - this.fetchAndCachePage(index).subscribe(() => { + this.fetchAndCachePage(index).subscribe(() => { this.updateImage(); }); } else { // Если страница уже в кэше, просто обновляем изображение this.updateImage(); } + } else if (index == this.pages.length) { + const thisChapterIndex = this.chaptersInfo.findIndex((chapter) => { + return +chapter.number === this.chapterNum && +chapter.volume === this.chapterVol; + }); + const nextChapterIndex = Math.min(thisChapterIndex + 1, this.chaptersInfo.length - 1); + const nextChapter = this.chaptersInfo[nextChapterIndex]; + if (nextChapter !== this.chaptersInfo[thisChapterIndex]) { + this.cachedPages = []; + this.router.navigate(["/", "reader"], { + queryParams: { + url: this.url, + chapter: nextChapter.number, + volume: nextChapter.volume, + }, + }); + } + } else if (index == -1) { + const thisChapterIndex = this.chaptersInfo.findIndex((chapter) => { + return +chapter.number === this.chapterNum && +chapter.volume === this.chapterVol; + }); + const prevChapterIndex = Math.max(thisChapterIndex - 1, 0); + const prevChapter = this.chaptersInfo[prevChapterIndex]; + if (prevChapter !== this.chaptersInfo[thisChapterIndex]) { + this.cachedPages = []; + this.router.navigate(["/", "reader"], { + queryParams: { + url: this.url, + chapter: prevChapter.number, + volume: prevChapter.volume, + from_towards: 1, + }, + }); + } } } @@ -65,14 +125,14 @@ export class ReaderComponent implements AfterViewInit { private cachePage(index: number) { const startIndex = Math.max(0, index - 2); const endIndex = Math.min(this.pages.length - 1, index + 2); - + for (let i = startIndex; i <= endIndex; i++) { if (!this.isPageCached(i)) { this.fetchAndCachePage(i).subscribe(() => { - if (i === this.currentPageIndex) { - this.updateImage(); + if (i === this.currentPageIndex) { + this.updateImage(); } - }); + }); } } } @@ -86,19 +146,19 @@ export class ReaderComponent implements AfterViewInit { return this.searchService .getImageData(this.searchService.getImageServer() + this.pages[index].url) .pipe( - map((imageData) => { + map((imageData) => { this.cachedPages[index] = { ...this.pages[index], imageData, }; - }) + }), ); } // Выгрузка из кэша старых страниц private unloadCachedPages(index: number) { for (const key in this.cachedPages) { - const pageIndex = parseInt(key, 10); + const pageIndex = +key; if (index - pageIndex > 2) { delete this.cachedPages[pageIndex]; } @@ -108,13 +168,13 @@ export class ReaderComponent implements AfterViewInit { // Обновляем изображение на странице private updateImage() { const currentPage = this.cachedPages[this.currentPageIndex]; - if (this.mangaImage?.nativeElement && currentPage?.imageData) { - const blob = new Blob([currentPage.imageData], { type: 'image/jpeg' }); + if (currentPage && currentPage.imageData) { + const blob = new Blob([currentPage.imageData], { type: "image/jpeg" }); const urlCreator = window.URL || window.webkitURL; - this.mangaImage.nativeElement.src = urlCreator.createObjectURL(blob); + this.imageUrl = urlCreator.createObjectURL(blob); } } - + nextPage() { this.loadPage(this.currentPageIndex + 1); } @@ -122,4 +182,4 @@ export class ReaderComponent implements AfterViewInit { prevPage() { this.loadPage(this.currentPageIndex - 1); } -} \ No newline at end of file +} diff --git a/apps/NwaifuAnime/src/app/components/reader/reader.dto.ts b/apps/NwaifuAnime/src/app/components/reader/reader.dto.ts new file mode 100644 index 0000000..5463643 --- /dev/null +++ b/apps/NwaifuAnime/src/app/components/reader/reader.dto.ts @@ -0,0 +1,9 @@ +import { Page } from "../../services/parsers/rulib/rulib.chapter.dto"; + +interface CachedPage extends Page { + imageData?: Uint8Array; +} + +export interface CachedPages { + [key: number]: CachedPage; +} diff --git a/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.html b/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.html new file mode 100644 index 0000000..0384096 --- /dev/null +++ b/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.html @@ -0,0 +1,3 @@ +
+ Manga page +
diff --git a/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.less b/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.less new file mode 100644 index 0000000..5dab6e4 --- /dev/null +++ b/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.less @@ -0,0 +1,12 @@ +.image-container { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} +img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} diff --git a/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.ts b/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.ts new file mode 100644 index 0000000..2308069 --- /dev/null +++ b/apps/NwaifuAnime/src/app/components/scale-image/scale-image.component.ts @@ -0,0 +1,60 @@ +import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from "@angular/core"; +import { Subscription, debounceTime, fromEvent } from "rxjs"; + +@Component({ + selector: "app-scale-image", + templateUrl: "./scale-image.component.html", + styleUrls: ["./scale-image.component.less"], + standalone: true, +}) +export class ScaleImageComponent implements AfterViewInit, OnDestroy { + @Input({ required: true }) imageSrc: string = ""; + @ViewChild("container", { static: true }) containerRef: ElementRef | null = null; + @ViewChild("image", { static: true }) imageRef: ElementRef | null = null; + + private resizeSubscription: Subscription = new Subscription(); + + ngAfterViewInit(): void { + this.setupResizeListener(); + } + + ngOnDestroy(): void { + if (this.resizeSubscription) this.resizeSubscription.unsubscribe(); + } + + onImageLoad() {} + + private setupResizeListener() { + this.resizeSubscription = fromEvent(window, "resize") + .pipe(debounceTime(200)) + .subscribe(() => this.scaleImage()); + } + + private scaleImage() { + if (this.containerRef && this.imageRef) { + const container = this.containerRef.nativeElement; + const img = this.imageRef.nativeElement; + + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + const imgRatio = img.naturalWidth / img.naturalHeight; + const containerRatio = containerWidth / containerHeight; + + let newWidth, newHeight; + + if (imgRatio > containerRatio) { + newWidth = containerWidth; + newHeight = containerWidth / imgRatio; + } else { + newHeight = containerHeight; + newWidth = containerHeight * imgRatio; + } + + newWidth = Math.min(newWidth, img.naturalWidth); + newHeight = Math.min(newHeight, img.naturalHeight); + + img.style.width = `${newWidth}px`; + img.style.height = `${newHeight}px`; + } + } +}