From 75797c3bebe4fc8dad6a2556a90617681d5e1844 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 12:37:59 +0200 Subject: [PATCH 1/9] refactor(viewer): remove unused viewer components and clean up template --- src/app/file/zip/zip.component.html | 1 - src/app/file/zip/zip.component.ts | 4 +-- .../components/page/page.component.html | 1 - .../components/page/page.component.scss | 0 .../viewer/components/page/page.component.ts | 9 ------ .../components/pages/pages.component.html | 1 - .../components/pages/pages.component.scss | 20 ------------ .../components/pages/pages.component.ts | 11 ------- src/app/viewer/viewer.component.html | 20 ------------ src/app/viewer/viewer.component.scss | 32 ------------------- src/app/viewer/viewer.component.ts | 24 -------------- 11 files changed, 1 insertion(+), 122 deletions(-) delete mode 100644 src/app/viewer/components/page/page.component.html delete mode 100644 src/app/viewer/components/page/page.component.scss delete mode 100644 src/app/viewer/components/page/page.component.ts delete mode 100644 src/app/viewer/components/pages/pages.component.html delete mode 100644 src/app/viewer/components/pages/pages.component.scss delete mode 100644 src/app/viewer/components/pages/pages.component.ts delete mode 100644 src/app/viewer/viewer.component.html delete mode 100644 src/app/viewer/viewer.component.scss delete mode 100644 src/app/viewer/viewer.component.ts diff --git a/src/app/file/zip/zip.component.html b/src/app/file/zip/zip.component.html index 2e84688..71b0c6c 100644 --- a/src/app/file/zip/zip.component.html +++ b/src/app/file/zip/zip.component.html @@ -1,5 +1,4 @@ @if(episode && episode.images && episode.images.length > 0){ - } @else { diff --git a/src/app/file/zip/zip.component.ts b/src/app/file/zip/zip.component.ts index 6a4c1f5..843ec6b 100644 --- a/src/app/file/zip/zip.component.ts +++ b/src/app/file/zip/zip.component.ts @@ -9,12 +9,10 @@ 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"; @Component({ selector: 'app-zip', - imports: [SharedModule, /*ViewerComponent*/], + imports: [SharedModule], templateUrl: './zip.component.html', styleUrl: './zip.component.scss' }) 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.scss b/src/app/viewer/components/page/page.component.scss deleted file mode 100644 index e69de29..0000000 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/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); - } -} From 5bd340a70d896d63cc88c7b9797c95174571bc45 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 12:44:52 +0200 Subject: [PATCH 2/9] style: enhance SVG wrapping with dedicated class for improved layout and responsiveness --- .../chytanka-logo-with-tags.component.html | 2 +- .../chytanka-logo-with-tags.component.scss | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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 From b99d12a6472ad0957e4079d223ce1f53151b90e2 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 13:00:11 +0200 Subject: [PATCH 3/9] fix(nhentai): update image source URL for correct gallery access --- src/app/@site-modules/nhentai/nhentai.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }; From 3de4a58317ef8b437a9d823581ae7e231ce2f386 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 14:38:11 +0200 Subject: [PATCH 4/9] fix(gamepad): conditionally render gamepad cursor based on connection status in app and dialog components --- src/app/app.component.html | 5 ++++- src/app/shared/ui/dialog/dialog.component.html | 3 ++- src/app/shared/ui/viewer/viewer.component.html | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) 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/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/viewer/viewer.component.html b/src/app/shared/ui/viewer/viewer.component.html index d1d50cb..dd36eb5 100644 --- a/src/app/shared/ui/viewer/viewer.component.html +++ b/src/app/shared/ui/viewer/viewer.component.html @@ -1,7 +1,9 @@ @let dir = viewer.viewModeOption().dir; @let mode = viewer.viewModeOption().mode; @let nsfw = episode().nsfw; +@if (gamepad.connected()) { +}
From b2b6ebcb488bfd0dd98bb9a619213965f23ad51f Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 15:11:31 +0200 Subject: [PATCH 5/9] refactor(history): simplify history and file history management by updating signal types and initialization --- .../history-list/history-list.component.html | 24 +++++++++---------- .../history-list/history-list.component.scss | 4 ++++ .../ui/history-list/history-list.component.ts | 21 +++++++++------- 3 files changed, 28 insertions(+), 21 deletions(-) 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) From 995696391b028b89e26b7eed906c7991e493c52d Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 17:42:32 +0200 Subject: [PATCH 6/9] feat: add source copyright components and integrate into various modules --- .../@common-read/common-read.module.ts | 3 ++- .../imgchest/imgchest-shell.component.ts | 18 ++++++---------- .../imgur/imgur-shell.component.ts | 20 +++++++----------- .../mangadex/mangadex-shell.component.ts | 19 ++++++----------- .../reddit/reddit-shell.component.ts | 13 +++++------- .../telegraph/telegraph-shell.component.ts | 12 +++++------ .../yandere/yandere-shell.component.ts | 12 ++++++----- .../zenko/zenko-shell.component.ts | 12 +++++------ .../link-parser/link-parser.component.ts | 3 +-- src/app/shared/shared.module.ts | 4 +++- .../source-copyright-logo.component.html | 3 +++ .../source-copyright-logo.component.scss | 9 ++++++++ .../source-copyright-logo.component.ts | 14 ++++++++++++ .../source-copyright.component.html | 3 +++ .../source-copyright.component.scss | 0 .../source-copyright.component.ts | 16 ++++++++++++++ src/assets/logos/yandere-logo.png | Bin 0 -> 32082 bytes 17 files changed, 96 insertions(+), 65 deletions(-) create mode 100644 src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.html create mode 100644 src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.scss create mode 100644 src/app/shared/ui/source-copyright-logo/source-copyright-logo.component.ts create mode 100644 src/app/shared/ui/source-copyright/source-copyright.component.html create mode 100644 src/app/shared/ui/source-copyright/source-copyright.component.scss create mode 100644 src/app/shared/ui/source-copyright/source-copyright.component.ts create mode 100644 src/assets/logos/yandere-logo.png 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..162e728 100644 --- a/src/app/@site-modules/@common-read/common-read.module.ts +++ b/src/app/@site-modules/@common-read/common-read.module.ts @@ -16,7 +16,8 @@ import { RouterModule } from '@angular/router'; SharedModule ], 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/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/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/shared/shared.module.ts b/src/app/shared/shared.module.ts index f2c79fa..c68d5ef 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -38,8 +38,10 @@ import { EpisodeInfoTableComponent } from './ui/viewer/components/episode-info-t 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'; -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, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, PageComponent, EpisodeInfoTableComponent, EpisodeShareFormComponent, EpisodeDownloadFormComponent, DropZoneComponent, SourceCopyrightComponent, SourceCopyrightLogoComponent] @NgModule({ declarations: [ 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/assets/logos/yandere-logo.png b/src/assets/logos/yandere-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a841c50eec04970f928b9b13412cd71bc31b7925 GIT binary patch literal 32082 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 N002ovPDHLkV1g;_#T5Vm literal 0 HcmV?d00001 From 8522791182135c14a37cd6c5f1503c7220de8b85 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Thu, 26 Mar 2026 23:22:11 +0200 Subject: [PATCH 7/9] feat(link-parser): implement link parsing facades and components, enhance seasonal theme handling --- src/app/app.module.ts | 4 +- .../link-parser-settings.service.ts | 6 +- .../data-access/link-parser.service.ts | 14 +- .../data-access/parser.providers.ts | 19 +++ .../link-parser/data-access/parser.tokens.ts | 6 + src/app/link-parser/link-parser.module.ts | 18 ++- .../ui/go-button/go-button.component.html | 9 ++ .../ui/go-button/go-button.component.scss | 37 +++++ .../ui/go-button/go-button.component.ts | 14 ++ .../ui/parser-form/facades/file-net.facade.ts | 22 +++ .../ui/parser-form/facades/index.ts | 4 + .../parser-form/facades/link-init.facade.ts | 39 ++++++ .../parser-form/facades/link-parser.facade.ts | 55 ++++++++ .../parser-form/facades/navigation.facade.ts | 14 ++ .../ui/parser-form/parser-form.component.html | 31 +---- .../ui/parser-form/parser-form.component.scss | 36 +---- .../ui/parser-form/parser-form.component.ts | 128 ++---------------- .../list/list-shell/list-shell.component.ts | 26 +--- src/app/list/list.module.ts | 6 + src/app/shared/shared.module.ts | 3 +- .../shared/ui/slogan/slogan.component.html | 6 + .../shared/ui/slogan/slogan.component.scss | 0 src/app/shared/ui/slogan/slogan.component.ts | 46 +++++++ 23 files changed, 334 insertions(+), 209 deletions(-) create mode 100644 src/app/link-parser/data-access/parser.providers.ts create mode 100644 src/app/link-parser/data-access/parser.tokens.ts create mode 100644 src/app/link-parser/ui/go-button/go-button.component.html create mode 100644 src/app/link-parser/ui/go-button/go-button.component.scss create mode 100644 src/app/link-parser/ui/go-button/go-button.component.ts create mode 100644 src/app/link-parser/ui/parser-form/facades/file-net.facade.ts create mode 100644 src/app/link-parser/ui/parser-form/facades/index.ts create mode 100644 src/app/link-parser/ui/parser-form/facades/link-init.facade.ts create mode 100644 src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts create mode 100644 src/app/link-parser/ui/parser-form/facades/navigation.facade.ts create mode 100644 src/app/shared/ui/slogan/slogan.component.html create mode 100644 src/app/shared/ui/slogan/slogan.component.scss create mode 100644 src/app/shared/ui/slogan/slogan.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 03325ab..8c3445e 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/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..ae1880f 100644 --- a/src/app/link-parser/data-access/link-parser.service.ts +++ b/src/app/link-parser/data-access/link-parser.service.ts @@ -1,9 +1,11 @@ -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' -}) +// @Injectable({ +// providedIn: 'root' +// }) +@Injectable() export class LinkParserService { supportSites = signal([ "Imgur", @@ -21,7 +23,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..bc9a7af 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,11 @@ import { ParserFormComponent } from './ui/parser-form/parser-form.component'; FormsModule, SharedModule, HistoryModule + ], + providers: [ + ...parserProviders, + LinkParserService, + ...FACADES ] }) export class LinkParserModule { } 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..5e4c125 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({ @@ -16,6 +18,10 @@ import { SharedModule } from '../shared/shared.module'; ListRoutingModule, FormsModule, SharedModule + ], + providers: [ + ...parserProviders, + LinkParserService ] }) export class ListModule { } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c68d5ef..a55ad8d 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -40,8 +40,9 @@ import { EpisodeDownloadFormComponent } from './ui/viewer/components/episode-dow 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, SourceCopyrightComponent, SourceCopyrightLogoComponent] +const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, PageComponent, EpisodeInfoTableComponent, EpisodeShareFormComponent, EpisodeDownloadFormComponent, DropZoneComponent, SourceCopyrightComponent, SourceCopyrightLogoComponent, SloganComponent] @NgModule({ declarations: [ 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/shared/ui/slogan/slogan.component.scss b/src/app/shared/ui/slogan/slogan.component.scss new file mode 100644 index 0000000..e69de29 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); +} From de38c78ea05ea4835131708c2f0124680f8c72d4 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Fri, 27 Mar 2026 14:05:05 +0200 Subject: [PATCH 8/9] refactor(viewer): decouple viewer components from SharedModule Moved all viewer-specific components into a dedicated ViewerModule to improve separation of concerns and avoid bloating SharedModule. --- .vscode/settings.json | 10 +++++++ .../@common-read/common-read.module.ts | 6 +++- src/app/app.module.ts | 2 +- src/app/file/mobi/mobi.component.ts | 3 +- src/app/file/pdf/pdf.component.html | 1 - src/app/file/pdf/pdf.component.ts | 3 +- src/app/file/zip/zip.component.ts | 3 +- .../data-access/link-parser.service.ts | 7 ++--- src/app/link-parser/link-parser.module.ts | 2 -- src/app/list/list.module.ts | 4 --- src/app/shared/shared.module.ts | 29 ++----------------- src/app/viewer/viewer.declarables.ts | 17 +++++++++++ src/app/viewer/viewer.module.ts | 16 ++++++++++ .../episode-download-form.component.html | 0 .../episode-download-form.component.scss | 0 .../episode-download-form.component.ts | 8 ++--- .../episode-info-table.component.html | 0 .../episode-info-table.component.scss | 0 .../episode-info-table.component.ts | 4 +-- .../episode-share-form.component.html | 0 .../episode-share-form.component.scss | 0 .../episode-share-form.component.ts | 8 ++--- .../hint-page/hint-page.component.html | 0 .../hint-page/hint-page.component.scss | 0 .../hint-page/hint-page.component.ts | 6 ++-- src/app/viewer/viewer/components/index.ts | 10 +++++++ .../manga-page/manga-page-even.component.html | 0 .../manga-page/manga-page-even.component.scss | 0 .../manga-page/manga-page-even.component.ts | 2 +- .../manga-page/manga-page.component.html | 0 .../manga-page/manga-page.component.scss | 0 .../manga-page/manga-page.component.ts | 2 +- .../components/page/page-long.component.scss | 0 .../components/page/page-pages.component.scss | 0 .../components/page/page.component.html | 0 .../components/page/page.component.scss | 0 .../viewer/components/page/page.component.ts | 2 +- .../thanks-page/thanks-page.component.html | 0 .../thanks-page/thanks-page.component.scss | 0 .../thanks-page/thanks-page.component.ts | 8 ++--- .../viewer-footer.component.html | 0 .../viewer-footer.component.scss | 0 .../viewer-footer/viewer-footer.component.ts | 10 +++---- .../viewer-header.component.html | 0 .../viewer-header.component.scss | 0 .../viewer-header/viewer-header.component.ts | 20 ++++++------- .../viewer/viewer.component.html | 0 .../viewer/viewer.component.scss | 0 .../ui => viewer}/viewer/viewer.component.ts | 22 +++++++------- .../viewer/viewer.long.component.scss | 0 .../viewer/viewer.pages.component.scss | 0 51 files changed, 116 insertions(+), 89 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/app/viewer/viewer.declarables.ts create mode 100644 src/app/viewer/viewer.module.ts rename src/app/{shared/ui => viewer}/viewer/components/episode-download-form/episode-download-form.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/episode-download-form/episode-download-form.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/episode-download-form/episode-download-form.component.ts (66%) rename src/app/{shared/ui => viewer}/viewer/components/episode-info-table/episode-info-table.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/episode-info-table/episode-info-table.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/episode-info-table/episode-info-table.component.ts (71%) rename src/app/{shared/ui => viewer}/viewer/components/episode-share-form/episode-share-form.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/episode-share-form/episode-share-form.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/episode-share-form/episode-share-form.component.ts (88%) rename src/app/{shared/ui => viewer}/viewer/components/hint-page/hint-page.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/hint-page/hint-page.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/hint-page/hint-page.component.ts (79%) create mode 100644 src/app/viewer/viewer/components/index.ts rename src/app/{shared/ui => viewer/viewer/components}/manga-page/manga-page-even.component.html (100%) rename src/app/{shared/ui => viewer/viewer/components}/manga-page/manga-page-even.component.scss (100%) rename src/app/{shared/ui => viewer/viewer/components}/manga-page/manga-page-even.component.ts (87%) rename src/app/{shared/ui => viewer/viewer/components}/manga-page/manga-page.component.html (100%) rename src/app/{shared/ui => viewer/viewer/components}/manga-page/manga-page.component.scss (100%) rename src/app/{shared/ui => viewer/viewer/components}/manga-page/manga-page.component.ts (87%) rename src/app/{shared/ui => viewer}/viewer/components/page/page-long.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/page/page-pages.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/page/page.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/page/page.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/page/page.component.ts (95%) rename src/app/{shared/ui => viewer}/viewer/components/thanks-page/thanks-page.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/thanks-page/thanks-page.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/thanks-page/thanks-page.component.ts (81%) rename src/app/{shared/ui => viewer}/viewer/components/viewer-footer/viewer-footer.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/viewer-footer/viewer-footer.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/viewer-footer/viewer-footer.component.ts (79%) rename src/app/{shared/ui => viewer}/viewer/components/viewer-header/viewer-header.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/components/viewer-header/viewer-header.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/components/viewer-header/viewer-header.component.ts (82%) rename src/app/{shared/ui => viewer}/viewer/viewer.component.html (100%) rename src/app/{shared/ui => viewer}/viewer/viewer.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/viewer.component.ts (93%) rename src/app/{shared/ui => viewer}/viewer/viewer.long.component.scss (100%) rename src/app/{shared/ui => viewer}/viewer/viewer.pages.component.scss (100%) 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/src/app/@site-modules/@common-read/common-read.module.ts b/src/app/@site-modules/@common-read/common-read.module.ts index 162e728..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,7 +15,9 @@ import { RouterModule } from '@angular/router'; imports: [ CommonModule, RouterModule, - SharedModule + SharedModule, + ViewerModule, + LinkParserModule ], exports: [ CommonReadComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8c3445e..ed72431 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -36,7 +36,7 @@ registerLocaleData(localeUk) // }) // ), provideHttpClient(withFetch()), - // ...parserProviders + ...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.ts b/src/app/file/zip/zip.component.ts index 843ec6b..14e04e1 100644 --- a/src/app/file/zip/zip.component.ts +++ b/src/app/file/zip/zip.component.ts @@ -9,10 +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 { ViewerModule } from '../../viewer/viewer.module'; @Component({ selector: 'app-zip', - imports: [SharedModule], + imports: [SharedModule, ViewerModule], templateUrl: './zip.component.html', styleUrl: './zip.component.scss' }) 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 ae1880f..27a54d7 100644 --- a/src/app/link-parser/data-access/link-parser.service.ts +++ b/src/app/link-parser/data-access/link-parser.service.ts @@ -2,10 +2,9 @@ import { Inject, Injectable, signal, Type } from '@angular/core'; import { LinkParseResult, LinkParser } from '../utils'; import { LINK_PARSERS } from './parser.tokens'; -// @Injectable({ -// providedIn: 'root' -// }) -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class LinkParserService { supportSites = signal([ "Imgur", diff --git a/src/app/link-parser/link-parser.module.ts b/src/app/link-parser/link-parser.module.ts index bc9a7af..a041e6f 100644 --- a/src/app/link-parser/link-parser.module.ts +++ b/src/app/link-parser/link-parser.module.ts @@ -41,8 +41,6 @@ const FACADES = [ HistoryModule ], providers: [ - ...parserProviders, - LinkParserService, ...FACADES ] }) diff --git a/src/app/list/list.module.ts b/src/app/list/list.module.ts index 5e4c125..58314f0 100644 --- a/src/app/list/list.module.ts +++ b/src/app/list/list.module.ts @@ -18,10 +18,6 @@ import { parserProviders } from '../link-parser/data-access/parser.providers'; ListRoutingModule, FormsModule, SharedModule - ], - providers: [ - ...parserProviders, - LinkParserService ] }) export class ListModule { } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index a55ad8d..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,49 +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, SourceCopyrightComponent, SourceCopyrightLogoComponent, SloganComponent] +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/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..3197ef1 --- /dev/null +++ b/src/app/viewer/viewer.module.ts @@ -0,0 +1,16 @@ +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'; + +@NgModule({ + declarations: [...VIEWER_DECLARABLES], + imports: [ + CommonModule, + RouterModule, + SharedModule + ], + 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 100% 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 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/shared/ui/viewer/viewer.component.html b/src/app/viewer/viewer/viewer.component.html similarity index 100% rename from src/app/shared/ui/viewer/viewer.component.html rename to src/app/viewer/viewer/viewer.component.html 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/shared/ui/viewer/viewer.component.ts b/src/app/viewer/viewer/viewer.component.ts similarity index 93% rename from src/app/shared/ui/viewer/viewer.component.ts rename to src/app/viewer/viewer/viewer.component.ts index 2a055d2..88f4962 100644 --- a/src/app/shared/ui/viewer/viewer.component.ts +++ b/src/app/viewer/viewer/viewer.component.ts @@ -1,16 +1,16 @@ 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 { CompositionEpisode } from '../../@site-modules/@common-read'; +import { ViewerService, DomManipulationService } from '../../shared/data-access'; import { ActivatedRoute, Router } from '@angular/router'; -import { LangService } from '../../data-access/lang.service'; +import { LangService } from '../../shared/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 { isPlaylist, Playlist, PlaylistItem } from '../../playlist/data-access/playlist.service'; +import { EmbedHalperService } from '../../shared/data-access/embed-halper.service'; +import { DownloadService } from '../../shared/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'; +import { VibrationService } from '../../shared/data-access/vibration.service'; +import { GamepadButton } from '../../shared/models'; +import { GamepadService } from '../../shared/data-access'; const CHTNK_LOAD_EVENT_NAME = 'chtnkload' const CHTNK_CHANGE_PAGE_EVENT_NAME = 'changepage'; @@ -25,8 +25,8 @@ const CHTNK_LIST_REQUEST_EVENT_NAME = 'listrequest' './viewer.component.scss', './viewer.pages.component.scss', './viewer.long.component.scss', - '../../../shared/ui/@styles/details.scss', - '../../../shared/ui/@styles/input-group.scss' + '../../shared/ui/@styles/details.scss', + '../../shared/ui/@styles/input-group.scss' ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false 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 From 9d473cfe2fce13d445711ee37aa35ad8abceb4e6 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Sat, 28 Mar 2026 11:13:18 +0200 Subject: [PATCH 9/9] Refactor viewer component and related services - Removed unused page change event from zip component. - Added isInteractiveElement method to DomManipulationService for better element interaction handling. - Cleaned up ViewerService by removing unnecessary console logs. - Added readlist phrase to phrases utility and updated translations. - Enhanced ViewerModule by adding multiple facades for better separation of concerns. - Updated viewer-footer component to use readlist title and improved playlist handling. - Refactored viewer component to utilize new facades for better state management and interaction. - Introduced PageTrackingFacade to manage active indexes and page tracking logic. - Created ReadlistFacade to manage readlist state and interactions. - Implemented ViewModeFacade to handle view mode toggling and state. - Added ViewerEmbedFacade for handling embedded viewer interactions. - Created GamepadFacade to manage gamepad interactions and actions. - Developed KeyboardFacade for keyboard event handling and hotkey management. - Introduced ViewerUiFacade to manage UI state such as fullscreen and overlay. - Updated translations to include readlist terminology. --- package.json | 2 +- src/app/file/zip/zip.component.html | 2 +- src/app/file/zip/zip.component.ts | 5 - .../data-access/dom-manipulation.service.ts | 4 + src/app/shared/data-access/viewer.service.ts | 3 - src/app/shared/utils/phrases.ts | 1 + src/app/viewer/facades/index.ts | 9 + .../viewer/facades/page-tracking.facade.ts | 89 +++++ src/app/viewer/facades/readlist.facade.ts | 40 ++ src/app/viewer/facades/view-mode.facade.ts | 33 ++ src/app/viewer/facades/viewer-embed.facade.ts | 48 +++ .../viewer/facades/viewer-gamepad.facade.ts | 39 ++ .../viewer/facades/viewer-keyboard.facade.ts | 35 ++ src/app/viewer/facades/viewer-nsfw.facade.ts | 24 ++ .../viewer/facades/viewer-scroll.facade.ts | 66 ++++ src/app/viewer/facades/viewer-ui.facade.ts | 49 +++ src/app/viewer/viewer.module.ts | 14 +- .../viewer-footer.component.html | 10 +- src/app/viewer/viewer/viewer.component.html | 48 +-- src/app/viewer/viewer/viewer.component.ts | 342 +++--------------- src/assets/langs/uk.json | 3 +- src/environments/environment.development.ts | 2 +- src/environments/environment.ts | 2 +- 23 files changed, 531 insertions(+), 339 deletions(-) create mode 100644 src/app/viewer/facades/index.ts create mode 100644 src/app/viewer/facades/page-tracking.facade.ts create mode 100644 src/app/viewer/facades/readlist.facade.ts create mode 100644 src/app/viewer/facades/view-mode.facade.ts create mode 100644 src/app/viewer/facades/viewer-embed.facade.ts create mode 100644 src/app/viewer/facades/viewer-gamepad.facade.ts create mode 100644 src/app/viewer/facades/viewer-keyboard.facade.ts create mode 100644 src/app/viewer/facades/viewer-nsfw.facade.ts create mode 100644 src/app/viewer/facades/viewer-scroll.facade.ts create mode 100644 src/app/viewer/facades/viewer-ui.facade.ts 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/file/zip/zip.component.html b/src/app/file/zip/zip.component.html index 71b0c6c..0ec8be5 100644 --- a/src/app/file/zip/zip.component.html +++ b/src/app/file/zip/zip.component.html @@ -1,5 +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 14e04e1..ba32907 100644 --- a/src/app/file/zip/zip.component.ts +++ b/src/app/file/zip/zip.component.ts @@ -151,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/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/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/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.module.ts b/src/app/viewer/viewer.module.ts index 3197ef1..370f1cd 100644 --- a/src/app/viewer/viewer.module.ts +++ b/src/app/viewer/viewer.module.ts @@ -3,14 +3,26 @@ 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({ - declarations: [...VIEWER_DECLARABLES], 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/viewer/viewer/components/viewer-footer/viewer-footer.component.html b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html index 0c449ae..fc4a076 100644 --- a/src/app/viewer/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/viewer/viewer/viewer.component.html b/src/app/viewer/viewer/viewer.component.html index dd36eb5..c3579f4 100644 --- a/src/app/viewer/viewer/viewer.component.html +++ b/src/app/viewer/viewer/viewer.component.html @@ -1,31 +1,32 @@ -@let dir = viewer.viewModeOption().dir; -@let mode = viewer.viewModeOption().mode; -@let nsfw = episode().nsfw; -@if (gamepad.connected()) { - +@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 { 👀 } - - + + @@ -38,15 +39,16 @@ @defer{ @if (episode()) { - + } } @defer{ - + } -@defer{ } \ No newline at end of file +@defer{ } \ No newline at end of file diff --git a/src/app/viewer/viewer/viewer.component.ts b/src/app/viewer/viewer/viewer.component.ts index 88f4962..afd0169 100644 --- a/src/app/viewer/viewer/viewer.component.ts +++ b/src/app/viewer/viewer/viewer.component.ts @@ -1,22 +1,10 @@ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, input, PLATFORM_ID, Signal, ViewChild, WritableSignal, computed, effect, inject, output, signal } from '@angular/core'; +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 { ViewerService, DomManipulationService } from '../../shared/data-access'; -import { ActivatedRoute, Router } from '@angular/router'; import { LangService } from '../../shared/data-access/lang.service'; -import { DomSanitizer } from '@angular/platform-browser'; -import { isPlaylist, Playlist, PlaylistItem } from '../../playlist/data-access/playlist.service'; -import { EmbedHalperService } from '../../shared/data-access/embed-halper.service'; -import { DownloadService } from '../../shared/data-access/download.service'; -import { DOCUMENT, isPlatformBrowser, isPlatformServer } from '@angular/common'; -import { VibrationService } from '../../shared/data-access/vibration.service'; -import { GamepadButton } from '../../shared/models'; -import { GamepadService } from '../../shared/data-access'; - -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' +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', @@ -24,325 +12,85 @@ const CHTNK_LIST_REQUEST_EVENT_NAME = 'listrequest' styleUrls: [ './viewer.component.scss', './viewer.pages.component.scss', - './viewer.long.component.scss', - '../../shared/ui/@styles/details.scss', - '../../shared/ui/@styles/input-group.scss' + './viewer.long.component.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[] }>() + 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: [] - }); + 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; + 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); } - // 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.viewerUi.initViewElement(this.el.nativeElement); + this.viewerUi.initFullscreenListener(); 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); - } + 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('scrollend', ['$event']) - onScrollEnd(event: Event) { - this.vibration.vibrate([5, 5, 10]); - this.isScrollStart = false; + @HostListener('scroll') + onScroll() { + this.pageTracking.updateActiveIndexes(); + this.scroll.scrollStartVibration(); } - @HostListener('window:resize', ['$event']) - onResize(event: Event) { - this.initActiveIndexes() + @HostListener('scrollend') + onScrollEnd() { + this.scroll.scrollEndVibration(); } - 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:resize') + onResize() { + this.pageTracking.updateActiveIndexes(); } @HostListener('window:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent) { - this.domMan.setHotkeys(event, this.hotKeys) + this.keyboard.handleKeyboardEvent(event); } - 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.viewerUi.isDialogOpen() || this.viewMode.mode() != "pages") return; - 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()) + this.scroll.scrollByWheel(event, this.viewMode.dir()); } onViewClick(event: Event) { - if ((event.target as HTMLElement).nodeName === 'INPUT') return; - if ((event.target as HTMLElement).nodeName === 'SUMMARY') return; - - this.toggleOverlay(); + if (!this.dom.isInteractiveElement(event.target as HTMLElement)) this.viewerUi.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(['/']) + if (!this.dom.isInteractiveElement(event.target as HTMLElement)) this.viewerUi.toggleFullScreen(); } - 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/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/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/`,