Custom loadanimations for custom components

This commit is contained in:
2025-09-04 18:44:46 +02:00
parent 22e3ec7929
commit ec694279e0
6 changed files with 2142 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import { Button, CircularProgress } from '@mui/joy'
import { Button } from '@mui/joy'
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
import { MouseEventHandler, ReactNode, useState } from 'react'
@@ -21,7 +21,7 @@ export default function TButton(props: TButtonProps) {
disabled={props.disabled ?? TDisabled(state)}
aria-disabled={props.disabled ?? TDisabled(state)}
onClick={clicked}
startDecorator={TDisabled(state) ? <CircularProgress /> : null}
className={'t-loadable'}
>
{props.children}
</Button>

View File

@@ -1,7 +1,8 @@
import { Button, CircularProgress, Input } from '@mui/joy'
import { Button, Input } from '@mui/joy'
import { MouseEventHandler, useEffect, useState } from 'react'
import * as React from 'react'
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
import './loadingBorder.css'
export default function TInput(props: TInputProps) {
const [state, setState] = useState<TState>(TState.clean)
@@ -63,7 +64,7 @@ export default function TInput(props: TInputProps) {
value={value}
onChange={inputValueChanged}
onKeyDown={keyDownHandler}
startDecorator={TDisabled(state) ? <CircularProgress /> : null}
className={'t-loadable'}
endDecorator={
props.submitButtonHidden ? null : (
<Button

View File

@@ -0,0 +1,56 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
@keyframes rotate {
100% {
transform: rotate(1turn);
}
}
.t-loadable {
position: relative;
border: none;
border-radius: 4px;
overflow: hidden;
&::before {
content: '';
position: absolute !important;
z-index: -2 !important;
transform-origin: 50% 50%;
left: -100vw !important;
top: -100vh !important;
width: 200vw !important;
height: 200vh !important;
background-repeat: no-repeat !important;
background-image: linear-gradient(
var(--joy-palette-primary-200),
var(--joy-palette-primary-200)
);
animation: rotate 4s linear infinite;
}
&::after {
content: '';
position: absolute;
z-index: -1;
left: 2px;
top: 2px;
width: calc(100% - 4px);
height: calc(100% - 4px);
background: black;
border-radius: 3px;
}
}
.t-loadable[aria-disabled='true'] {
&::before {
background-image: linear-gradient(
var(--joy-palette-danger-300),
var(--joy-palette-success-600)
);
}
}

1411
tranga-website/src/api/V2.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,405 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export enum WorkerExecutionState {
Failed = 'Failed',
Cancelled = 'Cancelled',
Created = 'Created',
Waiting = 'Waiting',
Running = 'Running',
Completed = 'Completed',
}
export enum RequestType {
Default = 'Default',
MangaDexFeed = 'MangaDexFeed',
MangaImage = 'MangaImage',
MangaCover = 'MangaCover',
MangaDexImage = 'MangaDexImage',
MangaInfo = 'MangaInfo',
}
export enum MangaReleaseStatus {
Continuing = 'Continuing',
Completed = 'Completed',
OnHiatus = 'OnHiatus',
Cancelled = 'Cancelled',
Unreleased = 'Unreleased',
}
export enum LibraryType {
Komga = 'Komga',
Kavita = 'Kavita',
}
/** API.Schema.MangaContext.AltTitle DTO */
export interface AltTitle {
/**
* Language of the Title
* @minLength 1
*/
language: string
/**
* Title
* @minLength 1
*/
title: string
}
/** The API.Schema.MangaContext.Author DTO */
export interface Author {
/**
* Name of the Author.
* @minLength 1
*/
name: string
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
/** API.Schema.MangaContext.Chapter DTO */
export interface Chapter {
/**
* Identifier of the Manga this Chapter belongs to
* @minLength 1
*/
mangaId: string
/**
* Volume number
* @format int32
*/
volume: number
/**
* Chapter number
* @minLength 1
*/
chapterNumber: string
/**
* Title of the Chapter
* @minLength 1
*/
title: string
/** Whether Chapter is Downloaded (on disk) */
downloaded: boolean
/** Ids of the Manga on MangaConnectors */
mangaConnectorIds: MangaConnectorId[]
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
export interface FileLibrary {
/**
* @minLength 0
* @maxLength 256
*/
basePath: string
/**
* @minLength 0
* @maxLength 512
*/
libraryName: string
/**
* @minLength 16
* @maxLength 64
*/
key: string
}
export interface GotifyRecord {
name?: string | null
endpoint?: string | null
appToken?: string | null
/** @format int32 */
priority?: number
}
export interface LibraryConnector {
libraryType: LibraryType
/**
* @format uri
* @minLength 0
* @maxLength 256
*/
baseUrl: string
/**
* @minLength 0
* @maxLength 256
*/
auth: string
/**
* @minLength 16
* @maxLength 64
*/
key: string
}
/** API.Schema.MangaContext.Link DTO */
export interface Link {
/**
* Name of the Provider
* @minLength 1
*/
provider: string
/**
* Url
* @minLength 1
*/
url: string
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
/** API.Schema.MangaContext.Manga DTO */
export interface Manga {
/**
* Chapter cutoff for Downloads (Chapters before this will not be downloaded)
* @format float
*/
ignoreChaptersBefore: number
/**
* Release Year
* @format int32
*/
year?: number | null
/** Release Language */
originalLanguage?: string | null
/** Keys of ChapterDTOs */
chapterIds: string[]
/** Author-names */
authors: Author[]
/** Manga Tags */
tags: string[]
/** Links for more Metadata */
links: Link[]
/** Alt Titles of Manga */
altTitles: AltTitle[]
/**
* Name of the Manga
* @minLength 1
*/
name: string
/**
* Description of the Manga
* @minLength 1
*/
description: string
releaseStatus: MangaReleaseStatus
/** Ids of the Manga on MangaConnectors */
mangaConnectorIds: MangaConnectorId[]
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
export interface MangaConnector {
name?: string | null
/** Whether Connector is used for Searches and Downloads */
enabled: boolean
/** Languages supported by the Connector */
supportedLanguages: string[]
/**
* Url of the Website Icon
* @minLength 1
*/
iconUrl: string
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
/** API.Schema.MangaContext.MangaConnectorId`1 DTO */
export interface MangaConnectorId {
/**
* Name of the Connector
* @minLength 1
*/
mangaConnectorName: string
/**
* Key of the referenced DTO
* @minLength 1
*/
foreignKey: string
/** Website Link for reference, if any */
websiteUrl?: string | null
/** Whether this Link is used for downloads */
useForDownload: boolean
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
export interface MetadataEntry {
mangaId?: string | null
metadataFetcherName?: string | null
identifier?: string | null
}
export interface MetadataSearchResult {
identifier?: string | null
name?: string | null
url?: string | null
description?: string | null
coverUrl?: string | null
}
/** Shortened Version of API.Controllers.DTOs.Manga */
export interface MinimalManga {
/**
* Name of the Manga
* @minLength 1
*/
name: string
/**
* Description of the Manga
* @minLength 1
*/
description: string
releaseStatus: MangaReleaseStatus
/** Ids of the Manga on MangaConnectors */
mangaConnectorIds: MangaConnectorId[]
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}
export interface NotificationConnector {
/**
* @minLength 0
* @maxLength 64
*/
name: string
/**
* @format uri
* @minLength 0
* @maxLength 2048
*/
url: string
headers: Record<string, string>
/**
* @minLength 0
* @maxLength 8
*/
httpMethod: string
/**
* @minLength 0
* @maxLength 4096
*/
body: string
}
export interface NtfyRecord {
name?: string | null
endpoint?: string | null
username?: string | null
password?: string | null
topic?: string | null
/** @format int32 */
priority?: number
}
export interface ProblemDetails {
type?: string | null
title?: string | null
/** @format int32 */
status?: number | null
detail?: string | null
instance?: string | null
[key: string]: any
}
export interface PushoverRecord {
name?: string | null
appToken?: string | null
user?: string | null
}
export interface TrangaSettings {
downloadLocation?: string | null
userAgent?: string | null
/** @format int32 */
imageCompression?: number
blackWhiteImages?: boolean
flareSolverrUrl?: string | null
/**
* Placeholders:
* %M Obj Name
* %V Volume
* %C Chapter
* %T Title
* %A Author (first in list)
* %I Chapter Internal ID
* %i Obj Internal ID
* %Y Year (Obj)
*
* ?_(...) replace _ with a value from above:
* Everything inside the braces will only be added if the value of %_ is not null
*/
chapterNamingScheme?: string | null
/** @format int32 */
workCycleTimeoutMs?: number
requestLimits?: {
/** @format int32 */
Default?: number
/** @format int32 */
MangaDexFeed?: number
/** @format int32 */
MangaImage?: number
/** @format int32 */
MangaCover?: number
/** @format int32 */
MangaDexImage?: number
/** @format int32 */
MangaInfo?: number
} | null
downloadLanguage?: string | null
}
/** API.Workers.BaseWorker DTO */
export interface Worker {
/** Workers this worker depends on having ran. */
dependencies: string[]
/** Workers that have not yet ran, that need to run for this Worker to run. */
missingDependencies: string[]
/** Worker can run. */
dependenciesFulfilled: boolean
state: WorkerExecutionState
/**
* Unique Identifier of the DTO
* @minLength 16
* @maxLength 64
*/
key: string
}

View File

@@ -0,0 +1,265 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export type QueryParamsType = Record<string | number, any>
export type ResponseFormat = keyof Omit<Body, 'body' | 'bodyUsed'>
export interface FullRequestParams extends Omit<RequestInit, 'body'> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean
/** request path */
path: string
/** content type of request body */
type?: ContentType
/** query params */
query?: QueryParamsType
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat
/** request body */
body?: unknown
/** base url */
baseUrl?: string
/** request cancellation token */
cancelToken?: CancelToken
}
export type RequestParams = Omit<
FullRequestParams,
'body' | 'method' | 'query' | 'path'
>
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string
baseApiParams?: Omit<RequestParams, 'baseUrl' | 'cancelToken' | 'signal'>
securityWorker?: (
securityData: SecurityDataType | null
) => Promise<RequestParams | void> | RequestParams | void
customFetch?: typeof fetch
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
extends Response {
data: D
error: E
}
type CancelToken = Symbol | string | number
export enum ContentType {
Json = 'application/json',
JsonApi = 'application/vnd.api+json',
FormData = 'multipart/form-data',
UrlEncoded = 'application/x-www-form-urlencoded',
Text = 'text/plain',
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = ''
private securityData: SecurityDataType | null = null
private securityWorker?: ApiConfig<SecurityDataType>['securityWorker']
private abortControllers = new Map<CancelToken, AbortController>()
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
fetch(...fetchParams)
private baseApiParams: RequestParams = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
referrerPolicy: 'no-referrer',
}
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig)
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data
}
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key)
return `${encodedKey}=${encodeURIComponent(typeof value === 'number' ? value : `${value}`)}`
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key])
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key]
return value.map((v: any) => this.encodeQueryParam(key, v)).join('&')
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {}
const keys = Object.keys(query).filter(
(key) => 'undefined' !== typeof query[key]
)
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key)
)
.join('&')
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery)
return queryString ? `?${queryString}` : ''
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null &&
(typeof input === 'object' || typeof input === 'string')
? JSON.stringify(input)
: input,
[ContentType.JsonApi]: (input: any) =>
input !== null &&
(typeof input === 'object' || typeof input === 'string')
? JSON.stringify(input)
: input,
[ContentType.Text]: (input: any) =>
input !== null && typeof input !== 'string'
? JSON.stringify(input)
: input,
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key]
formData.append(
key,
property instanceof Blob
? property
: typeof property === 'object' && property !== null
? JSON.stringify(property)
: `${property}`
)
return formData
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
}
protected mergeRequestParams(
params1: RequestParams,
params2?: RequestParams
): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
}
}
protected createAbortSignal = (
cancelToken: CancelToken
): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken)
if (abortController) {
return abortController.signal
}
return void 0
}
const abortController = new AbortController()
this.abortControllers.set(cancelToken, abortController)
return abortController.signal
}
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken)
if (abortController) {
abortController.abort()
this.abortControllers.delete(cancelToken)
}
}
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === 'boolean'
? secure
: this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{}
const requestParams = this.mergeRequestParams(params, secureParams)
const queryString = query && this.toQueryString(query)
const payloadFormatter =
this.contentFormatters[type || ContentType.Json]
const responseFormat = format || requestParams.format
return this.customFetch(
`${baseUrl || this.baseUrl || ''}${path}${queryString ? `?${queryString}` : ''}`,
{
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData
? { 'Content-Type': type }
: {}),
},
signal:
(cancelToken
? this.createAbortSignal(cancelToken)
: requestParams.signal) || null,
body:
typeof body === 'undefined' || body === null
? null
: payloadFormatter(body),
}
).then(async (response) => {
const r = response.clone() as HttpResponse<T, E>
r.data = null as unknown as T
r.error = null as unknown as E
const data = !responseFormat
? r
: await response[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data
} else {
r.error = data
}
return r
})
.catch((e) => {
r.error = e
return r
})
if (cancelToken) {
this.abortControllers.delete(cancelToken)
}
if (!response.ok) throw data
return data
})
}
}