From 5d1d4190a4918474d40dc02beaf1fde136ab7c2d Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Tue, 21 Apr 2026 15:11:50 +0300 Subject: [PATCH 1/3] fix: title overflow; change zenko host, add reddit proxy --- src/app/@site-modules/reddit/reddit.service.ts | 4 ++-- src/app/@site-modules/zenko/zenko.service.ts | 15 ++++++++------- .../ui/go-button/go-button.component.scss | 11 ++++++++++- .../ui/svg-text-box/svg-text-box.component.ts | 6 +++++- .../viewer-header/viewer-header.component.html | 2 +- src/environments/environment.development.ts | 5 ++++- src/environments/environment.ts | 5 ++++- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/app/@site-modules/reddit/reddit.service.ts b/src/app/@site-modules/reddit/reddit.service.ts index 058b7d5..e304f0e 100644 --- a/src/app/@site-modules/reddit/reddit.service.ts +++ b/src/app/@site-modules/reddit/reddit.service.ts @@ -4,7 +4,6 @@ import { Observable, map } from 'rxjs'; import { environment } from '../../../environments/environment'; import { CompositionEpisode } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; -import { Base64 } from '../../shared/utils'; @Injectable({ providedIn: 'root' @@ -15,7 +14,8 @@ export class RedditService { constructor(private http: HttpClient) { } getComposition(postId: string): Observable { - return this.http.get(environment.redditHost + postId + '.json') + const url = environment.redditHost + postId + '.json'; + return this.http.get(this.proxy.proxyUrl(url)) .pipe(map((data: any) => { return this.map(data) })) } diff --git a/src/app/@site-modules/zenko/zenko.service.ts b/src/app/@site-modules/zenko/zenko.service.ts index b42acd9..acda94e 100644 --- a/src/app/@site-modules/zenko/zenko.service.ts +++ b/src/app/@site-modules/zenko/zenko.service.ts @@ -13,12 +13,13 @@ export class ZenkoService { proxy: ProxyService = inject(ProxyService) getComposition(id: string): Observable { - return this.http.get(this.proxy.proxyUrl((environment.zenkoHost + id))+'&ref=https://zenko.online') + return this.http.get(this.proxy.proxyUrl((environment.zenkoChapters + id)) + `&ref=${environment.zenkoHost}`) .pipe(map((data) => { return this.map(data) })) } map(data: any): CompositionEpisode { - const {vol, ch, name, output} = this.titleDecode(data.name); + const imgParams = `?optimizer=image&width=900&quality=90&height=auto`; + const { vol, ch, name, output } = this.titleDecode(data.name); const x = parseFloat(ch); const part = Math.round((x - Math.floor(x)) * 10) const mappedResponse = { @@ -29,9 +30,9 @@ export class ZenkoService { publisher: { id: data.publisher.id as string, - site: `https://zenko.online/teams/`+data.publisher.id as string, + site: environment.zenkoTeams + data.publisher.id as string, name: data.publisher.name as string, - avatar: this.proxy.proxyUrl(`https://zenko.b-cdn.net/${data.publisher.avatar}?optimizer=image&width=900&quality=90&height=auto`) + '&ref=https://zenko.online' as string, + avatar: this.proxy.proxyUrl(environment.zenkoCdn + data.publisher.avatar + imgParams) + `&ref=${environment.zenkoHost}` as string, description: data.publisher.description as string, links: data.publisher.links?.map((l: any) => { return { link: l.link, title: l.title }; }) } as unknown as CompositionPublisher, @@ -40,8 +41,8 @@ export class ZenkoService { return { src: item.imgUrl || item.content }; - })).filter((i: any) => i.src).map((img: any) => { return { src: this.proxy.proxyUrl(`https://zenko.b-cdn.net/${img.src}?optimizer=image&width=900&quality=90&height=auto`) + '&ref=https://zenko.online' } }) - + })).filter((i: any) => i.src).map((img: any) => { return { src: this.proxy.proxyUrl(environment.zenkoCdn + img.src + imgParams) + `&ref=${environment.zenkoHost}` } }) + }; return mappedResponse; @@ -55,7 +56,7 @@ export class ZenkoService { const name = parts[2] ?? 'Без назви'; const output = `Том ${vol} Розділ ${ch}: ${name}`; - return {vol, ch, name, output} + return { vol, ch, name, output } } } 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 index 2de6e19..5923d77 100644 --- a/src/app/link-parser/ui/go-button/go-button.component.scss +++ b/src/app/link-parser/ui/go-button/go-button.component.scss @@ -1,5 +1,7 @@ .go-btn { - display: flex; + display: grid; + grid-template-columns: auto auto auto; + align-content: center; gap: 1ch; align-items: center; --dot-color: var(--border-color); @@ -14,6 +16,7 @@ font-weight: bold; box-shadow: var(--shadow-2); + @media (prefers-color-scheme: light) { --dot-color: #166496; color: #166496; @@ -29,6 +32,12 @@ &:active { box-shadow: 0 0 transparent; } + + .site-address { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } } .favicon { diff --git a/src/app/shared/ui/svg-text-box/svg-text-box.component.ts b/src/app/shared/ui/svg-text-box/svg-text-box.component.ts index 9697475..9de37b3 100644 --- a/src/app/shared/ui/svg-text-box/svg-text-box.component.ts +++ b/src/app/shared/ui/svg-text-box/svg-text-box.component.ts @@ -1,4 +1,5 @@ -import { AfterViewInit, Component, computed, ElementRef, input, signal, ViewChild } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; +import { AfterViewInit, Component, computed, ElementRef, inject, input, PLATFORM_ID, signal, ViewChild } from '@angular/core'; @Component({ selector: 'svg-text-box', @@ -8,6 +9,7 @@ import { AfterViewInit, Component, computed, ElementRef, input, signal, ViewChil styleUrl: './svg-text-box.component.scss' }) export class SvgTextBoxComponent implements AfterViewInit { + platformId = inject(PLATFORM_ID) link = input(""); params = input({}); @@ -26,6 +28,8 @@ export class SvgTextBoxComponent implements AfterViewInit { } updateBox() { + if (isPlatformServer(this.platformId)) return; + const bbox = this.textEl.nativeElement.getBBox(); this.box.set({ x: bbox.x - this.padding(), diff --git a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html index 640ce55..c05e86b 100644 --- a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html +++ b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html @@ -12,7 +12,7 @@ } - Date: Wed, 22 Apr 2026 13:47:32 +0300 Subject: [PATCH 2/3] docs: add C4 architecture documentation (context + container + flow diagrams) --- README.md | 8 +++ docs/architecture/README.md | 96 +++++++++++++++++++++++++++++++++ docs/architecture/container.mmd | 76 ++++++++++++++++++++++++++ docs/architecture/context.mmd | 58 ++++++++++++++++++++ docs/architecture/flow.mmd | 62 +++++++++++++++++++++ docs/architecture/proxy.mmd | 0 6 files changed, 300 insertions(+) create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/container.mmd create mode 100644 docs/architecture/context.mmd create mode 100644 docs/architecture/flow.mmd create mode 100644 docs/architecture/proxy.mmd diff --git a/README.md b/README.md index bfc45f9..7b853a4 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,14 @@ Perfect for: --- +## 🧠 Architecture + +Explore the system design: + +👉 docs/architecture/README.md + +--- + ## 🛠️ Development Chytanka requires a proxy server for handling some external sources. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..b311570 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,96 @@ +# 🧠 Chytanka Architecture + +This section documents the architecture of Chytanka using a C4-style approach. + +Chytanka is designed as a **multi-entry, embeddable reader platform** with hybrid data access. + +--- + +## 🚀 Key Concepts + +* 📖 **Embeddable Reader** — can run standalone or inside other websites (iframe) +* 🔌 **Multiple Entry Points**: + + * Direct (PWA) + * Browser Extension + * Embedded (Host Website) +* 🌐 **Hybrid Data Access**: + + * Direct requests to APIs + * Optional proxy for restricted sources + * Local file support (ZIP/CBZ/PDF) + +--- + +## 📊 Diagrams + +### 1. System Context + +👉 [Open context diagram](./context.mmd) + +Shows the ecosystem: + +* User interactions +* External systems +* Entry points + +--- + +### 2. Container Architecture + +👉 [Open container diagram](./container.mmd) + +Shows: + +* Main runtime parts of the system +* Frontend + proxy separation + +--- + +### 3. Data Flow + +👉 [Open flow diagram](./flow.mmd) + +Shows: + +* How requests are resolved +* Direct vs proxy logic + +--- + +### 4. Proxy Internals + +👉 [Open proxy diagram](./proxy.mmd) + +Shows: + +* Request pipeline +* Validation and fetch logic + +--- + +## 🧭 How to Read + +Start with **Context**, then go deeper: + +```text +Context → Container → Flow → Details +``` + +--- + +## 🧩 Architecture Style + +Chytanka follows a pragmatic interpretation of the C4 model: + +* Context diagrams describe the ecosystem +* Container diagrams describe runtime structure +* Flow diagrams explain behavior + +--- + +## 📌 Notes + +* Diagrams are written in **Mermaid** +* Stored alongside the code for versioning +* Designed to be readable without external tools diff --git a/docs/architecture/container.mmd b/docs/architecture/container.mmd new file mode 100644 index 0000000..91ce6a5 --- /dev/null +++ b/docs/architecture/container.mmd @@ -0,0 +1,76 @@ +C4Container +title Chytanka - Container Architecture + +UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="2") + +Person(user, "User") + +System_Ext(host, "🧱 Host Website", "3rd-party site embedding Chytanka via iframe") + +%% Enterprise_Boundary(b1, "Chytanka") { + + Container_Boundary(pwa, "📖 Chytanka PWA") { + + Container(app, "Viewer Application", "Angular", "UI, routing, rendering pages") + Container(embed, "Embed Bridge", "postMessage API", "Handles communication with host website") + Container(storage, "Local Storage", "IndexedDB / Cache API", "History, cache, metadata") + Container(facade, "Facade Layer", "Angular Services", "High-level API for viewer logic") + Container(worker, "File Worker", "Web Worker", "Reads ZIP/CBZ/PDF and extracts images") + Container(router, "Source Router", "TypeScript", "Decides data source (direct vs proxy)") + Container(api, "API Client", "HTTP", "Fetches remote resources") + } + + Boundary(b2, "Browser Extension / Proxy") { + Container(extension, "🧩 Chytanka Helper", "Browser Extension", "Adds 'Read in Chytanka' and opens links") + + Container(proxy, "🌐 Proxy Service", "Node.js (Express)", "Handles CORS, headers, rate limiting") + } +%% } + +Boundary(b_sources, "External Data Sources") { + System_Ext(online, "🌐 Online APIs", "MangaDex, Imgur, Reddit etc.") + System_Ext(files, "🗂️ File System", "Local files (CBZ, PDF, etc.)") +} + +Rel(user, app, "Uses UI") +Rel(user, extension, "Clicks extension UI") +Rel(user, host, "Visits website") + +Rel(extension, app, "Opens reader with URL") + +Rel(host, embed, "Sends/receives messages (postMessage)") + +Rel(app, facade, "Uses") +Rel(facade, router, "Delegates data loading") + +Rel(router, api, "Direct requests") +Rel(router, proxy, "Fallback / restricted requests") + +Rel(api, online, "Fetches data") +Rel(proxy, online, "Fetches data") + +Rel(app, worker, "Loads local files") +Rel(worker, files, "Reads files") + +Rel(app, storage, "Reads/Writes") + +UpdateElementStyle(app, $bgColor="#166496", $fontColor="#ffd60a", $borderColor="#166496") +UpdateElementStyle(facade, $bgColor="#2c7da0", $fontColor="#fff") +UpdateElementStyle(router, $bgColor="#2c7da0", $fontColor="#fff") +UpdateElementStyle(api, $bgColor="#2c7da0", $fontColor="#fff") +UpdateElementStyle(worker, $bgColor="#2c7da0", $fontColor="#fff") +UpdateElementStyle(storage, $bgColor="#2c7da0", $fontColor="#fff") +UpdateElementStyle(embed, $bgColor="#2c7da0", $fontColor="#fff") + +UpdateElementStyle(proxy, $bgColor="#88a8c8", $fontColor="#fff") +UpdateElementStyle(extension, $bgColor="#88a8c8", $fontColor="#fff") + +UpdateElementStyle(host, $bgColor="#444", $fontColor="#eee") +UpdateElementStyle(files, $bgColor="#444", $fontColor="#eee") +UpdateElementStyle(online, $bgColor="#444", $fontColor="#eee") + +UpdateRelStyle(router, proxy, $textColor="blue", $lineColor="blue") +UpdateRelStyle(proxy, online, $textColor="blue", $lineColor="blue") + +UpdateRelStyle(api, online, $textColor="green", $lineColor="green") +UpdateRelStyle(worker, files, $textColor="green", $lineColor="green") \ No newline at end of file diff --git a/docs/architecture/context.mmd b/docs/architecture/context.mmd new file mode 100644 index 0000000..806d2d3 --- /dev/null +++ b/docs/architecture/context.mmd @@ -0,0 +1,58 @@ +C4Context +title Chytanka - System Context + +Person(user, "User", "End user who reads manga and comics using Chytanka") + +Enterprise_Boundary(b_chytanka, "Chytanka") { + System(extension, "🧩 Chytanka Helper", "Browser extension") + System(chytanka, "📖 Chytanka", "PWA for reading manga and comics") + System(proxy, "🌐 Proxy Service", "Optional proxy for restricted requests") +} + +System_Ext(host, "🧱 Host Website", "3rd-party site embedding Chytanka and providing UI") + +Boundary(b_sources, "External Data Sources") { + System_Ext(files, "🗂️ File System", "Local files (CBZ, PDF, etc.)") + System_Ext(online, "🌐 Online APIs", "MangaDex, Imgur, Reddit etc.") +} + +Rel(user, chytanka, "Uses directly") +Rel(user, extension, "Clicks extension UI") +Rel(user, host, "Visits website") +Rel(extension, chytanka, "Opens links") + +Rel(host, chytanka, "Embeds via iframe") +Rel(host, chytanka, "Controls via postMessage") + +Rel(chytanka, online, "Fetches data (direct)") +Rel(chytanka, files, "Reads local files") +Rel(chytanka, proxy, "Optional requests") + +Rel(proxy, online, "Fetches data") + +%% Styles + +UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2") + +UpdateElementStyle(chytanka, $fontColor="#ffd60a", $bgColor="#166496", $borderColor="#166496") +UpdateElementStyle(extension, $fontColor="#fff", $bgColor="#88a8c8", $borderColor="#88a8c8") +UpdateElementStyle(proxy, $fontColor="#fff", $bgColor="#5f7f9f", $borderColor="#5f7f9f") + +UpdateElementStyle(host, $fontColor="#eee", $bgColor="#444") +UpdateElementStyle(files, $fontColor="#eee", $bgColor="#444") +UpdateElementStyle(online, $fontColor="#eee", $bgColor="#444") + +UpdateRelStyle(chytanka, proxy, $textColor="blue", $lineColor="blue", $offsetX="50", $offsetY="-10") +UpdateRelStyle(proxy, online, $textColor="blue", $lineColor="blue", $offsetX="0", $offsetY="10") + +UpdateRelStyle(chytanka, online, $textColor="#166496", $lineColor="#166496", $offsetX="0", $offsetY="-10") +UpdateRelStyle(chytanka, files, $textColor="#166496", $lineColor="#166496", $offsetX="-30", $offsetY="10") + +UpdateRelStyle(user, extension, $textColor="#88a8c8", $lineColor="#88a8c8", $offsetY="-20", $offsetX="15") +UpdateRelStyle(user, chytanka, $textColor="#166496", $lineColor="#166496", $offsetY="-20") +UpdateRelStyle(extension, chytanka, $textColor="#88a8c8", $lineColor="#88a8c8", $offsetX="-30", $offsetY="15") +UpdateRelStyle(user, host, $textColor="#88a8c8", $lineColor="#88a8c8", $offsetY="-20", $offsetX="-35") + +UpdateRelStyle(host, chytanka, $textColor="#88a8c8", $lineColor="#88a8c8", $offsetY="-20", $offsetX="35") + +UpdateElementStyle(b_sources, $fontColor="#ffeeee") \ No newline at end of file diff --git a/docs/architecture/flow.mmd b/docs/architecture/flow.mmd new file mode 100644 index 0000000..ea04a5c --- /dev/null +++ b/docs/architecture/flow.mmd @@ -0,0 +1,62 @@ +graph TD + %% Entry points + Start([Open Manga]) + + User[User Action] + Extension[Extension] + Host[Host Website] + + User --> Start + Extension --> Start + Host --> Start + + %% Input normalization + Start --> Input[Resolve Input Source] + + %% Source types + Input -->|URL| Remote[Remote Source] + Input -->|File| Local[Local File] + + %% --- LOCAL FLOW --- + Local --> Worker[File Worker] + Worker --> Extract[Extract Pages] + Extract --> Render[Render Viewer] + + %% --- REMOTE FLOW --- + Remote --> Router[Source Router] + + %% Decision + Router -->|Direct Allowed| Direct[Direct Fetch] + Router -->|CORS / Restricted| Proxy[Proxy Fetch] + + %% Direct path + Direct --> API[External API] + + %% Proxy path + Proxy --> ProxyService[Proxy Service] + ProxyService --> API + + %% Response + API --> Decode[Decode / Normalize] + Decode --> Render + + %% --- EMBED CONTROL --- + Host -.->|postMessage| Control[Embed Control Layer] + Control -.-> Render + + %% --- EXTENSION FLOW --- + Extension -->|Open URL| Start + + %% --- FINAL --- + Render --> Viewer[Viewer UI] + + %% Styling (optional readability) + classDef core fill:#166496,color:#ffd60a; + classDef logic fill:#2c7da0,color:#fff; + classDef external fill:#444,color:#eee; + classDef proxy fill:#88a8c8,color:#fff; + + class Start,Viewer core; + class Router,Input,Decode logic; + class API external; + class ProxyService proxy; \ No newline at end of file diff --git a/docs/architecture/proxy.mmd b/docs/architecture/proxy.mmd new file mode 100644 index 0000000..e69de29 From f906e07b838a06a4178f4e6b937984c26cb28d99 Mon Sep 17 00:00:00 2001 From: Andrii Rodzyk Date: Wed, 22 Apr 2026 17:35:50 +0300 Subject: [PATCH 3/3] feat: add extention button --- package.json | 2 +- src/app/link-parser/link-parser.module.ts | 4 +- .../ui/footer/footer.component.html | 4 -- .../ui/header/header.component.html | 1 + .../extension-button.component.html | 16 ++++++ .../extension-button.component.scss | 0 .../extension-button.component.ts | 18 ++++++ .../ui/extension-button/extension.service.ts | 57 +++++++++++++++++++ src/environments/environment.development.ts | 2 +- src/environments/environment.ts | 2 +- 10 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 src/app/shared/ui/extension-button/extension-button.component.html create mode 100644 src/app/shared/ui/extension-button/extension-button.component.scss create mode 100644 src/app/shared/ui/extension-button/extension-button.component.ts create mode 100644 src/app/shared/ui/extension-button/extension.service.ts diff --git a/package.json b/package.json index d74f5ad..338868e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chytanka", - "version": "0.13.58", + "version": "0.13.59", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/link-parser/link-parser.module.ts b/src/app/link-parser/link-parser.module.ts index a041e6f..403fcd6 100644 --- a/src/app/link-parser/link-parser.module.ts +++ b/src/app/link-parser/link-parser.module.ts @@ -15,6 +15,7 @@ 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'; +import { ExtensionButtonComponent } from '../shared/ui/extension-button/extension-button.component'; const FACADES = [ LinkParserFacade, @@ -38,7 +39,8 @@ const FACADES = [ LinkParserRoutingModule, FormsModule, SharedModule, - HistoryModule + HistoryModule, + ExtensionButtonComponent ], providers: [ ...FACADES diff --git a/src/app/link-parser/ui/footer/footer.component.html b/src/app/link-parser/ui/footer/footer.component.html index 14d44fd..1ab99c8 100644 --- a/src/app/link-parser/ui/footer/footer.component.html +++ b/src/app/link-parser/ui/footer/footer.component.html @@ -1,8 +1,4 @@ - - @for (item of social; track $index) { diff --git a/src/app/link-parser/ui/header/header.component.html b/src/app/link-parser/ui/header/header.component.html index 4cb8dfd..8ecb0f1 100644 --- a/src/app/link-parser/ui/header/header.component.html +++ b/src/app/link-parser/ui/header/header.component.html @@ -5,6 +5,7 @@ + diff --git a/src/app/shared/ui/extension-button/extension-button.component.html b/src/app/shared/ui/extension-button/extension-button.component.html new file mode 100644 index 0000000..79b9e4f --- /dev/null +++ b/src/app/shared/ui/extension-button/extension-button.component.html @@ -0,0 +1,16 @@ +@let b = extension.brawser.brouserInfo(); +@let mobile = b?.isMobile || false; + +@let link = extension.links.get((b?.name || '').toLowerCase()) || null; + +@if(extension.isInstalled() === false && link && (!mobile || b.name === 'Edge')) { + +🧩 + +} + + \ No newline at end of file diff --git a/src/app/shared/ui/extension-button/extension-button.component.scss b/src/app/shared/ui/extension-button/extension-button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/ui/extension-button/extension-button.component.ts b/src/app/shared/ui/extension-button/extension-button.component.ts new file mode 100644 index 0000000..a028f14 --- /dev/null +++ b/src/app/shared/ui/extension-button/extension-button.component.ts @@ -0,0 +1,18 @@ +import { Component, inject, signal } from '@angular/core'; +import { ExtensionService } from './extension.service'; + +@Component({ + selector: 'app-extension-button', + imports: [], + providers: [ExtensionService], + templateUrl: './extension-button.component.html', + styleUrl: './extension-button.component.scss' +}) +export class ExtensionButtonComponent { + extension: ExtensionService = inject(ExtensionService); + + ngOnInit() { + this.extension.detectExtension(); + } + +} \ No newline at end of file diff --git a/src/app/shared/ui/extension-button/extension.service.ts b/src/app/shared/ui/extension-button/extension.service.ts new file mode 100644 index 0000000..6417ea1 --- /dev/null +++ b/src/app/shared/ui/extension-button/extension.service.ts @@ -0,0 +1,57 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { BrowserService } from '../../data-access'; + +@Injectable({ + providedIn: 'root' +}) +export class ExtensionService { + isInstalled = signal(null); + + brawser = inject(BrowserService); + + // Chrome + Edge ID + private EXT_IDS = [ + 'nfmpooealaccocijodheenpfmndpiake', // Chrome + 'jneiemmgkdnlighokokpgknglniiebmc', // Edge + ]; + + readonly links = new Map([ + ['chrome', 'https://chromewebstore.google.com/detail/chytanka-helper/nfmpooealaccocijodheenpfmndpiake'], + ['opera', 'https://chromewebstore.google.com/detail/chytanka-helper/nfmpooealaccocijodheenpfmndpiake'], + ['edge', 'https://microsoftedge.microsoft.com/addons/detail/jneiemmgkdnlighokokpgknglniiebmc'] + ]); + + + async detectExtension() { + for (const id of this.EXT_IDS) { + try { + const res = await fetch(`chrome-extension://${id}/installed.json`, { + method: 'GET', + }); + + if (res.ok) { + this.isInstalled.set(true); + return; + } + } catch (e) { + // ignore + } + } + + this.isInstalled.set(false); + } + + installChrome() { + window.open( + 'https://chromewebstore.google.com/detail/chytanka-helper/nfmpooealaccocijodheenpfmndpiake', + '_blank' + ); + } + + installEdge() { + window.open( + 'https://microsoftedge.microsoft.com/addons/detail/jneiemmgkdnlighokokpgknglniiebmc', + '_blank' + ); + } +} diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 811476d..6bddb90 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.58-2026.4.11", + version: "0.13.59-2026.4.22", prod: false, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 1d4b8d4..6e856ef 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.58-2026.4.11", + version: "0.13.59-2026.4.22", prod: true, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`,