diff --git a/src/app/@site-modules/@common-read/ui/common-read/common-read.component.html b/src/app/@site-modules/@common-read/ui/common-read/common-read.component.html index 0200d3d..48ae029 100644 --- a/src/app/@site-modules/@common-read/ui/common-read/common-read.component.html +++ b/src/app/@site-modules/@common-read/ui/common-read/common-read.component.html @@ -16,8 +16,8 @@

🤨

{{ error }}

diff --git a/src/app/@site-modules/@common-read/ui/common-read/common-read.component.ts b/src/app/@site-modules/@common-read/ui/common-read/common-read.component.ts index 1902aa3..5d0b5e1 100644 --- a/src/app/@site-modules/@common-read/ui/common-read/common-read.component.ts +++ b/src/app/@site-modules/@common-read/ui/common-read/common-read.component.ts @@ -1,8 +1,8 @@ import { Component, output, inject, input } from '@angular/core'; import { LangService } from '../../../../shared/data-access/lang.service'; import { BehaviorSubject, Observable, of } from 'rxjs'; -import { CompositionEpisode } from '../../utils'; import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ selector: 'app-common-read', @@ -15,7 +15,7 @@ export class CommonReadComponent { error$ = input.required>(); loading$ = input.required>(); - episode$ = input.required>(); + episode$ = input.required>(); playlist = input([]); playlistLink = input(""); diff --git a/src/app/@site-modules/@common-read/utils/composition.ts b/src/app/@site-modules/@common-read/utils/composition.ts deleted file mode 100644 index fcf66b4..0000000 --- a/src/app/@site-modules/@common-read/utils/composition.ts +++ /dev/null @@ -1,56 +0,0 @@ -export interface CompositionImage { - src: string; - width?: number; - height?: number; - size?: number; - type?: string; - alt?: string; - nsfw?: string; -} - -export interface CompositionPublisher { - id: string; - site: string; - avatar?: CompositionImage; - description: string - name: string; - links?: Array<{ - link: string, - title: string - }> -} - -export interface CompositionEpisode { - title: string; - episode?: number; - nsfw?: boolean | undefined; - mangaId?: string; - volume?: number; - chapter?: number; - part?: number; - extra?: boolean; - publisher?: CompositionPublisher, - images: CompositionImage[]; -} - -export function isCompositionEpisode(data: any): data is CompositionEpisode { - return ( - typeof data.title === 'string' && - Array.isArray(data.images) && - data.images.every((image: CompositionImage) => { - return ( - typeof image.src === 'string' - ); - }) - ); -} - -// export interface Composition { -// title: string; -// description: string; -// artist: string; -// author: string; -// cover: CompositionImage; - -// episodes: CompositionEpisode[]; -// } \ No newline at end of file diff --git a/src/app/@site-modules/@common-read/utils/index.ts b/src/app/@site-modules/@common-read/utils/index.ts index 9edefc4..ae98fcb 100644 --- a/src/app/@site-modules/@common-read/utils/index.ts +++ b/src/app/@site-modules/@common-read/utils/index.ts @@ -1,2 +1 @@ -export * from './composition'; export * from './read-base-component' \ No newline at end of file diff --git a/src/app/@site-modules/@common-read/utils/read-base-component.ts b/src/app/@site-modules/@common-read/utils/read-base-component.ts index 99cb96b..2c3a796 100644 --- a/src/app/@site-modules/@common-read/utils/read-base-component.ts +++ b/src/app/@site-modules/@common-read/utils/read-base-component.ts @@ -1,19 +1,18 @@ import { BehaviorSubject, MonoTypeOperatorFunction, Observable, OperatorFunction, Subscription, catchError, combineLatest, finalize, of, tap } from "rxjs"; -import { CompositionEpisode } from "./composition"; import { ActivatedRoute, ParamMap } from "@angular/router"; -import { Title } from "@angular/platform-browser"; -import { ChangeDetectorRef, OnDestroy, WritableSignal, inject, output, signal } from "@angular/core"; +import { ChangeDetectorRef, WritableSignal, inject, signal } from "@angular/core"; import { LangService } from "../../../shared/data-access/lang.service"; import { HistoryService } from "../../../history/data-access/history.service"; import { PlaylistItem, PlaylistService, isPlaylist } from "../../../playlist/data-access/playlist.service"; import { MetaTagsService } from "../../../shared/data-access/meta-tags.service"; import { ViewerService } from "../../../viewer/services"; +import { ChtnkEpisode } from "../../../shared/models/chtnk-composition"; export abstract class ReadBaseComponent { protected refresh$: BehaviorSubject = new BehaviorSubject(null); error$: BehaviorSubject = new BehaviorSubject(null); loading$: BehaviorSubject = new BehaviorSubject(false); - episode$: Observable = of(null); + episode$: Observable = of(null); plObserv: Subscription | undefined; playlistLink = signal('') @@ -23,7 +22,7 @@ export abstract class ReadBaseComponent { this.route.pathFromRoot[0].queryParams.subscribe(q => { const vm = q['vm'] // view mode param - if(vm && vm !== this.viewerService.viewModeOption().code) + if (vm && vm !== this.viewerService.viewModeOption().code) this.viewerService.setViewModeOptionByCode(vm) const pl = q['list'] // playlist @@ -113,8 +112,8 @@ export abstract class ReadBaseComponent { } site = ''; post_id = ''; - protected tapSaveToHistory(site: string, post_id: string): MonoTypeOperatorFunction { - return tap(async (episode: CompositionEpisode) => { + protected tapSaveToHistory(site: string, post_id: string): MonoTypeOperatorFunction { + return tap(async (episode: ChtnkEpisode) => { if (episode) { this.site = site; this.post_id = post_id; @@ -135,8 +134,8 @@ export abstract class ReadBaseComponent { cdr = inject(ChangeDetectorRef) - protected tapSaveToCurrentPlaylistItem(site: string, post_id: string): MonoTypeOperatorFunction { - return tap(async (episode: CompositionEpisode) => { + protected tapSaveToCurrentPlaylistItem(site: string, post_id: string): MonoTypeOperatorFunction { + return tap(async (episode: ChtnkEpisode) => { if (episode) { diff --git a/src/app/@site-modules/comick/comick.service.ts b/src/app/@site-modules/comick/comick.service.ts index b66b528..5b500f2 100644 --- a/src/app/@site-modules/comick/comick.service.ts +++ b/src/app/@site-modules/comick/comick.service.ts @@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Observable, map } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { CompositionEpisode } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; import { isPlatformServer } from '@angular/common'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -14,7 +14,7 @@ export class ComickService { http: HttpClient = inject(HttpClient) proxy: ProxyService = inject(ProxyService) - getComposition(id: string): Observable { + getComposition(id: string): Observable { const url = isPlatformServer(this.platformId) ? environment.comickHost + id : this.proxy.proxyUrl(environment.comickHost + id); @@ -23,7 +23,7 @@ export class ComickService { .pipe(map((data) => { return this.map(data) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const nsfw = data.matureContent; const mappedResponse = { title: data.chapTitle, diff --git a/src/app/@site-modules/imgchest/imgchest.service.ts b/src/app/@site-modules/imgchest/imgchest.service.ts index 6cc9c0f..b80b55f 100644 --- a/src/app/@site-modules/imgchest/imgchest.service.ts +++ b/src/app/@site-modules/imgchest/imgchest.service.ts @@ -2,9 +2,10 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { environment } from '../../../environments/environment'; import { Observable, map } from 'rxjs'; -import { CompositionEpisode, CompositionImage } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; import { isPlatformServer } from '@angular/common'; +import { ChtnkEpisode, ChtnkImage } from '../../shared/models/chtnk-composition'; + interface ImgchestRespCompImage { id: string; description: string; @@ -40,7 +41,7 @@ export class ImgchestService { constructor() { } - getComposition(id: string): Observable { + getComposition(id: string): Observable { const headers = new HttpHeaders({ 'Authorization': `Bearer ${this.clientId}` }); @@ -54,12 +55,12 @@ export class ImgchestService { } - map(data: ImgchestRespComp): CompositionEpisode { - const res: CompositionEpisode = { + map(data: ImgchestRespComp): ChtnkEpisode { + const res: ChtnkEpisode = { title: data.title, episode: 0, nsfw: (data.nsfw) as unknown as boolean, - images: data.images.map((i): CompositionImage => { + images: data.images.map((i): ChtnkImage => { return { src: this.proxy.proxyUrl(i.link), alt: i.description, diff --git a/src/app/@site-modules/imgur/imgur.service.ts b/src/app/@site-modules/imgur/imgur.service.ts index 421aa43..803aa02 100644 --- a/src/app/@site-modules/imgur/imgur.service.ts +++ b/src/app/@site-modules/imgur/imgur.service.ts @@ -2,7 +2,9 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from '../../../environments/environment'; import { Observable, map } from 'rxjs'; -import { CompositionEpisode, CompositionImage } from '../@common-read'; +import { ChtnkEpisode, ChtnkImage } from '../../shared/models/chtnk-composition'; + + interface ImgurRespCompImage { link: string; width: number; @@ -32,7 +34,7 @@ export class ImgurService { constructor(private http: HttpClient) { } - getComposition(id: string): Observable { + getComposition(id: string): Observable { const headers = new HttpHeaders({ 'Authorization': `Client-ID ${this.clientId}` }); @@ -42,14 +44,14 @@ export class ImgurService { } - map(data: ImgurRespComp): CompositionEpisode { - const res: CompositionEpisode = { + map(data: ImgurRespComp): ChtnkEpisode { + const res: ChtnkEpisode = { title: data.title, episode: 0, nsfw: (data.nsfw) as unknown as boolean, images: data.images .filter(i => i.type.startsWith('image/')) - .map((i): CompositionImage => { + .map((i): ChtnkImage => { return { src: i.link, width: i.width, diff --git a/src/app/@site-modules/mangadex/mangadex.service.ts b/src/app/@site-modules/mangadex/mangadex.service.ts index 29b29e8..601c8fc 100644 --- a/src/app/@site-modules/mangadex/mangadex.service.ts +++ b/src/app/@site-modules/mangadex/mangadex.service.ts @@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { environment } from '../../../environments/environment'; import { Observable, catchError, map, throwError } from 'rxjs'; -import { CompositionEpisode, CompositionImage, CompositionPublisher } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; import { isPlatformServer } from '@angular/common'; +import { ChtnkEpisode, ChtnkImage, ChtnkPublisher } from '../../shared/models/chtnk-composition'; interface MdChapterImages { hash: string; @@ -94,7 +94,7 @@ export class MangadexService { http: HttpClient = inject(HttpClient) proxy: ProxyService = inject(ProxyService) - getChapterImages(id: string): Observable { + getChapterImages(id: string): Observable { const url = isPlatformServer(this.platformId) ? environment.mangadexHost + id : this.proxy.proxyUrl(environment.mangadexHost + id); @@ -111,7 +111,7 @@ export class MangadexService { ) } - getChapter(id: string): Observable { + getChapter(id: string): Observable { const url = isPlatformServer(this.platformId) ? environment.mangadexChapter + id : this.proxy.proxyUrl(environment.mangadexChapter + id); @@ -132,7 +132,7 @@ export class MangadexService { volume: data.data.attributes.volume, chapter: data.data.attributes.chapter, images: [] - } as unknown as CompositionEpisode + } as unknown as ChtnkEpisode }), catchError(error => throwError(() => error)) ) @@ -156,7 +156,7 @@ export class MangadexService { }) } - getScanlationGroup(id: string): Observable { + getScanlationGroup(id: string): Observable { const endpoint = environment.mangadexScanlationGroup + id; const url = isPlatformServer(this.platformId) ? endpoint : this.proxy.proxyUrl(endpoint); diff --git a/src/app/@site-modules/nhentai/nhentai.service.ts b/src/app/@site-modules/nhentai/nhentai.service.ts index 8db307f..c202cf9 100644 --- a/src/app/@site-modules/nhentai/nhentai.service.ts +++ b/src/app/@site-modules/nhentai/nhentai.service.ts @@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Observable, map } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { CompositionEpisode } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; import { isPlatformServer } from '@angular/common'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -14,7 +14,7 @@ export class NhentaiService { http: HttpClient = inject(HttpClient) proxy: ProxyService = inject(ProxyService) - getComposition(id: string): Observable { + getComposition(id: string): Observable { const url = isPlatformServer(this.platformId) ? environment.nhentaiHost + id : this.proxy.proxyUrl(environment.nhentaiHost + id); @@ -23,7 +23,7 @@ export class NhentaiService { .pipe(map((data) => { return this.map(data) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const mediaId = data.media_id; const mappedResponse = { title: data.title.pretty, diff --git a/src/app/@site-modules/pixiv/pixiv.service.ts b/src/app/@site-modules/pixiv/pixiv.service.ts index 2cbfd9d..2c76fc3 100644 --- a/src/app/@site-modules/pixiv/pixiv.service.ts +++ b/src/app/@site-modules/pixiv/pixiv.service.ts @@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Observable, map } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { CompositionEpisode, CompositionPublisher } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; import { isPlatformServer } from '@angular/common'; +import { ChtnkEpisode, ChtnkPublisher } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -14,7 +14,7 @@ export class PixivService { http: HttpClient = inject(HttpClient) proxy: ProxyService = inject(ProxyService) - getComposition(id: string): Observable { + getComposition(id: string): Observable { const url = isPlatformServer(this.platformId) ? environment.pixivHost + id : this.proxy.proxyUrl(environment.pixivHost + id); @@ -23,7 +23,7 @@ export class PixivService { .pipe(map((data) => { return this.map(data.body) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const mappedResponse = { title: data.illust_details.title, @@ -35,7 +35,7 @@ export class PixivService { avatar: this.proxy.proxyUrl(data.author_details.profile_img.main) + '&ref=https://www.pixiv.net' as string, description: '', links: [] - } as unknown as CompositionPublisher, + } as unknown as ChtnkPublisher, images: (data.illust_details.manga_a ? data.illust_details.manga_a.map((item: any, index: number) => { return { diff --git a/src/app/@site-modules/read/read.service.ts b/src/app/@site-modules/read/read.service.ts index 1e654ed..1c50949 100644 --- a/src/app/@site-modules/read/read.service.ts +++ b/src/app/@site-modules/read/read.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, catchError, tap, throwError } from 'rxjs'; -import { CompositionEpisode, isCompositionEpisode } from '../@common-read'; +import { ChtnkEpisode, isChtnkEpisode } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -10,10 +10,10 @@ export class ReadService { constructor(private http: HttpClient) { } - getComposition(url: string): Observable { - return this.http.get(url) + getComposition(url: string): Observable { + return this.http.get(url) .pipe( - tap(data => { if (!isCompositionEpisode(data)) throw new Error() }), + tap(data => { if (!isChtnkEpisode(data)) throw new Error() }), catchError(error => throwError(() => error)) ); } diff --git a/src/app/@site-modules/reddit/reddit.service.ts b/src/app/@site-modules/reddit/reddit.service.ts index e304f0e..0373156 100644 --- a/src/app/@site-modules/reddit/reddit.service.ts +++ b/src/app/@site-modules/reddit/reddit.service.ts @@ -2,8 +2,8 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { Observable, map } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { CompositionEpisode } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -13,13 +13,13 @@ export class RedditService { constructor(private http: HttpClient) { } - getComposition(postId: string): Observable { + getComposition(postId: string): Observable { const url = environment.redditHost + postId + '.json'; return this.http.get(this.proxy.proxyUrl(url)) .pipe(map((data: any) => { return this.map(data) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const media_metadata = data[0].data.children[0].data.media_metadata @@ -27,7 +27,7 @@ export class RedditService { ?? Object.keys(media_metadata).map(i => { return { media_id: i } }); const post = data[0].data.children[0].data; - const res: CompositionEpisode = { + const res: ChtnkEpisode = { title: post.title, nsfw: post.thumbnail == "nsfw" || post.over_18, images: imgs.map((i: any): any => { diff --git a/src/app/@site-modules/telegraph/telegraph.service.ts b/src/app/@site-modules/telegraph/telegraph.service.ts index c474362..1a3d7bb 100644 --- a/src/app/@site-modules/telegraph/telegraph.service.ts +++ b/src/app/@site-modules/telegraph/telegraph.service.ts @@ -1,9 +1,9 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { Observable, map } from 'rxjs'; -import { CompositionEpisode } from '../@common-read'; import { environment } from '../../../environments/environment'; import { ProxyService } from '../../shared/data-access/proxy.service'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -13,14 +13,14 @@ export class TelegraphService { constructor(private http: HttpClient) { } - getComposition(id: string): Observable { + getComposition(id: string): Observable { const params = new HttpParams().set('return_content', 'true'); return this.http.get(environment.telegraphHost + id , { params: params }) .pipe(map((data) => { return this.map(data) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const mappedResponse = { title: data.result.title, images: (data.result.content.map((item: any) => { diff --git a/src/app/@site-modules/yandere/yandere.service.ts b/src/app/@site-modules/yandere/yandere.service.ts index 385b8a8..0694bfa 100644 --- a/src/app/@site-modules/yandere/yandere.service.ts +++ b/src/app/@site-modules/yandere/yandere.service.ts @@ -2,10 +2,9 @@ import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { environment } from '../../../environments/environment'; import { HttpClient } from '@angular/common/http'; import { Observable, map } from 'rxjs'; -import { CompositionEpisode } from '../@common-read'; -import { Base64 } from '../../shared/utils'; import { ProxyService } from '../../shared/data-access/proxy.service'; import { isPlatformServer } from '@angular/common'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -15,7 +14,7 @@ export class YandereService { http: HttpClient = inject(HttpClient) proxy: ProxyService = inject(ProxyService) - getComposition(id: string): Observable { + getComposition(id: string): Observable { const url = isPlatformServer(this.platformId) ? environment.yanderePoolsHost + id : this.proxy.proxyUrl(environment.yanderePoolsHost + id); @@ -24,7 +23,7 @@ export class YandereService { .pipe(map((data) => { return this.map(data) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const mappedResponse = { title: data.name, diff --git a/src/app/@site-modules/zenko/zenko.service.ts b/src/app/@site-modules/zenko/zenko.service.ts index acda94e..aadc3ce 100644 --- a/src/app/@site-modules/zenko/zenko.service.ts +++ b/src/app/@site-modules/zenko/zenko.service.ts @@ -2,8 +2,8 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { Observable, map } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { CompositionEpisode, CompositionPublisher } from '../@common-read'; import { ProxyService } from '../../shared/data-access/proxy.service'; +import { ChtnkEpisode, ChtnkPublisher } from '../../shared/models/chtnk-composition'; @Injectable({ providedIn: 'root' @@ -12,12 +12,12 @@ export class ZenkoService { http: HttpClient = inject(HttpClient) proxy: ProxyService = inject(ProxyService) - getComposition(id: string): Observable { + getComposition(id: string): Observable { return this.http.get(this.proxy.proxyUrl((environment.zenkoChapters + id)) + `&ref=${environment.zenkoHost}`) .pipe(map((data) => { return this.map(data) })) } - map(data: any): CompositionEpisode { + map(data: any): ChtnkEpisode { const imgParams = `?optimizer=image&width=900&quality=90&height=auto`; const { vol, ch, name, output } = this.titleDecode(data.name); const x = parseFloat(ch); @@ -35,7 +35,7 @@ export class ZenkoService { avatar: this.proxy.proxyUrl(environment.zenkoCdn + data.publisher.avatar + imgParams) + `&ref=${environment.zenkoHost}` as string, description: data.publisher.description as string, links: data.publisher.links?.map((l: any) => { return { link: l.link, title: l.title }; }) - } as unknown as CompositionPublisher, + } as unknown as ChtnkPublisher, images: (data.pages.map((item: any) => { return { diff --git a/src/app/file/data-access/zip.worker.ts b/src/app/file/data-access/zip.worker.ts index b6b81d0..0fea698 100644 --- a/src/app/file/data-access/zip.worker.ts +++ b/src/app/file/data-access/zip.worker.ts @@ -85,6 +85,7 @@ addEventListener('message', async ({ data }) => { type: ZipWorkerMessageType.ImageLoad, url, index: start + index, + filename: filename, }); }) ); diff --git a/src/app/file/mobi/mobi.component.ts b/src/app/file/mobi/mobi.component.ts index baa688a..a4cfc56 100644 --- a/src/app/file/mobi/mobi.component.ts +++ b/src/app/file/mobi/mobi.component.ts @@ -1,10 +1,10 @@ import { Component, effect, inject } from '@angular/core'; -import { CompositionEpisode } from '../../@site-modules/@common-read'; import { Router } from '@angular/router'; import { FileService } from '../data-access/file.service'; import { MobiFileReader } from 'readiverse'; import { SharedModule } from '../../shared/shared.module'; import { ViewerModule } from '../../viewer/viewer.module'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; @Component({ selector: 'app-mobi', @@ -13,7 +13,7 @@ import { ViewerModule } from '../../viewer/viewer.module'; styleUrl: './mobi.component.scss' }) export class MobiComponent { - episode: CompositionEpisode | undefined; + episode: ChtnkEpisode | undefined; router = inject(Router) fs = inject(FileService) diff --git a/src/app/file/pdf/pdf.component.ts b/src/app/file/pdf/pdf.component.ts index fd6226e..9f05e47 100644 --- a/src/app/file/pdf/pdf.component.ts +++ b/src/app/file/pdf/pdf.component.ts @@ -5,8 +5,8 @@ import { SharedModule } from '../../shared/shared.module'; import { Router } from '@angular/router'; import { getDocument, GlobalWorkerOptions, PDFPageProxy } from 'pdfjs-dist'; import { RenderParameters } from 'pdfjs-dist/types/src/display/api'; -import { CompositionEpisode } from '../../@site-modules/@common-read'; import { ViewerModule } from '../../viewer/viewer.module'; +import { ChtnkEpisode } from '../../shared/models/chtnk-composition'; GlobalWorkerOptions.workerSrc = '/assets/pdf.worker.min.mjs' @@ -19,7 +19,7 @@ const MDASH = '—'; styleUrl: './pdf.component.scss' }) export class PdfComponent { - episode: CompositionEpisode | undefined; + episode: ChtnkEpisode | undefined; router = inject(Router) fs = inject(FileService) diff --git a/src/app/file/zip/facades/zip-history.facade.ts b/src/app/file/zip/facades/zip-history.facade.ts index 31d2c03..fcdd275 100644 --- a/src/app/file/zip/facades/zip-history.facade.ts +++ b/src/app/file/zip/facades/zip-history.facade.ts @@ -2,7 +2,7 @@ import { inject, Injectable, WritableSignal } from "@angular/core"; import { FileSettingsService } from "../../data-access/file-settings.service"; import { FileHistoryService } from "../../data-access/file-history.service"; import { ZipWorkerFacade } from "./zip-worker.facade"; -import { CompositionEpisode } from "../../../@site-modules/@common-read"; +import { ChtnkEpisode } from "../../../shared/models/chtnk-composition"; @Injectable() export class ZipHistoryFacade { @@ -11,7 +11,7 @@ export class ZipHistoryFacade { fileSetts = inject(FileSettingsService) - async loadFromHistory(sha256: string, episode: WritableSignal) { + async loadFromHistory(sha256: string, episode: WritableSignal) { const { arrayBuffer, title } = await this.fileHistory.getItemBySha256(sha256) if (!arrayBuffer) return; diff --git a/src/app/file/zip/facades/zip-worker.facade.ts b/src/app/file/zip/facades/zip-worker.facade.ts index bf26f55..fd854b6 100644 --- a/src/app/file/zip/facades/zip-worker.facade.ts +++ b/src/app/file/zip/facades/zip-worker.facade.ts @@ -1,6 +1,6 @@ import { Injectable, WritableSignal } from "@angular/core"; -import { CompositionEpisode } from "../../../@site-modules/@common-read"; import { ZipWorkerCommandType } from "../../models"; +import { ChtnkEpisode } from "../../../shared/models/chtnk-composition"; @Injectable() export class ZipWorkerFacade { @@ -27,7 +27,7 @@ export class ZipWorkerFacade { } // worker - async openArrayBuffer(ab: ArrayBuffer, filename: string, sha256: string = '', episode: WritableSignal) { + async openArrayBuffer(ab: ArrayBuffer, filename: string, sha256: string = '', episode: WritableSignal) { // if (sha256 == '') this.sha256 = await this.fileHash.sha256(this.fs.file() as File) episode.set({ title: filename, images: [] }); diff --git a/src/app/file/zip/zip.component.ts b/src/app/file/zip/zip.component.ts index 4135aa8..e7d73ea 100644 --- a/src/app/file/zip/zip.component.ts +++ b/src/app/file/zip/zip.component.ts @@ -2,7 +2,6 @@ import { Component, effect, inject, OnDestroy, OnInit, signal } from '@angular/c import { FileService } from '../data-access/file.service'; import { SharedModule } from '../../shared/shared.module'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { CompositionEpisode, CompositionImage } from '../../@site-modules/@common-read'; import { DomManipulationService } from '../../shared/data-access'; import { ComicInfo } from '../../shared/utils/comic-info'; import { Acbf } from '../../shared/utils/acbf'; @@ -10,6 +9,7 @@ import { FileHashService } from '../data-access/file-hash.service'; import { ViewerModule } from '../../viewer/viewer.module'; import { ZipHistoryFacade, ZipWorkerFacade } from './facades'; import { ZipWorkerMessageType } from '../models'; +import { ChtnkEpisode, ChtnkImage } from '../../shared/models/chtnk-composition'; @Component({ selector: 'app-zip', @@ -22,7 +22,7 @@ export class ZipComponent implements OnInit, OnDestroy { workerFacade = inject(ZipWorkerFacade) historyFacade = inject(ZipHistoryFacade) - episode = signal({ title: '', images: [] }); + episode = signal({ title: '', images: [] }); fileHash = inject(FileHashService) @@ -64,6 +64,9 @@ export class ZipComponent implements OnInit, OnDestroy { private acbfHandler(msg: any) { const acbf = new Acbf(msg.data) + console.log(acbf.scene) + this.episode().captions = acbf.scene.captions + this.episode().frames = acbf.scene.frames } private comicinfoHandler(msg: any) { @@ -80,7 +83,7 @@ export class ZipComponent implements OnInit, OnDestroy { this.loading.set(false); if (msg.data.count == 0) return; - const imgs: CompositionImage[] = [...Array(msg.data.count)].map((item: CompositionImage, index) => { return { src: `/assets/no-image.svg?id=${index}` } }); + const imgs: ChtnkImage[] = [...Array(msg.data.count)].map((item: ChtnkImage, index) => { return { src: `/assets/no-image.svg?id=${index}` } }); this.workerFacade.loadNextBatch(0); @@ -98,10 +101,11 @@ export class ZipComponent implements OnInit, OnDestroy { } private imageloadHandler(msg: any) { - const { index, url } = msg; + const { index, url, filename } = msg; if (this.episode() && this.episode().images[index]) this.episode().images[index].src = url + this.episode().images[index].id = filename } fileChange() { diff --git a/src/app/history/history.module.ts b/src/app/history/history.module.ts index 815d176..810d97b 100644 --- a/src/app/history/history.module.ts +++ b/src/app/history/history.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HistoryListComponent } from './ui/history-list/history-list.component'; import { SharedModule } from '../shared/shared.module'; +import { ChButtonComponent } from '../shared/ui/ch-button/ch-button.component'; @@ -11,7 +12,8 @@ import { SharedModule } from '../shared/shared.module'; ], imports: [ CommonModule, - SharedModule + SharedModule, + ChButtonComponent ], exports: [ HistoryListComponent 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 3fd6de1..9228576 100644 --- a/src/app/history/ui/history-list/history-list.component.html +++ b/src/app/history/ui/history-list/history-list.component.html @@ -10,7 +10,7 @@
{{lang.ph().sitesHistory}} -
@@ -43,7 +43,7 @@ {{lang.ph().filesHistory}} | {{fileSize() | filesize}} | {{fileCount()}} - diff --git a/src/app/link-parser/link-parser.module.ts b/src/app/link-parser/link-parser.module.ts index 403fcd6..63f1a88 100644 --- a/src/app/link-parser/link-parser.module.ts +++ b/src/app/link-parser/link-parser.module.ts @@ -16,6 +16,7 @@ import { parserProviders } from './data-access/parser.providers'; import { LinkParserService } from './data-access/link-parser.service'; import { LinkParserFacade, LinkInitFacade, NavigationFacade, FileNetFacade } from './ui/parser-form/facades'; import { ExtensionButtonComponent } from '../shared/ui/extension-button/extension-button.component'; +import { ChButtonComponent } from '../shared/ui/ch-button/ch-button.component'; const FACADES = [ LinkParserFacade, @@ -40,7 +41,8 @@ const FACADES = [ FormsModule, SharedModule, HistoryModule, - ExtensionButtonComponent + ExtensionButtonComponent, + ChButtonComponent ], providers: [ ...FACADES diff --git a/src/app/link-parser/ui/go-button/go-button.component.html b/src/app/link-parser/ui/go-button/go-button.component.html index eb7c85f..12c930b 100644 --- a/src/app/link-parser/ui/go-button/go-button.component.html +++ b/src/app/link-parser/ui/go-button/go-button.component.html @@ -1,5 +1,5 @@ @if (data()) { - + {{ ph().letsgo }} diff --git a/src/app/link-parser/ui/header/header.component.html b/src/app/link-parser/ui/header/header.component.html index 8ecb0f1..be0dedd 100644 --- a/src/app/link-parser/ui/header/header.component.html +++ b/src/app/link-parser/ui/header/header.component.html @@ -1,10 +1,13 @@ - - - - - + + + + + @@ -19,4 +22,4 @@ @defer{ } - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts b/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts index a76fe0d..64b4826 100644 --- a/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts +++ b/src/app/link-parser/ui/parser-form/facades/link-init.facade.ts @@ -13,6 +13,20 @@ export class LinkInitFacade { const routeUrl = this.route.root.firstChild?.snapshot.params['url']; const queryUrl = this.route.root.firstChild?.snapshot.queryParamMap.get('url'); + const tgWebAppStartParam = new URLSearchParams(location.search).get('tgWebAppStartParam'); + + if (tgWebAppStartParam) { + const rp = tgWebAppStartParam.split('-'); + + if (rp[0] && rp[1]) { + this.linkFacade.setRedirectParams({ + site: rp[0], + id: rp[1] + }) + return 'tgWebAppStartParam'; + } + } + if (routeUrl) { this.linkFacade.setLink(routeUrl); return 'route'; diff --git a/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts b/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts index bdf2661..e515c60 100644 --- a/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts +++ b/src/app/link-parser/ui/parser-form/facades/link-parser.facade.ts @@ -1,5 +1,5 @@ // link-parser.facade.ts -import { Injectable, computed, signal, Signal } from '@angular/core'; +import { Injectable, computed, signal, Signal, WritableSignal } from '@angular/core'; import { LinkParserService } from '../../../data-access/link-parser.service'; import { Base64 } from '../../../../shared/utils'; import { LinkParseResult } from '../../../utils'; @@ -17,12 +17,20 @@ const FAVICONS: Map = new Map([ ['read', 'data:image/svg+xml,🗯️'] ]); +interface RedirectParams { + site: string; + id: string; + queryParams?: string; +} + @Injectable() export class LinkParserFacade { constructor(private parser: LinkParserService) { } readonly link = signal(''); + readonly redirectParams: WritableSignal = signal(undefined); + readonly linkParams: Signal = computed(() => this.parser.parse(this.link()) ); @@ -39,6 +47,10 @@ export class LinkParserFacade { }; }); + setRedirectParams(redirectParams: RedirectParams) { + this.redirectParams.set(redirectParams) + } + setLink(value: string) { this.link.set(value); } diff --git a/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts b/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts index ac72eef..f0404e1 100644 --- a/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts +++ b/src/app/link-parser/ui/parser-form/facades/navigation.facade.ts @@ -11,4 +11,10 @@ export class NavigationFacade { if (!data) return; this.router.navigateByUrl(`/${data.site}/${data.id64}`); } + + goByRedirectParams() { + const data = this.linkFacade.redirectParams(); + if (!data) return; + this.router.navigateByUrl(`/${data.site}/${data.id}`); + } } \ No newline at end of file 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 ac9238e..968449f 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 @@ -24,6 +24,9 @@ export class ParserFormComponent { if (source === 'route') { this.navFacade.goToParsedLink(); } + if (source === 'tgWebAppStartParam') { + this.navFacade.goByRedirectParams(); + } }); } } diff --git a/src/app/list/list-shell/list-shell.component.html b/src/app/list/list-shell/list-shell.component.html index a462dca..9b334f1 100644 --- a/src/app/list/list-shell/list-shell.component.html +++ b/src/app/list/list-shell/list-shell.component.html @@ -17,7 +17,7 @@

Відредагуйте назви (за бажанням) ✏️🔢📝

@@ -36,7 +36,7 @@

- +
@@ -44,7 +44,7 @@

Скопіюйте JSON код, та опублікуйте його на Вставте посилання на RAW JSON-файл 🔗📄

- @if(rawJsonLink()){ {{getItem(firstLink())?.title != ''? getItem(firstLink())?.title : @@ -54,6 +54,6 @@

Вставте посилання на RAW JSON-файл 🔗📄

-
🏠 + 🏠 diff --git a/src/app/list/list.module.ts b/src/app/list/list.module.ts index 58314f0..21d5813 100644 --- a/src/app/list/list.module.ts +++ b/src/app/list/list.module.ts @@ -7,6 +7,7 @@ import { FormsModule } from '@angular/forms'; import { SharedModule } from '../shared/shared.module'; import { LinkParserService } from '../link-parser/data-access/link-parser.service'; import { parserProviders } from '../link-parser/data-access/parser.providers'; +import { ChButtonComponent } from '../shared/ui/ch-button/ch-button.component'; @NgModule({ @@ -17,7 +18,8 @@ import { parserProviders } from '../link-parser/data-access/parser.providers'; CommonModule, ListRoutingModule, FormsModule, - SharedModule + SharedModule, + ChButtonComponent ] }) export class ListModule { } diff --git a/src/app/shared/data-access/download.service.ts b/src/app/shared/data-access/download.service.ts index f08f49e..857f8dd 100644 --- a/src/app/shared/data-access/download.service.ts +++ b/src/app/shared/data-access/download.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { CompositionEpisode, CompositionImage } from '../../@site-modules/@common-read' +import { ChtnkEpisode, ChtnkImage } from '../models/chtnk-composition'; enum DownloadStatus { Downloading = "downloading", @@ -17,11 +17,11 @@ type DownloadQueue = { dateDownloaded: Date | undefined; } -interface DownloadCompositionImage extends CompositionImage { +interface DownloadCompositionImage extends ChtnkImage { image: number[] status: DownloadStatus; } -interface DowloadCompositionEpisode extends CompositionEpisode { +interface DowloadCompositionEpisode extends ChtnkEpisode { images: DownloadCompositionImage[] } @@ -37,11 +37,11 @@ export class DownloadService { * -[ ] */ - queue: DownloadQueue[] = [] + queue: DownloadQueue[] = [] constructor() { } - map(ep: CompositionEpisode): DowloadCompositionEpisode { + map(ep: ChtnkEpisode): DowloadCompositionEpisode { return { title: ep.title, chapter: ep.chapter, @@ -68,7 +68,7 @@ export class DownloadService { } } - addToQueue(episode: CompositionEpisode, id: string) { + addToQueue(episode: ChtnkEpisode, id: string) { const qi = this.getFromQueue(id) if (qi) return; this.queue.push( diff --git a/src/app/shared/directives/viewport-zone.directive.ts b/src/app/shared/directives/viewport-zone.directive.ts new file mode 100644 index 0000000..04579e2 --- /dev/null +++ b/src/app/shared/directives/viewport-zone.directive.ts @@ -0,0 +1,69 @@ +import { + Directive, + ElementRef, + Renderer2, + HostListener, +} from '@angular/core'; + +type Zone = + | 'top-left' | 'top-center' | 'top-right' + | 'middle-left' | 'middle-center' | 'middle-right' + | 'bottom-left' | 'bottom-center' | 'bottom-right'; + +@Directive({ + selector: '[viewportZone]', + standalone: false +}) +export class ViewportZoneDirective { + private currentZone: Zone | null = null; + + constructor( + private el: ElementRef, + private renderer: Renderer2 + ) {} + + @HostListener('pointerover', ['$event']) + onPointerOver(event: PointerEvent) { + const vw = window.innerWidth; + const vh = window.innerHeight; + + const x = event.clientX; + const y = event.clientY; + + const col = this.getColumn(x, vw); + const row = this.getRow(y, vh); + + const zone = `${row}-${col}` as Zone; + + if (zone !== this.currentZone) { + this.updateClass(zone); + this.currentZone = zone; + } + } + + private getColumn(x: number, vw: number) { + if (x < vw / 3) return 'left'; + if (x < (vw / 3) * 2) return 'center'; + return 'right'; + } + + private getRow(y: number, vh: number) { + if (y < vh / 3) return 'top'; + if (y < (vh / 3) * 2) return 'middle'; + return 'bottom'; + } + + private updateClass(zone: Zone) { + if (this.currentZone) { + this.renderer.removeClass( + this.el.nativeElement, + `zone-${this.currentZone}` + ); + } + + this.renderer.addClass( + this.el.nativeElement, + `zone-${zone}` + ); + } +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/chtnk-captions.ts b/src/app/shared/models/chtnk-composition/chtnk-captions.ts new file mode 100644 index 0000000..fb79ac1 --- /dev/null +++ b/src/app/shared/models/chtnk-composition/chtnk-captions.ts @@ -0,0 +1,9 @@ +export interface ChtnkCaption { + id?: string; + lang: string; + points: string; + text: string; + fill?: string; + color?: string; + type?: string; +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/chtnk-composition.ts b/src/app/shared/models/chtnk-composition/chtnk-composition.ts new file mode 100644 index 0000000..3688544 --- /dev/null +++ b/src/app/shared/models/chtnk-composition/chtnk-composition.ts @@ -0,0 +1,12 @@ +import { ChtnkEpisode } from "./chtnk-episode"; +import { ChtnkImage } from "./chtnk-image"; + +export interface ChtnkComposition { + title: string; + episodes: ChtnkEpisode[]; + + description?: string; + artist?: string; + author?: string; + cover?: ChtnkImage; +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/chtnk-episode.ts b/src/app/shared/models/chtnk-composition/chtnk-episode.ts new file mode 100644 index 0000000..62d6316 --- /dev/null +++ b/src/app/shared/models/chtnk-composition/chtnk-episode.ts @@ -0,0 +1,21 @@ +import { ChtnkCaption } from "./chtnk-captions"; +import { ChtnkFrame } from "./chtnk-frame"; +import { ChtnkImage } from "./chtnk-image"; +import { ChtnkPublisher } from "./chtnk-publisher"; + +export interface ChtnkEpisode { + title: string; + images: ChtnkImage[]; + + frames?: ChtnkFrame[]; + captions?: ChtnkCaption[]; + + episode?: number; + nsfw?: boolean | undefined; + mangaId?: string; + volume?: number; + chapter?: number; + part?: number; + extra?: boolean; + publisher?: ChtnkPublisher, +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/chtnk-frame.ts b/src/app/shared/models/chtnk-composition/chtnk-frame.ts new file mode 100644 index 0000000..1e9100b --- /dev/null +++ b/src/app/shared/models/chtnk-composition/chtnk-frame.ts @@ -0,0 +1,5 @@ +export interface ChtnkFrame { + id?: string; + points: string; // "x1,y1 x2,y2 x3,y3 ..." + fill?: string; // Optional fill color for background the frame, e.g., "#fff" for white +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/chtnk-image.ts b/src/app/shared/models/chtnk-composition/chtnk-image.ts new file mode 100644 index 0000000..a826f84 --- /dev/null +++ b/src/app/shared/models/chtnk-composition/chtnk-image.ts @@ -0,0 +1,10 @@ +export interface ChtnkImage { + src: string; // URL of the image (BLOB or external URL) + id?: string; // Optional unique identifier for the image + width?: number; // Optional width of the image in pixels + height?: number; // Optional height of the image in pixels + size?: number; // Optional size of the image in bytes + type?: string; // Optional MIME type of the image (e.g., "image/jpeg") + alt?: string; // Optional alternative text for the image + nsfw?: string; // Optional NSFW status of the image (e.g., "safe", "nsfw", "explicit") +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/chtnk-publisher.ts b/src/app/shared/models/chtnk-composition/chtnk-publisher.ts new file mode 100644 index 0000000..ab25237 --- /dev/null +++ b/src/app/shared/models/chtnk-composition/chtnk-publisher.ts @@ -0,0 +1,13 @@ +import { ChtnkImage } from "./chtnk-image"; + +export interface ChtnkPublisher { + id: string; + site: string; + description: string + name: string; + avatar?: ChtnkImage; + links?: Array<{ + link: string, + title: string + }> +} \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/index.ts b/src/app/shared/models/chtnk-composition/index.ts new file mode 100644 index 0000000..fb1a426 --- /dev/null +++ b/src/app/shared/models/chtnk-composition/index.ts @@ -0,0 +1,8 @@ +export * from "./chtnk-captions"; +export * from "./chtnk-composition"; +export * from "./chtnk-episode"; +export * from "./chtnk-frame"; +export * from "./chtnk-image"; +export * from "./chtnk-publisher"; + +export * from "./is-chtnk-episode"; \ No newline at end of file diff --git a/src/app/shared/models/chtnk-composition/is-chtnk-episode.ts b/src/app/shared/models/chtnk-composition/is-chtnk-episode.ts new file mode 100644 index 0000000..e04c40e --- /dev/null +++ b/src/app/shared/models/chtnk-composition/is-chtnk-episode.ts @@ -0,0 +1,10 @@ +import { ChtnkEpisode } from "./chtnk-episode"; +import { ChtnkImage } from "./chtnk-image"; + +export function isChtnkEpisode(data: any): data is ChtnkEpisode { + return ( + typeof data.title === 'string' && + Array.isArray(data.images) && + data.images.every((image: ChtnkImage) => typeof image.src === 'string') + ); +} \ No newline at end of file diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 044b00f..12ccc61 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -32,8 +32,10 @@ import { SourceCopyrightLogoComponent } from './ui/source-copyright-logo/source- import { SloganComponent } from './ui/slogan/slogan.component'; import { ToggleBarComponent } from './ui/toggle-bar/toggle-bar.component'; import { SvgTextBoxComponent } from './ui/svg-text-box/svg-text-box.component'; +import { ViewportZoneDirective } from './directives/viewport-zone.directive'; +import { ChButtonComponent } from './ui/ch-button/ch-button.component'; -const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, DropZoneComponent, SourceCopyrightComponent, SourceCopyrightLogoComponent, SloganComponent, NsfwWarningComponent, ImgMetaDirective, NewTabDirective, PagesIndicatorComponent, WarmFilterComponent, WarmControlComponent, ToggleBarComponent, SvgTextBoxComponent] +const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe, VibrateHapticDirective, SircleBlurComponent, DropZoneComponent, SourceCopyrightComponent, SourceCopyrightLogoComponent, SloganComponent, NsfwWarningComponent, ImgMetaDirective, NewTabDirective, PagesIndicatorComponent, WarmFilterComponent, WarmControlComponent, ToggleBarComponent, SvgTextBoxComponent, ViewportZoneDirective] @NgModule({ declarations: [...components], @@ -42,7 +44,8 @@ const components = [GamepadCursorComponent, TruncatePipe, TextEmbracerComponent, FormsModule, RouterModule, RoughPaperComponent, - SharpenComponent + SharpenComponent, + ChButtonComponent ], exports: [...components] }) diff --git a/src/app/shared/ui/ch-button/ch-button.component.scss b/src/app/shared/ui/ch-button/ch-button.component.scss new file mode 100644 index 0000000..4c4ebe5 --- /dev/null +++ b/src/app/shared/ui/ch-button/ch-button.component.scss @@ -0,0 +1,103 @@ +:host { + --q: 1; + user-select: none; + border: 0; + font: inherit; + color: inherit; + padding-inline: calc(var(--q) * 1ch); + padding-block: calc(var(--q) * 1ch); + border-radius: .5ch; + cursor: pointer; + background: hsl(203.44 8% 16%); + + @media (prefers-color-scheme: light) { + background: hsl(203.44 8% 96%); + } + + & { + transition: all var(--t); + text-wrap: balance; + -webkit-tap-highlight-color: transparent; + box-shadow: var(--shadow-1); + } + + &.small { + --q: 0.5 + } + + &.mediun { + --q: 1.5 + } + + &.large { + --q: 2 + } + + &.tshadow { + text-shadow: 0 0 0.25ch #000a; + } + + &.empty { + background: transparent; + box-shadow: unset; + } + + &.primary { + background: #166496; + color: white; + } + + &.border { + border: var(--border-size) solid var(--border-color); + } + + &.gamepad-hover, + &:hover { + color: #103651; + background-color: #ffd60a; + + @media (prefers-color-scheme: light) { + color: #f8d755; + background-color: #166496; + } + } + + &.delete { + &.gamepad-hover, + &:hover, + &:focus { + background-color: #541f20; + } + } + + &:active, + &.active { + background-color: rgba(0, 15, 30, 1); + box-shadow: unset; + transform: translate(2px, 2px); + } + + &.full { + width: 100%; + } + + &:disabled { + cursor: not-allowed; + // pointer-events: none; + filter: grayscale(.64); + box-shadow: unset; + background-color: rgba(0, 15, 30, .5); + + @media (prefers-color-scheme: light) { + background-color: rgba(0, 15, 30, .25); + opacity: .64; + } + } +} + +@supports (corner-shape: squircle) { + :host { + corner-shape: squircle; + border-radius: 1ch; + } +} \ No newline at end of file diff --git a/src/app/shared/ui/ch-button/ch-button.component.ts b/src/app/shared/ui/ch-button/ch-button.component.ts new file mode 100644 index 0000000..2a4625b --- /dev/null +++ b/src/app/shared/ui/ch-button/ch-button.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: '[ch-button]', + imports: [], + template: '', + styleUrl: './ch-button.component.scss' +}) +export class ChButtonComponent { + +} diff --git a/src/app/shared/ui/dialog/dialog.component.html b/src/app/shared/ui/dialog/dialog.component.html index 9283c56..8016e8e 100644 --- a/src/app/shared/ui/dialog/dialog.component.html +++ b/src/app/shared/ui/dialog/dialog.component.html @@ -7,7 +7,7 @@

{{title()}}

@if (closeHeaderButton()) {
-
@@ -19,7 +19,7 @@

{{title()}}

@if(footer()){
-
diff --git a/src/app/shared/ui/dialog/dialog.component.scss b/src/app/shared/ui/dialog/dialog.component.scss index 8c8c582..825847d 100644 --- a/src/app/shared/ui/dialog/dialog.component.scss +++ b/src/app/shared/ui/dialog/dialog.component.scss @@ -23,10 +23,6 @@ dialog { --dots: radial-gradient(var(--surface) 4px, transparent 0px); --size: 10px; background-color: transparent; - // background-image: var(--dots); - // background-repeat: repeat; - // background-size: var(--size) var(--size); - // background-position: center; --brightness: .6; backdrop-filter: grayscale(.8) url(#circleBlur); } @@ -34,10 +30,7 @@ dialog { .dialog-wrapper { color: inherit; display: grid; - // max-height: 100%; - // width: 100%; grid-template: auto 1fr auto / 1fr; - // width: min(90vw, 60ch); margin: 4ch auto; max-width: min(calc(100vw - 4ch), 60ch); max-height: calc(100dvh - 8ch); @@ -130,4 +123,48 @@ dialog { corner-shape: squircle; border-radius: calc(var(--r) * 3); } +} + + +@supports (transition-behavior: allow-discrete) { + + dialog, + dialog::backdrop { + transition-property: opacity, transform, overlay, display, filter; + transition-duration: var(--t); + transition-behavior: allow-discrete; + opacity: 0; + transform: scale(calc(1 + var(--scale-diff-x, .1)), calc(1 + var(--scale-diff-y, .1))); + filter: blur(var(--blur)); + } + + dialog[open], + dialog[open]::backdrop { + opacity: 1; + transform: scale(1); + filter: blur(0); + } + + @starting-style { + + dialog[open], + dialog[open]::backdrop { + opacity: 0; + transform: scale(calc(1 + var(--scale-diff-x, .1)), calc(1 + var(--scale-diff-y, .1))); + filter: blur(var(--blur)); + } + } +} + +@media (prefers-reduced-motion: reduce) { + + dialog, + dialog::backdrop, + dialog[open], + dialog[open]::backdrop { + transition: none !important; + transform: none !important; + opacity: 1 !important; + filter: none !important; + } } \ No newline at end of file diff --git a/src/app/shared/ui/extension-button/extension-button.component.html b/src/app/shared/ui/extension-button/extension-button.component.html index 79b9e4f..1130461 100644 --- a/src/app/shared/ui/extension-button/extension-button.component.html +++ b/src/app/shared/ui/extension-button/extension-button.component.html @@ -5,7 +5,7 @@ @if(extension.isInstalled() === false && link && (!mobile || b.name === 'Edge')) { -🧩 +🧩 } 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 778fc81..a0e5e72 100644 --- a/src/app/shared/ui/file-change/file-change.component.html +++ b/src/app/shared/ui/file-change/file-change.component.html @@ -1,4 +1,4 @@ - @if(showDragAndDropZone()){ 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 1007dcd..77ffb99 100644 --- a/src/app/shared/ui/file-change/file-change.component.scss +++ b/src/app/shared/ui/file-change/file-change.component.scss @@ -1,4 +1,4 @@ -.button { +button { width: 100%; text-align: unset; } \ No newline at end of file diff --git a/src/app/shared/ui/title-card/title-card.component.html b/src/app/shared/ui/title-card/title-card.component.html index 639cbc8..0394603 100644 --- a/src/app/shared/ui/title-card/title-card.component.html +++ b/src/app/shared/ui/title-card/title-card.component.html @@ -42,4 +42,4 @@ {{date | date : 'shortTime'}} - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/shared/ui/title-card/title-card.component.ts b/src/app/shared/ui/title-card/title-card.component.ts index 9e3af47..cba2c77 100644 --- a/src/app/shared/ui/title-card/title-card.component.ts +++ b/src/app/shared/ui/title-card/title-card.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, InputSignal, output, computed, inject, input } from '@angular/core'; import { LangService } from '../../data-access/lang.service'; -import { CompositionEpisode } from '../../../@site-modules/@common-read'; +import { ChtnkEpisode } from '../../models/chtnk-composition'; export interface TitleCartItem { site: string[]; @@ -8,7 +8,7 @@ export interface TitleCartItem { cover: string; title: string; updated: string; - episode?: CompositionEpisode; + episode?: ChtnkEpisode; id: number; size: number; page: number; diff --git a/src/app/shared/ui/toggle-bar/toggle-bar.component.html b/src/app/shared/ui/toggle-bar/toggle-bar.component.html index 9cbdc19..95b09ca 100644 --- a/src/app/shared/ui/toggle-bar/toggle-bar.component.html +++ b/src/app/shared/ui/toggle-bar/toggle-bar.component.html @@ -3,7 +3,7 @@ - diff --git a/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html b/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html index e3459a8..e29449a 100644 --- a/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html +++ b/src/app/shared/ui/view-mode-bar/view-mode-bar.component.html @@ -3,7 +3,7 @@ - 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 760b611..b57b658 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 + \ No newline at end of file diff --git a/src/app/shared/utils/acbf.ts b/src/app/shared/utils/acbf.ts index 227e9a4..4eb4ce2 100644 --- a/src/app/shared/utils/acbf.ts +++ b/src/app/shared/utils/acbf.ts @@ -1,13 +1,75 @@ +import { ChtnkFrame, ChtnkCaption } from "../models/chtnk-composition"; + +export type ChtnkScene = { + frames?: ChtnkFrame[]; + captions?: ChtnkCaption[]; +} + export class Acbf { + private _doc: Document | undefined; + constructor(xml: string) { this.fromXmlString(xml) } - fromXmlString(xml: string) { + private fromXmlString(xml: string) { const parser = new DOMParser(); const doc: Document = parser.parseFromString(xml, "text/xml"); - const v = (tag: string) => doc.getElementsByTagName(tag)[0]?.textContent; - console.log(doc); - + this._doc = doc; } + + get scene(): ChtnkScene { + if (!this._doc) return { frames: [], captions: [] }; + const pageElements = this._doc.getElementsByTagName('page'); + + const frames: ChtnkFrame[] = []; + const captions: ChtnkCaption[] = []; + + for (let i = 0; i < pageElements.length; i++) { + const pageElement = pageElements[i]; + + const id = pageElement.getElementsByTagName('image')[0]?.getAttribute('href') || undefined; + const pageColor = pageElement.getAttribute('bgcolor') || undefined; + + // Process text layers + const textLayerElements = pageElement.getElementsByTagName('text-layer'); + for (let j = 0; j < textLayerElements.length; j++) { + const textLayerElement = textLayerElements[j]; + const lang = textLayerElement.getAttribute('lang') || ''; + + + // Process captions + const captionElements = textLayerElement.getElementsByTagName('text-area'); + for (let k = 0; k < captionElements.length; k++) { + const captionElement = captionElements[k]; + const caption: ChtnkCaption = { + lang: lang, + id: id, + points: captionElement.getAttribute('points') || '', + text: captionElement.innerHTML || '', + fill: captionElement.getAttribute('bgcolor') || undefined, + color: captionElement.getAttribute('color') || undefined, + type: captionElement.getAttribute('type') || undefined + }; + captions.push(caption); + } + } + + // Process frame layers + const frameLayerElements = pageElement.getElementsByTagName('frame'); + for (let j = 0; j < frameLayerElements.length; j++) { + const frameLayerElement = frameLayerElements[j]; + const frameLayer: ChtnkFrame = { + id: id, + points: frameLayerElement.getAttribute('points') || '', + fill: frameLayerElement.getAttribute('bgcolor') || pageColor || undefined + }; + frames.push(frameLayer); + } + + } + return { frames, captions }; + } + + } \ No newline at end of file diff --git a/src/app/viewer/facades/page-tracking.facade.ts b/src/app/viewer/facades/page-tracking.facade.ts index b2a537e..16e3a34 100644 --- a/src/app/viewer/facades/page-tracking.facade.ts +++ b/src/app/viewer/facades/page-tracking.facade.ts @@ -2,8 +2,8 @@ import { isPlatformServer } from "@angular/common"; import { Injectable, signal, computed, PLATFORM_ID, inject, Signal, effect } from "@angular/core"; import { ViewModeFacade } from "./view-mode.facade"; import { EmbedFacade } from "./viewer-embed.facade"; -import { CompositionEpisode } from "../../@site-modules/@common-read"; -import { ActivatedRoute, Router } from "@angular/router"; +import { Router } from "@angular/router"; +import { ChtnkEpisode } from "../../shared/models/chtnk-composition"; @Injectable() export class PageTrackingFacade { @@ -51,7 +51,7 @@ export class PageTrackingFacade { }); } - connectPagesCount(episode: Signal) { + connectPagesCount(episode: Signal) { effect(() => this.pagesCount.set(episode().images.length)); } diff --git a/src/app/viewer/facades/viewer-title-tag.facade.ts b/src/app/viewer/facades/viewer-title-tag.facade.ts index 8d8339d..3a2b23a 100644 --- a/src/app/viewer/facades/viewer-title-tag.facade.ts +++ b/src/app/viewer/facades/viewer-title-tag.facade.ts @@ -1,7 +1,7 @@ import { inject, Injectable, Signal } from "@angular/core"; import { parseTags, resolveViewMode } from "../../shared/utils"; import { ViewModeFacade } from "./view-mode.facade"; -import { CompositionEpisode } from "../../@site-modules/@common-read"; +import { ChtnkEpisode } from "../../shared/models/chtnk-composition"; /** * Facade for the viewer title tag. @@ -13,9 +13,9 @@ import { CompositionEpisode } from "../../@site-modules/@common-read"; export class ViewerTitleTagFacade { viewMode = inject(ViewModeFacade); - private _episode!: Signal; + private _episode!: Signal; - initialize(ep: Signal) { + initialize(ep: Signal) { this._episode = ep; const tags = parseTags(this._episode().title); diff --git a/src/app/viewer/viewer.module.ts b/src/app/viewer/viewer.module.ts index 75409a1..a7c1092 100644 --- a/src/app/viewer/viewer.module.ts +++ b/src/app/viewer/viewer.module.ts @@ -5,12 +5,15 @@ import { VIEWER_DECLARABLES } from './viewer.declarables'; import { SharedModule } from '../shared/shared.module'; import { EmbedFacade, GamepadFacade, KeyboardFacade, NsfwFacade, PageTrackingFacade, ReadlistFacade, ViewerScrollFacade, ViewerTitleTagFacade, ViewerUiFacade, ViewModeFacade } from './facades'; import { ViewerService } from './services'; +import { TextComponent } from './viewer/components/text/text.component'; +import { ChButtonComponent } from '../shared/ui/ch-button/ch-button.component'; @NgModule({ imports: [ CommonModule, RouterModule, - SharedModule + SharedModule, + ChButtonComponent ], providers: [ ViewerService, @@ -25,7 +28,7 @@ import { ViewerService } from './services'; ReadlistFacade, ViewerTitleTagFacade ], - declarations: [...VIEWER_DECLARABLES], + declarations: [...VIEWER_DECLARABLES, TextComponent], exports: [...VIEWER_DECLARABLES] }) export class ViewerModule { } diff --git a/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.html b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.html index 3f61ef8..f867448 100644 --- a/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.html +++ b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.html @@ -3,7 +3,7 @@
{{episode().title}} -
📥 {{episode().images.length}} pages @@ -17,12 +17,12 @@ {{dl.getFromQueue(genId()).object.title}} @if(dl.getFromQueue(genId()).status != "done"){ - } @if(dl.getFromQueue(genId()).status == "done"){ - + }
diff --git a/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts index 1f1a1b7..0a76200 100644 --- a/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts +++ b/src/app/viewer/viewer/components/episode-download-form/episode-download-form.component.ts @@ -1,8 +1,8 @@ import { Component, inject, input } from '@angular/core'; -import { CompositionEpisode } from '../../../../@site-modules/@common-read'; import { DownloadService } from '../../../../shared/data-access/download.service'; import { Base64 } from '../../../../shared/utils'; import { PlaylistItem } from '../../../../playlist/data-access/playlist.service'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ @@ -16,7 +16,7 @@ export class EpisodeDownloadFormComponent { dl = inject(DownloadService); currentPlaylistItem = input(); - episode = input({ + episode = input({ title: '', images: [] }) diff --git a/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts b/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts index 8ff6587..a39e002 100644 --- a/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts +++ b/src/app/viewer/viewer/components/episode-info-table/episode-info-table.component.ts @@ -1,6 +1,6 @@ import { Component, inject, input } from '@angular/core'; -import { CompositionEpisode } from '../../../../@site-modules/@common-read'; import { LangService } from '../../../../shared/data-access/lang.service'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ selector: 'episode-info-table', @@ -12,7 +12,7 @@ import { LangService } from '../../../../shared/data-access/lang.service'; export class EpisodeInfoTableComponent { lang = inject(LangService) - episode = input({ + episode = input({ title: '', images: [] }) diff --git a/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.html b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.html index 53df151..2a3990b 100644 --- a/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.html +++ b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.html @@ -3,7 +3,7 @@
- +
@@ -11,8 +11,8 @@ 🔌 Embed
- +
- \ No newline at end of file + \ No newline at end of file diff --git a/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts index 9920a7b..d4b5409 100644 --- a/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts +++ b/src/app/viewer/viewer/components/episode-share-form/episode-share-form.component.ts @@ -1,10 +1,10 @@ import { Component, computed, inject, input, output, PLATFORM_ID, Signal } from '@angular/core'; import { DomManipulationService } from '../../../../shared/data-access'; -import { CompositionEpisode } from '../../../../@site-modules/@common-read'; import { isPlatformBrowser } from '@angular/common'; import { LangService } from '../../../../shared/data-access/lang.service'; import { DomSanitizer } from '@angular/platform-browser'; import { ViewerService } from '../../../services'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ selector: 'episode-share-form', @@ -21,7 +21,7 @@ export class EpisodeShareFormComponent { viewer = inject(ViewerService) - episode = input({ + episode = input({ title: '', images: [] }) diff --git a/src/app/viewer/viewer/components/page/page.component.html b/src/app/viewer/viewer/components/page/page.component.html index 84db5ae..3046b9f 100644 --- a/src/app/viewer/viewer/components/page/page.component.html +++ b/src/app/viewer/viewer/components/page/page.component.html @@ -10,10 +10,36 @@ }
@if(src()){ - + } @else { } + @if(captionByLang()) { + + + @for (f of frames(); track $index) { + + } + + + + } +
+ @for (ct of captionByLang(); track $index) { + @let c = getCenter(ct.points); + +
+
+ } +
\ No newline at end of file diff --git a/src/app/viewer/viewer/components/page/page.component.scss b/src/app/viewer/viewer/components/page/page.component.scss index 2414013..1355172 100644 --- a/src/app/viewer/viewer/components/page/page.component.scss +++ b/src/app/viewer/viewer/components/page/page.component.scss @@ -1,5 +1,5 @@ :host { - position: relative; + position: relative; } img { @@ -33,4 +33,37 @@ loading { z-index: 0; width: 100%; padding: 2rem; +} + +svg.captions { + position: absolute; + top: 0; + left: 0; + z-index: 2; + // pointer-events: none; + max-width: 100%; + max-height: 100%; + + .frames { + polygon { + cursor: pointer; + fill: rgba(255, 255, 0, 0); + stroke: transparent; + stroke-width: 2px; + + &:hover { + stroke: #166496; + + } + } + } +} + +.text-layer { + position: absolute; + top: 0; + left: 0; + z-index: 2; + max-width: 100%; + max-height: 100%; } \ No newline at end of file diff --git a/src/app/viewer/viewer/components/page/page.component.ts b/src/app/viewer/viewer/components/page/page.component.ts index 3bc78dd..41ffd09 100644 --- a/src/app/viewer/viewer/components/page/page.component.ts +++ b/src/app/viewer/viewer/components/page/page.component.ts @@ -1,5 +1,11 @@ -import { Component, computed, inject, input, InputSignal, output, OutputEmitterRef, Signal, signal } from '@angular/core'; +import { Component, computed, Host, HostListener, inject, input, InputSignal, output, OutputEmitterRef, Signal, signal } from '@angular/core'; import { LangService } from '../../../../shared/data-access/lang.service'; +import { ChtnkCaption, ChtnkFrame } from '../../../../shared/models/chtnk-composition'; + +type TSpanChunk = { + text: string; + attrs: Record; +}; @Component({ selector: 'chtnk-page', @@ -18,6 +24,14 @@ export class PageComponent { showNsfw: InputSignal = input(false); src: InputSignal = input(''); alt: InputSignal = input(); + filename: InputSignal = input(); + captions: InputSignal = input([] as ChtnkCaption[]); + frames: InputSignal = input([] as ChtnkFrame[]); + captionLang: InputSignal = input('en'); + + captionByLang = computed(() => { + return this.captions()?.filter(f => f.lang == this.captionLang()) + }); inputWidth = input(1000); inputHeight = input(1000); @@ -28,6 +42,8 @@ export class PageComponent { width = computed(() => this.naturalWidth() ?? this.inputWidth()); height = computed(() => this.naturalHeight() ?? this.inputHeight()); + viewBox = computed(() => `0 0 ${this.width()} ${this.height()}`); + widthPx = computed(() => this.width().toString() + 'px'); heightPx = computed(() => this.height().toString() + 'px'); @@ -47,6 +63,7 @@ export class PageComponent { async imageLoad(event: Event) { const img = event.target as HTMLImageElement; + this.image = img; this.imageLoading.set(false) this.naturalWidth.set(img.naturalWidth); this.naturalHeight.set(img.naturalHeight); @@ -68,6 +85,7 @@ export class PageComponent { console.warn('Failed to get bitmap size, fallback to naturalWidth/naturalHeight', e); } + this.calcScale(); this.detectLongPage(this.naturalWidth(), this.naturalHeight()) } @@ -86,4 +104,46 @@ export class PageComponent { } //#endregion + + ///// + + image: HTMLImageElement | null = null; + + imageWidth = signal(0); + imageHeight = signal(0); + scale = signal(1); + + @HostListener('window:resize') + calcScale() { + if (!this.image) return; + + this.imageWidth.set(this.image.width); + this.imageHeight.set(this.image.height); + + this.scale.set( this.image.width / this.naturalWidth()); + + } + + + getCenter(pointsStr: string) { + const pts = pointsStr.split(' ').map(p => p.split(',').map(Number)); + const xs = pts.map(p => p[0]); + const ys = pts.map(p => p[1]); + + return { + x: (Math.min(...xs) + Math.max(...xs)) / 2, + y: (Math.min(...ys) + Math.max(...ys)) / 2, + w: Math.max(...xs) - Math.min(...xs), + h: Math.max(...ys) - Math.min(...ys), + }; + } + + /// + + xmlToHtml(xml: string): string { + const result = xml + .replace('','').replace('','') + .replace('','').replace('',''); + return result; + } } diff --git a/src/app/viewer/viewer/components/text/text.component.html b/src/app/viewer/viewer/components/text/text.component.html new file mode 100644 index 0000000..e2f35e4 --- /dev/null +++ b/src/app/viewer/viewer/components/text/text.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/app/viewer/viewer/components/text/text.component.scss b/src/app/viewer/viewer/components/text/text.component.scss new file mode 100644 index 0000000..bb07827 --- /dev/null +++ b/src/app/viewer/viewer/components/text/text.component.scss @@ -0,0 +1,97 @@ +:host { + --fill: white; + --a: .1; + position: absolute; + border: 1px dashed currentColor; + border-radius: .25ch; + background-color: oklch(from var(--fill) l c h / var(--a)); + translate: -50% -50%; + font-size: .72rem; + text-align: center; + padding: 1.25ch; + z-index: 1; + font-family: 'Troubleside'; + + + &.commentary { + font-family: Arial, Helvetica, sans-serif; + text-align: unset; + } + + &.code { + font-family: 'Source Code Pro', monospace; + text-align: unset; + } +} + + +.note { + text-wrap: balance; + border-radius: .25ch; + z-index: 42; + color: inherit; + font-size: inherit; + position: absolute; + top: calc(100% + 1ch); + left: 0; + background-color: var(--surface); + direction: ltr !important; + display: none; + padding: 1ch; + opacity: 1; + transition: all var(--t) ease-in-out; + + box-shadow: var(--shadow-1); + + ::ng-deep { + p { + direction: inherit; + margin: 0; + } + + span.emphasis { + font-weight: bold; + font-style: oblique; + } + + span.strong { + font-weight: bold; + } + + // span.italic { + // font-style: italic; + // } + + // span.underline { + // text-decoration: underline; + // } + } +} + + @starting-style { + .note { + opacity: 0; + } + } + + +:host:hover { + --a: .4; + z-index: 43; + + .note { + display: block; + } +} + +:host { + + &.zone-bottom-left, + &.zone-bottom-center, + &.zone-bottom-right { + .note { + top: unset; + bottom: calc(100% + 1ch); + } + } +} \ No newline at end of file diff --git a/src/app/viewer/viewer/components/text/text.component.ts b/src/app/viewer/viewer/components/text/text.component.ts new file mode 100644 index 0000000..ceb112a --- /dev/null +++ b/src/app/viewer/viewer/components/text/text.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'chtnk-text', + standalone: false, + + templateUrl: './text.component.html', + styleUrl: './text.component.scss' +}) +export class TextComponent { + +} diff --git a/src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts b/src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts index 0424bad..b743c2c 100644 --- a/src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts +++ b/src/app/viewer/viewer/components/thanks-page/thanks-page.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; import { ViewerService } from '../../../services'; import { LangService } from '../../../../shared/data-access/lang.service'; -import { CompositionEpisode } from '../../../../@site-modules/@common-read'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ selector: 'app-thanks-page', @@ -14,7 +14,7 @@ import { CompositionEpisode } from '../../../../@site-modules/@common-read'; export class ThanksPageComponent { viewer: ViewerService = inject(ViewerService) lang: LangService = inject(LangService) - episode = input(); + episode = input(); playlist = input([]); playlistLink = input(""); diff --git a/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html index fc4a076..3f8b6e1 100644 --- a/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html +++ b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.html @@ -4,14 +4,14 @@ } @if (playlist().length >0) { - + } @if (domMan.fullscreenEnabled()) { - } diff --git a/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.scss b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.scss index e050ea1..03cb7a3 100644 --- a/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.scss +++ b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.scss @@ -1,4 +1,5 @@ app-overlay { padding-bottom: 2ch; padding-top: unset; + z-index: 100; } \ No newline at end of file diff --git a/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts index 78236ad..ca473fe 100644 --- a/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts +++ b/src/app/viewer/viewer/components/viewer-footer/viewer-footer.component.ts @@ -1,10 +1,10 @@ -import { Component, EventEmitter, HostListener, inject, input, InputSignal, output, signal, ViewChild } from '@angular/core'; +import { Component, HostListener, inject, input, output, signal, ViewChild } from '@angular/core'; import { BrowserService, DomManipulationService } from '../../../../shared/data-access'; import { LangService } from '../../../../shared/data-access/lang.service'; import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; -import { CompositionEpisode } from '../../../../@site-modules/@common-read'; import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; import { ViewerService } from '../../../services'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ selector: 'app-viewer-footer', @@ -20,7 +20,7 @@ export class ViewerFooterComponent { show = input(false); activeIndexs = input([]) - episode = input(); + episode = input(); playlist = input([]); playlistLink = input(""); currentPlaylistItem = input(); diff --git a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html index c05e86b..cbcf348 100644 --- a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html +++ b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.html @@ -1,20 +1,25 @@ @let ph = lang.ph(); @if(!embedHelper.isEmbedded()){ - 🏠 + 🏠 } @else { - } @if (!isFileRoute()) { - - + + } - - + @let episodeLangs = getCaptionLangs(); + @if(episodeLangs.length > 0){ + } diff --git a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.scss b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.scss index 09cf789..c305687 100644 --- a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.scss +++ b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.scss @@ -1,3 +1,7 @@ +app-overlay { + z-index: 100; +} + app-view-mode-bar { margin-left: auto; } diff --git a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts index 4696dfb..cecdf2c 100644 --- a/src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts +++ b/src/app/viewer/viewer/components/viewer-header/viewer-header.component.ts @@ -1,6 +1,5 @@ -import { Component, computed, effect, HostListener, inject, input, output, PLATFORM_ID, Signal, signal, ViewChild } from '@angular/core'; +import { Component, computed, effect, HostListener, inject, input, output, PLATFORM_ID, signal, ViewChild } from '@angular/core'; import { DomManipulationService } from '../../../../shared/data-access'; -import { CompositionEpisode } from '../../../../@site-modules/@common-read'; import { PlaylistItem } from '../../../../playlist/data-access/playlist.service'; import { LangService } from '../../../../shared/data-access/lang.service'; import { DialogComponent } from '../../../../shared/ui/dialog/dialog.component'; @@ -11,6 +10,7 @@ import { GamepadButton } from '../../../../shared/models'; import { FileService } from '../../../../file/data-access/file.service'; import { ViewerService } from '../../../services'; import { Router } from '@angular/router'; +import { ChtnkEpisode } from '../../../../shared/models/chtnk-composition'; @Component({ selector: 'app-viewer-header', @@ -37,14 +37,20 @@ export class ViewerHeaderComponent { playlistLink = input(""); currentPlaylistItem = input(); - episode = input({ + episode = input({ title: '', - images: [] + images: [], + captions: [] }) onToggle = output(); + episodeLangChange = output(); //#endregion + onEpisodeLangChange($event: any) { + this.episodeLangChange.emit($event.target?.value); + } + // #region 🧠 State (signals) isDialogOpen = signal(false); link = signal(''); @@ -115,4 +121,13 @@ export class ViewerHeaderComponent { this.domMan.setHotkeys(event, this.hotKeys) } //#endregion + + getCaptionLangs() { + const langs = [...new Set( + this.episode().captions?. + map(c => c.lang) + .filter(Boolean) ?? [] + )]; + return langs; + } } diff --git a/src/app/viewer/viewer/viewer.component.html b/src/app/viewer/viewer/viewer.component.html index 166b192..ea7c829 100644 --- a/src/app/viewer/viewer/viewer.component.html +++ b/src/app/viewer/viewer/viewer.component.html @@ -18,7 +18,7 @@ + [index]="i" [class]="mode" [dir]="dir" [showLoading]="true" [filename]="img.id" [captions]="getCaption(img.id)" [captionLang]="captionLang()" [frames]="getFrame(img.id)"/> } - 🏠 + 🏠 } @@ -47,7 +47,7 @@ @defer{ @if (episode()) { + [currentPlaylistItem]="currentPlaylistItem()" (onToggle)="viewerUi.isDialogOpen.set($event)" (episodeLangChange)="captionLang.set($event)"/> } } diff --git a/src/app/viewer/viewer/viewer.component.ts b/src/app/viewer/viewer/viewer.component.ts index 618c4f1..65e3b26 100644 --- a/src/app/viewer/viewer/viewer.component.ts +++ b/src/app/viewer/viewer/viewer.component.ts @@ -1,10 +1,10 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, HostListener, input, Signal, ViewChild, WritableSignal, computed, inject, signal, output } from '@angular/core'; -import { CompositionEpisode } from '../../@site-modules/@common-read'; import { LangService } from '../../shared/data-access/lang.service'; import { Playlist, PlaylistItem } from '../../playlist/data-access/playlist.service'; import { DOCUMENT } from '@angular/common'; import { EmbedFacade, GamepadFacade, KeyboardFacade, NsfwFacade, PageTrackingFacade, ReadlistFacade, ViewerScrollFacade, ViewerTitleTagFacade, ViewerUiFacade, ViewModeFacade } from '../facades'; import { DomManipulationService } from '../../shared/data-access'; +import { ChtnkCaption, ChtnkEpisode, ChtnkFrame } from '../../shared/models/chtnk-composition'; @Component({ selector: 'app-viewer', @@ -32,7 +32,7 @@ export class ViewerComponent implements AfterViewInit { private readonly dom = inject(DomManipulationService); private readonly document = inject(DOCUMENT); - episode = input({ title: '', images: [] }); + episode = input({ title: '', images: [] }); playlistLink = input(""); currentPlaylistItem = input(); playlistInput = input([]); @@ -104,4 +104,17 @@ export class ViewerComponent implements AfterViewInit { if (!this.dom.isInteractiveElement(event.target as HTMLElement)) this.viewerUi.toggleFullScreen(); } + // + // + // + + getCaption(filename: string | undefined): ChtnkCaption[] { + return this.episode().captions?.filter(caption => caption.id === filename) || []; + } + + getFrame(filename: string | undefined): ChtnkFrame[] { + return this.episode().frames?.filter(frame => frame.id === filename) || []; + } + + captionLang = signal('en'); } diff --git a/src/styles.scss b/src/styles.scss index 010e375..d658387 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -74,112 +74,6 @@ input { border-radius: .5ch; } -.button { - --q: 1; - - user-select: none; - border: 0; - // border: 1px solid #5299ce; - font: inherit; - color: inherit; - padding-inline: calc(var(--q) * 1ch); - padding-block: calc(var(--q) * 1ch); - border-radius: .5ch; - cursor: pointer; - background: hsl(203.44 8% 16%); - - @media (prefers-color-scheme: light) { - background: hsl(203.44 8% 96%); - } - - & { - transition: all var(--t); - text-wrap: balance; - -webkit-tap-highlight-color: transparent; - box-shadow: var(--shadow-1); - } - - &.small { - --q: 0.5 - } - - &.mediun { - --q: 1.5 - } - - &.large { - --q: 2 - } - - &.tshadow { - text-shadow: 0 0 0.25ch #000a; - } - - &.empty { - background: transparent; - box-shadow: unset; - } - - &.primary { - background: #166496; - color: white; - } - - &.border { - border: var(--border-size) solid var(--border-color); - } - - &.gamepad-hover, - &:hover { - color: #103651; - background-color: #ffd60a; - - @media (prefers-color-scheme: light) { - color: #f8d755; - background-color: #166496; - } - } - - &.delete { - &.gamepad-hover, - &:hover, - &:focus { - background-color: #541f20; - } - } - - &:active, - &.active { - background-color: rgba(0, 15, 30, 1); - box-shadow: unset; - transform: translate(2px, 2px); - } - - &.full { - width: 100%; - } - - &:disabled { - cursor: not-allowed; - // pointer-events: none; - filter: grayscale(.64); - box-shadow: unset; - background-color: rgba(0, 15, 30, .5); - - @media (prefers-color-scheme: light) { - background-color: rgba(0, 15, 30, .25); - opacity: .64; - } - } -} - -@supports (corner-shape: squircle) { - .button { - corner-shape: squircle; - border-radius: 1ch; - } -} - a { color: #4081b1; text-decoration: none; @@ -206,8 +100,8 @@ a[target="_blank"]:after { --ratio: 257/364; --accent: #ffd60a; - --avarage-l: 0.48; - --avarage-l-2: 0.36; + --avarage-l: 0.64; + --avarage-l-2: 0.48; @media (prefers-color-scheme: light) { --accent: #166496; @@ -235,48 +129,6 @@ a[target="_blank"]:after { } } -@supports (transition-behavior: allow-discrete) { - - dialog, - dialog::backdrop { - transition-property: opacity, transform, overlay, display, filter; - transition-duration: var(--t); - transition-behavior: allow-discrete; - opacity: 0; - transform: scale(calc(1 + var(--scale-diff-x, .1)), calc(1 + var(--scale-diff-y, .1))); - filter: blur(var(--blur)); - } - - dialog[open], - dialog[open]::backdrop { - opacity: 1; - transform: scale(1); - filter: blur(0); - } - - @starting-style { - - dialog[open], - dialog[open]::backdrop { - opacity: 0; - transform: scale(calc(1 + var(--scale-diff-x, .1)), calc(1 + var(--scale-diff-y, .1))); - filter: blur(var(--blur)); - } - } -} - -@media (prefers-reduced-motion: reduce) { - - dialog, - dialog::backdrop, - dialog[open], - dialog[open]::backdrop { - transition: none !important; - transform: none !important; - opacity: 1 !important; - filter: none !important; - } -} ::selection { background: var(--accent);

Скопіюйте JSON код, та опублікуйте його на Gist 📋💻🌐