6 Commits

22 changed files with 505 additions and 87 deletions

View File

@@ -8,4 +8,6 @@
} }
<app-header></app-header> <app-header></app-header>
<div class="h-10"></div> <div class="h-10"></div>
<router-outlet></router-outlet> <main>
<router-outlet></router-outlet>
</main>

View File

@@ -0,0 +1,4 @@
main {
overflow-y: auto;
height: calc(100vh - 3rem);
}

View File

@@ -3,13 +3,36 @@
<h1>{{ detail_item.name }}</h1> <h1>{{ detail_item.name }}</h1>
<h2>{{ detail_item.rus_name }}</h2> <h2>{{ detail_item.rus_name }}</h2>
<img [src]="detail_item.cover.default" [alt]="detail_item.slug" /> <img [src]="detail_item.cover.default" [alt]="detail_item.slug" />
@for (chapter of chapters.data; track $index) {
<h3> <details class="w-full">
<strong>{{ chapter.number }}.</strong> {{ chapter.name || "Нет названия" }} <summary class="text-center sticky top-0 bg-white z-10 py-2">
</h3> <h3>Главы</h3>
} </summary>
<button class="p-3 text-white bg-slate-600 w-[400px] mt-5 rounded-lg" (click)="goToReader()"> <div class="flex flex-col items-center pb-16">
@for (chapter of chapters.data; track $index) {
<a
routerLink="/reader"
[queryParams]="{
url: detail_item.slug_url,
chapter: chapter.number,
volume: chapter.volume,
}"
[title]="chapter.name"
class="p-3 text-white bg-slate-600 w-[300px] mt-3 rounded-lg"
>
<h3>
<strong>{{ chapter.number }}.</strong> {{ chapter.name || "Нет названия" }}
</h3>
</a>
}
</div>
</details>
<a
routerLink="/reader"
class="p-3 text-white bg-slate-600 w-[300px] mt-5 rounded-lg text-center"
[queryParams]="{ url: detail_item.slug_url, volume: 1, chapter: 1 }"
>
Читать Читать
</button> </a>
} }
</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, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { IRulibChaptersResult } from "../../services/parsers/rulib/rulib.chapters.dto"; import { IRulibChaptersResult } from "../../services/parsers/rulib/rulib.chapters.dto";
import { Data } from "../../services/parsers/rulib/rulib.detail.dto"; import { Data } from "../../services/parsers/rulib/rulib.detail.dto";
import { SearchService } from "../../services/search.service"; import { SearchService } from "../../services/search.service";
@@ -10,11 +11,12 @@ import { SearchService } from "../../services/search.service";
templateUrl: "./detail.component.html", templateUrl: "./detail.component.html",
styleUrls: ["./detail.component.less"], styleUrls: ["./detail.component.less"],
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, RouterLink],
}) })
export class DetailComponent implements AfterViewInit { export class DetailComponent implements AfterViewInit, OnDestroy {
detail_item: Data | null = null; detail_item: Data | null = null;
chapters: IRulibChaptersResult = { data: [] }; chapters: IRulibChaptersResult = { data: [] };
private destroy$ = new Subject<void>();
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private searchService: SearchService, private searchService: SearchService,
@@ -22,16 +24,23 @@ export class DetailComponent implements AfterViewInit {
) {} ) {}
private getDetails(url: string) { private getDetails(url: string) {
this.searchService.getDetails(url).subscribe((data) => { this.searchService
this.detail_item = data.data; .getDetails(url)
}); .pipe(takeUntil(this.destroy$))
this.searchService.getChapters(url).subscribe((data) => { .subscribe((data) => {
this.chapters = data; this.detail_item = data.data;
}); });
this.searchService
.getChapters(url)
.pipe(takeUntil(this.destroy$))
.subscribe((data) => {
this.chapters = data;
console.log(data);
});
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.route.queryParams.subscribe((params) => { this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
const url = params["url"]; const url = params["url"];
if (url) { if (url) {
this.getDetails(url); this.getDetails(url);
@@ -41,14 +50,8 @@ export class DetailComponent implements AfterViewInit {
}); });
} }
goToReader() { ngOnDestroy(): void {
//TODO: Not only first chapter this.destroy$.next();
this.router.navigate(["/", "reader"], { this.destroy$.complete();
queryParams: {
url: this.detail_item?.slug_url,
chapter: this.chapters.data[0].number,
volume: this.chapters.data[0].volume,
},
});
} }
} }

View File

