diff --git a/ngsw-config.json b/ngsw-config.json index 7a349ed..78e64d2 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -9,6 +9,7 @@ "files": [ "/favicon.ico", "/index.html", + "/index.csr.html", "/manifest.webmanifest", "/*.css", "/*.js" diff --git a/package.json b/package.json index f0a6c35..26e5d1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chytanka", - "version": "0.13.43", + "version": "0.13.48", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/file/data-access/file.service.ts b/src/app/file/data-access/file.service.ts index 8fcfb6a..336a0f9 100644 --- a/src/app/file/data-access/file.service.ts +++ b/src/app/file/data-access/file.service.ts @@ -1,9 +1,13 @@ -import { Injectable, signal, WritableSignal } from '@angular/core'; +import { effect, inject, Injectable, signal, WritableSignal } from '@angular/core'; +import { FILE_PATH } from '../../app-routing.module'; +import { Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class FileService { + router = inject(Router); + private _file: WritableSignal = signal(null); public set file(f: File) { @@ -14,8 +18,36 @@ export class FileService { return this._file; } + constructor() { + effect(() => this.navigate()); + } + reset() { this._file.set(null); } + private getRouteType(file: File): string | undefined { + const fileType = file.type || file.name.split('.').pop()?.toLowerCase(); + + if (!fileType) return undefined; + + if (fileType.includes('pdf')) return 'pdf'; + if (fileType.includes('mobi')) return 'mobi'; + if (/zip|cbz/.test(fileType)) return 'zip'; + + return undefined; + } + + private navigate() { + const file = this.file(); + if (file === null) return; + + const t = this.getRouteType(file) + + if (t) { + const url = `/${FILE_PATH}/${t}`; + this.router.navigateByUrl(url) + } + } + } 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 d87ef7b..34ddaea 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 @@ -49,7 +49,6 @@ form { backdrop-filter: blur(var(--blur)); z-index: 3; border-radius: .5ch; - max-width: 80ch; width: 100%; position: relative; } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 832788e..f2c79fa 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -37,8 +37,9 @@ import { PageComponent } from './ui/viewer/components/page/page.component'; import { EpisodeInfoTableComponent } from './ui/viewer/components/episode-info-table/episode-info-table.component'; import { EpisodeShareFormComponent } from './ui/viewer/components/episode-share-form/episode-share-form.component'; import { EpisodeDownloadFormComponent } from './ui/viewer/components/episode-download-form/episode-download-form.component'; +import { DropZoneComponent } from './ui/drop-zone/drop-zone.component'; -const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, PageComponent, EpisodeInfoTableComponent, EpisodeShareFormComponent, EpisodeDownloadFormComponent] +const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, PageComponent, EpisodeInfoTableComponent, EpisodeShareFormComponent, EpisodeDownloadFormComponent, DropZoneComponent] @NgModule({ declarations: [ diff --git a/src/app/shared/ui/drop-zone/drop-zone.component.html b/src/app/shared/ui/drop-zone/drop-zone.component.html new file mode 100644 index 0000000..0bcf114 --- /dev/null +++ b/src/app/shared/ui/drop-zone/drop-zone.component.html @@ -0,0 +1,2 @@ +📃 +

{{lang.ph().dropIt}}

\ No newline at end of file diff --git a/src/app/shared/ui/drop-zone/drop-zone.component.scss b/src/app/shared/ui/drop-zone/drop-zone.component.scss new file mode 100644 index 0000000..4444e92 --- /dev/null +++ b/src/app/shared/ui/drop-zone/drop-zone.component.scss @@ -0,0 +1,22 @@ +:host { + font-size: 3rem; + position: fixed; + inset: 0; + display: grid; + place-content: center; + z-index: 100; + background-color: var(--surface); + text-align: center; + + color: #166496; + font-weight: bold; + + &::before { + content: ''; + position: absolute; + inset: 2ch; + outline: .25ch dashed #166496; + z-index: 1; + border-radius: .25ch; + } +} \ No newline at end of file diff --git a/src/app/shared/ui/drop-zone/drop-zone.component.ts b/src/app/shared/ui/drop-zone/drop-zone.component.ts new file mode 100644 index 0000000..181c76b --- /dev/null +++ b/src/app/shared/ui/drop-zone/drop-zone.component.ts @@ -0,0 +1,13 @@ +import { Component, inject } from '@angular/core'; +import { LangService } from '../../data-access/lang.service'; + +@Component({ + selector: 'drop-zone', + standalone: false, + + templateUrl: './drop-zone.component.html', + styleUrl: './drop-zone.component.scss' +}) +export class DropZoneComponent { + lang = inject(LangService); +} diff --git a/src/app/shared/ui/file-change/file-change.component.html b/src/app/shared/ui/file-change/file-change.component.html index b5985f5..778fc81 100644 --- a/src/app/shared/ui/file-change/file-change.component.html +++ b/src/app/shared/ui/file-change/file-change.component.html @@ -1,10 +1,6 @@ - + - - -@if(showDragAndDropZone){ -
- 📃 -

{{lang.ph().dropIt}}

-
+@if(showDragAndDropZone()){ + } \ No newline at end of file diff --git a/src/app/shared/ui/file-change/file-change.component.scss b/src/app/shared/ui/file-change/file-change.component.scss index be14bb1..1007dcd 100644 --- a/src/app/shared/ui/file-change/file-change.component.scss +++ b/src/app/shared/ui/file-change/file-change.component.scss @@ -1,26 +1,3 @@ -.drop-zone { - font-size: 3rem; - position: fixed; - inset: 0; - display: grid; - place-content: center; - z-index: 100; - background-color: var(--surface); - text-align: center; - - color: #166496; - font-weight: bold; - - &::before { - content: ''; - position: absolute; - inset: 2ch; - outline: .25ch dashed #166496; - z-index: 1; - border-radius: .25ch; - } -} - .button { width: 100%; text-align: unset; diff --git a/src/app/shared/ui/file-change/file-change.component.ts b/src/app/shared/ui/file-change/file-change.component.ts index cdbf4b5..65c9ba4 100644 --- a/src/app/shared/ui/file-change/file-change.component.ts +++ b/src/app/shared/ui/file-change/file-change.component.ts @@ -1,7 +1,5 @@ -import { Component, HostListener, inject, input, OnInit, PLATFORM_ID } from '@angular/core'; +import { Component, computed, HostListener, inject, input, OnInit, PLATFORM_ID, signal } from '@angular/core'; import { FileService } from '../../../file/data-access/file.service'; -import { Router } from '@angular/router'; -import { FILE_PATH } from '../../../app-routing.module'; import { LangService } from '../../data-access/lang.service'; import { isPlatformServer } from '@angular/common'; @@ -17,76 +15,63 @@ type LaunchParams = { standalone: false }) export class FileChangeComponent implements OnInit { - platformId = inject(PLATFORM_ID) - fs = inject(FileService) - router = inject(Router) - lang = inject(LangService) + private platformId = inject(PLATFORM_ID) + private fs = inject(FileService) + protected lang = inject(LangService) accept = input([]) - label = input("Open File") + label = input(this.lang.ph().openFile) - input: HTMLInputElement | undefined; - showDragAndDropZone: boolean = false; + private input: HTMLInputElement | undefined; + protected showDragAndDropZone = signal(false); + + private readonly ctrlKey = signal('Ctrl'); + private readonly symbolKey = signal('KeyO'); + private readonly hotKey = computed(() => `${this.ctrlKey()}+${this.symbolKey()}`); + protected readonly hotKeyHint = computed(() => ` (${this.hotKey().replace('Key', '')})`); ngOnInit(): void { if (isPlatformServer(this.platformId)) return; this.initFileInput(); + this.initializeFileLaunchQueue(); + } - if ("launchQueue" in window) { - (window as any).launchQueue.setConsumer(async (launchParams: LaunchParams) => { + private fileHandler(file: File | undefined) { + if (!file) return; - if (!launchParams.files?.length) return; + this.fs.file = file; + } - for (const handle of launchParams.files) { + //#region Launch Queue - if (handle.kind === "file") { - const fileHandle = handle as FileSystemFileHandle; - console.log("fileHandle", fileHandle) - const file = await fileHandle.getFile(); - console.log("File:", file.name); + private initializeFileLaunchQueue() { + if ("launchQueue" in window) { + (window as any).launchQueue.setConsumer(async (launchParams: LaunchParams) => { + const fileHandle = launchParams.files?.find(f => f.kind === "file") as FileSystemFileHandle | undefined; + if (!fileHandle) return; - this.fileHandler(file); - return; - } - } + const file = await fileHandle.getFile(); + this.fileHandler(file); }); } } - onFileSelected(event: any) { - const file: File = event.target.files[0]; + //#endregion - this.fileHandler(file) - } - - fileHandler(file: File | undefined) { - if (!file) return; + //#region File Input - this.fs.file = file; - - // should be output - const t = this.getRouteType(file) - - if (t) { - const url = `/${FILE_PATH}/${t}`; - this.router.navigateByUrl(url) - } + protected openFileDialog() { + if (this.input) this.input.click(); } - getRouteType(file: File): string | undefined { - const fileType = file.type || file.name.split('.').pop()?.toLowerCase(); - - if (!fileType) return undefined; - - if (fileType.includes('pdf')) return 'pdf'; - if (fileType.includes('mobi')) return 'mobi'; - if (/zip|cbz/.test(fileType)) return 'zip'; + private onFileSelected(event: any) { + const file: File = event.target.files[0]; - return undefined; + this.fileHandler(file) } - initFileInput() { + private initFileInput() { this.input = document.createElement('input') this.input.type = 'file'; @@ -97,16 +82,15 @@ export class FileChangeComponent implements OnInit { } } - openFileDialog() { - if (this.input) this.input.click(); - } + //#endregion - @HostListener('window:keydown', ['$event']) - handleKeyboardEvent(event: KeyboardEvent) { + //#region Drag & Drop - const code = event.ctrlKey ? `Ctrl+${event.code}` : event.code + @HostListener('window:keydown', ['$event']) + protected handleKeyboardEvent(event: KeyboardEvent) { + const code = event.ctrlKey ? `${this.ctrlKey()}+${event.code}` : event.code; - if (code == "Ctrl+KeyO") { + if (code == this.hotKey()) { event.preventDefault(); this.openFileDialog() @@ -114,17 +98,35 @@ export class FileChangeComponent implements OnInit { } @HostListener('document:dragover', ["$event"]) - dragOverHandler = (ev: DragEvent) => { ev.preventDefault(); this.showDragAndDropZone = true } + protected dragOverHandler = (ev: DragEvent) => { + ev.preventDefault(); + + const dt = ev.dataTransfer; + if (!dt) return; + + const hasFile = + dt.items && Array.from(dt.items).some(i => i.kind === 'file'); + + const hasText = + dt.types?.includes('text/plain') || + Array.from(dt.items || []).some(i => i.kind === 'string'); + + if (hasFile && !hasText) { + this.showDragAndDropZone.set(true); + } + } @HostListener('dragleave', ["$event"]) @HostListener('dragend', ["$event"]) - dragLeaveHandler = (ev: DragEvent) => { ev.preventDefault(); this.showDragAndDropZone = false } + protected dragLeaveHandler = (ev: DragEvent) => { ev.preventDefault(); this.showDragAndDropZone.set(false) } - dropHandler(ev: DragEvent) { + protected dropHandler(ev: DragEvent) { ev.preventDefault(); const file: File | undefined = ev.dataTransfer?.files[0]; this.fileHandler(file) } + + //#endregion } diff --git a/src/app/shared/ui/text-embracer/text-embracer.component.html b/src/app/shared/ui/text-embracer/text-embracer.component.html index 2875690..8246275 100644 --- a/src/app/shared/ui/text-embracer/text-embracer.component.html +++ b/src/app/shared/ui/text-embracer/text-embracer.component.html @@ -1,14 +1,3 @@ -@for(item of letters(); track item) { +@for(item of letters(); track $index) { {{item}} -} - - - - - - - - - - - \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/shared/ui/text-embracer/text-embracer.component.scss b/src/app/shared/ui/text-embracer/text-embracer.component.scss index d61ca07..1fc5031 100644 --- a/src/app/shared/ui/text-embracer/text-embracer.component.scss +++ b/src/app/shared/ui/text-embracer/text-embracer.component.scss @@ -36,7 +36,6 @@ left: -0.05ch; top: -0.05ch; position: relative; - // filter: url("#oneBitShadow"); } } } \ No newline at end of file diff --git a/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.scss b/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.scss index fca8139..09cf789 100644 --- a/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.scss +++ b/src/app/shared/ui/viewer/components/viewer-header/viewer-header.component.scss @@ -2,6 +2,10 @@ app-view-mode-bar { margin-left: auto; } +::ng-deep app-file-change button { + border: unset !important; +} + .open-chtnk-link { aspect-ratio: 1; width: 2rem; diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 8630538..4713780 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,7 +1,7 @@ const PROXY = `http://192.168.10.107:3003/api?url=` export const environment = { - version: "0.13.43-2026.3.23", + version: "0.13.48-2026.3.24", prod: false, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index b5970b6..25ea463 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,7 +1,7 @@ const PROXY = `https://proxy.chytanka.ink/api?url=` export const environment = { - version: "0.13.43-2026.3.23", + version: "0.13.48-2026.3.24", prod: true, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`,