diff --git a/README.md b/README.md
index 6e1a803..29ad77e 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
Chytanka supports opening episodes from the following platforms:
- [x] ~~[Blankary](https://blankary.com)~~ (image support has been discontinued)
-- [x] [Comick](https://comick.io)
+- [x] ~~[Comick](https://comick.io)~~ (baned)
- [x] [Imgur](https://imgur.com)
- [x] [Mangadex](https://mangadex.org)
- [x] [Nhentai](https://nhentai.net)
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index f0e5dde..32a1370 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,8 +1,9 @@
-import { Component, HostListener, PLATFORM_ID, WritableSignal, inject, signal } from '@angular/core';
+import { Component, HostListener, PLATFORM_ID, WritableSignal, effect, inject, signal } from '@angular/core';
import { LangService } from './shared/data-access/lang.service';
import { ActivatedRoute } from '@angular/router';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { environment } from '../environments/environment';
+import { DISPLAY_MODES, LinkParserSettingsService } from './link-parser/data-access/link-parser-settings.service';
const SCALE_GAP = 128;
@@ -15,6 +16,8 @@ const SCALE_GAP = 128;
export class AppComponent {
private readonly document = inject(DOCUMENT);
platformId = inject(PLATFORM_ID)
+ setts = inject(LinkParserSettingsService)
+
constructor(public lang: LangService, private route: ActivatedRoute) {
this.lang.updateManifest()
@@ -22,7 +25,7 @@ export class AppComponent {
this.route.pathFromRoot[0].queryParams.subscribe(async q => {
const l = q['lang']
-
+
if (l) {
this.lang.setLang(l)
}
@@ -37,11 +40,25 @@ export class AppComponent {
console.log(`%c${msg}`, "background-color: #166496; color: #ffd60a; font-size: 4rem; font-family: monospace; padding: 8px 16px");
}
-
+ effect(() => {
+ this.updateDisplayMode();
+ });
}
ngOnInit() {
this.initScaleDifference();
+ this.updateDisplayMode();
+ }
+
+ updateDisplayMode() {
+ if (this.setts.displayMode != undefined) {
+ for (const mode of DISPLAY_MODES) {
+ this.document.documentElement.classList.remove(mode);
+ }
+ this.document.documentElement.classList.add(this.setts.displayMode() + 'mode');
+
+ }
+
}
@HostListener('window:resize')
diff --git a/src/app/file/data-access/zip.worker.ts b/src/app/file/data-access/zip.worker.ts
index 70f7732..d8a5923 100644
--- a/src/app/file/data-access/zip.worker.ts
+++ b/src/app/file/data-access/zip.worker.ts
@@ -1,6 +1,16 @@
///
+// TODO: change to https://github.com/101arrowz/fflate
+// because jszip is toooo slow
+// https://chatgpt.com/c/68cadcf9-6c28-8329-add8-cb20abaa2f85
+
import JSZip from 'jszip';
+import { filterImages, getAcbfFile, getComicInfoFile, processFile, processImagesInBatches } from '../utils';
+
+const metadataFiles = [
+ { getter: getComicInfoFile, type: 'comicinfo' },
+ { getter: getAcbfFile, type: 'acbf' },
+];
addEventListener('message', ({ data }) => {
const arrayBuffer = data.arrayBuffer;
@@ -11,59 +21,19 @@ addEventListener('message', ({ data }) => {
.then(async zip => {
const filesName: string[] = Object.keys(zip.files);
- // console.dir(zip.files)
-
- const comicInfoFile = getComicInfoFile(filesName)
-
- if (comicInfoFile) {
- const comicinfo = zip.files[comicInfoFile]
- await comicinfo.async('text').then(text => { postMessage({ type: 'comicinfo', data: text }); })
- }
+ console.log(filesName);
+
- const acbf = getAcbfFile(filesName)
- if (acbf) {
- const acbfF = zip.files[acbf]
- await acbfF.async('text').then(text => { postMessage({ type: 'acbf', data: text }); })
+ // metadata
+ for (const { getter, type } of metadataFiles) {
+ await processFile(getter(filesName), zip, type);
}
- const images = filterImages(filesName).sort()
+ // images
+ const images = filterImages(filesName).sort();
postMessage({ type: 'zipopen', data: { count: images.length } });
- for (let i = 0; i < images.length; i++) {
- const filename = images[i];
-
- await zip.files[filename].async('blob').then(blob => {
- const url = URL.createObjectURL(blob);
- postMessage({ type: 'file', url: url, index: i });
- });
- }
-
+ await processImagesInBatches(zip, images, 30);
});
-});
-
-function filterImages(fileList: Array) {
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
-
- return fileList.filter(file => {
- const extension = file.substring(file.lastIndexOf('.')).toLowerCase();
- return imageExtensions.includes(extension);
- });
-}
-
-function getComicInfoFile(fileList: Array) {
- const resultArray = fileList.filter(f => f.toLowerCase() == 'comicinfo.xml')
-
- return resultArray.length > 0 ? resultArray[0] : false
-}
-
-function getAcbfFile(fileList: Array) {
- const imageExtensions = ['.acbf'];
-
- const result = fileList.filter(file => {
- const extension = file.substring(file.lastIndexOf('.')).toLowerCase();
- return imageExtensions.includes(extension);
- })
-
- return result.length > 0 ? result[0] : null;
-}
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/app/file/utils/comic-metadata-files.ts b/src/app/file/utils/comic-metadata-files.ts
new file mode 100644
index 0000000..196da54
--- /dev/null
+++ b/src/app/file/utils/comic-metadata-files.ts
@@ -0,0 +1,16 @@
+export function getComicInfoFile(fileList: Array) {
+ const resultArray = fileList.filter(f => f.toLowerCase() == 'comicinfo.xml')
+
+ return resultArray.length > 0 ? resultArray[0] : false
+}
+
+export function getAcbfFile(fileList: Array) {
+ const imageExtensions = ['.acbf'];
+
+ const result = fileList.filter(file => {
+ const extension = file.substring(file.lastIndexOf('.')).toLowerCase();
+ return imageExtensions.includes(extension);
+ })
+
+ return result.length > 0 ? result[0] : null;
+}
\ No newline at end of file
diff --git a/src/app/file/utils/filter-images.ts b/src/app/file/utils/filter-images.ts
new file mode 100644
index 0000000..bfd6853
--- /dev/null
+++ b/src/app/file/utils/filter-images.ts
@@ -0,0 +1,8 @@
+export function filterImages(fileList: Array) {
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
+
+ return fileList.filter(file => {
+ const extension = file.substring(file.lastIndexOf('.')).toLowerCase();
+ return imageExtensions.includes(extension);
+ });
+}
\ No newline at end of file
diff --git a/src/app/file/utils/index.ts b/src/app/file/utils/index.ts
new file mode 100644
index 0000000..b56be60
--- /dev/null
+++ b/src/app/file/utils/index.ts
@@ -0,0 +1,4 @@
+export * from './filter-images';
+export * from './comic-metadata-files';
+export * from './process-file';
+export * from './process-images-in-batches';
\ No newline at end of file
diff --git a/src/app/file/utils/process-file.ts b/src/app/file/utils/process-file.ts
new file mode 100644
index 0000000..fa22eab
--- /dev/null
+++ b/src/app/file/utils/process-file.ts
@@ -0,0 +1,13 @@
+export async function processFile(
+ fileName: string | false | null,
+ zip: any,
+ type: string
+) {
+ if (!fileName) return;
+
+ const file = zip.files[fileName];
+ if (!file) return;
+
+ const text = await file.async('text');
+ postMessage({ type, data: text });
+}
\ No newline at end of file
diff --git a/src/app/file/utils/process-images-in-batches.ts b/src/app/file/utils/process-images-in-batches.ts
new file mode 100644
index 0000000..3d6a5ef
--- /dev/null
+++ b/src/app/file/utils/process-images-in-batches.ts
@@ -0,0 +1,23 @@
+import JSZip from "jszip";
+
+export async function processImagesInBatches(
+ zip: JSZip,
+ images: string[],
+ batchSize = 20
+) {
+ for (let i = 0; i < images.length; i += batchSize) {
+ const batch = images.slice(i, i + batchSize);
+
+ await Promise.all(
+ batch.map(async (filename, index) => {
+ const blob = await zip.files[filename].async('blob');
+ const url = URL.createObjectURL(blob);
+ postMessage({
+ type: 'file',
+ url,
+ index: i + index,
+ });
+ })
+ );
+ }
+}
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 a8b7375..023149b 100644
--- a/src/app/history/ui/history-list/history-list.component.html
+++ b/src/app/history/ui/history-list/history-list.component.html
@@ -2,7 +2,7 @@
@let sites = historyItems() | async;
@if (sites && sites.length > 0) {
-
+
SITES HISTORY
@if ( sites.length > 0) {
@@ -21,8 +21,8 @@
}
@if (files && files.length > 0) {
-
-
+
+
FILES HISTORY |
{{fileSize() | filesize}} |
{{fileCount()}}
@@ -35,7 +35,7 @@
@for (item of files; track item.sha256;) {
- @let ab = item.arrayBuffer;
+ @let ab = item.arrayBuffer;
{{lang.ph().historyEmpty}}
+{{lang.ph().histyryEmptyDesc}}
}
\ 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 f38b586..059ecd4 100644
--- a/src/app/history/ui/history-list/history-list.component.ts
+++ b/src/app/history/ui/history-list/history-list.component.ts
@@ -6,7 +6,7 @@ import { FileHistoryService } from '../../../file/data-access/file-history.servi
@Component({
selector: 'app-history-list',
templateUrl: './history-list.component.html',
- styleUrl: './history-list.component.scss',
+ styleUrls: ['./history-list.component.scss', '../../../shared/ui/@styles/details.scss'],
standalone: false
})
export class HistoryListComponent {
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 9c8551b..2d94b41 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
@@ -1,6 +1,11 @@
import { isPlatformBrowser } from '@angular/common';
import { Injectable, PLATFORM_ID, WritableSignal, computed, inject, signal } from '@angular/core';
+export const DISPLAY_MODES = ['softmode', 'truemode'];
+const NAME_DISPLAY_MODE = 'displayMode';
+const NAME_AUTO_PASTE_LINK = 'autoPasteLink';
+const NAME_SEASONAL_THEME = 'seasonalTheme';
+
@Injectable({
providedIn: 'root'
})
@@ -12,6 +17,7 @@ export class LinkParserSettingsService {
constructor() {
this.initAutoPasteLink()
this.initSeasonalTheme()
+ this.initDisplayMode()
}
initAutoPasteLink() {
@@ -28,6 +34,23 @@ export class LinkParserSettingsService {
localStorage.setItem('autoPasteLink', n.toString())
}
+ displayMode!: WritableSignal;
+
+ initDisplayMode() {
+ if (!isPlatformBrowser(this.platformId)) return;
+
+ const n = localStorage.getItem(NAME_DISPLAY_MODE) === null ? 'soft' : localStorage.getItem(NAME_DISPLAY_MODE) as string;
+ this.displayMode = signal(n);
+ this.setDisplayMode(n);
+ }
+
+ setDisplayMode(n: string) {
+ if (!isPlatformBrowser(this.platformId)) return;
+
+ this.displayMode.update(v => n);
+ localStorage.setItem(NAME_DISPLAY_MODE, n)
+ }
+
/**
*
*/
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 509b665..913a647 100644
--- a/src/app/link-parser/data-access/link-parser.service.ts
+++ b/src/app/link-parser/data-access/link-parser.service.ts
@@ -14,7 +14,7 @@ export class LinkParserService {
"Reddit",
"MangaDex",
"Zenko",
- "Comick",
+ // "Comick",
"NHentai",
"Yandere Pools",
"Imgchest",
diff --git a/src/app/link-parser/link-parser/link-parser.component.html b/src/app/link-parser/link-parser/link-parser.component.html
index 21977e4..e1f42ac 100644
--- a/src/app/link-parser/link-parser/link-parser.component.html
+++ b/src/app/link-parser/link-parser/link-parser.component.html
@@ -1,9 +1,9 @@
-
+
@defer (on immediate) {
diff --git a/src/app/link-parser/link-parser/link-parser.component.scss b/src/app/link-parser/link-parser/link-parser.component.scss
index 44bab4c..53b1836 100644
--- a/src/app/link-parser/link-parser/link-parser.component.scss
+++ b/src/app/link-parser/link-parser/link-parser.component.scss
@@ -17,38 +17,29 @@
app-chytanka-logo-with-tags {
height: unset;
- }
- }
- &.pride {
- --pride-red: oklch(from #e40303 0.2624 0.064157 h);
- --pride-ora: oklch(from #ff8c00 0.2624 0.064157 h);
- --pride-yel: oklch(from #ffed00 0.2624 0.064157 h);
- --pride-gre: oklch(from #008026 0.2624 0.064157 h);
- --pride-blu: oklch(from #004dff 0.2624 0.064157 h);
- --pride-fio: oklch(from #750787 0.2624 0.064157 h);
- background: linear-gradient(120deg, var(--pride-red), var(--pride-ora), var(--pride-yel), var(--pride-gre), var(--pride-blu), var(--pride-fio));
+ ::ng-deep svg {
+ margin: auto;
+ height: auto;
+ width: 40vw;
+ }
+ }
}
- &.halloween {
- background: #222;
+ @media (max-width: 640px) and (max-height: 640px) {
+ grid-template-rows: unset;
}
@media (prefers-color-scheme: light) {
background: var(--surface-avarage);
+ }
+}
- &.pride {
- --pride-red: oklch(from #e40303 0.96 0.0128 h);
- --pride-ora: oklch(from #ff8c00 0.96 0.0128 h);
- --pride-yel: oklch(from #ffed00 0.96 0.0128 h);
- --pride-gre: oklch(from #008026 0.96 0.0128 h);
- --pride-blu: oklch(from #004dff 0.96 0.0128 h);
- --pride-fio: oklch(from #750787 0.96 0.0128 h);
- }
+:root.truemode :host {
+ background: black;
- &.halloween {
- background: #f9ece5;
- }
+ @media (prefers-color-scheme: light) {
+ background: white;
}
}
@@ -68,7 +59,10 @@ aside {
@media (max-aspect-ratio: 1) or (max-width: 640px) {
padding: 2ch;
+ }
+ @media (max-width: 640px) and (max-height: 640px) {
+ display: none;
}
}
@@ -86,15 +80,6 @@ lp-header {
bottom: unset;
}
-.logo {
- display: block;
- max-width: 100%;
- max-height: 100%;
- min-height: 0;
- height: auto;
- margin: auto;
-}
-
:host ::ng-deep app-overlay {
&::after,
@@ -109,15 +94,10 @@ lp-header {
:host:has(dialog[open]) {
transform: scale(calc(1 - var(--scale-diff-x, .1)), calc(1 - var(--scale-diff-y, .1)));
- // filter: blur(3px);
}
-// :host:has(input[type=url]:focus) {
-
-// lp-footer,
-// lp-header,
-// #createListLink {
-// opacity: 0;
-// pointer-events: none;
-// }
-// }
\ No newline at end of file
+@media (prefers-reduced-motion: reduce) {
+ :host:has(dialog[open]) {
+ transform: none !important;
+ }
+}
\ No newline at end of file
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 0223394..d2486e8 100644
--- a/src/app/link-parser/link-parser/link-parser.component.ts
+++ b/src/app/link-parser/link-parser/link-parser.component.ts
@@ -10,7 +10,12 @@ import { take } from 'rxjs';
templateUrl: './link-parser.component.html',
styleUrls: [
'./link-parser.component.scss',
- './link-parser.dual-screen.component.scss'
+ './link-parser.dual-screen.component.scss',
+ './themes/pride.scss',
+ './themes/halloween.scss',
+ './themes/newyear.scss',
+ './themes/valentine.scss'
+
],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
diff --git a/src/app/link-parser/link-parser/themes/halloween.scss b/src/app/link-parser/link-parser/themes/halloween.scss
new file mode 100644
index 0000000..3780508
--- /dev/null
+++ b/src/app/link-parser/link-parser/themes/halloween.scss
@@ -0,0 +1,56 @@
+@property --halloween-base {
+ syntax: "";
+ inherits: true;
+ initial-value: #FF7518;
+}
+
+:host.halloween {
+ background: #222;
+
+ @media (prefers-color-scheme: light) {
+ background: #f9ece5;
+ }
+
+ ::ng-deep #logoPath {
+
+ animation: halloween 5s steps(3) alternate infinite;
+
+ .light {
+ fill: oklch(from var(--halloween-base) .7 0.2 h);
+ }
+
+ .dark {
+ fill: oklch(from var(--halloween-base) .7 0.2 h);
+ }
+
+ }
+}
+
+::ng-deep .slogan-halloween {
+ color: #FF7518;
+}
+
+::ng-deep app-text-embracer.halloween>span {
+ --shc: oklch(from var(--halloween-base) var(--avarage-l-2) c h);
+ --border-color: oklch(from var(--halloween-base) .64 0.2 h);
+ --dot-color: oklch(from var(--halloween-base) .7 0.2 h);
+ color: oklch(from var(--halloween-base) .7 0.2 h);
+ -webkit-text-stroke: var(--shc) var(--border-width);
+
+ animation: halloween 5s steps(3) alternate infinite;
+}
+
+@keyframes halloween {
+ 0% {
+ --halloween-base: #A0FF00;
+
+ }
+
+ 50% {
+ --halloween-base: #FF7518;
+ }
+
+ 100% {
+ --halloween-base: #6C2DC7;
+ }
+}
\ No newline at end of file
diff --git a/src/app/link-parser/link-parser/themes/newyear.scss b/src/app/link-parser/link-parser/themes/newyear.scss
new file mode 100644
index 0000000..b50edc1
--- /dev/null
+++ b/src/app/link-parser/link-parser/themes/newyear.scss
@@ -0,0 +1,10 @@
+:host.newyear {
+ --c1: oklch(from #0A0A2A 0.2624 0.064157 h);
+ --c2: oklch(from #2C0C4C 0.2624 0.064157 h);
+ background: linear-gradient(to bottom, var(--c1), var(--c2));
+
+ @media (prefers-color-scheme: light) {
+ --c1: oklch(from #0A0A2A 0.96 0.0128 h);
+ --c2: oklch(from #2C0C4C 0.96 0.0128 h);
+ }
+}
\ No newline at end of file
diff --git a/src/app/link-parser/link-parser/themes/pride.scss b/src/app/link-parser/link-parser/themes/pride.scss
new file mode 100644
index 0000000..1622d7f
--- /dev/null
+++ b/src/app/link-parser/link-parser/themes/pride.scss
@@ -0,0 +1,99 @@
+:host.pride {
+ --pride-red: oklch(from #e40303 0.2624 0.064157 h);
+ --pride-ora: oklch(from #ff8c00 0.2624 0.064157 h);
+ --pride-yel: oklch(from #ffed00 0.2624 0.064157 h);
+ --pride-gre: oklch(from #008026 0.2624 0.064157 h);
+ --pride-blu: oklch(from #004dff 0.2624 0.064157 h);
+ --pride-fio: oklch(from #750787 0.2624 0.064157 h);
+
+ background: linear-gradient(120deg, var(--pride-red), var(--pride-ora), var(--pride-yel), var(--pride-gre), var(--pride-blu), var(--pride-fio));
+
+ @media (prefers-color-scheme: light) {
+ --pride-red: oklch(from #e40303 0.96 0.0128 h);
+ --pride-ora: oklch(from #ff8c00 0.96 0.0128 h);
+ --pride-yel: oklch(from #ffed00 0.96 0.0128 h);
+ --pride-gre: oklch(from #008026 0.96 0.0128 h);
+ --pride-blu: oklch(from #004dff 0.96 0.0128 h);
+ --pride-fio: oklch(from #750787 0.96 0.0128 h);
+ }
+}
+
+::ng-deep app-text-embracer.pride>span {
+ --theme-base: #166496;
+ --shc: oklch(from var(--theme-base) var(--avarage-l-2) c h);
+ --border-color: oklch(from var(--theme-base) .64 0.2 h);
+ --dot-color: oklch(from var(--theme-base) .7 0.2 h);
+ color: oklch(from var(--theme-base) .7 0.2 h);
+ -webkit-text-stroke: var(--shc) var(--border-width);
+
+ // --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0);
+
+ border-image: linear-gradient(90deg, #e40303, #ff8c00, #ffed00, #008026, #004dff, #750787);
+ border-image-slice: 1;
+
+ &:nth-of-type(2),
+ &:nth-of-type(1) {
+ --theme-base: #e40303;
+ }
+
+ &:nth-of-type(3) {
+ --theme-base: #ff8c00;
+ }
+
+ &:nth-of-type(4) {
+ --theme-base: #ffed00;
+ }
+
+ &:nth-of-type(5) {
+ --theme-base: #008026;
+ }
+
+ &:nth-of-type(6) {
+ --theme-base: #004dff;
+ }
+
+ &:nth-of-type(7) {
+ --theme-base: #750787;
+ }
+}
+
+::ng-deep app-text-embracer.pride:has(span:nth-of-type(8))>span {
+
+ &:nth-of-type(3),
+ &:nth-of-type(1),
+ &:nth-of-type(2) {
+ --theme-base: #e40303;
+ }
+
+ &:nth-of-type(4) {
+ --theme-base: #ff8c00;
+ }
+
+ &:nth-of-type(5) {
+ --theme-base: #ffed00;
+ }
+
+ &:nth-of-type(6) {
+ --theme-base: #008026;
+ }
+
+ &:nth-of-type(7) {
+ --theme-base: #004dff;
+ }
+
+ &:nth-of-type(8) {
+ --theme-base: #750787;
+ }
+}
+
+::ng-deep .slogan-rainbow {
+ background: linear-gradient(90deg, #ff826e, #ff9600, #d5c100, #54de68, #78b8ff, #f68dff);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-weight: bold;
+ filter: brightness(1.25);
+
+ @media (prefers-color-scheme: light) {
+ filter: brightness(0.8);
+ }
+}
\ No newline at end of file
diff --git a/src/app/link-parser/link-parser/themes/valentine.scss b/src/app/link-parser/link-parser/themes/valentine.scss
new file mode 100644
index 0000000..2a34294
--- /dev/null
+++ b/src/app/link-parser/link-parser/themes/valentine.scss
@@ -0,0 +1,13 @@
+:host.valentine {
+ --c1: oklch(from #FFC0CB 0.2624 0.064157 h);
+ --c2: oklch(from #FF69B4 0.2624 0.064157 h);
+ --c3: oklch(from #800080 0.2624 0.064157 h);
+
+ background: radial-gradient(circle at 30.9%, var(--c1), var(--c2), var(--c3));
+
+ @media (prefers-color-scheme: light) {
+ --c1: oklch(from #FFC0CB 0.96 0.0128 h);
+ --c2: oklch(from #FF69B4 0.96 0.0128 h);
+ --c3: oklch(from #800080 0.96 0.0128 h);
+ }
+}
\ No newline at end of file
diff --git a/src/app/link-parser/ui/faq/faq.component.html b/src/app/link-parser/ui/faq/faq.component.html
index aec45e7..153d295 100644
--- a/src/app/link-parser/ui/faq/faq.component.html
+++ b/src/app/link-parser/ui/faq/faq.component.html
@@ -1,15 +1,15 @@
-
- 🤔 {{lang.ph().whatIsChytanka}}
+
+ 🤔 {{lang.ph().whatIsChytanka}}
{{lang.ph().description}}
-
- 📖 {{lang.ph().howToUseChytanka}}
+
+ 📖 {{lang.ph().howToUseChytanka}}
-
- 🌐 {{lang.ph().whatLinks}}
+
+ 🌐 {{lang.ph().whatLinks}}
Imgur
@@ -48,12 +48,12 @@
Zenko
- -
-
- zenko.online/titles/{titleId}/{id}
-
-
+ rel="noopener noreferrer">Zenko
+ -
+
+ zenko.online/titles/{titleId}/{id}
+
+
{{lang.ph().whereIdIs}}
@@ -61,13 +61,13 @@
-
- 📄 {{lang.ph().canIPasteJsonLink}}
+
+ 📄 {{lang.ph().canIPasteJsonLink}}
-
- 📝 {{lang.ph().whatJsonModel}}
+
+ 📝 {{lang.ph().whatJsonModel}}
{
"title": "Title of the episode",
"nsfw": false,
diff --git a/src/app/link-parser/ui/header/header.component.html b/src/app/link-parser/ui/header/header.component.html
index b12b1bb..4cb8dfd 100644
--- a/src/app/link-parser/ui/header/header.component.html
+++ b/src/app/link-parser/ui/header/header.component.html
@@ -1,8 +1,8 @@
-
-
+
+
-
+
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 813b4af..db5c11d 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
@@ -2,7 +2,7 @@
@@ -10,7 +10,7 @@ {{lang.ph().orOpenFile}}
-->
-
+
@@ -28,7 +28,7 @@
@if (linkParams()) {
-
+
{{lang.ph().letsgo}}
{{linkParams()?.id | truncate}}
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 1681b9b..d87ef7b 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
@@ -35,105 +35,6 @@ app-text-embracer {
}
}
-::ng-deep app-text-embracer.pride>span {
- --theme-base: #166496;
- --shc: oklch(from var(--theme-base) var(--avarage-l-2) c h);
- --border-color: oklch(from var(--theme-base) .64 0.2 h);
- --dot-color: oklch(from var(--theme-base) .7 0.2 h);
- color: oklch(from var(--theme-base) .7 0.2 h);
- -webkit-text-stroke: var(--shc) var(--border-width);
-
- // --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0);
-
- border-image: linear-gradient(90deg, #e40303, #ff8c00, #ffed00, #008026, #004dff, #750787);
- border-image-slice: 1;
-
- &:nth-of-type(2),
- &:nth-of-type(1) {
- --theme-base: #e40303;
- }
-
- &:nth-of-type(3) {
- --theme-base: #ff8c00;
- }
-
- &:nth-of-type(4) {
- --theme-base: #ffed00;
- }
-
- &:nth-of-type(5) {
- --theme-base: #008026;
- }
-
- &:nth-of-type(6) {
- --theme-base: #004dff;
- }
-
- &:nth-of-type(7) {
- --theme-base: #750787;
- }
-}
-
-::ng-deep app-text-embracer.pride:has(span:nth-of-type(8))>span {
-
- &:nth-of-type(3),
- &:nth-of-type(1),
- &:nth-of-type(2) {
- --theme-base: #e40303;
- }
-
- &:nth-of-type(4) {
- --theme-base: #ff8c00;
- }
-
- &:nth-of-type(5) {
- --theme-base: #ffed00;
- }
-
- &:nth-of-type(6) {
- --theme-base: #008026;
- }
-
- &:nth-of-type(7) {
- --theme-base: #004dff;
- }
-
- &:nth-of-type(8) {
- --theme-base: #750787;
- }
-}
-
-@property --halloween-base {
- syntax: "";
- inherits: true;
- initial-value: #FF7518;
-}
-
-::ng-deep app-text-embracer.halloween>span {
- --shc: oklch(from var(--halloween-base) var(--avarage-l-2) c h);
- --border-color: oklch(from var(--halloween-base) .64 0.2 h);
- --dot-color: oklch(from var(--halloween-base) .7 0.2 h);
- color: oklch(from var(--halloween-base) .7 0.2 h);
- -webkit-text-stroke: var(--shc) var(--border-width);
-
- animation: halloween 5s steps(3) alternate infinite;
-}
-
-@keyframes halloween {
- 0% {
- --halloween-base: #A0FF00;
-
- }
-
- 50% {
- --halloween-base: #FF7518;
- }
-
- 100% {
- --halloween-base: #6C2DC7;
- }
-}
-
.form-wrapper {
min-width: 0;
// grid-column: 2;
@@ -178,7 +79,7 @@ textarea {
border: var(--border-size) solid var(--border-color);
box-shadow: var(--shadow-1);
// border: 0;
- background-color: #16649680;
+ background-color: #16649610;
color: #ffd60a;
@media (prefers-color-scheme: light) {
@@ -227,22 +128,6 @@ input[type=url]::placeholder {
text-wrap: balance;
}
-.slogan-rainbow {
- background: linear-gradient(90deg, #ff826e, #ff9600, #d5c100, #54de68, #78b8ff, #f68dff);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- font-weight: bold;
- filter: brightness(1.25);
-
- @media (prefers-color-scheme: light) {
- filter: brightness(0.8);
- }
-}
-
-.slogan-halloween {
- color: #FF7518;
-}
-
.go-btn {
display: flex;
gap: 1ch;
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 a2fd831..0e337f0 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
@@ -43,7 +43,7 @@ export class ParserFormComponent {
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 ComickLinkParser)
this.parser.parsers.push(new YandereParser)
this.parser.parsers.push(new PixivLinkParser)
this.parser.parsers.push(new ImgchestLinkParser)
@@ -107,7 +107,7 @@ export class ParserFormComponent {
mangadex: '//mangadex.org/favicon.ico',
telegraph: '//telegra.ph/favicon.ico',
nhentai: '//nhentai.net/favicon.ico',
- comick: '//comick.io/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',
diff --git a/src/app/link-parser/ui/settings/settings.component.html b/src/app/link-parser/ui/settings/settings.component.html
index 19ce273..6a06aec 100644
--- a/src/app/link-parser/ui/settings/settings.component.html
+++ b/src/app/link-parser/ui/settings/settings.component.html
@@ -27,7 +27,8 @@
{{'Use Vibration API'}}
-
+
}
@@ -45,7 +46,27 @@
{{lang.ph().settingAutoPasteLinkDesc}}
-
+
+
+
+
+
+
+ 🖥️ Display Mode
+ {{setts.displayMode() | titlecase}}
+
+
+
+
+
+
@@ -83,8 +105,8 @@
{{'Automatically saves files opened locally.'}}
-
+
@if(fileSetts.saveFileToHistory()){
@@ -102,8 +124,8 @@
{{'Adds copies of local files to history.'}}
-
+
@if(fileSetts.copyFileToHistory()){
diff --git a/src/app/link-parser/ui/settings/settings.component.scss b/src/app/link-parser/ui/settings/settings.component.scss
index 49535c1..0eece6b 100644
--- a/src/app/link-parser/ui/settings/settings.component.scss
+++ b/src/app/link-parser/ui/settings/settings.component.scss
@@ -1,4 +1,5 @@
-:host, fieldset {
+:host,
+fieldset {
display: grid;
gap: 2ch;
}
@@ -9,7 +10,7 @@ section {
align-items: center;
gap: 2ch;
border-bottom: 2px solid #16649680;
- padding: 0 0 1.5ch;
+ padding: 0 0 1.5ch;
transition: all var(--t) ease-in-out;
@@ -32,13 +33,13 @@ section {
}
input[type=checkbox] {
- --q:1;
+ --q: 1;
width: calc(5ch - 2px);
aspect-ratio: 1;
border-radius: .5ch;
appearance: none;
border: 2px solid #16649680;
-overflow: hidden;
+ overflow: hidden;
position: relative;
transition: all var(--t) ease-in-out;
cursor: pointer;
@@ -65,10 +66,11 @@ overflow: hidden;
&:checked {
border-color: #166496;
+
&::before {
transform: translate(0, 100%) scale(0);
}
-
+
&::after {
transform: translate(0, 0%) scale(1);
filter: brightness(0) saturate(100%) invert(86%) sepia(42%) saturate(2655%) hue-rotate(350deg) brightness(106%) contrast(102%);
@@ -83,7 +85,29 @@ fieldset {
border-radius: var(--r);
}
+
+@supports (corner-shape: squircle)
+{
+ section {
+ input[type=checkbox] {
+ corner-shape: squircle;
+ border-radius: 1ch;
+ }
+ }
+ fieldset {
+ corner-shape: squircle;
+ border-radius: calc(var(--r) * 2);
+ }
+}
+
input[type=number] {
width: 8ch;
text-align: center;
+}
+
+.radio-group {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(8ch, 1fr));
+ gap: 1ch;
+ margin-top: 1ch;
}
\ No newline at end of file
diff --git a/src/app/playlist/data-access/playlist.service.ts b/src/app/playlist/data-access/playlist.service.ts
index d25a1c2..dfd52a8 100644
--- a/src/app/playlist/data-access/playlist.service.ts
+++ b/src/app/playlist/data-access/playlist.service.ts
@@ -61,10 +61,10 @@ export class PlaylistService {
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 ComickLinkParser)
this.parser.parsers.push(new YandereParser)
this.parser.parsers.push(new PixivLinkParser)
- this.parser.parsers.push(new BlankaryLinkParser)
+ // this.parser.parsers.push(new BlankaryLinkParser)
this.parser.parsers.push(new JsonLinkParser)
}
diff --git a/src/app/shared/data-access/dom-manipulation.service.ts b/src/app/shared/data-access/dom-manipulation.service.ts
index 0867422..4351052 100644
--- a/src/app/shared/data-access/dom-manipulation.service.ts
+++ b/src/app/shared/data-access/dom-manipulation.service.ts
@@ -1,4 +1,4 @@
-import { inject, Injectable } from '@angular/core';
+import { inject, Injectable, signal } from '@angular/core';
import { copyText } from '../utils/clipboard';
import { DOCUMENT } from '@angular/common';
@@ -8,6 +8,8 @@ import { DOCUMENT } from '@angular/common';
export class DomManipulationService {
private readonly document = inject(DOCUMENT);
+ fullscreenEnabled = signal(this.document.fullscreenEnabled);
+
scrollInterval: any;
constructor() { }
diff --git a/src/app/shared/data-access/vibration.service.ts b/src/app/shared/data-access/vibration.service.ts
index 88c77c9..69aec9e 100644
--- a/src/app/shared/data-access/vibration.service.ts
+++ b/src/app/shared/data-access/vibration.service.ts
@@ -1,11 +1,12 @@
import { isPlatformServer } from '@angular/common';
import { computed, inject, Injectable, PLATFORM_ID, Signal, signal, WritableSignal } from '@angular/core';
-const DOT = 40; // Коротка вібрація для точки
-const DASH = 80; // Довга вібрація для тире
-const INTRA_LETTER_PAUSE = 64; // Пауза між елементами в одній букві
-const LETTER_PAUSE = 96; // Пауза між літерами
-const WORD_PAUSE = 128; // Пауза між словами
+const DOT = 24;
+const DASH = DOT * 3;
+const INTRA_LETTER_PAUSE = DOT;
+const LETTER_PAUSE = DOT * 3;
+const WORD_PAUSE = DOT * 7;
+const Q = 1;
const morseCode = new Map([
['A', ['.', '-']], // .-
@@ -77,12 +78,28 @@ export class VibrationService {
localStorage.setItem('vibrationOn', n.toString())
}
- vibrate(pattern: VibratePattern = DOT) {
+ vibrateIOS(style: string = "light") {
+ const handler = (window as any).webkit?.messageHandlers?.hapticFeedback;
+ if (handler) {
+ try {
+ handler.postMessage({
+ type: 'impact',
+ style: style // light / medium / heavy
+ });
+ } catch { }
+ }
+ }
+
+ vibrate(pattern: VibratePattern = DOT * Q) {
if (isPlatformServer(this._platformId) || !this.vibrationOn()) return
- navigator.vibrate(0);
- navigator.vibrate(pattern)
+ if (this.supportsVibration()) {
+ navigator.vibrate(0);
+ navigator.vibrate(pattern)
+ } else {
+ this.vibrateIOS();
+ }
}
vibrateForSettings = (isEnabled: boolean) => this.vibrate(this.getVibrationPattern(isEnabled ? "ON" : "OFF"));
@@ -97,24 +114,35 @@ export class VibrationService {
const morse = morseCode.get(char);
if (morse) {
morse.forEach((signal, signalIndex) => {
- pattern.push(signal === '.' ? DOT : DASH); // Вібрація для точки або тире
+ pattern.push(signal === '.' ? DOT * Q : DASH * Q); // Вібрація для точки або тире
if (signalIndex < morse.length - 1) {
- pattern.push(INTRA_LETTER_PAUSE); // Пауза між елементами букви
+ pattern.push(INTRA_LETTER_PAUSE * Q); // Пауза між елементами букви
}
});
// Пауза між літерами
if (index < array.length - 1) {
- pattern.push(LETTER_PAUSE);
+ pattern.push(LETTER_PAUSE * Q);
}
}
// Пауза між словами
if (index < array.length - 1 && array[index + 1] === ' ') {
- pattern.push(WORD_PAUSE);
+ pattern.push(WORD_PAUSE * Q);
}
});
return pattern;
}
+
+ supportsVibration(): boolean {
+ if (!('vibrate' in navigator)) return false;
+ if (typeof navigator.vibrate !== 'function') return false;
+
+ try {
+ return navigator.vibrate(0) !== false;
+ } catch {
+ return false;
+ }
+ }
}
diff --git a/src/app/shared/directives/vibrate-haptic.directive.ts b/src/app/shared/directives/vibrate-haptic.directive.ts
new file mode 100644
index 0000000..57eaf74
--- /dev/null
+++ b/src/app/shared/directives/vibrate-haptic.directive.ts
@@ -0,0 +1,47 @@
+import { Directive, HostListener, inject, Input } from '@angular/core';
+import { VibrationService } from '../data-access/vibration.service';
+
+@Directive({
+ selector: '[vibrateHaptic]',
+ standalone: false
+})
+export class VibrateHapticDirective {
+ vibration = inject(VibrationService);
+
+ @Input() vibrateHaptic: number | number[] = 10;
+
+ @Input() vibrateTouch: boolean = false;
+ @Input() vibrateClick: boolean = true;
+ @Input() vibrateInput: boolean = false;
+
+ constructor() { }
+
+ @HostListener('pointerdown')
+ onPointerDown() {
+ if (!this.vibrateTouch) return;
+
+ this.vibration.vibrate(this.vibrateHaptic);
+ }
+
+ @HostListener('touchstart')
+ onTouchStart() {
+ if (!this.vibrateTouch) return;
+
+ this.vibration.vibrate(this.vibrateHaptic);
+ }
+
+ @HostListener('click')
+ onClick() {
+ if (!this.vibrateClick) return;
+
+ this.vibration.vibrate(this.vibrateHaptic);
+ }
+
+ @HostListener('input')
+ onInput() {
+ if (!this.vibrateInput) return;
+
+ this.vibration.vibrate(this.vibrateHaptic);
+ }
+
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 7a1bf7c..3f26a1b 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -30,6 +30,7 @@ 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';
@@ -60,7 +61,8 @@ import { NewTabDirective } from './directives/new-tab.directive';
FileSizePipe,
ThanksPageComponent,
ImgMetaDirective,
- NewTabDirective
+ NewTabDirective,
+ VibrateHapticDirective
],
imports: [
CommonModule,
@@ -69,6 +71,6 @@ import { NewTabDirective } from './directives/new-tab.directive';
RoughPaperComponent,
SharpenComponent
],
- exports: [TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe]
+ exports: [TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective]
})
export class SharedModule { }
diff --git a/src/app/shared/ui/dialog/dialog.component.html b/src/app/shared/ui/dialog/dialog.component.html
index 5a1068f..92c3685 100644
--- a/src/app/shared/ui/dialog/dialog.component.html
+++ b/src/app/shared/ui/dialog/dialog.component.html
@@ -3,11 +3,11 @@
{{title}}
@if (closeHeaderButton) {
-
+
}
@@ -16,7 +16,7 @@ {{title}}
@if(footer){