@@ -1,5 +1,5 @@
<div <div
class="header fixed flex justify-between w-full p-2 bg-gray-700 md:h-8 h-auto items-center md:flex-row flex-col md:gap-0 gap-3" class="header fixed flex justify-between w-full p-2 bg-gray-700 md:h-8 items-center md:flex-row flex-col md:gap-0 gap-3 text-xl md:text-base"
> >
<div class="flex justify-between flex-row w-full align-middle"> <div class="flex justify-between flex-row w-full align-middle">
<a href="/"><h1 class="text-white" title="Main page">NwaifuAnime</h1></a> <a href="/"><h1 class="text-white" title="Main page">NwaifuAnime</h1></a>
@@ -17,7 +17,11 @@
class="outline-none ps-1 text-sm leading-6 w-full border-0 bg-transparent border-r border-gray-700" class="outline-none ps-1 text-sm leading-6 w-full border-0 bg-transparent border-r border-gray-700"
(keydown.enter)="search()" (keydown.enter)="search()"
/> />
<button class="align-middle w-[100px] text-center" type="submit" (click)="search()"> <button
class="align-middle w-[100px] text-center border-0 flex flex-col items-center justify-center"
type="submit"
(click)="search()"
>
<span class="text-sm text-black h-full">Поиск</span> <span class="text-sm text-black h-full">Поиск</span>
</button> </button>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core"; import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { Subject, filter, takeUntil } from "rxjs";
@Component({ @Component({
selector: "app-header", selector: "app-header",
@@ -9,13 +10,24 @@ import { ActivatedRoute, Router } from "@angular/router";
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
}) })
export class HeaderComponent implements AfterViewInit { export class HeaderComponent implements AfterViewInit, OnDestroy {
@ViewChild("searchInput") searchInput: ElementRef<HTMLInputElement> | null = null; @ViewChild("searchInput") searchInput: ElementRef<HTMLInputElement> | null = null;
menuOpened = false; menuOpened = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private router: Router, private router: Router,
private route: ActivatedRoute, 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")) {
this.menuOpened = false;
}
});
}
changeMenu() { changeMenu() {
this.menuOpened = !this.menuOpened; this.menuOpened = !this.menuOpened;
} }
@@ -25,7 +37,7 @@ export class HeaderComponent implements AfterViewInit {
} }
get searchBarClass(): string { get searchBarClass(): string {
return `search-bar bg-slate-300 md:w-[50%] w-full md:m-0 ms-2 me-2 max-h-6 md:flex justify-start flex-row items-center rounded-md ${this.menuOpened ? "flex" : "hidden"}`; return `search-bar bg-slate-300 md:w-[50%] w-full md:m-0 ms-2 me-2 md:h-6 h-10 md:flex justify-start flex-row items-center rounded-md ${this.menuOpened ? "flex" : "hidden"}`;
} }
search() { search() {
@@ -36,11 +48,16 @@ export class HeaderComponent implements AfterViewInit {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.route.queryParams.subscribe((params) => { this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
const search = params["search"]; const search = params["search"];
if (search && this.searchInput) { if (search && this.searchInput) {
this.searchInput.nativeElement.value = search; this.searchInput.nativeElement.value = search;
} }
}); });
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
} }

View File

@@ -1,11 +1,16 @@
<h1>It's home component</h1> <h1>It's home component</h1>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center w-full px-3">
@for (item of items; track $index) { @for (item of items; track $index) {
<button (click)="getDetails(item.slug_url)" title="{{ item.name }}" class="mb-6"> <a
<div class="card flex flex-col items-center border-black border-2 rounded-md p-4"> routerLink="/detail"
[queryParams]="{ url: item.slug_url }"
title="{{ item.name }}"
class="mb-6 max-w-[700px] w-full"
>
<div class="card flex flex-col items-center border-black border-2 rounded-md p-4 w-full">
<h1>{{ item.rus_name }}</h1> <h1>{{ item.rus_name }}</h1>
<img [src]="item.cover.thumbnail" [alt]="item.slug" class="w-[200px] h-auto aspect-auto" /> <img [src]="item.cover.thumbnail" [alt]="item.slug" class="w-[200px] h-auto aspect-auto" />
</div> </div>
</button> </a>
} }
</div> </div>

View File

