Skip to content

Commit b75f9fa

Browse files
authored
Merge pull request #54 from chytanka/develop
Develop
2 parents bc915cc + a7c0e9a commit b75f9fa

60 files changed

Lines changed: 737 additions & 240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 140 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
# Chytanka
22

3-
**Chytanka** is a versatile and user-friendly PWA for reading manga, comics, and other visual stories. Whether you prefer to read from popular online platforms, your own server, or local files, Chytanka is here to enhance your reading experience.
3+
**Chytanka** is a lightweight, privacy-friendly PWA for reading manga, comics, and visual stories — from online sources, your own server, or local files.
4+
5+
No accounts. No tracking. Just reading.
6+
7+
## 🚀 Get Started
8+
9+
👉 [https://chytanka.ink](https://chytanka.ink)
10+
11+
## ⚡ Why Chytanka?
12+
13+
- 🌍 Open anything — links, APIs, or local files
14+
- 📂 No uploads — your files stay on your device
15+
- 🧠 Smart behavior — auto-detect reading mode via tags
16+
- 🎮 Gamepad ready — full control without mouse
17+
- 🌓 Comfort first — night filter, fullscreen, responsive
18+
19+
## 🖼️ Preview
20+
21+
![Chytanka Screenshot](./src/assets/screenshots/12fps-drop-file.webp)
422

523
## Features
624

@@ -22,7 +40,7 @@ Chytanka supports opening episodes from the following platforms:
2240
- [ ] [Bluesky](https://bsky.app)
2341
<!-- - [ ] [Catbox](https://catbox.moe/) -->
2442

25-
### 🌐 **Custom JSON API**
43+
#### 🌐 **Custom JSON API**
2644

2745
Chytanka can open episodes from any custom JSON API returning the following format:
2846

@@ -44,7 +62,7 @@ Chytanka can open episodes from any custom JSON API returning the following form
4462
}
4563
```
4664

47-
### 📚 **Create and Share Readlists**
65+
#### 📚 **Create and Share Readlists**
4866

4967
Compile a readlist using [Chytanka Readlist Creator](https://chytanka.ink/list):
5068

@@ -85,7 +103,7 @@ Immerse yourself in reading with a fullscreen option.
85103
### 🕒 **Viewing History**
86104

87105
- [x] Tracks history of supported links.
88-
- [ ] File history support is planned.
106+
- [x] File history support is planned.
89107

90108
### ⌨️ **Keyboard Shortcuts**
91109

@@ -138,32 +156,137 @@ Chytanka includes built-in support for gamepads (tested with PlayStation-style c
138156

139157
If supported by the API, Chytanka warns users about NSFW content.
140158

159+
### 🏷️ Tags in Titles
160+
161+
Chytanka supports special tags inside file names or episode titles to automatically control viewer behavior.
162+
163+
Tags are parsed from the title and applied on load.
164+
165+
#### 📌 Supported Tags
166+
167+
You can include any of the following words in the title:
168+
169+
```
170+
rtl, ltr, ver, long, scroll, nsfw, sfw, color, bw, demo, extra
171+
```
172+
173+
Example:
174+
175+
```
176+
My Manga Vol.1 [rtl][nsfw]
177+
Chapter 5 - [long][scroll]
178+
Demo Episode [ltr]
179+
```
180+
181+
#### 🎯 What Tags Do
182+
183+
##### 📖 View Mode
184+
185+
These tags control how pages are displayed:
186+
187+
| Tag | Description | Mode |
188+
| ----------- | ----------------------- | ---- |
189+
| `[rtl]` | Right-to-left reading | 1 |
190+
| `[ltr]` | Left-to-right reading | 2 |
191+
| `[ver]` | Vertical / webtoon mode | 3 |
192+
| `[long]` | Vertical / long strip | 3 |
193+
| `[scroll]` | Vertical scrolling | 3 |
194+
195+
> If multiple tags are present, the first matched tag wins.
196+
197+
##### 🔞 Content Flags
198+
199+
| Tag | Effect |
200+
| -------- | --------------------- |
201+
| `[nsfw]` | Marks episode as NSFW |
202+
| `[sfw]` | (Reserved / optional) |
203+
204+
##### 🎨 Additional Tags (for future use)
205+
206+
These tags are parsed but may be used later:
207+
208+
- `[color]` – colored pages
209+
- `[bw]` – black & white
210+
- `[demo]` – demo content
211+
- `[extra]` – bonus materials
212+
213+
#### ⚙️ How It Works
214+
215+
- Tags are case-insensitive
216+
- Tags are detected as standalone words (e.g. rtl, not ultra)
217+
- Parsed automatically from:
218+
- file name
219+
- episode title
220+
221+
#### 💡 Example
222+
223+
```
224+
Attack on Titan - Chapter 1 [rtl][nsfw].cbz
225+
```
226+
227+
Result:
228+
229+
- View mode → Right-to-left
230+
- NSFW flag → enabled
231+
232+
#### 🧠 Notes
233+
234+
- *Tags are processed on episode load*
235+
- *They override default viewer settings*
236+
- *Designed for compatibility with local files (.cbz, .zip) and remote sources*
237+
141238
### 🖇️ **Embed Chytanka on Your Website**
142239

143-
Embed Chytanka using an iframe and interact with it via `postMessage`. Learn more in the [Embedding Guide](https://github.com/chytanka/chytanka.github.io/wiki/Embedding-Chytanka-on-Your-Website).
240+
You can embed Chytanka into your website and control it via `postMessage`.
241+
242+
Perfect for:
243+
244+
- manga sites
245+
- blogs
246+
- personal collections
247+
248+
Learn more in the [Embedding Guide](https://github.com/chytanka/chytanka.github.io/wiki/Embedding-Chytanka-on-Your-Website).
144249

145250
---
146251

147-
## Development server
252+
## 🛠️ Development
148253

149-
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
254+
Chytanka requires a proxy server for handling some external sources.
255+
Proxy repository: [https://github.com/chytanka/proxy](https://github.com/chytanka/proxy)
150256

151-
## Code scaffolding
257+
> ⚠️ *Important*: Both Chytanka and the proxy must run on the *same host.*
152258
153-
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
259+
### ▶️ Run locally on Windows
154260

155-
## Build
261+
```bash
262+
# Start proxy on the same host
263+
set NODE_ENV=dev&& set HOST=192.168.0.0 && node index.js
264+
```
265+
266+
```bash
267+
# Start Chytanka app
268+
ng serve --host 192.168.0.0
269+
```
156270

157-
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
271+
### 💡 Notes
158272

159-
## Running unit tests
273+
- *The proxy handles CORS and headers for external image sources.*
274+
- *Make sure both services use the same host IP (e.g., 192.168.0.0).*
275+
- *Adjust the host if your network requires a different local IP.*
160276

161-
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
277+
### 🧠 Why a Proxy?
162278

163-
## Running end-to-end tests
279+
- Bypasses CORS restrictions from image hosts
280+
- Normalizes headers
281+
- Ensures safe and consistent fetching of external content
164282

165-
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
283+
## 🔗 Other Chytanka Projects
166284

167-
## Further help
285+
Chytanka is more than just a reader — it’s an ecosystem. Check out these companion projects:
168286

169-
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
287+
| Project | What it does | Repository |
288+
| ------------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
289+
| **Meta Image Generator** | Generates social preview images (meta tags) for episodes | [chytanka-meta-image](https://github.com/chytanka/chytanka-meta-image) |
290+
| **Chytanka Helper** | Browser extension: adds a button to open supported site links directly in Chytanka | [chytanka-helper](https://github.com/chytanka/chytanka-hepler) |
291+
| **Opera GX Theme** | Custom theme for Opera GX tailored to Chytanka | [chytanka-gx-mod](https://github.com/chytanka/chytanka-gx-mod) |
292+
| **Chytanka Make** | Create and edit CBZ files from images, reorder pages, add metadata | [chytanka-make](https://github.com/chytanka/chytanka-make) |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chytanka",
3-
"version": "0.13.51",
3+
"version": "0.13.54",
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve",

src/app/@site-modules/@common-read/ui/common-read/common-read.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@if(episode$() | async; as episode){
22

33
@if (!(loading$() | async)) {
4-
<app-viewer [playlistInput]="playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlaylistItem()"
4+
<app-viewer (pageChange)="pageChange.emit($event)" [playlistInput]="playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlaylistItem()"
55
[episode]="episode">
66
<div ngProjectAs="source-logo"><ng-content select="source-logo" /></div>
77
<ng-content />

src/app/@site-modules/@common-read/ui/common-read/common-read.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class CommonReadComponent {
2222
currentPlaylistItem = input<PlaylistItem | undefined>();
2323

2424
refreshData = output();
25+
pageChange = output<{ total: number, current: number[] }>();
2526

2627
onRefreshData() {
2728
this.refreshData.emit();

src/app/@site-modules/@common-read/utils/composition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export interface CompositionImage {
1111
export interface CompositionPublisher {
1212
id: string;
1313
site: string;
14-
avatar: CompositionImage;
14+
avatar?: CompositionImage;
1515
description: string
1616
name: string;
17-
links: Array<{
17+
links?: Array<{
1818
link: string,
1919
title: string
2020
}>

src/app/@site-modules/@common-read/utils/read-base-component.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { BehaviorSubject, MonoTypeOperatorFunction, Observable, OperatorFunction
22
import { CompositionEpisode } from "./composition";
33
import { ActivatedRoute, ParamMap } from "@angular/router";
44
import { Title } from "@angular/platform-browser";
5-
import { ChangeDetectorRef, OnDestroy, WritableSignal, inject, signal } from "@angular/core";
5+
import { ChangeDetectorRef, OnDestroy, WritableSignal, inject, output, signal } from "@angular/core";
66
import { LangService } from "../../../shared/data-access/lang.service";
77
import { HistoryService } from "../../../history/data-access/history.service";
8-
import { ViewerService } from "../../../shared/data-access";
98
import { PlaylistItem, PlaylistService, isPlaylist } from "../../../playlist/data-access/playlist.service";
109
import { MetaTagsService } from "../../../shared/data-access/meta-tags.service";
10+
import { ViewerService } from "../../../viewer/services";
1111

1212
export abstract class ReadBaseComponent {
1313
protected refresh$: BehaviorSubject<null> = new BehaviorSubject<null>(null);
@@ -109,21 +109,25 @@ export abstract class ReadBaseComponent {
109109

110110
})
111111
}
112-
112+
site = '';
113+
post_id = '';
113114
protected tapSaveToHistory(site: string, post_id: string): MonoTypeOperatorFunction<CompositionEpisode> {
114115
return tap(async (episode: CompositionEpisode) => {
115116
if (episode) {
117+
this.site = site;
118+
this.post_id = post_id;
116119
let e = structuredClone(episode);
120+
const pages = e.images?.length ?? 0;
117121
e.images = [];
118-
await this.saveToHistory(site, post_id, episode.title, episode.images[0]?.src, e);
122+
await this.saveToHistory(site, post_id, episode.title, episode.images[0]?.src, e, pages, 1);
119123
}
120124
})
121125
}
122126

123127
public history: HistoryService = inject(HistoryService);
124128

125-
async saveToHistory(site: string, post_id: string, title: string, cover: string, episode: any) {
126-
await this.history.addHistory(site, post_id, title, cover, episode);
129+
async saveToHistory(site: string, post_id: string, title: string, cover: string, episode: any, pages: number = 0, page: number = 1) {
130+
await this.history.addHistory(site, post_id, title, cover, episode, pages, page);
127131
}
128132

129133
cdr = inject(ChangeDetectorRef)
@@ -143,4 +147,10 @@ export abstract class ReadBaseComponent {
143147

144148
})
145149
}
150+
151+
protected onPageChange(event: { total: number, current: number[] }) {
152+
const lastPageIndex = event.current[event.current.length - 1] - 1;
153+
this.history.updatePage(this.site, this.post_id, lastPageIndex + 1);
154+
155+
}
146156
}

src/app/@site-modules/imgchest/imgchest-shell.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IMGCHEST_PATH } from '../../app-routing.module';
88
@Component({
99
imports: [CommonReadModule],
1010
selector: 'app-imgchest-shell',
11-
template: `<app-common-read [episode$]="episode$" [error$]="error$" [loading$]="loading$" (refreshData)="refreshData()" [playlist]="playlistService.playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlItem()">
11+
template: `<app-common-read (pageChange)="onPageChange($event)" [episode$]="episode$" [error$]="error$" [loading$]="loading$" (refreshData)="refreshData()" [playlist]="playlistService.playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlItem()">
1212
<source-copyright [sourceName]="sourceName()" [sourceUrl]="sourceUrl()" />
1313
<source-copyright-logo ngProjectAs="source-logo" [sourceName]="sourceName()" [sourceUrl]="sourceUrl()" [sourceImageSrc]="sourceImageSrc()" />
1414
</app-common-read>`

src/app/@site-modules/imgur/imgur-shell.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IMGUR_PATH } from '../../app-routing.module';
88
@Component({
99
imports: [CommonReadModule],
1010
selector: 'app-imgur-shell',
11-
template: `<app-common-read [episode$]="episode$" [error$]="error$" [loading$]="loading$" (refreshData)="refreshData()" [playlist]="playlistService.playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlItem()">
11+
template: `<app-common-read (pageChange)="onPageChange($event)" [episode$]="episode$" [error$]="error$" [loading$]="loading$" (refreshData)="refreshData()" [playlist]="playlistService.playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlItem()">
1212
<source-copyright [sourceName]="sourceName()" [sourceUrl]="sourceUrl()" />
1313
<source-copyright-logo ngProjectAs="source-logo" [sourceName]="sourceName()" [sourceUrl]="sourceUrl()" [sourceImageSrc]="sourceImageSrc()" />
1414
</app-common-read>`

src/app/@site-modules/mangadex/mangadex-shell.component.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { MANGADEX_PATH } from '../../app-routing.module';
88
@Component({
99
imports: [CommonReadModule],
1010
selector: 'app-mangadex-shell',
11-
template: `<app-common-read [episode$]="episode$" [error$]="error$" [loading$]="loading$" (refreshData)="refreshData()" [playlist]="playlistService.playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlItem()" >
11+
template: `<app-common-read (pageChange)="onPageChange($event)" [episode$]="episode$" [error$]="error$" [loading$]="loading$" (refreshData)="refreshData()" [playlist]="playlistService.playlist()" [playlistLink]="playlistLink()" [currentPlaylistItem]="currentPlItem()" >
1212
<source-copyright [sourceName]="sourceName()" [sourceUrl]="sourceUrl()" />
1313
<source-copyright-logo ngProjectAs="source-logo" [sourceName]="sourceName()" [sourceUrl]="sourceUrl()" [sourceImageSrc]="sourceImageSrc()" />
1414
</app-common-read>`
@@ -33,11 +33,13 @@ export default class MangadexShellComponent extends ReadBaseComponent {
3333
switchMap(ch => {
3434
const imgs$ = this.mangadex.getChapterImages(id);
3535
const manga$ = (ch.mangaId) ? this.mangadex.getManga(ch.mangaId) : of(null);
36+
const group$ = (ch.publisher?.id !== null) ? this.mangadex.getScanlationGroup(ch.publisher?.id!) : of(null);
3637

37-
return forkJoin([imgs$, manga$]).pipe(
38-
map(([imgs, manga]) => {
38+
return forkJoin([imgs$, manga$, group$]).pipe(
39+
map(([imgs, manga, group]) => {
3940
ch.images = imgs;
4041
ch.nsfw = manga?.nsfw ?? undefined;
42+
ch.publisher = group ?? undefined;
4143
return ch;
4244
}),
4345
this.catchError()

0 commit comments

Comments
 (0)