diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad49110 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "files.exclude": { + ".directory": true, + "**/.angular": true, + "**/.github": true, + "**/.vscode": true, + "**/dist": true, + "**/node_modules": true + } +} \ No newline at end of file diff --git a/package.json b/package.json index a8231ec..b9032dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chytanka", - "version": "0.13.50", + "version": "0.13.51", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/@site-modules/@common-read/common-read.module.ts b/src/app/@site-modules/@common-read/common-read.module.ts index fd698ba..06672ba 100644 --- a/src/app/@site-modules/@common-read/common-read.module.ts +++ b/src/app/@site-modules/@common-read/common-read.module.ts @@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common'; import { CommonReadComponent } from './ui/common-read/common-read.component'; import { SharedModule } from '../../shared/shared.module'; import { RouterModule } from '@angular/router'; +import { ViewerModule } from '../../viewer/viewer.module'; +import { LinkParserModule } from '../../link-parser/link-parser.module'; @@ -13,10 +15,13 @@ import { RouterModule } from '@angular/router'; imports: [ CommonModule, RouterModule, - SharedModule + SharedModule, + ViewerModule, + LinkParserModule ], exports: [ - CommonReadComponent + CommonReadComponent, + SharedModule ] }) export class CommonReadModule { } diff --git a/src/app/@site-modules/imgchest/imgchest-shell.component.ts b/src/app/@site-modules/imgchest/imgchest-shell.component.ts index 2439035..ea26d7c 100644 --- a/src/app/@site-modules/imgchest/imgchest-shell.component.ts +++ b/src/app/@site-modules/imgchest/imgchest-shell.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { ImgchestService } from './imgchest.service'; import { Base64 } from '../../shared/utils'; import { of, switchMap } from 'rxjs'; @@ -9,18 +9,14 @@ import { IMGCHEST_PATH } from '../../app-routing.module'; imports: [CommonReadModule], selector: 'app-imgchest-shell', template: ` - - - Imgchest logo - - -
-