@@ -1,7 +1,7 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { AfterViewInit, Component, Input, OnDestroy, OnInit } from "@angular/core"; import { AfterViewInit, Component, Input, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, RouterLink } from "@angular/router";
import { Subscription } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { Datum } from "../../services/parsers/rulib/rulib.search.dto"; import { Datum } from "../../services/parsers/rulib/rulib.search.dto";
import { SearchService } from "../../services/search.service"; import { SearchService } from "../../services/search.service";
@@ -10,38 +10,33 @@ import { SearchService } from "../../services/search.service";
selector: "app-home", selector: "app-home",
templateUrl: "./home.component.html", templateUrl: "./home.component.html",
styleUrls: ["./home.component.less"], 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[] = []; @Input() items: Datum[] = [];
private subscription: Subscription = new Subscription();
private destroy$ = new Subject<void>();
constructor( constructor(
private searchService: SearchService, private searchService: SearchService,
private route: ActivatedRoute, 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 { ngOnDestroy(): void {
this.subscription.unsubscribe(); this.destroy$.next();
this.destroy$.complete();
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.route.queryParams.subscribe((params) => { this.route.queryParams.subscribe((params) => {
const search = params["search"]; const search = params["search"];
console.log(params);
if (search) { if (search) {
this.searchService.search(search); this.searchService
.search(search)
.pipe(takeUntil(this.destroy$))
.subscribe((data) => {
this.items = data.data;
});
} }
}); });
} }

View File

@@ -1,6 +1,28 @@
<h1>It's reader page</h1> <div
class="flex flex-row items-center justify-between me-4 max-w-[900px] relative -translate-x-[50%] left-[50%]"
>
<a
[routerLink]="['/', 'detail']"
[queryParams]="{ url: url }"
class="bg-slate-600 text-white rounded-lg p-3 m-4"
>
Назад к тайтлу
</a>
<h3>{{ currentChapterInfo?.number }}. {{ currentChapterInfo?.name || "Нет названия" }}</h3>
</div>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
@for (page of pages; track $index) { @if (pages.length > 0 && cachedPages[currentPageIndex]) {
<img [src]="page.url" [alt]="page.slug" /> <div [class]="imageContainerClass">
<app-scale-image [imageSrc]="imageUrl"></app-scale-image>
</div>
<div class="flex items-center justify-center space-x-4 my-10">
<button (click)="prevPage()" class="p-3 text-white bg-slate-600 w-[100px] rounded-lg">
</button>
<p>{{ pages[currentPageIndex].slug }} / {{ pages.length }}</p>
<button (click)="nextPage()" class="p-3 text-white bg-slate-600 w-[100px] rounded-lg">
</button>
</div>
} }
</div> </div>

View File

@@ -0,0 +1,5 @@
:host {
overflow-y: auto;
height: 100%;
display: block;
}

View File

@@ -1,38 +1,218 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { AfterViewInit, Component } from "@angular/core"; import { AfterViewInit, Component, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import { Page } from "../../services/parsers/rulib/rulib.chapter.dto"; 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"; import { SearchService } from "../../services/search.service";
import { ScaleImageComponent } from "../scale-image/scale-image.component";
import { CachedPages } from "./reader.dto";
@Component({ @Component({
selector: "app-reader", selector: "app-reader",
templateUrl: "./reader.component.html", templateUrl: "./reader.component.html",
styleUrls: ["./reader.component.less"], styleUrls: ["./reader.component.less"],
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, ScaleImageComponent, RouterLink],
}) })
export class ReaderComponent implements AfterViewInit { export class ReaderComponent implements AfterViewInit, OnDestroy {
pages: Page[] = []; pages: Page[] = [];
currentPageIndex = 0;
cachedPages: CachedPages = {};
imageUrl: string = "";
isManhwa = false;
currentChapterInfo: Chapter | null = null;
private chaptersInfo: IRulibChapter[] = [];
private chapterNum: number = 0;
private chapterVol: number = 0;
url = "";
private fromTowards = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private searchService: SearchService, private searchService: SearchService,
) {} ) {}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.route.queryParams.subscribe((params) => { this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
const url = params["url"]; const url = params["url"];
const chapter = params["chapter"]; const chapter = params["chapter"];
const volume = params["volume"]; const volume = params["volume"];
const fromTowards = params["from_towards"];
if (url && chapter && volume) { if (url && chapter && volume) {
this.searchService.getChapter(url, chapter, volume).subscribe((data) => { if (fromTowards) this.fromTowards = Boolean(+fromTowards);
this.pages = data.data.pages.map((page) => { else this.fromTowards = false;
return { ...page, url: this.searchService.getImageServer() + page.url }; this.chapterNum = +chapter;
this.chapterVol = +volume;
this.url = url;
this.loadChapter(url, chapter, volume);
this.searchService
.getChapters(url)
.pipe(takeUntil(this.destroy$))
.subscribe((data) => {
this.chaptersInfo = data.data;
}); });
});
} else { } else {
this.router.navigate(["/"]); 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)
.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) {
if (index >= 0 && index < this.pages.length) {
this.currentPageIndex = index;
this.cachePage(index); // Кэшируем текущую и соседние страницы
this.unloadCachedPages(index); // Сгружаем ненужные страницы из кэша
const container = document.querySelector("app-reader");
if (container) {
container.scrollTo({
top: 0,
behavior: "smooth",
});
}
if (!this.cachedPages[index]?.imageData) {
// Если страница не закэширована, загружаем её
this.fetchAndCachePage(index)
.pipe(takeUntil(this.destroy$))
.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,
},
});
}
}
}
// Кэширование текущей страницы и несколько соседних для ускорения процесса навигации по страницам
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)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
if (i === this.currentPageIndex) {
this.updateImage();
}
});
}
}
}
private isPageCached(index: number): boolean {
return !!this.cachedPages[index]?.imageData;
}
// Загрузка и сохранение изображения в кэш
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 = +key;
if (index - pageIndex > 2) {
delete this.cachedPages[pageIndex];
}
}
}
// Обновляем изображение на странице
private updateImage() {
const currentPage = this.cachedPages[this.currentPageIndex];
if (currentPage && currentPage.imageData) {
const blob = new Blob([currentPage.imageData], { type: "image/jpeg" });
const urlCreator = window.URL || window.webkitURL;
this.imageUrl = urlCreator.createObjectURL(blob);
const imageUrl = this.imageUrl;
const image = new Image();
image.onload = () => {
this.isManhwa = image.naturalHeight / image.naturalWidth >= 5;
URL.revokeObjectURL(imageUrl);
};
image.src = imageUrl;
}
}
nextPage() {
this.loadPage(this.currentPageIndex + 1);
}
prevPage() {
this.loadPage(this.currentPageIndex - 1);
}
get imageContainerClass() {
return `${this.isManhwa ? "h-auto" : "h-[70vh]"} w-[95vw] md:w-[700px] md:h-auto`;
}
} }

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
<div class="image-container" #container>
<img #image [src]="imageSrc" (load)="onImageLoad()" alt="Manga page" />
</div>

