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;
+
@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}}
MangaDexImgur
@@ -48,12 +48,12 @@
MangaDexZenko
-
-
    -
  • 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}} +

+
+ + +
+
@@ -62,7 +83,8 @@ {{lang.ph().seasonalThemeDesc}}

- +
@@ -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){
@@ -31,11 +31,11 @@ 🔌 Embed
- +
- + diff --git a/src/app/shared/ui/viewer/viewer.component.html b/src/app/shared/ui/viewer/viewer.component.html index 7d5c0e5..55709a0 100644 --- a/src/app/shared/ui/viewer/viewer.component.html +++ b/src/app/shared/ui/viewer/viewer.component.html @@ -1,4 +1,4 @@ -
diff --git a/src/app/shared/ui/viewer/viewer.component.scss b/src/app/shared/ui/viewer/viewer.component.scss index a64db1c..4de2d0a 100644 --- a/src/app/shared/ui/viewer/viewer.component.scss +++ b/src/app/shared/ui/viewer/viewer.component.scss @@ -9,11 +9,15 @@ transition: all var(--t) ease-in-out; &:fullscreen { - background-color: var(--surface); + background-color: #000000; app-overlay.top { padding-top: min(0vmin, env(safe-area-inset-top)); } + + @media (prefers-color-scheme: light) { + background: #ffffff; + } } .filter { @@ -111,4 +115,4 @@ figure { img { pointer-events: none; user-select: none; -} +} \ No newline at end of file diff --git a/src/app/shared/ui/viewer/viewer.component.ts b/src/app/shared/ui/viewer/viewer.component.ts index dd0119f..586cef7 100644 --- a/src/app/shared/ui/viewer/viewer.component.ts +++ b/src/app/shared/ui/viewer/viewer.component.ts @@ -8,6 +8,7 @@ import { Playlist, PlaylistItem } from '../../../playlist/data-access/playlist.s import { EmbedHalperService } from '../../data-access/embed-halper.service'; import { DownloadService } from '../../data-access/download.service'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { VibrationService } from '../../data-access/vibration.service'; const CHTNK_LOAD_EVENT_NAME = 'chtnkload' const CHTNK_CHANGE_PAGE_EVENT_NAME = 'changepage'; @@ -41,6 +42,7 @@ export class ViewerComponent implements AfterViewInit { platformId = inject(PLATFORM_ID) private readonly document = inject(DOCUMENT); + vibration = inject(VibrationService); initListFromParrentWindow() { if (!this.embedHelper.isEmbedded() || !isPlatformBrowser(this.platformId)) return @@ -146,10 +148,22 @@ export class ViewerComponent implements AfterViewInit { } + isScrollStart: boolean = false; @HostListener('scroll', ['$event']) onScroll(event: Event) { this.initActiveIndexes() + + if (!this.isScrollStart) { + this.isScrollStart = true; + this.vibration.vibrate(10); + } + } + + @HostListener('scrollend', ['$event']) + onScrollEnd(event: Event) { + this.vibration.vibrate([5,5,10]); + this.isScrollStart = false; } @HostListener('window:resize', ['$event']) diff --git a/src/app/shared/ui/viewer/viewer.pages.component.scss b/src/app/shared/ui/viewer/viewer.pages.component.scss index 33638fe..0d2d347 100644 --- a/src/app/shared/ui/viewer/viewer.pages.component.scss +++ b/src/app/shared/ui/viewer/viewer.pages.component.scss @@ -1,3 +1,7 @@ +:host:has(.view.pages) { + overflow-y: hidden; +} + .view.pages { --r: 0; overflow-x: auto; diff --git a/src/app/shared/ui/warm-control/warm-control.component.html b/src/app/shared/ui/warm-control/warm-control.component.html index 85a1d02..2892ee5 100644 --- a/src/app/shared/ui/warm-control/warm-control.component.html +++ b/src/app/shared/ui/warm-control/warm-control.component.html @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/src/app/shared/utils/phrases.ts b/src/app/shared/utils/phrases.ts index 285142d..d43b70f 100644 --- a/src/app/shared/utils/phrases.ts +++ b/src/app/shared/utils/phrases.ts @@ -59,6 +59,8 @@ export class Phrases { ] seasonalTheme = `SeasonalTheme`; seasonalThemeDesc = `🏳️‍🌈 Pride month, 🎃 Halloween, 🎄 New Year, 💖 Valentine etc.`; + historyEmpty = "Hmm… looks like you haven’t read anything yet 👀"; + histyryEmptyDesc = "It’s the perfect time to open your first chapter! 📖"; getByKey = (key: string) => (Object.keys(this).includes(key)) ? this[key as keyof Phrases] : null; static getTemplate(phrase: string, value: string) { diff --git a/src/assets/icons/icon-128x128.png b/src/assets/icons/icon-128x128.png index 5cedcf0..90d1a57 100755 Binary files a/src/assets/icons/icon-128x128.png and b/src/assets/icons/icon-128x128.png differ diff --git a/src/assets/icons/icon-512x512.png b/src/assets/icons/icon-512x512.png index ad28e37..7f45703 100755 Binary files a/src/assets/icons/icon-512x512.png and b/src/assets/icons/icon-512x512.png differ diff --git a/src/assets/icons/icon-512x512.svg b/src/assets/icons/icon-512x512.svg index dff6029..e870bac 100755 --- a/src/assets/icons/icon-512x512.svg +++ b/src/assets/icons/icon-512x512.svg @@ -1,13 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/langs/uk.json b/src/assets/langs/uk.json index 5b58941..289956b 100644 --- a/src/assets/langs/uk.json +++ b/src/assets/langs/uk.json @@ -2,7 +2,7 @@ "title": "Читанка — читай легко і комфортно!", "shortTitle": "Читанка", "enterLink": "Встав посилання на епізод з сайтів, що підтримуютсья", - "orOpenFile":" або відкрий файл", + "orOpenFile": " або відкрий файл", "slogan": "і читай легко та зручно!", "letsgo": "Вйо до", "dataLoadErr": "Помилка завантаження даних. Будь ласка, спробуйте пізніше.", @@ -29,7 +29,7 @@ "canIPasteJsonLink": "Чи можна вставити посилання на JSON-файл?", "whatJsonModel": "Якою має бути модель JSON файлу?", "yesYouCanPasteJsonLink": "Так, можна, наприклад, на {value} або на власний сайт.", - "howToUseChytankaAnswer" : "🔗 Просто встав посилання на епізод у поле введення.
🔳Якщо посилання підтримується, з'явиться кнопка,
🖱️ натисни її,
📖 і читай легко та зручно 🛋️.", + "howToUseChytankaAnswer": "🔗 Просто встав посилання на епізод у поле введення.
🔳Якщо посилання підтримується, з'явиться кнопка,
🖱️ натисни її,
📖 і читай легко та зручно 🛋️.", "whereIdIs": ", де {id} — унікальний ідентифікатор допису.", "language": "Мова", "settingLangDesc": "Змінити мову інтерфейсу користувача.", @@ -42,7 +42,7 @@ "untitled": "Без назви", "loading": "завантаження", "createList": "Створити список", - "openFile" : "Відкрити файл", + "openFile": "Відкрити файл", "dropIt": "Кинь це!", "pageNotFound": [ "О ні! Сторінка, яку ви шукаєте, загубилася між сторінками манґи...", @@ -55,5 +55,7 @@ "sloganNewYear": "Читай зручно. Мрій масштабно.", "sloganValentine": "Гортай сторінки, а не профілі.", "seasonalTheme": "Сезонна тема", - "seasonalThemeDesc": "🏳️‍🌈 Місяць Прайду, 🎃 Геловін, 🎄 Новий Рік, 💖 День Св. Валентина тощо" + "seasonalThemeDesc": "🏳️‍🌈 Місяць Прайду, 🎃 Геловін, 🎄 Новий Рік, 💖 День Св. Валентина тощо", + "historyEmpty": "Хмм… здається, ви ще нічого не читали 👀", + "histyryEmptyDesc": "Саме час відкрити першу главу! 📖" } \ No newline at end of file diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index feff8d8..c204c82 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,4 +1,4 @@ -const PROXY = `http://localhost:5000/api?url=` +const PROXY = `http://192.168.10.225:5000/api?url=` export const environment = { version: "0.13.32-2025.6.8", diff --git a/src/index.html b/src/index.html index 7f5e648..fac7f4e 100644 --- a/src/index.html +++ b/src/index.html @@ -19,6 +19,9 @@ + + +