From 683310560435d93e4d91f7ea686cc6302ab7c08f Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 8 Jul 2024 23:24:03 +0300 Subject: [PATCH] feat: abstract classes and destroy subscriptions --- .../components/detail/detail.component.html | 19 ++++-- .../app/components/detail/detail.component.ts | 44 ++++++------ .../app/components/header/header.component.ts | 15 +++-- .../app/components/home/home.component.html | 9 ++- .../src/app/components/home/home.component.ts | 34 +++++----- .../components/reader/reader.component.html | 14 ++-- .../app/components/reader/reader.component.ts | 67 ++++++++++++------- .../parsers/decorators/parser.decorator.ts | 19 ++++++ .../src/app/services/parsers/parser.ts | 18 +++++ .../rulib/lib.social.parser.service.ts | 16 +++-- .../parsers/rulib/mangalib.parser.service.ts | 10 +++ .../src/app/services/parsers/urls.ts | 1 + .../src/app/services/search.service.ts | 25 +++---- 13 files changed, 191 insertions(+), 100 deletions(-) create mode 100644 apps/NwaifuAnime/src/app/services/parsers/decorators/parser.decorator.ts create mode 100644 apps/NwaifuAnime/src/app/services/parsers/parser.ts create mode 100644 apps/NwaifuAnime/src/app/services/parsers/rulib/mangalib.parser.service.ts diff --git a/apps/NwaifuAnime/src/app/components/detail/detail.component.html b/apps/NwaifuAnime/src/app/components/detail/detail.component.html index 6516f74..53c86e3 100644 --- a/apps/NwaifuAnime/src/app/components/detail/detail.component.html +++ b/apps/NwaifuAnime/src/app/components/detail/detail.component.html @@ -10,20 +10,29 @@
@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 f0df833..62b5879 100644 --- a/apps/NwaifuAnime/src/app/components/detail/detail.component.ts +++ b/apps/NwaifuAnime/src/app/components/detail/detail.component.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; -import { AfterViewInit, Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { AfterViewInit, Component, OnDestroy } from "@angular/core"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; import { IRulibChaptersResult } from "../../services/parsers/rulib/rulib.chapters.dto"; import { Data } from "../../services/parsers/rulib/rulib.detail.dto"; import { SearchService } from "../../services/search.service"; @@ -10,11 +11,12 @@ import { SearchService } from "../../services/search.service"; templateUrl: "./detail.component.html", styleUrls: ["./detail.component.less"], standalone: true, - imports: [CommonModule], + imports: [CommonModule, RouterLink], }) -export class DetailComponent implements AfterViewInit { +export class DetailComponent implements AfterViewInit, OnDestroy { detail_item: Data | null = null; chapters: IRulibChaptersResult = { data: [] }; + private destroy$ = new Subject(); constructor( private route: ActivatedRoute, private searchService: SearchService, @@ -22,17 +24,23 @@ export class DetailComponent implements AfterViewInit { ) {} private getDetails(url: string) { - this.searchService.getDetails(url).subscribe((data) => { - this.detail_item = data.data; - }); - this.searchService.getChapters(url).subscribe((data) => { - this.chapters = data; - console.log(data); - }); + this.searchService + .getDetails(url) + .pipe(takeUntil(this.destroy$)) + .subscribe((data) => { + this.detail_item = data.data; + }); + this.searchService + .getChapters(url) + .pipe(takeUntil(this.destroy$)) + .subscribe((data) => { + this.chapters = data; + console.log(data); + }); } ngAfterViewInit(): void { - this.route.queryParams.subscribe((params) => { + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { const url = params["url"]; if (url) { this.getDetails(url); @@ -42,14 +50,8 @@ export class DetailComponent implements AfterViewInit { }); } - goToReader(chapter: string = "1", volume: string = "1") { - console.log(chapter, volume); - this.router.navigate(["/", "reader"], { - queryParams: { - url: this.detail_item?.slug_url, - chapter: +chapter, - volume: +volume, - }, - }); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } } diff --git a/apps/NwaifuAnime/src/app/components/header/header.component.ts b/apps/NwaifuAnime/src/app/components/header/header.component.ts index ffbd833..a04e6bf 100644 --- a/apps/NwaifuAnime/src/app/components/header/header.component.ts +++ b/apps/NwaifuAnime/src/app/components/header/header.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; -import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core"; +import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from "@angular/core"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; -import { filter } from "rxjs"; +import { Subject, filter, takeUntil } from "rxjs"; @Component({ selector: "app-header", @@ -10,15 +10,17 @@ import { filter } from "rxjs"; standalone: true, imports: [CommonModule], }) -export class HeaderComponent implements AfterViewInit { +export class HeaderComponent implements AfterViewInit, OnDestroy { @ViewChild("searchInput") searchInput: ElementRef | null = null; menuOpened = false; + private destroy$ = new Subject(); constructor( private router: Router, private route: ActivatedRoute, ) { this.router.events .pipe(filter((event) => event instanceof NavigationEnd)) + .pipe(takeUntil(this.destroy$)) // eslint-disable-next-line @typescript-eslint/no-explicit-any .subscribe((val: any) => { if (val.url.startsWith("/detail") || val.url.startsWith("/reader")) { @@ -46,11 +48,16 @@ export class HeaderComponent implements AfterViewInit { } ngAfterViewInit(): void { - this.route.queryParams.subscribe((params) => { + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { const search = params["search"]; if (search && this.searchInput) { this.searchInput.nativeElement.value = search; } }); } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/apps/NwaifuAnime/src/app/components/home/home.component.html b/apps/NwaifuAnime/src/app/components/home/home.component.html index 49628e3..4485005 100644 --- a/apps/NwaifuAnime/src/app/components/home/home.component.html +++ b/apps/NwaifuAnime/src/app/components/home/home.component.html @@ -1,11 +1,16 @@

It's home component

@for (item of items; track $index) { - + }
diff --git a/apps/NwaifuAnime/src/app/components/home/home.component.ts b/apps/NwaifuAnime/src/app/components/home/home.component.ts index 9250507..011815d 100644 --- a/apps/NwaifuAnime/src/app/components/home/home.component.ts +++ b/apps/NwaifuAnime/src/app/components/home/home.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; -import { AfterViewInit, Component, Input, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subscription } from "rxjs"; +import { AfterViewInit, Component, Input, OnDestroy } from "@angular/core"; +import { ActivatedRoute, RouterLink } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; import { Datum } from "../../services/parsers/rulib/rulib.search.dto"; import { SearchService } from "../../services/search.service"; @@ -10,37 +10,33 @@ import { SearchService } from "../../services/search.service"; selector: "app-home", templateUrl: "./home.component.html", styleUrls: ["./home.component.less"], - imports: [CommonModule], + imports: [CommonModule, RouterLink], }) -export class HomeComponent implements OnInit, OnDestroy, AfterViewInit { +export class HomeComponent implements OnDestroy, AfterViewInit { @Input() items: Datum[] = []; - private subscription: Subscription = new Subscription(); + + private destroy$ = new Subject(); constructor( private searchService: SearchService, private route: ActivatedRoute, - private router: Router, ) {} - ngOnInit(): void { - this.subscription = this.searchService.currentItemsTerm.subscribe((data) => { - this.items = data; - }); - } - - getDetails(slug_url: string) { - this.router.navigate(["/", "detail"], { queryParams: { url: slug_url } }); - } - ngOnDestroy(): void { - this.subscription.unsubscribe(); + this.destroy$.next(); + this.destroy$.complete(); } ngAfterViewInit(): void { this.route.queryParams.subscribe((params) => { const search = params["search"]; if (search) { - this.searchService.search(search); + this.searchService + .search(search) + .pipe(takeUntil(this.destroy$)) + .subscribe((data) => { + this.items = data.data; + }); } }); } diff --git a/apps/NwaifuAnime/src/app/components/reader/reader.component.html b/apps/NwaifuAnime/src/app/components/reader/reader.component.html index d44f003..5157610 100644 --- a/apps/NwaifuAnime/src/app/components/reader/reader.component.html +++ b/apps/NwaifuAnime/src/app/components/reader/reader.component.html @@ -1,8 +1,14 @@ -
- -

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

+
+

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

@if (pages.length > 0 && cachedPages[currentPageIndex]) { diff --git a/apps/NwaifuAnime/src/app/components/reader/reader.component.ts b/apps/NwaifuAnime/src/app/components/reader/reader.component.ts index f161a9e..73709f5 100644 --- a/apps/NwaifuAnime/src/app/components/reader/reader.component.ts +++ b/apps/NwaifuAnime/src/app/components/reader/reader.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; -import { AfterViewInit, Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Observable, map } from "rxjs"; +import { AfterViewInit, Component, OnDestroy } from "@angular/core"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { Observable, Subject, map, takeUntil } from "rxjs"; 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"; @@ -13,9 +13,9 @@ import { CachedPages } from "./reader.dto"; templateUrl: "./reader.component.html", styleUrls: ["./reader.component.less"], standalone: true, - imports: [CommonModule, ScaleImageComponent], + imports: [CommonModule, ScaleImageComponent, RouterLink], }) -export class ReaderComponent implements AfterViewInit { +export class ReaderComponent implements AfterViewInit, OnDestroy { pages: Page[] = []; currentPageIndex = 0; cachedPages: CachedPages = {}; @@ -25,17 +25,22 @@ export class ReaderComponent implements AfterViewInit { private chaptersInfo: IRulibChapter[] = []; private chapterNum: number = 0; private chapterVol: number = 0; - private url = ""; + url = ""; private fromTowards = false; + private destroy$ = new Subject(); constructor( private route: ActivatedRoute, private router: Router, private searchService: SearchService, ) {} + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } ngAfterViewInit(): void { - this.route.queryParams.subscribe((params) => { + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { const url = params["url"]; const chapter = params["chapter"]; const volume = params["volume"]; @@ -47,9 +52,12 @@ export class ReaderComponent implements AfterViewInit { this.chapterVol = +volume; this.url = url; this.loadChapter(url, chapter, volume); - this.searchService.getChapters(url).subscribe((data) => { - this.chaptersInfo = data.data; - }); + this.searchService + .getChapters(url) + .pipe(takeUntil(this.destroy$)) + .subscribe((data) => { + this.chaptersInfo = data.data; + }); } else { this.router.navigate(["/"]); } @@ -61,14 +69,17 @@ export class ReaderComponent implements AfterViewInit { } loadChapter(url: string, chapter: string, volume: string) { - this.searchService.getChapter(url, chapter, volume).subscribe((data) => { - this.currentChapterInfo = data.data; - this.pages = data.data.pages; - if (this.fromTowards) { - this.currentPageIndex = this.pages.length - 1; - this.loadPage(this.pages.length - 1); // Загрузить последнюю страницу при открытии главы - } else this.loadPage(0); // Загрузить первую страницу при открытии главы - }); + this.searchService + .getChapter(url, chapter, volume) + .pipe(takeUntil(this.destroy$)) + .subscribe((data) => { + this.currentChapterInfo = data.data; + this.pages = data.data.pages; + if (this.fromTowards) { + this.currentPageIndex = this.pages.length - 1; + this.loadPage(this.pages.length - 1); // Загрузить последнюю страницу при открытии главы + } else this.loadPage(0); // Загрузить первую страницу при открытии главы + }); } loadPage(index: number) { @@ -85,9 +96,11 @@ export class ReaderComponent implements AfterViewInit { } if (!this.cachedPages[index]?.imageData) { // Если страница не закэширована, загружаем её - this.fetchAndCachePage(index).subscribe(() => { - this.updateImage(); - }); + this.fetchAndCachePage(index) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.updateImage(); + }); } else { // Если страница уже в кэше, просто обновляем изображение this.updateImage(); @@ -135,11 +148,13 @@ export class ReaderComponent implements AfterViewInit { for (let i = startIndex; i <= endIndex; i++) { if (!this.isPageCached(i)) { - this.fetchAndCachePage(i).subscribe(() => { - if (i === this.currentPageIndex) { - this.updateImage(); - } - }); + this.fetchAndCachePage(i) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + if (i === this.currentPageIndex) { + this.updateImage(); + } + }); } } } diff --git a/apps/NwaifuAnime/src/app/services/parsers/decorators/parser.decorator.ts b/apps/NwaifuAnime/src/app/services/parsers/decorators/parser.decorator.ts new file mode 100644 index 0000000..4e3bd96 --- /dev/null +++ b/apps/NwaifuAnime/src/app/services/parsers/decorators/parser.decorator.ts @@ -0,0 +1,19 @@ +export function ParserDecorator({ + site_name, + url, + nsfw = false, + site_id = 1, +}: { + site_name: string; + url: string; + nsfw?: boolean; + site_id?: number; +}) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (constructor: Function) { + constructor.prototype.site_name = site_name; + constructor.prototype.nsfw = nsfw; + constructor.prototype.api_url = url; + constructor.prototype.site_id = site_id; + }; +} diff --git a/apps/NwaifuAnime/src/app/services/parsers/parser.ts b/apps/NwaifuAnime/src/app/services/parsers/parser.ts new file mode 100644 index 0000000..0a751cf --- /dev/null +++ b/apps/NwaifuAnime/src/app/services/parsers/parser.ts @@ -0,0 +1,18 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; + +@Injectable({ providedIn: "root" }) +export abstract class Parser { + constructor(protected http: HttpClient) {} + + protected abstract url: string; + + abstract searchManga(query: string): Observable; + + abstract getDetails(slug_url: string): Observable; + + abstract getChapters(url: string): Observable; + + abstract getChapter(url: string, chapter: string, volume: string): Observable; +} diff --git a/apps/NwaifuAnime/src/app/services/parsers/rulib/lib.social.parser.service.ts b/apps/NwaifuAnime/src/app/services/parsers/rulib/lib.social.parser.service.ts index d50c2a2..98df4bb 100644 --- a/apps/NwaifuAnime/src/app/services/parsers/rulib/lib.social.parser.service.ts +++ b/apps/NwaifuAnime/src/app/services/parsers/rulib/lib.social.parser.service.ts @@ -1,7 +1,6 @@ -import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, catchError, map, throwError } from "rxjs"; -import { ESiteUrls } from "../urls"; +import { Parser } from "../parser"; import { IRulibChapterResult } from "./rulib.chapter.dto"; import { IRulibChaptersResult } from "./rulib.chapters.dto"; import { IRulibDetailResult } from "./rulib.detail.dto"; @@ -11,17 +10,20 @@ import { IRulibSearchResult } from "./rulib.search.dto"; @Injectable({ providedIn: "root", }) -export class LibSocialParserService { - private readonly url = ESiteUrls.LIB_SOCIAL; - constructor(private readonly http: HttpClient) {} - +export class LibSocialParserService extends Parser { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected url = (this as any).api_url; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private site_id = (this as any).site_id; get imageServer() { return "https://img33.imgslib.link"; } searchManga(query: string): Observable { return this.http - .get(`${this.url}/api/manga?fields[]=rate_avg&fields[]=rate&q=${query}&site_id[]=1`) + .get( + `${this.url}/api/manga?fields[]=rate_avg&fields[]=rate&q=${query}&site_id[]=${this.site_id}`, + ) .pipe( map((data: object) => { return data as IRulibSearchResult; diff --git a/apps/NwaifuAnime/src/app/services/parsers/rulib/mangalib.parser.service.ts b/apps/NwaifuAnime/src/app/services/parsers/rulib/mangalib.parser.service.ts new file mode 100644 index 0000000..1b69e28 --- /dev/null +++ b/apps/NwaifuAnime/src/app/services/parsers/rulib/mangalib.parser.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@angular/core"; +import { ParserDecorator } from "../decorators/parser.decorator"; +import { ESiteUrls } from "../urls"; +import { LibSocialParserService } from "./lib.social.parser.service"; + +@Injectable({ + providedIn: "root", +}) +@ParserDecorator({ site_name: "MangaLib", url: ESiteUrls.MANGALIB, site_id: 1 }) +export class MangalibParserService extends LibSocialParserService {} diff --git a/apps/NwaifuAnime/src/app/services/parsers/urls.ts b/apps/NwaifuAnime/src/app/services/parsers/urls.ts index efd6f6d..bcf757c 100644 --- a/apps/NwaifuAnime/src/app/services/parsers/urls.ts +++ b/apps/NwaifuAnime/src/app/services/parsers/urls.ts @@ -1,3 +1,4 @@ export enum ESiteUrls { LIB_SOCIAL = "https://api.lib.social", + MANGALIB = "https://api.mangalib.me", } diff --git a/apps/NwaifuAnime/src/app/services/search.service.ts b/apps/NwaifuAnime/src/app/services/search.service.ts index b427bc0..7f173d3 100644 --- a/apps/NwaifuAnime/src/app/services/search.service.ts +++ b/apps/NwaifuAnime/src/app/services/search.service.ts @@ -1,25 +1,27 @@ +import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { BehaviorSubject, Observable, map } from "rxjs"; -import { LibSocialParserService } from "./parsers/rulib/lib.social.parser.service"; +import { MangalibParserService } from "./parsers/rulib/mangalib.parser.service"; import { IRulibChapterResult } from "./parsers/rulib/rulib.chapter.dto"; import { IRulibChaptersResult } from "./parsers/rulib/rulib.chapters.dto"; import { IRulibDetailResult } from "./parsers/rulib/rulib.detail.dto"; -import { Datum } from "./parsers/rulib/rulib.search.dto"; -import { HttpClient } from "@angular/common/http"; +import { Datum, IRulibSearchResult } from "./parsers/rulib/rulib.search.dto"; @Injectable({ providedIn: "root" }) export class SearchService { private itemsTerm = new BehaviorSubject([]); currentItemsTerm = this.itemsTerm.asObservable(); constructor( - private parser: LibSocialParserService, + private parser: MangalibParserService, private http: HttpClient, ) {} - search(query: string) { - this.parser.searchManga(query).subscribe((data) => { - this.itemsTerm.next(data.data); - }); + search(query: string): Observable { + return this.parser.searchManga(query).pipe( + map((data) => { + return data; + }), + ); } getDetails(slug_url: string): Observable { @@ -50,12 +52,11 @@ export class SearchService { return this.parser.imageServer; } - getImageData(imageUrl: string): Observable{ - return this.http.get(imageUrl, {responseType: 'arraybuffer'}).pipe( + getImageData(imageUrl: string): Observable { + return this.http.get(imageUrl, { responseType: "arraybuffer" }).pipe( map((arrayBuffer: ArrayBuffer) => { return new Uint8Array(arrayBuffer); - }) + }), ); } - }