{{lang.ph().imagesVia}}Imgchest - API. - {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

-
` + + + ` }) export default class ImgchestShellComponent extends ReadBaseComponent { + protected readonly sourceName = signal('Imgchest'); + protected readonly sourceUrl = signal('https://imgchest.com'); + protected readonly sourceImageSrc = signal('/assets/logos/imgchest.png'); override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), diff --git a/src/app/@site-modules/imgur/imgur-shell.component.ts b/src/app/@site-modules/imgur/imgur-shell.component.ts index 0932f0a..8bc3b17 100644 --- a/src/app/@site-modules/imgur/imgur-shell.component.ts +++ b/src/app/@site-modules/imgur/imgur-shell.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { ImgurService } from './imgur.service'; import { Base64 } from '../../shared/utils'; import { of, switchMap } from 'rxjs'; @@ -8,19 +8,15 @@ import { IMGUR_PATH } from '../../app-routing.module'; @Component({ imports: [CommonReadModule], selector: 'app-imgur-shell', - template: `
- - Imgur logo - -

{{lang.ph().imagesVia}}Imgur - API. - {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

-
` + template: ` + + +` }) export default class ImgurShellComponent extends ReadBaseComponent { - + protected readonly sourceName = signal('Imgur'); + protected readonly sourceUrl = signal('https://imgur.com'); + protected readonly sourceImageSrc = signal('/assets/logos/imgur-logo.svg'); override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), switchMap(([params]) => { diff --git a/src/app/@site-modules/mangadex/mangadex-shell.component.ts b/src/app/@site-modules/mangadex/mangadex-shell.component.ts index 016042f..3a8ea02 100644 --- a/src/app/@site-modules/mangadex/mangadex-shell.component.ts +++ b/src/app/@site-modules/mangadex/mangadex-shell.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { forkJoin, map, of, switchMap } from 'rxjs'; import { MangadexService } from './mangadex.service'; import { Base64 } from '../../shared/utils'; @@ -9,21 +9,14 @@ import { MANGADEX_PATH } from '../../app-routing.module'; imports: [CommonReadModule], selector: 'app-mangadex-shell', template: ` - -
- - MangaDex logo - MangaDex wordmark - -

Images via Mangadex API. - Thanks!
Details on their site. Respect copyrights.

-
- + +
` }) export default class MangadexShellComponent extends ReadBaseComponent { + protected readonly sourceName = signal('MangaDex'); + protected readonly sourceUrl = signal('https://mangadex.org'); + protected readonly sourceImageSrc = signal('/assets/logos/mangadex-logo.svg'); override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), diff --git a/src/app/@site-modules/nhentai/nhentai.service.ts b/src/app/@site-modules/nhentai/nhentai.service.ts index 7480de4..fccd67f 100644 --- a/src/app/@site-modules/nhentai/nhentai.service.ts +++ b/src/app/@site-modules/nhentai/nhentai.service.ts @@ -32,7 +32,7 @@ export class NhentaiService { nsfw: true, images: (data.images.pages.map((item: any, index: number) => { return { - src: `https://i7.nhentai.net/galleries/${mediaId}/${index + 1}.${this.imageType.get(item.t)}`, + src: `https://i1.nhentai.net/galleries/${mediaId}/${index + 1}.${this.imageType.get(item.t)}`, height: item.h, width: item.w }; diff --git a/src/app/@site-modules/reddit/reddit-shell.component.ts b/src/app/@site-modules/reddit/reddit-shell.component.ts index 8b94f4d..6934f09 100644 --- a/src/app/@site-modules/reddit/reddit-shell.component.ts +++ b/src/app/@site-modules/reddit/reddit-shell.component.ts @@ -9,17 +9,14 @@ import { REDDIT_PATH } from '../../app-routing.module'; imports: [CommonReadModule], selector: 'app-reddit-shell', template: ` - - MangaDex logo - -

{{lang.ph().imagesVia}}Reddit - API. - {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

- + +
` }) export default class RedditShellComponent extends ReadBaseComponent { - + protected readonly sourceName = () => 'Reddit'; + protected readonly sourceUrl = () => 'https://reddit.com'; + protected readonly sourceImageSrc = () => '/assets/logos/reddit-logo.svg'; override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), switchMap(([params]) => { diff --git a/src/app/@site-modules/telegraph/telegraph-shell.component.ts b/src/app/@site-modules/telegraph/telegraph-shell.component.ts index cbabc03..3dc0ed8 100644 --- a/src/app/@site-modules/telegraph/telegraph-shell.component.ts +++ b/src/app/@site-modules/telegraph/telegraph-shell.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy } from '@angular/core'; +import { Component, OnDestroy, signal } from '@angular/core'; import { Base64 } from '../../shared/utils'; import { of, switchMap } from 'rxjs'; import { TelegraphService } from './telegraph.service'; @@ -11,15 +11,15 @@ import { CommonReadModule } from '../@common-read'; imports: [CommonReadModule], template: ` - -

{{lang.ph().imagesVia}}Telegra.ph - API. - {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

+ +
` }) export default class TelegraphShellComponent extends ReadBaseComponent implements OnDestroy { - + protected readonly sourceName = signal('Telegra.ph'); + protected readonly sourceUrl = signal('https://telegra.ph'); + protected readonly sourceImageSrc = signal('/assets/logos/telegraph-logo.svg'); override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), switchMap(([params]) => { diff --git a/src/app/@site-modules/yandere/yandere-shell.component.ts b/src/app/@site-modules/yandere/yandere-shell.component.ts index 63438aa..40ce01e 100644 --- a/src/app/@site-modules/yandere/yandere-shell.component.ts +++ b/src/app/@site-modules/yandere/yandere-shell.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnDestroy } from '@angular/core'; +import { Component, inject, OnDestroy, signal } from '@angular/core'; import { switchMap, of } from 'rxjs'; import { CommonReadModule, ReadBaseComponent } from '../@common-read'; import { Base64 } from '../../shared/utils'; @@ -10,15 +10,17 @@ import { YANDERE_PATH } from '../../app-routing.module'; imports: [CommonReadModule], template: ` - -

{{lang.ph().imagesVia}}Yande.re - API. - {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

+ +
` }) export default class YandereShellComponent extends ReadBaseComponent implements OnDestroy { yandere = inject(YandereService) + protected readonly sourceName = signal('Yande.re'); + protected readonly sourceUrl = signal('https://yande.re'); + protected readonly sourceImageSrc = signal('/assets/logos/yandere-logo.png'); + override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), diff --git a/src/app/@site-modules/zenko/zenko-shell.component.ts b/src/app/@site-modules/zenko/zenko-shell.component.ts index e0645d0..a1b82a4 100644 --- a/src/app/@site-modules/zenko/zenko-shell.component.ts +++ b/src/app/@site-modules/zenko/zenko-shell.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnDestroy } from '@angular/core'; +import { Component, inject, OnDestroy, signal } from '@angular/core'; import { switchMap, of } from 'rxjs'; import { ZENKO_PATH } from '../../app-routing.module'; import { CommonReadModule, ReadBaseComponent } from '../@common-read'; @@ -10,15 +10,15 @@ import { ZenkoService } from './zenko.service'; imports: [CommonReadModule], template: ` - -

{{lang.ph().imagesVia}}Zenko - API. - {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

- + +
` }) export default class ZenkoShellComponent extends ReadBaseComponent implements OnDestroy { zenko = inject(ZenkoService) + protected readonly sourceName = signal('Zenko'); + protected readonly sourceUrl = signal('https://zenko.online'); + protected readonly sourceImageSrc = signal('/assets/logos/zenko-logo.svg'); override episode$ = this.combineParamMapAndRefresh() .pipe(this.tapStartLoading(), diff --git a/src/app/app.component.html b/src/app/app.component.html index 3c55ff9..db11b97 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,7 @@ - +@if (gamepad.gamepad.connected()) { +@defer{} +}
@defer{} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 03325ab..ed72431 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,6 +10,7 @@ import { SharedModule } from './shared/shared.module'; import { registerLocaleData } from '@angular/common'; import localeUk from "@angular/common/locales/uk"; +import { parserProviders } from './link-parser/data-access/parser.providers'; registerLocaleData(localeUk) @@ -34,7 +35,8 @@ registerLocaleData(localeUk) // includePostRequests: false, // }) // ), - provideHttpClient(withFetch()) + provideHttpClient(withFetch()), + ...parserProviders ] }) export class AppModule { } diff --git a/src/app/file/mobi/mobi.component.ts b/src/app/file/mobi/mobi.component.ts index 64cf306..baa688a 100644 --- a/src/app/file/mobi/mobi.component.ts +++ b/src/app/file/mobi/mobi.component.ts @@ -4,10 +4,11 @@ import { Router } from '@angular/router'; import { FileService } from '../data-access/file.service'; import { MobiFileReader } from 'readiverse'; import { SharedModule } from '../../shared/shared.module'; +import { ViewerModule } from '../../viewer/viewer.module'; @Component({ selector: 'app-mobi', - imports: [SharedModule], + imports: [SharedModule, ViewerModule], templateUrl: './mobi.component.html', styleUrl: './mobi.component.scss' }) diff --git a/src/app/file/pdf/pdf.component.html b/src/app/file/pdf/pdf.component.html index 4c52473..0ec8be5 100644 --- a/src/app/file/pdf/pdf.component.html +++ b/src/app/file/pdf/pdf.component.html @@ -1,4 +1,3 @@ - @if(episode && episode.images && episode.images.length > 0){ } @else { diff --git a/src/app/file/pdf/pdf.component.ts b/src/app/file/pdf/pdf.component.ts index 4879817..fd6226e 100644 --- a/src/app/file/pdf/pdf.component.ts +++ b/src/app/file/pdf/pdf.component.ts @@ -6,6 +6,7 @@ import { Router } from '@angular/router'; import { getDocument, GlobalWorkerOptions, PDFPageProxy } from 'pdfjs-dist'; import { RenderParameters } from 'pdfjs-dist/types/src/display/api'; import { CompositionEpisode } from '../../@site-modules/@common-read'; +import { ViewerModule } from '../../viewer/viewer.module'; GlobalWorkerOptions.workerSrc = '/assets/pdf.worker.min.mjs' @@ -13,7 +14,7 @@ const MDASH = 'โ€”'; @Component({ selector: 'app-pdf', - imports: [SharedModule], + imports: [SharedModule, ViewerModule], templateUrl: './pdf.component.html', styleUrl: './pdf.component.scss' }) diff --git a/src/app/file/zip/zip.component.html b/src/app/file/zip/zip.component.html index 2e84688..0ec8be5 100644 --- a/src/app/file/zip/zip.component.html +++ b/src/app/file/zip/zip.component.html @@ -1,6 +1,5 @@ @if(episode && episode.images && episode.images.length > 0){ - - + } @else { } \ No newline at end of file diff --git a/src/app/file/zip/zip.component.ts b/src/app/file/zip/zip.component.ts index 6a4c1f5..ba32907 100644 --- a/src/app/file/zip/zip.component.ts +++ b/src/app/file/zip/zip.component.ts @@ -9,12 +9,11 @@ import { Acbf } from '../../shared/utils/acbf'; import { FileHashService } from '../data-access/file-hash.service'; import { FileHistoryService } from '../data-access/file-history.service'; import { FileSettingsService } from '../data-access/file-settings.service'; -import { map } from 'rxjs'; -import { ViewerComponent } from "../../viewer/viewer.component"; +import { ViewerModule } from '../../viewer/viewer.module'; @Component({ selector: 'app-zip', - imports: [SharedModule, /*ViewerComponent*/], + imports: [SharedModule, ViewerModule], templateUrl: './zip.component.html', styleUrl: './zip.component.scss' }) @@ -152,9 +151,4 @@ export class ZipComponent implements OnInit, OnDestroy { this.episode = { title: filename, images: [] } this.worker.postMessage({ arrayBuffer: ab }); } - - onPageChange(e: { total: number, current: number[] }) { - const { current, total } = e - console.log(`${current}/${total}`); - } } diff --git a/src/app/history/ui/history-list/history-list.component.html b/src/app/history/ui/history-list/history-list.component.html index 9fee102..19a45c4 100644 --- a/src/app/history/ui/history-list/history-list.component.html +++ b/src/app/history/ui/history-list/history-list.component.html @@ -1,17 +1,18 @@ -@let files = historyFiles() | async; -@let sites = historyItems() | async; +@let files = historyFiles(); +@let sites = historyItems(); -@if (sites && sites.length > 0) { -
+@let sitesNotEmpty = sites.length > 0; +@let filesNotEmpty = files.length > 0; + + +@if (sitesNotEmpty) { +
{{lang.ph().sitesHistory}} - @if ( sites.length > 0) { - - }
@@ -23,19 +24,16 @@
} -@if (files && files.length > 0) { +@if (filesNotEmpty) {
{{lang.ph().filesHistory}} | {{fileSize() | filesize}} | {{fileCount()}} - @if ( files.length > 0) { - - }
diff --git a/src/app/history/ui/history-list/history-list.component.scss b/src/app/history/ui/history-list/history-list.component.scss index cbe7249..605e98f 100644 --- a/src/app/history/ui/history-list/history-list.component.scss +++ b/src/app/history/ui/history-list/history-list.component.scss @@ -16,4 +16,8 @@ gap: 1ch; align-items: center; width: calc(100% - 17px); + + button { + margin-left: auto; + } } \ No newline at end of file diff --git a/src/app/history/ui/history-list/history-list.component.ts b/src/app/history/ui/history-list/history-list.component.ts index 059ecd4..391a644 100644 --- a/src/app/history/ui/history-list/history-list.component.ts +++ b/src/app/history/ui/history-list/history-list.component.ts @@ -14,8 +14,8 @@ export class HistoryListComponent { public fileHistory: FileHistoryService = inject(FileHistoryService); lang: LangService = inject(LangService); - historyItems: WritableSignal> = signal(this.displayHistory() ?? []); - historyFiles: WritableSignal> = signal(this.displayFilesHistory() ?? []); + historyItems: WritableSignal = signal([]); + historyFiles: WritableSignal = signal([]); async displayHistory() { const history = await this.history.getAllHistory(); @@ -23,14 +23,19 @@ export class HistoryListComponent { return history; } + async ngOnInit() { + this.historyItems.set(await this.displayHistory()); + this.historyFiles.set(await this.displayFilesHistory()); + } + async delById(id: number) { await this.history.deleteHistoryItem(id); - this.historyItems.update(value => this.history.getAllHistory()) + this.historyItems.set(await this.history.getAllHistory()); } async clearHistory() { await this.history.clearHistory(); - this.historyItems.update(value => this.history.getAllHistory()) + this.historyItems.set(await this.history.getAllHistory()); } async displayFilesHistory() { @@ -42,21 +47,21 @@ export class HistoryListComponent { async clearFileHistory() { await this.fileHistory.clearHistory(); - this.historyFiles.update(value => this.fileHistory.getAllHistory()) + this.historyFiles.set(await this.fileHistory.getAllHistory()); this.getTotalSizeAndCount() } async delFileById(id: number) { await this.fileHistory.deleteHistoryItem(id); - this.historyFiles.update(value => this.fileHistory.getAllHistory()) + this.historyFiles.set(await this.fileHistory.getAllHistory()); this.getTotalSizeAndCount() } fileSize = signal(0); - fileCount= signal(0); + fileCount = signal(0); async getTotalSizeAndCount() { - const {count, size} = await this.fileHistory.getTotalSizeAndCount() + const { count, size } = await this.fileHistory.getTotalSizeAndCount() this.fileSize.set(size) this.fileCount.set(count) diff --git a/src/app/link-parser/data-access/link-parser-settings.service.ts b/src/app/link-parser/data-access/link-parser-settings.service.ts index 2d94b41..cc33fec 100644 --- a/src/app/link-parser/data-access/link-parser-settings.service.ts +++ b/src/app/link-parser/data-access/link-parser-settings.service.ts @@ -54,20 +54,20 @@ export class LinkParserSettingsService { /** * */ - seasonalTheme!: WritableSignal; + seasonalTheme: WritableSignal = signal(false); initSeasonalTheme() { if (!isPlatformBrowser(this.platformId)) return; const n = localStorage.getItem('seasonalTheme') === null ? true : Boolean(localStorage.getItem('seasonalTheme') == 'true'); - this.seasonalTheme = signal(n); + this.seasonalTheme.set(n); this.setSeasonalTheme(n); } setSeasonalTheme(n: boolean) { if (!isPlatformBrowser(this.platformId)) return; - this.seasonalTheme.update(v => n); + this.seasonalTheme.set(n); localStorage.setItem('seasonalTheme', n.toString()) } diff --git a/src/app/link-parser/data-access/link-parser.service.ts b/src/app/link-parser/data-access/link-parser.service.ts index 776b823..27a54d7 100644 --- a/src/app/link-parser/data-access/link-parser.service.ts +++ b/src/app/link-parser/data-access/link-parser.service.ts @@ -1,5 +1,6 @@ -import { Injectable, signal } from '@angular/core'; +import { Inject, Injectable, signal, Type } from '@angular/core'; import { LinkParseResult, LinkParser } from '../utils'; +import { LINK_PARSERS } from './parser.tokens'; @Injectable({ providedIn: 'root' @@ -21,7 +22,9 @@ export class LinkParserService { parsers: LinkParser[] = []; - constructor() { } + constructor(@Inject(LINK_PARSERS) parserClasses: Type[]) { + this.parsers = parserClasses.map(Parser => new Parser()); + } parse(link: string): LinkParseResult | null { diff --git a/src/app/link-parser/data-access/parser.providers.ts b/src/app/link-parser/data-access/parser.providers.ts new file mode 100644 index 0000000..274e5bf --- /dev/null +++ b/src/app/link-parser/data-access/parser.providers.ts @@ -0,0 +1,19 @@ +// parser.providers.ts +import { JsonLinkParser, NhentaiLinkParser, PixivLinkParser, RedditLinkParser, TelegraphLinkParser, YandereParser, ZenkoLinkParser, ImgurLinkParser, MangadexLinkParser } from '../utils'; +import { ImgchestLinkParser } from '../utils/imgchest-link-parser'; +import { LINK_PARSERS } from './parser.tokens'; + +export const parserProviders = [{ + provide: LINK_PARSERS, useValue: [ + ImgurLinkParser, + MangadexLinkParser, + TelegraphLinkParser, + RedditLinkParser, + ZenkoLinkParser, + NhentaiLinkParser, + YandereParser, + PixivLinkParser, + ImgchestLinkParser, + JsonLinkParser + ] +}]; \ No newline at end of file diff --git a/src/app/link-parser/data-access/parser.tokens.ts b/src/app/link-parser/data-access/parser.tokens.ts new file mode 100644 index 0000000..0779795 --- /dev/null +++ b/src/app/link-parser/data-access/parser.tokens.ts @@ -0,0 +1,6 @@ +import { InjectionToken, Type } from '@angular/core'; +import { LinkParser } from '../utils'; + +export const LINK_PARSERS = new InjectionToken[]>( + 'Available link parsers' +); \ No newline at end of file diff --git a/src/app/link-parser/link-parser.module.ts b/src/app/link-parser/link-parser.module.ts index 90cd26e..a041e6f 100644 --- a/src/app/link-parser/link-parser.module.ts +++ b/src/app/link-parser/link-parser.module.ts @@ -11,7 +11,17 @@ import { FooterComponent } from './ui/footer/footer.component'; import { HeaderComponent } from './ui/header/header.component'; import { HistoryModule } from '../history/history.module'; import { ParserFormComponent } from './ui/parser-form/parser-form.component'; +import { GoButtonComponent } from './ui/go-button/go-button.component'; +import { parserProviders } from './data-access/parser.providers'; +import { LinkParserService } from './data-access/link-parser.service'; +import { LinkParserFacade, LinkInitFacade, NavigationFacade, FileNetFacade } from './ui/parser-form/facades'; +const FACADES = [ + LinkParserFacade, + LinkInitFacade, + NavigationFacade, + FileNetFacade +]; @NgModule({ declarations: [ @@ -20,7 +30,8 @@ import { ParserFormComponent } from './ui/parser-form/parser-form.component'; SettingsComponent, FooterComponent, HeaderComponent, - ParserFormComponent + ParserFormComponent, + GoButtonComponent ], imports: [ CommonModule, @@ -28,6 +39,9 @@ import { ParserFormComponent } from './ui/parser-form/parser-form.component'; FormsModule, SharedModule, HistoryModule + ], + providers: [ + ...FACADES ] }) export class LinkParserModule { } diff --git a/src/app/link-parser/link-parser/link-parser.component.ts b/src/app/link-parser/link-parser/link-parser.component.ts index 9c3cfb3..6c72842 100644 --- a/src/app/link-parser/link-parser/link-parser.component.ts +++ b/src/app/link-parser/link-parser/link-parser.component.ts @@ -16,7 +16,6 @@ import { FileService } from '../../file/data-access/file.service'; './themes/halloween.scss', './themes/newyear.scss', './themes/valentine.scss' - ], standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, @@ -34,7 +33,7 @@ export class LinkParserComponent { constructor() { } ngOnInit() { - // this.initMeta() + this.initMeta() this.lang.langChanged$.pipe(take(1)).subscribe(() => { this.initMeta() }); diff --git a/src/app/link-parser/ui/go-button/go-button.component.html b/src/app/link-parser/ui/go-button/go-button.component.html new file mode 100644 index 0000000..eb7c85f --- /dev/null +++ b/src/app/link-parser/ui/go-button/go-button.component.html @@ -0,0 +1,9 @@ +@if (data()) { + + {{ ph().letsgo }} + + + {{ data().id }} + + +} \ No newline at end of file diff --git a/src/app/link-parser/ui/go-button/go-button.component.scss b/src/app/link-parser/ui/go-button/go-button.component.scss new file mode 100644 index 0000000..2de6e19 --- /dev/null +++ b/src/app/link-parser/ui/go-button/go-button.component.scss @@ -0,0 +1,37 @@ +.go-btn { + display: flex; + gap: 1ch; + align-items: center; + --dot-color: var(--border-color); + border: var(--border-size) solid var(--dot-color); + --stroke: #002741; + --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0); + --bg-1: var(--gl) 0px 0px / 8px 8px; + --bg-2: var(--gl) 0px 0px / 4px 4px, var(--gl) 1.5px 1.5px / 4px 4px; + background: var(--bg-1); + + text-transform: uppercase; + font-weight: bold; + box-shadow: var(--shadow-2); + + @media (prefers-color-scheme: light) { + --dot-color: #166496; + color: #166496; + --stroke: #eceff2; + } + + &:hover, + &:focus { + background: var(--bg-2); + color: var(--accent); + } + + &:active { + box-shadow: 0 0 transparent; + } +} + +.favicon { + width: 1.25rem; + aspect-ratio: 1; +} \ No newline at end of file diff --git a/src/app/link-parser/ui/go-button/go-button.component.ts b/src/app/link-parser/ui/go-button/go-button.component.ts new file mode 100644 index 0000000..5ff2c18 --- /dev/null +++ b/src/app/link-parser/ui/go-button/go-button.component.ts @@ -0,0 +1,14 @@ +import { Component, input } from '@angular/core'; +import { Phrases } from '../../../shared/utils/phrases'; + +@Component({ + selector: 'app-go-button', + standalone: false, + + templateUrl: './go-button.component.html', + styleUrl: './go-button.component.scss' +}) +export class GoButtonComponent { + data = input(); + ph = input.required(); +} diff --git a/src/app/link-parser/ui/parser-form/facades/file-net.facade.ts b/src/app/link-parser/ui/parser-form/facades/file-net.facade.ts new file mode 100644 index 0000000..8797b55 --- /dev/null +++ b/src/app/link-parser/ui/parser-form/facades/file-net.facade.ts @@ -0,0 +1,22 @@ +import { Injectable, computed } from '@angular/core'; +import { FileService } from '../../../../file/data-access/file.service'; +import { NetworkService } from '../../../../shared/data-access/network.service'; +import { LangService } from '../../../../shared/data-access/lang.service'; + +@Injectable({ providedIn: 'root' }) +export class FileNetFacade { + constructor( + public file: FileService, + public net: NetworkService, + private lang: LangService + ) { } + + readonly openFileLabel = computed(() => { + const ph = this.lang.ph(); + const label = this.net.online() + ? `๐Ÿ“ƒ ${ph.orOpenFile}` + : `๐Ÿ“ƒ ${ph.openFile}`; + + return `${label} (${this.file.supportFiles()})`; + }); +} \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/facades/index.ts b/src/app/link-parser/ui/parser-form/facades/index.ts new file mode 100644 index 0000000..35c1b15 --- /dev/null +++ b/src/app/link-parser/ui/parser-form/facades/index.ts @@ -0,0 +1,4 @@ +export * from './link-init.facade'; +export * from './link-parser.facade'; +export * from './navigation.facade'; +export * from './file-net.facade'; \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts b/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts new file mode 100644 index 0000000..0f599d5 --- /dev/null +++ b/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts @@ -0,0 +1,39 @@ +import { Injectable, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { LinkParserSettingsService } from '../../../data-access/link-parser-settings.service'; +import { LinkParserFacade } from './link-parser.facade'; + +@Injectable() +export class LinkInitFacade { + private route = inject(ActivatedRoute); + public setts = inject(LinkParserSettingsService); + private linkFacade = inject(LinkParserFacade); + + async init() { + const routeUrl = this.route.snapshot.paramMap.get('url'); + const queryUrl = this.route.snapshot.queryParamMap.get('url'); + + if (routeUrl) { + this.linkFacade.setLink(routeUrl); + return 'route'; + } + + if (queryUrl) { + this.linkFacade.setLink(queryUrl); + return 'query'; + } + + if (this.setts.autoPasteLink && this.setts.autoPasteLink()) { + try { + const text = await navigator.clipboard.readText(); + this.linkFacade.setLink(text ?? ''); + if (!this.linkFacade.linkParams()) this.linkFacade.clear(); + return 'clipboard'; + } catch { + return 'none'; + } + } + + return 'none'; + } +} \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts b/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts new file mode 100644 index 0000000..bdf2661 --- /dev/null +++ b/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts @@ -0,0 +1,55 @@ +// link-parser.facade.ts +import { Injectable, computed, signal, Signal } from '@angular/core'; +import { LinkParserService } from '../../../data-access/link-parser.service'; +import { Base64 } from '../../../../shared/utils'; +import { LinkParseResult } from '../../../utils'; + +const FAVICONS: Map = new Map([ + ['zenko', '//zenko.online/favicon.ico'], + ['reddit', '//reddit.com/favicon.ico'], + ['imgur', '//imgur.com/favicon.ico'], + ['mangadex', '//mangadex.org/favicon.ico'], + ['telegraph', '//telegra.ph/favicon.ico'], + ['nhentai', '//nhentai.net/favicon.ico'], + ['yandere', '//yande.re/favicon.ico'], + ['pixiv', '//pixiv.net/favicon.ico'], + ['imgchest', '//imgchest.com/assets/img/favicons/favicon-32x32.png?v=2'], + ['read', 'data:image/svg+xml,๐Ÿ—ฏ๏ธ'] +]); + +@Injectable() +export class LinkParserFacade { + constructor(private parser: LinkParserService) { } + + readonly link = signal(''); + + readonly linkParams: Signal = computed(() => + this.parser.parse(this.link()) + ); + + readonly linkData = computed(() => { + const params = this.linkParams(); + if (!params) return null; + + return { + site: params.site, + id: params.id, + id64: Base64.toBase64(params.id), + favicon: FAVICONS.get(params.site) + }; + }); + + setLink(value: string) { + this.link.set(value); + } + + inputLink(event: Event) { + const v: string = (event.target as HTMLInputElement).value; + + this.setLink(v) + } + + clear() { + this.setLink(''); + } +} \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts b/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts new file mode 100644 index 0000000..ac72eef --- /dev/null +++ b/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { LinkParserFacade } from './link-parser.facade'; + +@Injectable() +export class NavigationFacade { + constructor(private router: Router, private linkFacade: LinkParserFacade) { } + + goToParsedLink() { + const data = this.linkFacade.linkData(); + if (!data) return; + this.router.navigateByUrl(`/${data.site}/${data.id64}`); + } +} \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.html b/src/app/link-parser/ui/parser-form/parser-form.component.html index fa4fffe..87f690e 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.html +++ b/src/app/link-parser/ui/parser-form/parser-form.component.html @@ -1,6 +1,5 @@ @let ph = lang.ph(); -@let online = net.online(); -@let openFileLabel = (online) ? '๐Ÿ“ƒ '+ph.orOpenFile: '๐Ÿ“ƒ '+ph.openFile; +@let online = fileNetFacade.net.online();
@if (online === false ) { @@ -9,37 +8,21 @@
}

- +

@if (online) { -
+ + autofocus [placeholder]="'๐Ÿ”— '+ph.enterLink" (input)="linkFacade.inputLink($event)" [value]="linkFacade.link()">
} - - +

- @if (setts.seasonalTheme != undefined && setts.seasonalTheme()) { - @let theme = seasonalTheme().get(setts.theme()); - @if(theme) { - {{ph.getByKey(theme.phrase)}} {{theme?.emoji}} - } - } - @else { - {{ph.slogan}} - } +

- @if (linkParams()) { - - {{ph.letsgo}} - - {{linkParams()?.id | truncate}} - - } +
\ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.scss b/src/app/link-parser/ui/parser-form/parser-form.component.scss index 3d67cda..ba828dd 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.scss +++ b/src/app/link-parser/ui/parser-form/parser-form.component.scss @@ -98,10 +98,7 @@ input[type=url]::placeholder { opacity: 0.6; } -.favicon { - width: 1.25rem; - aspect-ratio: 1; -} + .site-name { text-transform: uppercase; @@ -127,38 +124,7 @@ input[type=url]::placeholder { text-wrap: balance; } -.go-btn { - display: flex; - gap: 1ch; - align-items: center; - --dot-color: var(--border-color); - border: var(--border-size) solid var(--dot-color); - --stroke: #002741; - --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0); - --bg-1: var(--gl) 0px 0px / 8px 8px; - --bg-2: var(--gl) 0px 0px / 4px 4px, var(--gl) 1.5px 1.5px / 4px 4px; - background: var(--bg-1); - - text-transform: uppercase; - font-weight: bold; - box-shadow: var(--shadow-2); - - @media (prefers-color-scheme: light) { - --dot-color: #166496; - color: #166496; - --stroke: #eceff2; - } - &:hover, - &:focus { - background: var(--bg-2); - color: var(--accent); - } - - &:active { - box-shadow: 0 0 transparent; - } -} .offline-banner { border-radius: .5ch; diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.ts b/src/app/link-parser/ui/parser-form/parser-form.component.ts index cad0761..5df1f2e 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.ts +++ b/src/app/link-parser/ui/parser-form/parser-form.component.ts @@ -1,13 +1,6 @@ -import { ChangeDetectionStrategy, Component, computed, inject, PLATFORM_ID, signal, Signal, WritableSignal } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { LangService } from '../../../shared/data-access/lang.service'; -import { Base64 } from '../../../shared/utils'; -import { LinkParserSettingsService } from '../../data-access/link-parser-settings.service'; -import { LinkParserService } from '../../data-access/link-parser.service'; -import { ImgurLinkParser, MangadexLinkParser, TelegraphLinkParser, RedditLinkParser, ZenkoLinkParser, NhentaiLinkParser, YandereParser, PixivLinkParser, JsonLinkParser } from '../../utils'; -import { ImgchestLinkParser } from '../../utils/imgchest-link-parser'; -import { NetworkService, BrowserService } from '../../../shared/data-access/'; -import { FileService } from '../../../file/data-access/file.service'; +import { FileNetFacade, LinkInitFacade, LinkParserFacade, NavigationFacade } from './facades'; @Component({ selector: 'app-parser-form', @@ -17,114 +10,17 @@ import { FileService } from '../../../file/data-access/file.service'; changeDetection: ChangeDetectionStrategy.OnPush }) export class ParserFormComponent { - private router: Router = inject(Router); - private route: ActivatedRoute = inject(ActivatedRoute); - file = inject(FileService); - setts = inject(LinkParserSettingsService) - net = inject(NetworkService); - browser = inject(BrowserService); - platformId = inject(PLATFORM_ID) - link: WritableSignal = signal(''); - linkParams: Signal = computed(() => this.parser.parse(this.link())); - linkParams64: Signal = computed(() => { - const foo = this.linkParams() - return { - site: foo.site, - id: Base64.toBase64(foo.id) - }; - }); - - osAcceptSupport = signal(["Windows", "Android", "Linux"].includes(this.browser.brouserInfo().os)); - - constructor(public parser: LinkParserService, public lang: LangService) { - this.initParser(); - } - - initParser() { - this.parser.parsers = []; - this.parser.parsers.push(new ImgurLinkParser) - this.parser.parsers.push(new MangadexLinkParser) - this.parser.parsers.push(new TelegraphLinkParser) - this.parser.parsers.push(new RedditLinkParser) - this.parser.parsers.push(new ZenkoLinkParser) - this.parser.parsers.push(new NhentaiLinkParser) - // this.parser.parsers.push(new ComickLinkParser) - this.parser.parsers.push(new YandereParser) - this.parser.parsers.push(new PixivLinkParser) - this.parser.parsers.push(new ImgchestLinkParser) - // this.parser.parsers.push(new BlankaryLinkParser) - this.parser.parsers.push(new JsonLinkParser) - } - - inputLink(event: Event) { - const v: string = (event.target as HTMLInputElement).value; - - this.link.set(v) - } + protected lang = inject(LangService); + protected linkFacade = inject(LinkParserFacade); + protected navFacade = inject(NavigationFacade); + protected fileNetFacade = inject(FileNetFacade); + protected linkInit = inject(LinkInitFacade); ngOnInit() { - this.initUrl() - } - - async initFromclipboard() { - try { - const text = await navigator.clipboard?.readText() - this.link.set(text ?? '') - } catch (error) { } - - if (!this.linkParams()) { this.link.set('') } - } - - initUrl() { - - const routeParamUrl: string | null = this.route.snapshot.paramMap.get('url'); - - if (routeParamUrl) { - - this.link.set(routeParamUrl); - - this.onSubmit(); - - return; - } - - const queryParamUrl: string | null = this.route.snapshot.queryParamMap.get('url'); - - if (queryParamUrl) { - this.link.set(queryParamUrl ?? '') - } else { - if (this.setts.autoPasteLink && this.setts.autoPasteLink()) this.initFromclipboard(); - } - } - - onSubmit() { - if (!this.linkParams) return; - - const link = `/${this.linkParams().site}/${this.linkParams64().id}` - - this.router.navigateByUrl(link); + this.linkInit.init().then(source => { + if (source === 'route') { + this.navFacade.goToParsedLink(); + } + }); } - - favicons: any = { - zenko: '//zenko.online/favicon.ico', - reddit: '//reddit.com/favicon.ico', - imgur: '//imgur.com/favicon.ico', - mangadex: '//mangadex.org/favicon.ico', - telegraph: '//telegra.ph/favicon.ico', - nhentai: '//nhentai.net/favicon.ico', - // comick: '//comick.art/favicon.ico', - yandere: '//yande.re/favicon.ico', - pixiv: '//pixiv.net/favicon.ico', - imgchest: '//imgchest.com/assets/img/favicons/favicon-32x32.png?v=2', - // blankary: '//blankary.com/favicon.ico', - read: 'data:image/svg+xml,๐Ÿ—ฏ๏ธ' - } - - seasonalTheme = signal(new Map([ - ["pride", { class: 'slogan-rainbow', phrase: "sloganPride", emoji: '๐Ÿณ๏ธโ€๐ŸŒˆ' }], - ["halloween", { class: 'slogan-halloween', phrase: 'sloganHalloween', emoji: '๐Ÿ•ท๏ธ' }], - ["newyear", { class: 'slogan-newyear', phrase: 'sloganNewYear', emoji: '๐ŸŽ‡' }], - ["valentine", { class: 'slogan-valentine', phrase: 'sloganValentine', emoji: 'โค๏ธ๐Ÿ“–' }] - ])); - } diff --git a/src/app/list/list-shell/list-shell.component.ts b/src/app/list/list-shell/list-shell.component.ts index 632842e..07d609d 100644 --- a/src/app/list/list-shell/list-shell.component.ts +++ b/src/app/list/list-shell/list-shell.component.ts @@ -1,10 +1,8 @@ -import { Component, EffectCleanupRegisterFn, WritableSignal, computed, effect, inject, signal } from '@angular/core'; +import { Component, WritableSignal, computed, inject, signal } from '@angular/core'; import { LinkParserService } from '../../link-parser/data-access/link-parser.service'; -import { BlankaryLinkParser, ImgurLinkParser, JsonLinkParser, LinkParser, MangadexLinkParser, NhentaiLinkParser, PixivLinkParser, RedditLinkParser, TelegraphLinkParser, YandereParser, ZenkoLinkParser } from '../../link-parser/utils'; +import { JsonLinkParser, LinkParser } from '../../link-parser/utils'; import { DomManipulationService } from '../../shared/data-access'; import { LangService } from '../../shared/data-access/lang.service'; -import { ComickLinkParser } from '../../link-parser/utils/comick-link-parser'; -import { ImgchestLinkParser } from '../../link-parser/utils/imgchest-link-parser'; @Component({ @@ -75,29 +73,11 @@ export class ListShellComponent { } } - constructor() { - this.initParser(); - } + constructor() { } firstLink = computed(() => this.listValue()[0] ?? ''); parsedFirstLink = computed(() => this.parser.parse(this.firstLink() ?? '')) - initParser() { - this.parser.parsers = []; - this.parser.parsers.push(new ImgurLinkParser) - this.parser.parsers.push(new MangadexLinkParser) - this.parser.parsers.push(new TelegraphLinkParser) - this.parser.parsers.push(new RedditLinkParser) - this.parser.parsers.push(new ZenkoLinkParser) - this.parser.parsers.push(new NhentaiLinkParser) - this.parser.parsers.push(new ComickLinkParser) - this.parser.parsers.push(new YandereParser) - this.parser.parsers.push(new PixivLinkParser) - this.parser.parsers.push(new ImgchestLinkParser) - // this.parser.parsers.push(new BlankaryLinkParser) - this.parser.parsers.push(new JsonLinkParser) - } - copy() { this.domMan.copyToClipboard(JSON.stringify(this.outputValue())) } diff --git a/src/app/list/list.module.ts b/src/app/list/list.module.ts index 6ebf97d..58314f0 100644 --- a/src/app/list/list.module.ts +++ b/src/app/list/list.module.ts @@ -5,6 +5,8 @@ import { ListRoutingModule } from './list-routing.module'; import { ListShellComponent } from './list-shell/list-shell.component'; import { FormsModule } from '@angular/forms'; import { SharedModule } from '../shared/shared.module'; +import { LinkParserService } from '../link-parser/data-access/link-parser.service'; +import { parserProviders } from '../link-parser/data-access/parser.providers'; @NgModule({ diff --git a/src/app/shared/data-access/dom-manipulation.service.ts b/src/app/shared/data-access/dom-manipulation.service.ts index 9328adf..9e8dd81 100644 --- a/src/app/shared/data-access/dom-manipulation.service.ts +++ b/src/app/shared/data-access/dom-manipulation.service.ts @@ -99,4 +99,8 @@ export class DomManipulationService { this._lastHover = el; } + + isInteractiveElement(el: HTMLElement) { + return ['INPUT', 'TEXTAREA', 'SELECT', 'SUMMARY'].includes(el.nodeName); + } } diff --git a/src/app/shared/data-access/viewer.service.ts b/src/app/shared/data-access/viewer.service.ts index 38f8fd9..9b635db 100644 --- a/src/app/shared/data-access/viewer.service.ts +++ b/src/app/shared/data-access/viewer.service.ts @@ -84,10 +84,7 @@ export class ViewerService { const currentOpt = this.viewModeOption(); const currentIndex = this.viewModeOptions.indexOf(currentOpt); const nextIndex = (currentIndex + 1) % this.viewModeOptions.length; - - const nextOpt = this.viewModeOptions[nextIndex]; - console.log(currentIndex, this.viewModeOptions, nextOpt.hintPhraceKey); this.setViewModeOption(nextOpt); } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index f2c79fa..62ccd91 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TruncatePipe } from './utils/truncate.pipe'; import { TextEmbracerComponent } from './ui/text-embracer/text-embracer.component'; -import { ViewerComponent } from './ui/viewer/viewer.component'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { WarmFilterComponent } from './ui/warm-filter/warm-filter.component'; @@ -17,46 +16,25 @@ import { LangToggleComponent } from './ui/lang-toggle/lang-toggle.component'; import { TitleCardComponent } from './ui/title-card/title-card.component'; import { LoadingComponent } from './ui/loading/loading.component'; import { SeparatorComponent } from './ui/separator/separator.component'; -import { MangaPageComponent } from './ui/manga-page/manga-page.component'; -import { HintPageComponent } from './ui/viewer/components/hint-page/hint-page.component'; -import { ViewerFooterComponent } from './ui/viewer/components/viewer-footer/viewer-footer.component'; -import { ViewerHeaderComponent } from './ui/viewer/components/viewer-header/viewer-header.component'; -import { MangaPageEvenComponent } from './ui/manga-page/manga-page-even.component'; import { FileChangeComponent } from './ui/file-change/file-change.component'; import { ChytankaLogoWithTagsComponent } from './ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component'; import { FileSizePipe } from './pipes/filesize.pipe'; import { RoughPaperComponent } from './ui/filters/rough-paper/rough-paper.component'; import { SharpenComponent } from './ui/filters/sharpen/sharpen.component'; -import { ThanksPageComponent } from './ui/viewer/components/thanks-page/thanks-page.component'; import { ImgMetaDirective } from './directives/img-meta.directive'; import { NewTabDirective } from './directives/new-tab.directive'; import { VibrateHapticDirective } from './directives/vibrate-haptic.directive'; import { GamepadCursorComponent } from './ui/gamepad-cursor/gamepad-cursor.component'; import { SircleBlurComponent } from './ui/filters/sircle-blur/sircle-blur.component'; -import { PageComponent } from './ui/viewer/components/page/page.component'; -import { EpisodeInfoTableComponent } from './ui/viewer/components/episode-info-table/episode-info-table.component'; -import { EpisodeShareFormComponent } from './ui/viewer/components/episode-share-form/episode-share-form.component'; -import { EpisodeDownloadFormComponent } from './ui/viewer/components/episode-download-form/episode-download-form.component'; import { DropZoneComponent } from './ui/drop-zone/drop-zone.component'; +import { SourceCopyrightComponent } from './ui/source-copyright/source-copyright.component'; +import { SourceCopyrightLogoComponent } from './ui/source-copyright-logo/source-copyright-logo.component'; +import { SloganComponent } from './ui/slogan/slogan.component'; -const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, PageComponent, EpisodeInfoTableComponent, EpisodeShareFormComponent, EpisodeDownloadFormComponent, DropZoneComponent] +const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, DropZoneComponent, SourceCopyrightComponent, SourceCopyrightLogoComponent, SloganComponent, NsfwWarningComponent, ImgMetaDirective, NewTabDirective, PagesIndicatorComponent, WarmFilterComponent, WarmControlComponent] @NgModule({ - declarations: [ - WarmFilterComponent, - WarmControlComponent, - PagesIndicatorComponent, - NsfwWarningComponent, - MangaPageComponent, - HintPageComponent, - ViewerFooterComponent, - ViewerHeaderComponent, - MangaPageEvenComponent, - ThanksPageComponent, - ImgMetaDirective, - NewTabDirective, - ...components - ], + declarations: [...components], imports: [ CommonModule, FormsModule, diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html index af75abe..d49b595 100644 --- a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html @@ -1,6 +1,6 @@ @let online = net.isReallyOnline(); -
+
diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss index 98b2203..2f84823 100644 --- a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss @@ -152,4 +152,19 @@ ul { --b: #4CAF50; --f: #FFC107; } +} + +.svg-wrap { + position: relative; + display: grid; + place-items: center; + max-width: 100%; + max-height: 100%; + min-height: 0; + + svg { + max-width: 100%; + max-height: 100%; + margin: auto; + } } \ No newline at end of file diff --git a/src/app/shared/ui/dialog/dialog.component.html b/src/app/shared/ui/dialog/dialog.component.html index 4e5f8f0..9283c56 100644 --- a/src/app/shared/ui/dialog/dialog.component.html +++ b/src/app/shared/ui/dialog/dialog.component.html @@ -1,6 +1,7 @@ + @if (gamepad.connected()) { - + }

{{title()}}

diff --git a/src/app/shared/ui/slogan/slogan.component.html b/src/app/shared/ui/slogan/slogan.component.html new file mode 100644 index 0000000..76e64b1 --- /dev/null +++ b/src/app/shared/ui/slogan/slogan.component.html @@ -0,0 +1,6 @@ + + {{ slogan() }} + +@if (sloganEmoji()) { +{{ sloganEmoji() }} +} \ No newline at end of file diff --git a/src/app/viewer/components/page/page.component.scss b/src/app/shared/ui/slogan/slogan.component.scss similarity index 100% rename from src/app/viewer/components/page/page.component.scss rename to src/app/shared/ui/slogan/slogan.component.scss diff --git a/src/app/shared/ui/slogan/slogan.component.ts b/src/app/shared/ui/slogan/slogan.component.ts new file mode 100644 index 0000000..f1bdaa1 --- /dev/null +++ b/src/app/shared/ui/slogan/slogan.component.ts @@ -0,0 +1,46 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { LinkParserSettingsService } from '../../../link-parser/data-access/link-parser-settings.service'; +import { LangService } from '../../data-access'; + +@Component({ + selector: 'slogan', + standalone: false, + + templateUrl: './slogan.component.html', + styleUrl: './slogan.component.scss' +}) +export class SloganComponent { + private setts = inject(LinkParserSettingsService) + private lang = inject(LangService); + + private seasonalTheme = signal(new Map([ + ["pride", { class: 'slogan-rainbow', phrase: "sloganPride", emoji: '๐Ÿณ๏ธโ€๐ŸŒˆ' }], + ["halloween", { class: 'slogan-halloween', phrase: 'sloganHalloween', emoji: '๐Ÿ•ท๏ธ' }], + ["newyear", { class: 'slogan-newyear', phrase: 'sloganNewYear', emoji: '๐ŸŽ‡' }], + ["valentine", { class: 'slogan-valentine', phrase: 'sloganValentine', emoji: 'โค๏ธ๐Ÿ“–' }] + ])); + + private readonly sloganData = computed(() => { + const defaultSlogan = { + text: this.lang.ph().slogan, + class: null, + emoji: '๐ŸŒป' + }; + + if (!this.setts.seasonalTheme()) return defaultSlogan; + + const theme = this.seasonalTheme().get(this.setts.theme()); + + if (!theme) return defaultSlogan; + + return { + text: this.lang.ph().getByKey(theme.phrase), + class: theme.class, + emoji: theme.emoji + }; + }); + + protected readonly slogan = computed(() => this.sloganData().text); + protected readonly sloganClass = computed(() => this.sloganData().class); + protected readonly sloganEmoji = computed(() => this.sloganData().emoji); +} diff --git a/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.html b/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.html new file mode 100644 index 0000000..0abca01 --- /dev/null +++ b/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.scss b/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.scss new file mode 100644 index 0000000..de6a5b4 --- /dev/null +++ b/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.scss @@ -0,0 +1,9 @@ +a { + display: flex; gap: 1ch; +} + +img { + display: block; + width: auto; + max-height: 48px; +} \ No newline at end of file diff --git a/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.ts b/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.ts new file mode 100644 index 0000000..116840d --- /dev/null +++ b/src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.ts @@ -0,0 +1,14 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'source-copyright-logo', + standalone: false, + + templateUrl: './source-copyright-logo.component.html', + styleUrl: './source-copyright-logo.component.scss' +}) +export class SourceCopyrightLogoComponent { + sourceUrl = input.required(); + sourceImageSrc = input.required(); + sourceName = input.required(); +} diff --git a/src/app/shared/ui/source-copyright/source-copyright.component.html b/src/app/shared/ui/source-copyright/source-copyright.component.html new file mode 100644 index 0000000..8becbde --- /dev/null +++ b/src/app/shared/ui/source-copyright/source-copyright.component.html @@ -0,0 +1,3 @@ +

{{lang.ph().imagesVia}}{{sourceName()}} + API. + {{lang.ph().thanks}}
{{lang.ph().detalisCopy}}

\ No newline at end of file diff --git a/src/app/shared/ui/source-copyright/source-copyright.component.scss b/src/app/shared/ui/source-copyright/source-copyright.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/ui/source-copyright/source-copyright.component.ts b/src/app/shared/ui/source-copyright/source-copyright.component.ts new file mode 100644 index 0000000..60fa7be --- /dev/null +++ b/src/app/shared/ui/source-copyright/source-copyright.component.ts @@ -0,0 +1,16 @@ +import { Component, inject, input } from '@angular/core'; +import { LangService } from '../../data-access'; + +@Component({ + selector: 'source-copyright', + standalone: false, + + templateUrl: './source-copyright.component.html', + styleUrl: './source-copyright.component.scss' +}) +export class SourceCopyrightComponent { + protected lang: LangService = inject(LangService) + + sourceUrl = input.required(); + sourceName = input.required(); +} diff --git a/src/app/shared/ui/viewer/viewer.component.html b/src/app/shared/ui/viewer/viewer.component.html deleted file mode 100644 index d1d50cb..0000000 --- a/src/app/shared/ui/viewer/viewer.component.html +++ /dev/null @@ -1,50 +0,0 @@ -@let dir = viewer.viewModeOption().dir; -@let mode = viewer.viewModeOption().mode; -@let nsfw = episode().nsfw; - -
- - - - - - @for(img of episode().images; track img.src; let i = $index) { - - } @empty { - ๐Ÿ‘€ - } - - - - - - - - - @if((episode().images.length || 0) % 2 > 0) { - - } -
- -@defer{ -@if (episode()) { - -} -} - -@defer{ - -} - -@defer{ } \ No newline at end of file diff --git a/src/app/shared/ui/viewer/viewer.component.ts b/src/app/shared/ui/viewer/viewer.component.ts deleted file mode 100644 index 2a055d2..0000000 --- a/src/app/shared/ui/viewer/viewer.component.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, input, PLATFORM_ID, Signal, ViewChild, WritableSignal, computed, effect, inject, output, signal } from '@angular/core'; -import { CompositionEpisode } from '../../../@site-modules/@common-read'; -import { ViewerService, DomManipulationService } from '../../data-access'; -import { ActivatedRoute, Router } from '@angular/router'; -import { LangService } from '../../data-access/lang.service'; -import { DomSanitizer } from '@angular/platform-browser'; -import { isPlaylist, Playlist, PlaylistItem } from '../../../playlist/data-access/playlist.service'; -import { EmbedHalperService } from '../../data-access/embed-halper.service'; -import { DownloadService } from '../../data-access/download.service'; -import { DOCUMENT, isPlatformBrowser, isPlatformServer } from '@angular/common'; -import { VibrationService } from '../../data-access/vibration.service'; -import { GamepadService } from '../../data-access/gamepad.service'; -import { GamepadButton } from '../../models'; - -const CHTNK_LOAD_EVENT_NAME = 'chtnkload' -const CHTNK_CHANGE_PAGE_EVENT_NAME = 'changepage'; -const CHTNK_NSFW_CHOICE_EVENT_NAME = 'nsfwchoice' -const CHTNK_LIST_RESPONCE_EVENT_NAME = 'listresponse' -const CHTNK_LIST_REQUEST_EVENT_NAME = 'listrequest' - -@Component({ - selector: 'app-viewer', - templateUrl: './viewer.component.html', - styleUrls: [ - './viewer.component.scss', - './viewer.pages.component.scss', - './viewer.long.component.scss', - '../../../shared/ui/@styles/details.scss', - '../../../shared/ui/@styles/input-group.scss' - ], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false -}) -export class ViewerComponent implements AfterViewInit { - readonly separator: string = 'โ”‚' - showNsfw: WritableSignal = signal(false); - gamepad = inject(GamepadService); - - pagechange = output<{ total: number, current: number[] }>() - - episode = input({ - title: '', - images: [] - }); - playlistLink = input(""); - currentPlaylistItem = input(); - - platformId = inject(PLATFORM_ID) - private readonly document = inject(DOCUMENT); - vibration = inject(VibrationService); - - private _playlist = signal([]); - - playlistInput = input([]); - playlist = computed(() => { - const inputList = this.playlistInput(); - return inputList.length ? inputList : this._playlist(); - }); - - initListFromParrentWindow() { - if (!this.embedHelper.isEmbedded() || !isPlatformBrowser(this.platformId)) return - - this.embedHelper.postMessage(this.currentPlaylistItem(), CHTNK_LIST_REQUEST_EVENT_NAME); - - window.addEventListener('message', ({ data }) => { - if (data.event != CHTNK_LIST_RESPONCE_EVENT_NAME) return; - - if (isPlaylist(data.data)) { - this._playlist.set(data.data); - this.cdr.detectChanges(); - } else { - console.warn('Received data is not a valid Playlist', data.data); - } - - }, false); - } - - getCyrrentIndex() { - for (let i = 0; i < this.playlist.length; i++) { - const item = this.playlist()[i]; - if (this.currentPlaylistItem()?.id == item.id && this.currentPlaylistItem()?.site == item.site) - return i; - } - - return -1; - } - - getNextIndex() { - const index = this.getCyrrentIndex(); - if (index < 0) return -1; - - return ((index + 1) < this.playlist.length) ? index + 1 : -1; - } - - getPrevIndex() { - const index = this.getCyrrentIndex(); - if (index < 0) return -1; - - return ((index - 1) >= 0) ? (index - 1) : -1; - } - - @ViewChild('viewRef', { static: true }) viewRef!: ElementRef; - - constructor(private el: ElementRef, public viewer: ViewerService, private dm: DomManipulationService, private router: Router, public lang: LangService) { - this.initHotKeys() - - effect(() => { - for (const [btn, action] of Object.entries(this.gamepadActionMap)) { - if (this.gamepad.buttons()[parseInt(btn)]?.pressed) action(); - } - }) - - if (isPlatformServer(this.platformId)) return; - - addEventListener("fullscreenchange", (event) => { - this.isFullScreen.set(this.document.fullscreenElement === this.el.nativeElement) - }) - } - - private gamepadActionMap: Record = { - [GamepadButton.L1]: () => this.scrollLeft(), - [GamepadButton.R1]: () => this.scrollRight(), - [GamepadButton.DPadLeft]: () => this.scrollLeft(), - [GamepadButton.DPadRight]: () => this.scrollRight(), - [GamepadButton.DPadUp]: () => this.scrollUp(), - [GamepadButton.DPadDown]: () => this.scrollDown(), - [GamepadButton.Square]: () => this.toggleFullScreen(), - [GamepadButton.Options]: () => this.toggleOverlay(), - [GamepadButton.Triangle]: () => this.toggleViewModeOption(), - }; - private _isViewOptToggle = false; - toggleViewModeOption() { - if (!this._isViewOptToggle) { - this.viewer.toggleViewModeOption(); - this.viewer.saveViewModeOption(); - setTimeout(() => { this._isViewOptToggle = false }, 100); - } - - this._isViewOptToggle = true; - } - - // TODO: Fix scroll position reset after exiting fullscreen in pages mode - toggleFullScreen = () => { - // const activeIndexs = this.activeIndexs(); - // const page = (activeIndexs.length == 1) ? activeIndexs[0] : activeIndexs.filter(v => v+1 % 2 != 0)[0]; - // console.log(activeIndexs, page); - - this.dm.toggleFullScreen(this.el.nativeElement); - - // if (page != undefined) - // setTimeout(() => {this.onActive(page)}, 100); - } - isFullScreen = signal(this.document.fullscreenElement === this.el.nativeElement); - - showOverlay = signal(false); - toggleOverlay = () => this.showOverlay.update(v => !v); - - viewElement: WritableSignal = signal(this.document.createElement('div')); - imageElements: Signal> = computed(() => this.viewElement().querySelectorAll('.page img[id*=page_]')); - imgsPos: any[] = [] - - ngAfterViewInit() { - this.viewElement.set(this.viewRef.nativeElement); - this.initActiveIndexes() - this.embedHelper.postMessage(this.currentPlaylistItem(), CHTNK_LOAD_EVENT_NAME); - this.initListFromParrentWindow(); - } - - activeIndexs: WritableSignal = signal([]) - initActiveIndexes() { - if (!isPlatformBrowser(this.platformId)) return - - const isPageMode = this.viewer.viewModeOption().mode == 'pages'; - - const viewRect: DOMRect = isPageMode - ? this.viewElement().getBoundingClientRect() - : this.el.nativeElement.getBoundingClientRect(); - - let activeIndxs: number[] = []; - - for (let i = 0; i < this.imageElements().length; i++) { - const img = this.imageElements()[i]; - const rect = img.getBoundingClientRect(); - - const hor = rect.right > viewRect.x && rect.right < viewRect.x + viewRect.width + 1; - - const ver = rect.top < viewRect.height && rect.bottom > viewRect.top - - if (isPageMode ? hor : ver) { - activeIndxs.push(i) - } - - } - - // this.showOverlay = false; - // console.log(); - - // if (JSON.stringify(this.activeIndexs()) !== JSON.stringify(activeIndxs)) - this.activeIndexs.set(activeIndxs); - - - const total = this.episode()?.images.length - const current = activeIndxs.map(i => i + 1) - - this.embedHelper.postMessage({ total, current }, CHTNK_CHANGE_PAGE_EVENT_NAME); - - this.pagechange.emit({ total: Number(total), current }) - - } - - isScrollStart: boolean = false; - - @HostListener('scroll', ['$event']) - onScroll(event: Event) { - this.initActiveIndexes() - - if (!this.isScrollStart) { - this.isScrollStart = true; - this.vibration.vibrate(10); - } - } - - @HostListener('scrollend', ['$event']) - onScrollEnd(event: Event) { - this.vibration.vibrate([5, 5, 10]); - this.isScrollStart = false; - } - - @HostListener('window:resize', ['$event']) - onResize(event: Event) { - this.initActiveIndexes() - } - - private hotKeys = new Map() - - private initHotKeys() { - this.hotKeys.set('KeyF', this.toggleFullScreen) - this.hotKeys.set('KeyE', this.toggleOverlay) - this.hotKeys.set('KeyA', () => this.scrollLeft()) - this.hotKeys.set('KeyD', () => this.scrollRight()) - this.hotKeys.set('KeyW', () => this.scrollUp()) - this.hotKeys.set('KeyS', () => this.scrollDown()) - } - - @HostListener('window:keydown', ['$event']) - handleKeyboardEvent(event: KeyboardEvent) { - this.domMan.setHotkeys(event, this.hotKeys) - } - - private readonly verAmount = 256; - - private scrollLeft() { - this.dm.scrollHor(this.viewElement(), -this.el.nativeElement.clientWidth) - } - - private scrollRight() { - this.dm.scrollHor(this.viewElement(), this.el.nativeElement.clientWidth) - } - - private scrollUp() { - this.dm.scrollVer(this.el.nativeElement, -this.verAmount) - } - private scrollDown() { - this.dm.scrollVer(this.el.nativeElement, this.verAmount) - } - - isDialogOpen = signal(false); - - @HostListener('wheel', ['$event']) - handleWheelEvent(event: WheelEvent): void { - - if (this.isDialogOpen()) return; - - if (this.viewer.viewModeOption().mode != "pages") return; - - const revers: number = this.viewer.viewModeOption().dir == "ltr" ? 1 : -1 - - const scrollAmountX = this.viewElement().clientWidth; - - if (event.deltaY !== 0 && !event.shiftKey) { - this.viewElement().scrollLeft += event.deltaY * revers > 0 ? scrollAmountX : -scrollAmountX; - event.preventDefault(); - } - } - - onActive(pageIndex: number) { - const foo = this.viewElement().querySelector(`#page_${pageIndex + 1}`) - const opt: ScrollIntoViewOptions = { behavior: "smooth", block: "start", inline: "center" } - foo?.scrollIntoView(opt) - } - - showNsfwToggle() { - this.showNsfw.set(!this.showNsfw()) - } - - onViewClick(event: Event) { - if ((event.target as HTMLElement).nodeName === 'INPUT') return; - if ((event.target as HTMLElement).nodeName === 'SUMMARY') return; - - this.toggleOverlay(); - } - onViewDblClick(event: Event) { - if ((event.target as HTMLElement).nodeName === 'INPUT') return; - - this.toggleFullScreen(); - } - - onAgree() { - this.showNsfw.set(true); - this.embedHelper.postMessage(true, CHTNK_NSFW_CHOICE_EVENT_NAME); - } - - onDisagree() { - this.showNsfw.set(false); - this.embedHelper.postMessage(false, CHTNK_NSFW_CHOICE_EVENT_NAME); - - if (!this.embedHelper.isEmbedded()) - this.router.navigate(['/']) - } - - preloadIndexes: Signal = computed(() => this.activeIndexs().map(item => item + 1)); - - preLoad(i: number): boolean { - return (this.preloadIndexes()).includes((i)) - } - - - longPageDetected() { - const longPageCode = "3"; - if (this.viewer.viewModeOption().code != longPageCode) { - this.viewer.setViewModeOptionByCode(longPageCode); - } - } - - - //#region Inject - - route = inject(ActivatedRoute) - domMan = inject(DomManipulationService) - dl: DownloadService = inject(DownloadService); - sanitizer: DomSanitizer = inject(DomSanitizer); - embedHelper = inject(EmbedHalperService); - cdr = inject(ChangeDetectorRef) - - //#endregion - - -} diff --git a/src/app/shared/utils/phrases.ts b/src/app/shared/utils/phrases.ts index 0c4dcb0..d3aa4a6 100644 --- a/src/app/shared/utils/phrases.ts +++ b/src/app/shared/utils/phrases.ts @@ -66,6 +66,7 @@ export class Phrases { sitesHistory = "Sites history" filesHistory = "Files history" noInternet = "No internet connection" + readlist = "Readlist" getByKey = (key: string) => (Object.keys(this).includes(key)) ? this[key as keyof Phrases] : null; static getTemplate(phrase: string, value: string) { diff --git a/src/app/viewer/components/page/page.component.html b/src/app/viewer/components/page/page.component.html deleted file mode 100644 index 95a0b70..0000000 --- a/src/app/viewer/components/page/page.component.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/viewer/components/page/page.component.ts b/src/app/viewer/components/page/page.component.ts deleted file mode 100644 index 8ddb695..0000000 --- a/src/app/viewer/components/page/page.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component, input } from '@angular/core'; - -@Component({ - selector: 'chtnk-page', - imports: [], - templateUrl: './page.component.html', - styleUrl: './page.component.scss' -}) -export class PageComponent {} diff --git a/src/app/viewer/components/pages/pages.component.html b/src/app/viewer/components/pages/pages.component.html deleted file mode 100644 index 95a0b70..0000000 --- a/src/app/viewer/components/pages/pages.component.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/viewer/components/pages/pages.component.scss b/src/app/viewer/components/pages/pages.component.scss deleted file mode 100644 index 4f27991..0000000 --- a/src/app/viewer/components/pages/pages.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -:host.right-to-left, -:host.left-to-right { - display: flex; - overflow-x: auto; - overflow-y: hidden; - scroll-behavior: smooth; - scroll-snap-type: x mandatory; - scrollbar-width: none; - user-select: none; - touch-action: pan-x; -} - -:host.right-to-left, -:host.vertical { - direction: rtl; -} - -:host.left-to-right { - direction: ltr; -} \ No newline at end of file diff --git a/src/app/viewer/components/pages/pages.component.ts b/src/app/viewer/components/pages/pages.component.ts deleted file mode 100644 index 10ab5a0..0000000 --- a/src/app/viewer/components/pages/pages.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'chtnk-pages', - imports: [], - templateUrl: './pages.component.html', - styleUrl: './pages.component.scss' -}) -export class PagesComponent { - -} diff --git a/src/app/viewer/facades/index.ts b/src/app/viewer/facades/index.ts new file mode 100644 index 0000000..dab12cc --- /dev/null +++ b/src/app/viewer/facades/index.ts @@ -0,0 +1,9 @@ +export * from './viewer-nsfw.facade'; +export * from './viewer-embed.facade'; +export * from './viewer-gamepad.facade'; +export * from './viewer-ui.facade'; +export * from './view-mode.facade'; +export * from './page-tracking.facade'; +export * from './viewer-keyboard.facade'; +export * from './viewer-scroll.facade'; +export * from './readlist.facade'; \ No newline at end of file diff --git a/src/app/viewer/facades/page-tracking.facade.ts b/src/app/viewer/facades/page-tracking.facade.ts new file mode 100644 index 0000000..a01aa7f --- /dev/null +++ b/src/app/viewer/facades/page-tracking.facade.ts @@ -0,0 +1,89 @@ +import { isPlatformServer } from "@angular/common"; +import { Injectable, signal, computed, PLATFORM_ID, inject, Signal, effect } from "@angular/core"; +import { ViewModeFacade } from "./view-mode.facade"; +import { EmbedFacade } from "./viewer-embed.facade"; +import { CompositionEpisode } from "../../@site-modules/@common-read"; + +@Injectable() +export class PageTrackingFacade { + private embedFacade = inject(EmbedFacade); + private viewMode = inject(ViewModeFacade); + + private _pagesElement = signal(null); + private _longStripElement = signal(null); + private _figuresElement = signal | null>(null); + + platformId = inject(PLATFORM_ID); + activeIndexes = signal([]); + pagesCount = signal(0); + + setActive(indexes: number[]) { + this.activeIndexes.set(indexes); + } + + preloadIndexes = computed(() => + this.activeIndexes().map(i => i + 1) + ); + + shouldPreload(i: number): boolean { + return this.preloadIndexes().includes(i); + } + + initZone(pagesElement: HTMLElement, longStripElement: HTMLElement, figuresElement: NodeListOf) { + this._pagesElement.set(pagesElement); + this._longStripElement.set(longStripElement); + this._figuresElement.set(figuresElement); + } + + private scheduled = false; + + updateActiveIndexes() { + if (this.scheduled || isPlatformServer(this.platformId)) return; + + this.scheduled = true; + + requestAnimationFrame(() => { + this.scheduled = false; + this._update(); + }); + } + + connectPagesCount(episode: Signal) { + effect(() => this.pagesCount.set(episode().images.length)); + } + + private _update() { + if (this._pagesElement() == null || this._longStripElement() == null || this._figuresElement() == null) return; + + const isPageMode = this.viewMode.mode() == 'pages'; + + const viewRect: DOMRect = isPageMode + ? this._pagesElement()!.getBoundingClientRect() + : this._longStripElement()!.getBoundingClientRect(); + + let activeIndxs: number[] = []; + + for (let i = 0; i < this._figuresElement()!.length; i++) { + const img = this._figuresElement()![i]; + const rect = img.getBoundingClientRect(); + + const hor = rect.right > viewRect.x && rect.right < viewRect.x + viewRect.width + 1; + + const ver = rect.top < viewRect.height && rect.bottom > viewRect.top + + if (isPageMode ? hor : ver) { + activeIndxs.push(i) + } + + } + + this.activeIndexes.set(activeIndxs); + + + const total = this.pagesCount() || this._figuresElement()!.length; + const current = activeIndxs.map(i => i + 1) + + this.embedFacade.postPageChange(total, current); + + } +} \ No newline at end of file diff --git a/src/app/viewer/facades/readlist.facade.ts b/src/app/viewer/facades/readlist.facade.ts new file mode 100644 index 0000000..d0add7e --- /dev/null +++ b/src/app/viewer/facades/readlist.facade.ts @@ -0,0 +1,40 @@ +import { Injectable, signal, computed, inject, Signal, effect } from "@angular/core"; +import { Playlist, PlaylistItem } from "../../playlist/data-access/playlist.service"; + +@Injectable() +export class ReadlistFacade { + readlist = signal([]); + currentReadlistItem = signal(undefined); + + connect( + readlist: Signal, + current: Signal + ) { + effect(() => this.readlist.set(readlist())); + effect(() => this.currentReadlistItem.set(current())); + } + + getCurrentIndex() { + for (let i = 0; i < this.readlist().length; i++) { + const item = this.readlist()[i]; + if (this.currentReadlistItem()?.id == item.id && this.currentReadlistItem()?.site == item.site) + return i; + } + + return -1; + } + + getNextIndex() { + const index = this.getCurrentIndex(); + if (index < 0) return -1; + + return ((index + 1) < this.readlist().length) ? index + 1 : -1; + } + + getPrevIndex() { + const index = this.getCurrentIndex(); + if (index < 0) return -1; + + return ((index - 1) >= 0) ? (index - 1) : -1; + } +} \ No newline at end of file diff --git a/src/app/viewer/facades/view-mode.facade.ts b/src/app/viewer/facades/view-mode.facade.ts new file mode 100644 index 0000000..5d04622 --- /dev/null +++ b/src/app/viewer/facades/view-mode.facade.ts @@ -0,0 +1,33 @@ +import { Injectable, computed, inject } from "@angular/core"; +import { ViewerService } from "../../shared/data-access"; + +@Injectable() +export class ViewModeFacade { + private _isViewOptToggle = false; + public viewer = inject(ViewerService); + + toggleViewModeOption() { + if (!this._isViewOptToggle) { + this.viewer.toggleViewModeOption(); + this.viewer.saveViewModeOption(); + setTimeout(() => { this._isViewOptToggle = false }, 100); + } + + this._isViewOptToggle = true; + } + + mode = computed(() => this.viewer.viewModeOption().mode); + dir = computed(() => this.viewer.viewModeOption().dir); + code = computed(() => this.viewer.viewModeOption().code); + + setModeByCode(code: string) { + this.viewer.setViewModeOptionByCode(code); + } + + setLongStripMode() { + const longPageCode = "3"; + if (this.code() != longPageCode) { + this.setModeByCode(longPageCode); + } + } +} \ No newline at end of file diff --git a/src/app/viewer/facades/viewer-embed.facade.ts b/src/app/viewer/facades/viewer-embed.facade.ts new file mode 100644 index 0000000..e7ba302 --- /dev/null +++ b/src/app/viewer/facades/viewer-embed.facade.ts @@ -0,0 +1,48 @@ +import { inject, Injectable, PLATFORM_ID } from "@angular/core"; +import { EmbedHalperService } from "../../shared/data-access/embed-halper.service"; +import { ReadlistFacade } from "./readlist.facade"; +import { isPlatformServer } from "@angular/common"; +import { isPlaylist } from "../../playlist/data-access/playlist.service"; + +const CHTNK_LOAD_EVENT_NAME = 'chtnkload' +const CHTNK_CHANGE_PAGE_EVENT_NAME = 'changepage'; +const CHTNK_NSFW_CHOICE_EVENT_NAME = 'nsfwchoice' +const CHTNK_LIST_RESPONCE_EVENT_NAME = 'listresponse' +const CHTNK_LIST_REQUEST_EVENT_NAME = 'listrequest' + +@Injectable() +export class EmbedFacade { + platformId = inject(PLATFORM_ID); + readlist = inject(ReadlistFacade); + embedHelper = inject(EmbedHalperService); + + initListFromParrentWindow() { + if (!this.embedHelper.isEmbedded() || !isPlatformServer(this.platformId)) return + + this.embedHelper.postMessage(this.readlist.currentReadlistItem(), CHTNK_LIST_REQUEST_EVENT_NAME); + + window.addEventListener('message', ({ data }) => { + if (data.event != CHTNK_LIST_RESPONCE_EVENT_NAME) return; + + if (isPlaylist(data.data)) { + this.readlist.readlist.set(data.data); + } else { + console.warn('Received data is not a valid Playlist', data.data); + } + + }, false); + } + + loadCurrentPlaylistItem() { + this.embedHelper.postMessage(this.readlist.currentReadlistItem(), CHTNK_LOAD_EVENT_NAME); + } + + postPageChange(total: number, current: number[]) { + this.embedHelper.postMessage({ total, current }, CHTNK_CHANGE_PAGE_EVENT_NAME); + } + + postNsfwChoice(isNsfw: boolean) { + this.embedHelper.postMessage(isNsfw, CHTNK_NSFW_CHOICE_EVENT_NAME); + } + +} \ No newline at end of file diff --git a/src/app/viewer/facades/viewer-gamepad.facade.ts b/src/app/viewer/facades/viewer-gamepad.facade.ts new file mode 100644 index 0000000..a49da6a --- /dev/null +++ b/src/app/viewer/facades/viewer-gamepad.facade.ts @@ -0,0 +1,39 @@ +import { effect, inject, Injectable } from "@angular/core"; +import { GamepadService } from "../../shared/data-access"; +import { GamepadButton } from "../../shared/models"; +import { ViewerUiFacade } from "./viewer-ui.facade"; +import { ViewModeFacade } from "./view-mode.facade"; +import { ViewerScrollFacade } from "./viewer-scroll.facade"; + +@Injectable() +export class GamepadFacade { + private viewerUi = inject(ViewerUiFacade); + private viewMode = inject(ViewModeFacade); + private scroll = inject(ViewerScrollFacade); + + gamepad = inject(GamepadService); + + constructor() { + this.initeGamepadKeys(); + } + + private initeGamepadKeys() { + effect(() => { + for (const [btn, action] of Object.entries(this.gamepadActionMap)) { + if (this.gamepad.buttons()[parseInt(btn)]?.pressed) action(); + } + }) + } + + private gamepadActionMap: Record = { + [GamepadButton.L1]: () => this.scroll.scrollLeft(), + [GamepadButton.R1]: () => this.scroll.scrollRight(), + [GamepadButton.DPadLeft]: () => this.scroll.scrollLeft(), + [GamepadButton.DPadRight]: () => this.scroll.scrollRight(), + [GamepadButton.DPadUp]: () => this.scroll.scrollUp(), + [GamepadButton.DPadDown]: () => this.scroll.scrollDown(), + [GamepadButton.Square]: () => this.viewerUi.toggleFullScreen(), + [GamepadButton.Options]: () => this.viewerUi.toggleOverlay(), + [GamepadButton.Triangle]: () => this.viewMode.toggleViewModeOption(), + }; +} \ No newline at end of file diff --git a/src/app/viewer/facades/viewer-keyboard.facade.ts b/src/app/viewer/facades/viewer-keyboard.facade.ts new file mode 100644 index 0000000..b6fe7ca --- /dev/null +++ b/src/app/viewer/facades/viewer-keyboard.facade.ts @@ -0,0 +1,35 @@ +import { Injectable, inject } from "@angular/core"; +import { ViewerUiFacade } from "./viewer-ui.facade"; +import { DomManipulationService } from "../../shared/data-access"; +import { ViewerScrollFacade } from "./viewer-scroll.facade"; +import { ViewModeFacade } from "./view-mode.facade"; + +@Injectable() +export class KeyboardFacade { + domMan = inject(DomManipulationService) + + private viewerUi = inject(ViewerUiFacade); + private viewMode = inject(ViewModeFacade); + private scroll = inject(ViewerScrollFacade); + + + private _hotKeys = new Map() + + constructor() { + this.initHotKeys(); + } + + private initHotKeys() { + this._hotKeys.set('KeyF', () => this.viewerUi.toggleFullScreen()) + this._hotKeys.set('KeyE', () => this.viewerUi.toggleOverlay()) + this._hotKeys.set('KeyM', () => this.viewMode.toggleViewModeOption()) + this._hotKeys.set('KeyA', () => this.scroll.scrollLeft()) + this._hotKeys.set('KeyD', () => this.scroll.scrollRight()) + this._hotKeys.set('KeyW', () => this.scroll.scrollUp()) + this._hotKeys.set('KeyS', () => this.scroll.scrollDown()) + } + + handleKeyboardEvent(event: KeyboardEvent) { + this.domMan.setHotkeys(event, this._hotKeys) + } +} \ No newline at end of file diff --git a/src/app/viewer/facades/viewer-nsfw.facade.ts b/src/app/viewer/facades/viewer-nsfw.facade.ts new file mode 100644 index 0000000..44f0dd2 --- /dev/null +++ b/src/app/viewer/facades/viewer-nsfw.facade.ts @@ -0,0 +1,24 @@ +import { inject, Injectable, signal } from "@angular/core"; +import { EmbedFacade } from "./viewer-embed.facade"; +import { Router } from "@angular/router"; + +@Injectable() +export class NsfwFacade { + private embedFacade = inject(EmbedFacade); + private router = inject(Router); + + show = signal(false); + + agree() { + this.show.set(true); + this.embedFacade.postNsfwChoice(true); + } + + disagree() { + this.show.set(false); + this.embedFacade.postNsfwChoice(false); + + if (!this.embedFacade.embedHelper.isEmbedded()) + this.router.navigate(['/']) + } +} \ No newline at end of file diff --git a/src/app/viewer/facades/viewer-scroll.facade.ts b/src/app/viewer/facades/viewer-scroll.facade.ts new file mode 100644 index 0000000..0adf4c5 --- /dev/null +++ b/src/app/viewer/facades/viewer-scroll.facade.ts @@ -0,0 +1,66 @@ +import { inject, Injectable, signal } from "@angular/core"; +import { DomManipulationService } from "../../shared/data-access"; +import { VibrationService } from "../../shared/data-access/vibration.service"; + +@Injectable() +export class ViewerScrollFacade { + private _isScrollStart: boolean = false; + + private readonly _verAmount = 256; + private _pagesElement = signal(null); + private _longStripElement = signal(null); + private dm = inject(DomManipulationService); + private vibration = inject(VibrationService); + + initZone(pagesElement: HTMLElement, longStripElement: HTMLElement) { + this._pagesElement.set(pagesElement); + this._longStripElement.set(longStripElement); + } + + scrollLeft() { + this.dm.scrollHor(this._pagesElement()!, -this._longStripElement()!.clientWidth) + } + + scrollRight() { + this.dm.scrollHor(this._pagesElement()!, this._longStripElement()!.clientWidth) + } + + scrollUp() { + this.dm.scrollVer(this._longStripElement()!, -this._verAmount) + } + scrollDown() { + this.dm.scrollVer(this._longStripElement()!, this._verAmount) + } + + scrollStartVibration() { + if (!this._isScrollStart) { + this._isScrollStart = true; + this.vibration.vibrate(10); + } + } + scrollEndVibration() { + this.vibration.vibrate([5, 5, 10]); + this._isScrollStart = false; + } + + scrollByWheel(event: WheelEvent, dir: "ltr" | "rtl" = "ltr") { + if (!this._pagesElement()) return; + + const revers: number = dir == "ltr" ? 1 : -1 + const scrollAmountX = this._pagesElement()!.clientWidth; + + if (event.deltaY !== 0 && !event.shiftKey) { + this._pagesElement()!.scrollLeft += event.deltaY * revers > 0 ? scrollAmountX : -scrollAmountX; + event.preventDefault(); + } + } + + scrollToPage(index: number) { + const el = this._pagesElement()!.querySelector(`#page_${index + 1}`); + el?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'center' + }); + } +} \ No newline at end of file diff --git a/src/app/viewer/facades/viewer-ui.facade.ts b/src/app/viewer/facades/viewer-ui.facade.ts new file mode 100644 index 0000000..ed6972c --- /dev/null +++ b/src/app/viewer/facades/viewer-ui.facade.ts @@ -0,0 +1,49 @@ +import { DOCUMENT, isPlatformServer } from "@angular/common"; +import { inject, Injectable, PLATFORM_ID, signal } from "@angular/core"; +import { DomManipulationService } from "../../shared/data-access"; + +@Injectable() +export class ViewerUiFacade { + private platformId = inject(PLATFORM_ID); + + private _viewElement = signal(null); + private document = inject(DOCUMENT); + private dm = inject(DomManipulationService); + + isFullScreen = signal(false); + showOverlay = signal(false); + isDialogOpen = signal(false); + + toggleOverlay = () => { + this.showOverlay.update(v => !v); + } + + // TODO: Fix scroll position reset after exiting fullscreen in pages mode + toggleFullScreen = () => { + if (this._viewElement() == null) return; + // const activeIndexs = this.activeIndexs(); + // const page = (activeIndexs.length == 1) ? activeIndexs[0] : activeIndexs.filter(v => v+1 % 2 != 0)[0]; + // console.log(activeIndexs, page); + + this.dm.toggleFullScreen(this._viewElement()!); + + // if (page != undefined) + // setTimeout(() => {this.onActive(page)}, 100); + } + + setDialog(open: boolean) { + this.isDialogOpen.set(open); + } + + initViewElement(element: HTMLElement) { + this._viewElement.set(element); + } + + initFullscreenListener() { + if (this._viewElement() == null || isPlatformServer(this.platformId)) return; + + addEventListener("fullscreenchange", () => { + this.isFullScreen.set(this.document.fullscreenElement === this._viewElement()); + }); + } +} \ No newline at end of file diff --git a/src/app/viewer/viewer.component.html b/src/app/viewer/viewer.component.html deleted file mode 100644 index 110eb08..0000000 --- a/src/app/viewer/viewer.component.html +++ /dev/null @@ -1,20 +0,0 @@ -@let pages = episode()?.images ?? []; - - -

๐ŸŒป๐Ÿ“–

- @for (page of pages; track $index) { - - @defer (on viewport) { - - - - } @placeholder { -
- } - - } - - ๐ŸŒป๐Ÿ“š๐Ÿ“–๐Ÿ“•๐Ÿ“˜๐Ÿ“—๐Ÿ“™๐Ÿ““๐Ÿ“’ - @if (pages.length % 2 != 0) { } - -
diff --git a/src/app/viewer/viewer.component.scss b/src/app/viewer/viewer.component.scss deleted file mode 100644 index e2c8a1a..0000000 --- a/src/app/viewer/viewer.component.scss +++ /dev/null @@ -1,32 +0,0 @@ -:host { - display: grid; - height: 100vh; - height: 100dvh; - position: relative; - align-content: center; -} - -chtnk-page { - display: block; - width: 50%; - flex: 1 0 auto; - position: relative; -} - -chtnk-page:nth-child(odd) { - scroll-snap-align: start; -} - -img { - max-height: 100%; - max-width: 100%; - display: block; -} - -.right-to-left chtnk-page:nth-child(odd) { - direction: ltr; -} - -.left-to-right chtnk-page:nth-child(odd) { - direction: rtl; -} diff --git a/src/app/viewer/viewer.component.ts b/src/app/viewer/viewer.component.ts deleted file mode 100644 index 7fc1f8e..0000000 --- a/src/app/viewer/viewer.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, input, signal } from '@angular/core'; -import { CompositionEpisode } from '../@site-modules/@common-read'; -import { PageComponent } from "./components/page/page.component"; -import { PagesComponent } from "./components/pages/pages.component"; - -@Component({ - standalone: true, - selector: 'chtnk-viewer', - templateUrl: './viewer.component.html', - styleUrl: './viewer.component.scss', - imports: [PageComponent, PagesComponent], - host: { - '[class.panels]': 'panels()', - '(click)': 'tooglePanels()', - } -}) -export class ViewerComponent { - panels = signal(true); - episode = input(); - - tooglePanels() { - this.panels.update(v => !v); - } -} diff --git a/src/app/viewer/viewer.declarables.ts b/src/app/viewer/viewer.declarables.ts new file mode 100644 index 0000000..dd7e85e --- /dev/null +++ b/src/app/viewer/viewer.declarables.ts @@ -0,0 +1,17 @@ +import { Type } from "@angular/core"; +import { ViewerComponent } from "./viewer/viewer.component"; +import { EpisodeDownloadFormComponent, EpisodeInfoTableComponent, EpisodeShareFormComponent, HintPageComponent, MangaPageComponent, MangaPageEvenComponent, PageComponent, ThanksPageComponent, ViewerFooterComponent, ViewerHeaderComponent } from "./viewer/components"; + +export const VIEWER_DECLARABLES: Type[] = [ + ViewerComponent, + PageComponent, + HintPageComponent, + ThanksPageComponent, + EpisodeInfoTableComponent, + EpisodeShareFormComponent, + EpisodeDownloadFormComponent, + MangaPageComponent, + MangaPageEvenComponent, + ViewerFooterComponent, + ViewerHeaderComponent, +]; \ No newline at end of file diff --git a/src/app/viewer/viewer.module.ts b/src/app/viewer/viewer.module.ts new file mode 100644 index 0000000..370f1cd --- /dev/null +++ b/src/app/viewer/viewer.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { VIEWER_DECLARABLES } from './viewer.declarables'; +import { SharedModule } from '../shared/shared.module'; +import { EmbedFacade, GamepadFacade, KeyboardFacade, NsfwFacade, PageTrackingFacade, ReadlistFacade, ViewerScrollFacade, ViewerUiFacade, ViewModeFacade } from './facades'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + SharedModule + ], + providers: [ + NsfwFacade, + GamepadFacade, + EmbedFacade, + ViewerUiFacade, + ViewModeFacade, + PageTrackingFacade, + KeyboardFacade, + ViewerScrollFacade, + ReadlistFacade + ], + declarations: [...VIEWER_DECLARABLES], + exports: [...VIEWER_DECLARABLES] +}) +export class ViewerModule { } diff --git a/src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.html b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.html rename to src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.html diff --git a/src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.scss b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.scss rename to src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.scss diff --git a/src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.ts b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts similarity index 66% rename from src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.ts rename to src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts index e0e6f86..1f1a1b7 100644 --- a/src/app/shared/ui/viewer/components/episode-download-form/episode-download-form.component.ts +++ b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts @@ -1,8 +1,8 @@ import { Component, inject, input } from '@angular/core'; -import { CompositionEpisode } from '../../../../../@site-modules/@common-read'; -import { DownloadService } from '../../../../data-access/download.service'; -import { Base64 } from '../../../../utils'; -import { PlaylistItem } from '../../../../../playlist/data-access/playlist.service'; +import { CompositionEpisode } from '../../../../@site-modules/@common-read'; +import { DownloadService } from '../../../../shared/data-access/download.service'; +import { Base64 } from '../../../../shared/utils'; +import { PlaylistItem } from '../../../../playlist/data-access/playlist.service'; @Component({ diff --git a/src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.html b/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.html rename to src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.html diff --git a/src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.scss b/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.scss rename to src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.scss diff --git a/src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.ts b/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts similarity index 71% rename from src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.ts rename to src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts index 79319b7..8ff6587 100644 --- a/src/app/shared/ui/viewer/components/episode-info-table/episode-info-table.component.ts +++ b/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts @@ -1,6 +1,6 @@ import { Component, inject, input } from '@angular/core'; -import { CompositionEpisode } from '../../../../../@site-modules/@common-read'; -import { LangService } from '../../../../data-access/lang.service'; +import { CompositionEpisode } from '../../../../@site-modules/@common-read'; +import { LangService } from '../../../../shared/data-access/lang.service'; @Component({ selector: 'episode-info-table', diff --git a/src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.html b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.html rename to src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.html diff --git a/src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.scss b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.scss rename to src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.scss diff --git a/src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.ts b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts similarity index 88% rename from src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.ts rename to src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts index 72650e3..103cce1 100644 --- a/src/app/shared/ui/viewer/components/episode-share-form/episode-share-form.component.ts +++ b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts @@ -1,8 +1,8 @@ import { Component, computed, inject, input, output, PLATFORM_ID, Signal } from '@angular/core'; -import { DomManipulationService, ViewerService } from '../../../../data-access'; -import { CompositionEpisode } from '../../../../../@site-modules/@common-read'; +import { DomManipulationService, ViewerService } from '../../../../shared/data-access'; +import { CompositionEpisode } from '../../../../@site-modules/@common-read'; import { isPlatformBrowser } from '@angular/common'; -import { LangService } from '../../../../data-access/lang.service'; +import { LangService } from '../../../../shared/data-access/lang.service'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ @@ -10,7 +10,7 @@ import { DomSanitizer } from '@angular/platform-browser'; standalone: false, templateUrl: './episode-share-form.component.html', - styleUrls: ['./episode-share-form.component.scss', '../../../../../shared/ui/@styles/input-group.scss'] + styleUrls: ['./episode-share-form.component.scss', '../../../../shared/ui/@styles/input-group.scss'] }) export class EpisodeShareFormComponent { platformId = inject(PLATFORM_ID) diff --git a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.html b/src/app/viewer/viewer/components/hint-page/hint-page.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/hint-page/hint-page.component.html rename to src/app/viewer/viewer/components/hint-page/hint-page.component.html diff --git a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.scss b/src/app/viewer/viewer/components/hint-page/hint-page.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/hint-page/hint-page.component.scss rename to src/app/viewer/viewer/components/hint-page/hint-page.component.scss diff --git a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.ts b/src/app/viewer/viewer/components/hint-page/hint-page.component.ts similarity index 79% rename from src/app/shared/ui/viewer/components/hint-page/hint-page.component.ts rename to src/app/viewer/viewer/components/hint-page/hint-page.component.ts index 5f7bfdb..67e803c 100644 --- a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.ts +++ b/src/app/viewer/viewer/components/hint-page/hint-page.component.ts @@ -1,7 +1,7 @@ import { Component, inject, input } from '@angular/core'; -import { ViewerService } from '../../../../data-access'; -import { LangService } from '../../../../data-access/lang.service'; -import { Playlist, PlaylistItem } from '../../../../../playlist/data-access/playlist.service'; +import { ViewerService } from '../../../../shared/data-access'; +import { LangService } from '../../../../shared/data-access/lang.service'; +import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; @Component({ selector: 'app-hint-page', diff --git a/src/app/viewer/viewer/components/index.ts b/src/app/viewer/viewer/components/index.ts new file mode 100644 index 0000000..2647d6d --- /dev/null +++ b/src/app/viewer/viewer/components/index.ts @@ -0,0 +1,10 @@ +export * from './viewer-header/viewer-header.component'; +export * from './viewer-footer/viewer-footer.component'; +export * from './hint-page/hint-page.component'; +export * from './page/page.component'; +export * from './episode-info-table/episode-info-table.component'; +export * from './episode-share-form/episode-share-form.component'; +export * from './episode-download-form/episode-download-form.component'; +export * from './thanks-page/thanks-page.component'; +export * from './manga-page/manga-page-even.component'; +export * from './manga-page/manga-page.component'; \ No newline at end of file diff --git a/src/app/shared/ui/manga-page/manga-page-even.component.html b/src/app/viewer/viewer/components/manga-page/manga-page-even.component.html similarity index 100% rename from src/app/shared/ui/manga-page/manga-page-even.component.html rename to src/app/viewer/viewer/components/manga-page/manga-page-even.component.html diff --git a/src/app/shared/ui/manga-page/manga-page-even.component.scss b/src/app/viewer/viewer/components/manga-page/manga-page-even.component.scss similarity index 100% rename from src/app/shared/ui/manga-page/manga-page-even.component.scss rename to src/app/viewer/viewer/components/manga-page/manga-page-even.component.scss diff --git a/src/app/shared/ui/manga-page/manga-page-even.component.ts b/src/app/viewer/viewer/components/manga-page/manga-page-even.component.ts similarity index 87% rename from src/app/shared/ui/manga-page/manga-page-even.component.ts rename to src/app/viewer/viewer/components/manga-page/manga-page-even.component.ts index 637dd68..fd5a331 100644 --- a/src/app/shared/ui/manga-page/manga-page-even.component.ts +++ b/src/app/viewer/viewer/components/manga-page/manga-page-even.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { ViewerService } from '../../data-access'; +import { ViewerService } from '../../../../shared/data-access'; @Component({ selector: 'app-manga-page-even', diff --git a/src/app/shared/ui/manga-page/manga-page.component.html b/src/app/viewer/viewer/components/manga-page/manga-page.component.html similarity index 100% rename from src/app/shared/ui/manga-page/manga-page.component.html rename to src/app/viewer/viewer/components/manga-page/manga-page.component.html diff --git a/src/app/shared/ui/manga-page/manga-page.component.scss b/src/app/viewer/viewer/components/manga-page/manga-page.component.scss similarity index 100% rename from src/app/shared/ui/manga-page/manga-page.component.scss rename to src/app/viewer/viewer/components/manga-page/manga-page.component.scss diff --git a/src/app/shared/ui/manga-page/manga-page.component.ts b/src/app/viewer/viewer/components/manga-page/manga-page.component.ts similarity index 87% rename from src/app/shared/ui/manga-page/manga-page.component.ts rename to src/app/viewer/viewer/components/manga-page/manga-page.component.ts index de986f4..548011c 100644 --- a/src/app/shared/ui/manga-page/manga-page.component.ts +++ b/src/app/viewer/viewer/components/manga-page/manga-page.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { ViewerService } from '../../data-access'; +import { ViewerService } from '../../../../shared/data-access'; @Component({ selector: 'manga-page', diff --git a/src/app/shared/ui/viewer/components/page/page-long.component.scss b/src/app/viewer/viewer/components/page/page-long.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/page/page-long.component.scss rename to src/app/viewer/viewer/components/page/page-long.component.scss diff --git a/src/app/shared/ui/viewer/components/page/page-pages.component.scss b/src/app/viewer/viewer/components/page/page-pages.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/page/page-pages.component.scss rename to src/app/viewer/viewer/components/page/page-pages.component.scss diff --git a/src/app/shared/ui/viewer/components/page/page.component.html b/src/app/viewer/viewer/components/page/page.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/page/page.component.html rename to src/app/viewer/viewer/components/page/page.component.html diff --git a/src/app/shared/ui/viewer/components/page/page.component.scss b/src/app/viewer/viewer/components/page/page.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/page/page.component.scss rename to src/app/viewer/viewer/components/page/page.component.scss diff --git a/src/app/shared/ui/viewer/components/page/page.component.ts b/src/app/viewer/viewer/components/page/page.component.ts similarity index 95% rename from src/app/shared/ui/viewer/components/page/page.component.ts rename to src/app/viewer/viewer/components/page/page.component.ts index be3941c..3d5055d 100644 --- a/src/app/shared/ui/viewer/components/page/page.component.ts +++ b/src/app/viewer/viewer/components/page/page.component.ts @@ -1,5 +1,5 @@ import { Component, computed, inject, input, InputSignal, output, OutputEmitterRef, Signal, signal } from '@angular/core'; -import { LangService } from '../../../../data-access/lang.service'; +import { LangService } from '../../../../shared/data-access/lang.service'; @Component({ selector: 'chtnk-page', diff --git a/src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.html b/src/app/viewer/viewer/components/thanks-page/thanks-page.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.html rename to src/app/viewer/viewer/components/thanks-page/thanks-page.component.html diff --git a/src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.scss b/src/app/viewer/viewer/components/thanks-page/thanks-page.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.scss rename to src/app/viewer/viewer/components/thanks-page/thanks-page.component.scss diff --git a/src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.ts b/src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts similarity index 81% rename from src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.ts rename to src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts index 19412dd..3725311 100644 --- a/src/app/shared/ui/viewer/components/thanks-page/thanks-page.component.ts +++ b/src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; -import { Playlist, PlaylistItem } from '../../../../../playlist/data-access/playlist.service'; -import { ViewerService } from '../../../../data-access'; -import { LangService } from '../../../../data-access/lang.service'; -import { CompositionEpisode } from '../../../../../@site-modules/@common-read'; +import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; +import { ViewerService } from '../../../../shared/data-access'; +import { LangService } from '../../../../shared/data-access/lang.service'; +import { CompositionEpisode } from '../../../../@site-modules/@common-read'; @Component({ selector: 'app-thanks-page', diff --git a/src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.html b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html similarity index 81% rename from src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.html rename to src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html index 0c449ae..fc4a076 100644 --- a/src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.html +++ b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html @@ -3,8 +3,8 @@ } - @if (playlist.length >0) { - + @if (playlist().length >0) { + } - +
@for (item of playlist(); track $index) { @if(currentPlaylistItem()?.id == item.id && currentPlaylistItem()?.site == item.site) { - {{item.title?? lang.ph().untitled + ' ' + ($index*1+1)}} + ๐Ÿ“– {{item.title?? lang.ph().untitled + ' ' + ($index*1+1)}} } @else { {{item.title?? + [queryParams]="{lang: lang.lang(), list: playlistLink(), vm: viewer.viewModeOption().code}">๐Ÿ“œ {{item.title?? lang.ph().untitled + ' ' + ($index*1+1)}} } } diff --git a/src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.scss b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.scss rename to src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.scss diff --git a/src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.ts b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts similarity index 79% rename from src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.ts rename to src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts index d2d3512..93c0151 100644 --- a/src/app/shared/ui/viewer/components/viewer-footer/viewer-footer.component.ts +++ b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts @@ -1,9 +1,9 @@ import { Component, EventEmitter, HostListener, inject, input, InputSignal, output, signal, ViewChild } from '@angular/core'; -import { BrowserService, DomManipulationService, ViewerService } from '../../../../data-access'; -import { LangService } from '../../../../data-access/lang.service'; -import { DialogComponent } from '../../../dialog/dialog.component'; -import { Playlist, PlaylistItem } from '../../../../../playlist/data-access/playlist.service'; -import { CompositionEpisode } from '../../../../../@site-modules/@common-read'; +import { BrowserService, DomManipulationService, ViewerService } from '../../../../shared/data-access'; +import { LangService } from '../../../../shared/data-access/lang.service'; +import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; +import { CompositionEpisode } from '../../../../@site-modules/@common-read'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; @Component({ selector: 'app-viewer-footer', diff --git a/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.html b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html similarity index 100% rename from src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.html rename to src/app/viewer/viewer/components/viewer-header/viewer-header.component.html diff --git a/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.scss b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.scss similarity index 100% rename from src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.scss rename to src/app/viewer/viewer/components/viewer-header/viewer-header.component.scss diff --git a/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.ts b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts similarity index 82% rename from src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.ts rename to src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts index a8a3582..ff89a21 100644 --- a/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.ts +++ b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts @@ -1,15 +1,15 @@ import { Component, computed, effect, HostListener, inject, input, output, PLATFORM_ID, Signal, signal, ViewChild } from '@angular/core'; -import { DomManipulationService, ViewerService } from '../../../../data-access'; -import { CompositionEpisode } from '../../../../../@site-modules/@common-read'; -import { PlaylistItem } from '../../../../../playlist/data-access/playlist.service'; -import { LangService } from '../../../../data-access/lang.service'; -import { DialogComponent } from '../../../dialog/dialog.component'; -import { EmbedHalperService } from '../../../../data-access/embed-halper.service'; -import { parseTags, resolveViewMode } from '../../../../utils'; +import { DomManipulationService, ViewerService } from '../../../../shared/data-access'; +import { CompositionEpisode } from '../../../../@site-modules/@common-read'; +import { PlaylistItem } from '../../../../playlist/data-access/playlist.service'; +import { LangService } from '../../../../shared/data-access/lang.service'; +import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; +import { EmbedHalperService } from '../../../../shared/data-access/embed-halper.service'; +import { parseTags, resolveViewMode } from '../../../../shared/utils'; import { isPlatformBrowser } from '@angular/common'; -import { GamepadService } from '../../../../data-access/gamepad.service'; -import { GamepadButton } from '../../../../models'; -import { FileService } from '../../../../../file/data-access/file.service'; +import { GamepadService } from '../../../../shared/data-access/gamepad.service'; +import { GamepadButton } from '../../../../shared/models'; +import { FileService } from '../../../../file/data-access/file.service'; @Component({ selector: 'app-viewer-header', diff --git a/src/app/viewer/viewer/viewer.component.html b/src/app/viewer/viewer/viewer.component.html new file mode 100644 index 0000000..c3579f4 --- /dev/null +++ b/src/app/viewer/viewer/viewer.component.html @@ -0,0 +1,54 @@ +@let dir = viewMode.dir(); +@let mode = viewMode.mode(); +@let nsfwEpisode = episode().nsfw; +@if (gamepad.gamepad.connected()) { + +} +
+ + + + + + @for(img of episode().images; track img.src; let i = $index) { + + } @empty { + ๐Ÿ‘€ + } + + + + + + + + + @if((episode().images.length || 0) % 2 > 0) { + + } +
+ +@defer{ +@if (episode()) { + +} +} + +@defer{ + +} + +@defer{ } \ No newline at end of file diff --git a/src/app/shared/ui/viewer/viewer.component.scss b/src/app/viewer/viewer/viewer.component.scss similarity index 100% rename from src/app/shared/ui/viewer/viewer.component.scss rename to src/app/viewer/viewer/viewer.component.scss diff --git a/src/app/viewer/viewer/viewer.component.ts b/src/app/viewer/viewer/viewer.component.ts new file mode 100644 index 0000000..afd0169 --- /dev/null +++ b/src/app/viewer/viewer/viewer.component.ts @@ -0,0 +1,96 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, HostListener, input, PLATFORM_ID, Signal, ViewChild, WritableSignal, computed, inject, signal } from '@angular/core'; +import { CompositionEpisode } from '../../@site-modules/@common-read'; +import { LangService } from '../../shared/data-access/lang.service'; +import { Playlist, PlaylistItem } from '../../playlist/data-access/playlist.service'; +import { DOCUMENT } from '@angular/common'; +import { EmbedFacade, GamepadFacade, KeyboardFacade, NsfwFacade, PageTrackingFacade, ReadlistFacade, ViewerScrollFacade, ViewerUiFacade, ViewModeFacade } from '../facades'; +import { DomManipulationService } from '../../shared/data-access'; + +@Component({ + selector: 'app-viewer', + templateUrl: './viewer.component.html', + styleUrls: [ + './viewer.component.scss', + './viewer.pages.component.scss', + './viewer.long.component.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false +}) +export class ViewerComponent implements AfterViewInit { + nsfw = inject(NsfwFacade); + gamepad = inject(GamepadFacade); + viewerUi = inject(ViewerUiFacade); + viewMode = inject(ViewModeFacade); + pageTracking = inject(PageTrackingFacade); + keyboard = inject(KeyboardFacade); + scroll = inject(ViewerScrollFacade); + embedFacade = inject(EmbedFacade); + readlist = inject(ReadlistFacade); + + private readonly dom = inject(DomManipulationService); + private readonly document = inject(DOCUMENT); + + episode = input({ title: '', images: [] }); + playlistLink = input(""); + currentPlaylistItem = input(); + playlistInput = input([]); + + @ViewChild('viewRef', { static: true }) viewRef!: ElementRef; + + constructor(private el: ElementRef, public lang: LangService) { + this.nsfw.show.set(false); + this.pageTracking.activeIndexes.set([]); + this.readlist.connect(this.playlistInput, this.currentPlaylistItem); + this.pageTracking.connectPagesCount(this.episode); + } + + viewElement: WritableSignal = signal(this.document.createElement('div')); + imageElements: Signal> = computed(() => this.viewElement().querySelectorAll('.page img[id*=page_]')); + + ngAfterViewInit() { + this.viewerUi.initViewElement(this.el.nativeElement); + this.viewerUi.initFullscreenListener(); + this.viewElement.set(this.viewRef.nativeElement); + this.pageTracking.initZone(this.viewElement(), this.el.nativeElement, this.imageElements()); + this.pageTracking.updateActiveIndexes(); + this.scroll.initZone(this.viewElement(), this.el.nativeElement); + this.embedFacade.loadCurrentPlaylistItem(); + } + + @HostListener('scroll') + onScroll() { + this.pageTracking.updateActiveIndexes(); + this.scroll.scrollStartVibration(); + } + + @HostListener('scrollend') + onScrollEnd() { + this.scroll.scrollEndVibration(); + } + + @HostListener('window:resize') + onResize() { + this.pageTracking.updateActiveIndexes(); + } + + @HostListener('window:keydown', ['$event']) + handleKeyboardEvent(event: KeyboardEvent) { + this.keyboard.handleKeyboardEvent(event); + } + + @HostListener('wheel', ['$event']) + handleWheelEvent(event: WheelEvent): void { + if (this.viewerUi.isDialogOpen() || this.viewMode.mode() != "pages") return; + + this.scroll.scrollByWheel(event, this.viewMode.dir()); + } + + onViewClick(event: Event) { + if (!this.dom.isInteractiveElement(event.target as HTMLElement)) this.viewerUi.toggleOverlay(); + } + onViewDblClick(event: Event) { + if (!this.dom.isInteractiveElement(event.target as HTMLElement)) this.viewerUi.toggleFullScreen(); + } + +} diff --git a/src/app/shared/ui/viewer/viewer.long.component.scss b/src/app/viewer/viewer/viewer.long.component.scss similarity index 100% rename from src/app/shared/ui/viewer/viewer.long.component.scss rename to src/app/viewer/viewer/viewer.long.component.scss diff --git a/src/app/shared/ui/viewer/viewer.pages.component.scss b/src/app/viewer/viewer/viewer.pages.component.scss similarity index 100% rename from src/app/shared/ui/viewer/viewer.pages.component.scss rename to src/app/viewer/viewer/viewer.pages.component.scss diff --git a/src/assets/langs/uk.json b/src/assets/langs/uk.json index 7695fd1..53ed1b8 100644 --- a/src/assets/langs/uk.json +++ b/src/assets/langs/uk.json @@ -62,5 +62,6 @@ "ch": "ะ ะพะทะด", "sitesHistory": "ะ†ัั‚ะพั€ั–ั ัะฐะนั‚ั–ะฒ", "filesHistory": "ะ†ัั‚ะพั€ั–ั ั„ะฐะนะปั–ะฒ", - "noInternet": "ะะตะผะฐั” ะฟั–ะดะบะปัŽั‡ะตะฝะฝั ะดะพ ั–ะฝั‚ะตั€ะฝะตั‚ัƒ" + "noInternet": "ะะตะผะฐั” ะฟั–ะดะบะปัŽั‡ะตะฝะฝั ะดะพ ั–ะฝั‚ะตั€ะฝะตั‚ัƒ", + "readlist": "ะกะฟะธัะพะบ ะดะปั ั‡ะธั‚ะฐะฝะฝั" } \ No newline at end of file diff --git a/src/assets/logos/yandere-logo.png b/src/assets/logos/yandere-logo.png new file mode 100644 index 0000000..a841c50 Binary files /dev/null and b/src/assets/logos/yandere-logo.png differ diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index d01f5bb..94f98fd 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,7 +1,7 @@ const PROXY = `http://192.168.10.107:3003/api?url=` export const environment = { - version: "0.13.50-2026.3.26", + version: "0.13.51-2026.3.28", prod: false, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index da9c301..dd56892 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,7 +1,7 @@ const PROXY = `https://proxy.chytanka.ink/api?url=` export const environment = { - version: "0.13.50-2026.3.26", + version: "0.13.51-2026.3.28", prod: true, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`,