diff --git a/ngsw-config.json b/ngsw-config.json index 78e64d2..617ce16 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -11,6 +11,7 @@ "/index.html", "/index.csr.html", "/manifest.webmanifest", + "/manifest-uk.webmanifest", "/*.css", "/*.js" ] 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 c1a776a..4dcaff7 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 @@ -1,7 +1,7 @@ -@if(episode$ | async; as episode){ +@if(episode$() | async; as episode){ -@if (!(loading$ | async)) { -
@@ -10,20 +10,21 @@ } -@if(error$ | async; as error){ +@if(error$() | async; as error){

🀨

{{ error }}

🏠 - +
} -@if(loading$ | async; as loading){ +@if(loading$() | async; as loading){ } @else { 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 ccd0a75..e91602f 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,27 +1,27 @@ -import { Component, EventEmitter, Input, Output, inject, input } from '@angular/core'; +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, PlaylistService } from '../../../../playlist/data-access/playlist.service'; +import { Playlist, PlaylistItem } from '../../../../playlist/data-access/playlist.service'; @Component({ - selector: 'app-common-read', - templateUrl: './common-read.component.html', - styleUrl: './common-read.component.scss', - standalone: false + selector: 'app-common-read', + templateUrl: './common-read.component.html', + styleUrl: './common-read.component.scss', + standalone: false }) export class CommonReadComponent { public lang: LangService = inject(LangService); - @Input({ required: true }) error$!: BehaviorSubject; - @Input({ required: true }) loading$!: BehaviorSubject; - @Input({ required: true }) episode$!: Observable; + error$ = input.required>(); + loading$ = input.required>(); + episode$ = input.required>(); - @Input() playlist: Playlist = []; - @Input() playlistLink: string = ""; - @Input() currentPlaylistItem: PlaylistItem | undefined; + playlist = input([]); + playlistLink = input(""); + currentPlaylistItem = input(); - @Output() refreshData: EventEmitter = new EventEmitter(); + refreshData = output(); onRefreshData() { this.refreshData.emit(); diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.html b/src/app/link-parser/ui/parser-form/parser-form.component.html index db5c11d..7c61aee 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.html +++ b/src/app/link-parser/ui/parser-form/parser-form.component.html @@ -1,16 +1,23 @@ +@let online = net.online(); +@let openFileLabel = (online) ? 'πŸ“ƒ '+lang.ph().orOpenFile: 'πŸ“ƒ '+lang.ph().openFile; +
+ @if (online === false ) { +
+ 🚫 No internet connection +
+ }

- -
- -
- - - + @if (online) { +
+ +
+ } + + +
@@ -28,7 +35,8 @@

}

@if (linkParams()) { - + {{lang.ph().letsgo}} {{linkParams()?.id | truncate}} diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.scss b/src/app/link-parser/ui/parser-form/parser-form.component.scss index 34ddaea..3d67cda 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 @@ -158,4 +158,22 @@ input[type=url]::placeholder { &:active { box-shadow: 0 0 transparent; } +} + +.offline-banner { + border-radius: .5ch; + --c: oklch(0.553 0.2211 20.3); + --shadow-color: oklch(from var(--c) var(--avarage-l-2) c h); + --shadow-2: 2px 2px var(--surface), var(--shadow-distance-2) var(--shadow-color); + + font-family: 'Courier New', Courier, monospace; + --gl: radial-gradient(circle 1px at 0px 0px, oklch(from currentColor l c h /.5) 1px, transparent 0); + --bg-1: var(--gl) 0px 0px / 8px 8px; + background: var(--bg-1); + box-shadow: var(--shadow-1); + border: var(--border-size) solid var(--border-color); + display: inline-block; + padding: 1ch; + font-weight: bold; + text-align: center; } \ 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 0e337f0..9a9a1e1 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 @@ -1,13 +1,12 @@ -import { AfterContentInit, AfterViewInit, ChangeDetectionStrategy, Component, computed, inject, PLATFORM_ID, signal, Signal, WritableSignal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, PLATFORM_ID, signal, Signal, WritableSignal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { LangService } from '../../../shared/data-access/lang.service'; import { Base64 } from '../../../shared/utils'; import { LinkParserSettingsService } from '../../data-access/link-parser-settings.service'; import { LinkParserService } from '../../data-access/link-parser.service'; -import { ImgurLinkParser, MangadexLinkParser, TelegraphLinkParser, RedditLinkParser, ZenkoLinkParser, NhentaiLinkParser, YandereParser, PixivLinkParser, BlankaryLinkParser, JsonLinkParser } from '../../utils'; -import { ComickLinkParser } from '../../utils/comick-link-parser'; +import { ImgurLinkParser, MangadexLinkParser, TelegraphLinkParser, RedditLinkParser, ZenkoLinkParser, NhentaiLinkParser, YandereParser, PixivLinkParser, JsonLinkParser } from '../../utils'; import { ImgchestLinkParser } from '../../utils/imgchest-link-parser'; -import { MetaTagsService } from '../../../shared/data-access/meta-tags.service'; +import { NetworkService, BrowserService } from '../../../shared/data-access/'; @Component({ selector: 'app-parser-form', @@ -20,6 +19,8 @@ export class ParserFormComponent { private router: Router = inject(Router); private route: ActivatedRoute = inject(ActivatedRoute); setts = inject(LinkParserSettingsService) + net = inject(NetworkService); + browser = inject(BrowserService); platformId = inject(PLATFORM_ID) link: WritableSignal = signal(''); linkParams: Signal = computed(() => this.parser.parse(this.link())); @@ -31,6 +32,8 @@ export class ParserFormComponent { }; }); + osAcceptSupport = signal(["Windows", "Android", "Linux"].includes(this.browser.brouserInfo().os)); + constructor(public parser: LinkParserService, public lang: LangService) { this.initParser(); } diff --git a/src/app/shared/data-access/browser.service.ts b/src/app/shared/data-access/browser.service.ts new file mode 100644 index 0000000..60f4ff4 --- /dev/null +++ b/src/app/shared/data-access/browser.service.ts @@ -0,0 +1,59 @@ +import { inject, Injectable, PLATFORM_ID, signal, } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; + +export interface BrowserInfo { + name: string; + os: string; + isMobile: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class BrowserService { + private platformId = inject(PLATFORM_ID) + + brouserInfo = signal(this.detectBrowser()); + + constructor() { } + + detectBrowser(): BrowserInfo { + if (isPlatformServer(this.platformId)) return { + name: 'Unknown', + os: 'Unknown', + isMobile: false + }; + + const ua = navigator.userAgent; + const platform = navigator.platform.toLowerCase(); + + const isMobile = /Mobi|Android|iPhone|iPad|iPod|webOS|BlackBerry/i.test(ua); + + let name = 'Unknown'; + + if (/Chrome|Chromium/i.test(ua) && !/Edg|OPR/i.test(ua)) { + name = 'Chromium'; + } else if (/Edg/i.test(ua)) { + name = 'Edge'; + } else if (/Firefox/i.test(ua)) { + name = 'Firefox'; + } else if (/Safari/i.test(ua) && !/Chrome|Chromium|OPR|Edg/i.test(ua)) { + name = 'Safari'; + } else if (/OPR|Opera/i.test(ua)) { + name = 'Opera'; + } + + let os = 'Unknown'; + if (/Win/i.test(platform)) os = 'Windows'; + else if (/Mac/i.test(platform)) os = 'Mac'; + else if (/Linux/i.test(platform)) os = 'Linux'; + else if (/iPhone|iPad|iPod/i.test(ua)) os = 'iOS'; + else if (/Android/i.test(ua)) os = 'Android'; + + return { + name, + os, + isMobile + }; + } +} diff --git a/src/app/shared/data-access/index.ts b/src/app/shared/data-access/index.ts index 78cd1a4..15f6b01 100644 --- a/src/app/shared/data-access/index.ts +++ b/src/app/shared/data-access/index.ts @@ -1,2 +1,6 @@ export * from './dom-manipulation.service' -export * from './viewer.service' \ No newline at end of file +export * from './viewer.service' +export * from './gamepad.service' +export * from './browser.service' +export * from './lang.service' +export * from './network.service' \ No newline at end of file diff --git a/src/app/shared/data-access/network.service.ts b/src/app/shared/data-access/network.service.ts new file mode 100644 index 0000000..445d269 --- /dev/null +++ b/src/app/shared/data-access/network.service.ts @@ -0,0 +1,62 @@ +import { isPlatformServer } from '@angular/common'; +import { computed, inject, Injectable, PLATFORM_ID, signal } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class NetworkService { + private platformId = inject(PLATFORM_ID) + + private _online = signal(navigator.onLine); + private _verified = signal(null); + + readonly online = computed(() => this._online()); + readonly verified = computed(() => this._verified()); + readonly isReallyOnline = computed(() => { + const verified = this._verified(); + return verified === null ? this._online() : verified; + }); + + constructor() { + if (isPlatformServer(this.platformId)) return; + + window.addEventListener('online', () => { + this._online.set(true); + this.verify(); + }); + + window.addEventListener('offline', () => { + this._online.set(false); + this._verified.set(false); + }); + + this.verify(); + + setInterval(() => this.verify(), 30000); + } + + async verify(timeout = 3000): Promise { + if (!navigator.onLine) { + this._verified.set(false); + return false; + } + + try { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + + await fetch('/favicon.ico', { + method: 'HEAD', + cache: 'no-store', + signal: controller.signal, + }); + + clearTimeout(id); + this._verified.set(true); + return true; + } catch { + this._verified.set(false); + return false; + } + } +} diff --git a/src/app/shared/directives/vibrate-haptic.directive.ts b/src/app/shared/directives/vibrate-haptic.directive.ts index 57eaf74..bd57ce2 100644 --- a/src/app/shared/directives/vibrate-haptic.directive.ts +++ b/src/app/shared/directives/vibrate-haptic.directive.ts @@ -1,4 +1,4 @@ -import { Directive, HostListener, inject, Input } from '@angular/core'; +import { Directive, HostListener, inject, input } from '@angular/core'; import { VibrationService } from '../data-access/vibration.service'; @Directive({ @@ -8,11 +8,11 @@ import { VibrationService } from '../data-access/vibration.service'; export class VibrateHapticDirective { vibration = inject(VibrationService); - @Input() vibrateHaptic: number | number[] = 10; + vibrateHaptic = input(10); - @Input() vibrateTouch: boolean = false; - @Input() vibrateClick: boolean = true; - @Input() vibrateInput: boolean = false; + vibrateTouch = input(false); + vibrateClick = input(true); + vibrateInput = input(false); constructor() { } @@ -20,28 +20,28 @@ export class VibrateHapticDirective { onPointerDown() { if (!this.vibrateTouch) return; - this.vibration.vibrate(this.vibrateHaptic); + this.vibration.vibrate(this.vibrateHaptic()); } @HostListener('touchstart') onTouchStart() { if (!this.vibrateTouch) return; - this.vibration.vibrate(this.vibrateHaptic); + this.vibration.vibrate(this.vibrateHaptic()); } @HostListener('click') onClick() { if (!this.vibrateClick) return; - this.vibration.vibrate(this.vibrateHaptic); + this.vibration.vibrate(this.vibrateHaptic()); } @HostListener('input') onInput() { if (!this.vibrateInput) return; - this.vibration.vibrate(this.vibrateHaptic); + this.vibration.vibrate(this.vibrateHaptic()); } } diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html index 88653d0..af75abe 100644 --- a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html @@ -1,3 +1,5 @@ +@let online = net.isReallyOnline(); +
@@ -9,23 +11,17 @@ - - -
    + @if(online) { @for (tag of siteTags(); track $index) { - @let c = getRandomCoordinates(); - - @let s = 1; -
  • +
  • {{formatTagname(tag)}}
  • } + } @for (tag of fileTags(); track $index) { - @let c = getRandomCoordinates(); - - @let s = 1; -
  • + +
  • {{formatTagname(tag)}}
  • }
diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts index 5adf6e3..9e5a448 100644 --- a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; import { environment } from '../../../../environments/environment'; +import { NetworkService } from '../../data-access/network.service'; type Coordinate = { x: number; y: number }; @@ -12,6 +13,8 @@ type Coordinate = { x: number; y: number }; changeDetection: ChangeDetectionStrategy.OnPush }) export class ChytankaLogoWithTagsComponent { + net = inject(NetworkService); + version = environment.version; fileTags = input([]) siteTags = input([]) diff --git a/src/app/shared/ui/dialog/dialog.component.html b/src/app/shared/ui/dialog/dialog.component.html index e14f85f..4e5f8f0 100644 --- a/src/app/shared/ui/dialog/dialog.component.html +++ b/src/app/shared/ui/dialog/dialog.component.html @@ -3,8 +3,8 @@
-

{{title}}

- @if (closeHeaderButton) { +

{{title()}}

+ @if (closeHeaderButton()) {
} @if (domMan.fullscreenEnabled()) {