View File

@@ -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;
}

View File

@@ -0,0 +1,67 @@
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() {
this.scaleImage();
}
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);
if (img.naturalHeight / img.naturalWidth >= 5) {
img.style.width = "100%";
img.style.height = "auto";
} else {
img.style.width = `${newWidth}px`;
img.style.height = `${newHeight}px`;
}
}
}
}

View File

@@ -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;
};
}

View File

@@ -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<object>;
abstract getDetails(slug_url: string): Observable<object>;
abstract getChapters(url: string): Observable<object>;
abstract getChapter(url: string, chapter: string, volume: string): Observable<object>;
}

View File

@@ -1,7 +1,6 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable, catchError, map, throwError } from "rxjs"; import { Observable, catchError, map, throwError } from "rxjs";
import { ESiteUrls } from "../urls"; import { Parser } from "../parser";
import { IRulibChapterResult } from "./rulib.chapter.dto"; import { IRulibChapterResult } from "./rulib.chapter.dto";
import { IRulibChaptersResult } from "./rulib.chapters.dto"; import { IRulibChaptersResult } from "./rulib.chapters.dto";
import { IRulibDetailResult } from "./rulib.detail.dto"; import { IRulibDetailResult } from "./rulib.detail.dto";
@@ -11,17 +10,20 @@ import { IRulibSearchResult } from "./rulib.search.dto";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
export class LibSocialParserService { export class LibSocialParserService extends Parser {
private readonly url = ESiteUrls.LIB_SOCIAL; // eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(private readonly http: HttpClient) {} 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() { get imageServer() {
return "https://img33.imgslib.link"; return "https://img33.imgslib.link";
} }
searchManga(query: string): Observable<IRulibSearchResult> { searchManga(query: string): Observable<IRulibSearchResult> {
return this.http 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( .pipe(
map((data: object) => { map((data: object) => {
return data as IRulibSearchResult; return data as IRulibSearchResult;

View File

@@ -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 {}

View File

@@ -1,3 +1,4 @@
export enum ESiteUrls { export enum ESiteUrls {
LIB_SOCIAL = "https://api.lib.social", LIB_SOCIAL = "https://api.lib.social",
MANGALIB = "https://api.mangalib.me",
} }

View File

@@ -1,21 +1,27 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable, map } from "rxjs"; 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 { 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, IRulibSearchResult } from "./parsers/rulib/rulib.search.dto";
@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: MangalibParserService,
private http: HttpClient,
) {}
search(query: string) { search(query: string): Observable<IRulibSearchResult> {
this.parser.searchManga(query).subscribe((data) => { return this.parser.searchManga(query).pipe(
this.itemsTerm.next(data.data); map((data) => {
}); return data;
}),
);
} }
getDetails(slug_url: string): Observable<IRulibDetailResult> { getDetails(slug_url: string): Observable<IRulibDetailResult> {
@@ -45,4 +51,12 @@ 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);
}),
);
}
} }

View File

@@ -1,3 +1,6 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
overflow-y: hidden;
}