rc-1
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import type { BackendResponse } from './BackendTypes'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__ENV__: {
|
||||
CDN: string
|
||||
API: string
|
||||
WEB: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const API_BASE = window.__ENV__.API
|
||||
export const CDN_BASE = window.__ENV__.CDN
|
||||
export const WEB_BASE = window.__ENV__.WEB
|
||||
|
||||
export function BackendDebounce(resp: BackendResponse<any>, iteration = 1) {
|
||||
const ratelimit = resp.status === 429 && resp.ratelimit.remains === 0
|
||||
return {
|
||||
ratelimit,
|
||||
sleep: new Promise<void>((next) => {
|
||||
if (ratelimit) {
|
||||
const sleep = resp.ratelimit.reset * 1000
|
||||
console.warn(`Ratelimited! sleeping for ${sleep}ms`)
|
||||
setTimeout(next, sleep)
|
||||
} else {
|
||||
setTimeout(next, iteration * 2000)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function BackendFetch<T>(path: string): Promise<BackendResponse<T>> {
|
||||
return new Promise((resolve) => {
|
||||
const data: BackendResponse<T> = {
|
||||
success: false,
|
||||
status: -1,
|
||||
error: '',
|
||||
ratelimit: { remains: 0, reset: 0, limit: 0 },
|
||||
text: '',
|
||||
json: null as T,
|
||||
}
|
||||
|
||||
fetch(API_BASE + path)
|
||||
.then(async (res) => {
|
||||
data.status = res.status
|
||||
data.success = res.ok
|
||||
data.ratelimit.limit = parseInt(res.headers.get('X-Ratelimit-Limit') || '0')
|
||||
data.ratelimit.reset = parseFloat(res.headers.get('X-Ratelimit-Reset') || '0')
|
||||
data.ratelimit.remains = parseInt(res.headers.get('X-Ratelimit-Remaining') || '0')
|
||||
|
||||
data.text = await res.text()
|
||||
data.json = (() => {
|
||||
try {
|
||||
return JSON.parse(data.text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
// Check for Backend Error
|
||||
if (!res.ok) {
|
||||
data.error = (data.json as any)?.message ?? `Request failed: ${res.status} ${res.statusText}`
|
||||
return
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Request failed due to a network error:', err)
|
||||
data.error = 'Network Unavailable'
|
||||
})
|
||||
.finally(() => resolve(data))
|
||||
})
|
||||
}
|
||||
|
||||
// export function BackendGET<T>(path: string): Promise<BackendResponse<T>> {
|
||||
// return BackendPOST(path, undefined, {})
|
||||
// }
|
||||
|
||||
// export function BackendPOST<T>(path: string, body: any, headers: Record<string, string>): Promise<BackendResponse<T>> {
|
||||
// return new Promise((resolve) => {
|
||||
// let requestHeaders = new Headers()
|
||||
// let requestMethod = 'GET'
|
||||
|
||||
// Object.entries(headers).forEach(([k, v]) => requestHeaders.set(k, v))
|
||||
// if (body) {
|
||||
// if (!(body instanceof FormData)) {
|
||||
// requestHeaders.set('Content-Type', 'application/json')
|
||||
// body = JSON.stringify(body)
|
||||
// }
|
||||
// requestMethod = 'POST'
|
||||
// }
|
||||
|
||||
// const data: BackendResponse<T> = {
|
||||
// success: false,
|
||||
// status: -1,
|
||||
// error: '',
|
||||
// ratelimit: { remains: 0, reset: 0, limit: 0 },
|
||||
// text: '',
|
||||
// json: null as T,
|
||||
// }
|
||||
|
||||
// fetch(API_BASE + path, {
|
||||
// method: requestMethod,
|
||||
// headers: requestHeaders,
|
||||
// body: body,
|
||||
// })
|
||||
// .then(async (res) => {
|
||||
// data.status = res.status
|
||||
// data.success = res.ok
|
||||
// data.ratelimit.limit = parseInt(res.headers.get('X-Ratelimit-Limit') || '0')
|
||||
// data.ratelimit.reset = parseFloat(res.headers.get('X-Ratelimit-Reset') || '0')
|
||||
// data.ratelimit.remains = parseInt(res.headers.get('X-Ratelimit-Remaining') || '0')
|
||||
|
||||
// data.text = await res.text()
|
||||
// data.json = (() => {
|
||||
// try {
|
||||
// return JSON.parse(data.text)
|
||||
// } catch {
|
||||
// return null
|
||||
// }
|
||||
// })()
|
||||
|
||||
// // Check for Backend Error
|
||||
// if (!res.ok) {
|
||||
// const debugID = res.headers.get('X-Debug-ID')
|
||||
// const message = (data.json as any)?.message ?? `Request failed: ${res.status} ${res.statusText}`
|
||||
|
||||
// data.error = `${message} ${debugID ? `(${debugID})` : ''}`
|
||||
// return
|
||||
// }
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error('Request failed due to a network error:', err)
|
||||
// data.error = 'Network Unavailable'
|
||||
// })
|
||||
// .finally(() => resolve(data))
|
||||
// })
|
||||
// }
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
export interface BackendResponse<T> {
|
||||
success: boolean
|
||||
status: number
|
||||
error: string
|
||||
ratelimit: {
|
||||
remains: number
|
||||
limit: number
|
||||
reset: number
|
||||
}
|
||||
text: string
|
||||
json: T
|
||||
}
|
||||
|
||||
export interface BackendLimitReportOption {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface BackendLimitType<T = undefined> {
|
||||
normalizers: {
|
||||
match: string
|
||||
replace: string
|
||||
comment: string
|
||||
}[]
|
||||
matcher: string
|
||||
max_length: number
|
||||
min_length: number
|
||||
values: T
|
||||
}
|
||||
|
||||
export interface BackendLimit {
|
||||
upload: {
|
||||
input_width_min: number
|
||||
input_height_min: number
|
||||
video_width_max: number
|
||||
video_height_max: number
|
||||
image_width_max: number
|
||||
image_height_max: number
|
||||
duration: number
|
||||
filesize: number
|
||||
mime_types: string[]
|
||||
}
|
||||
title: BackendLimitType
|
||||
tag: BackendLimitType
|
||||
comment: BackendLimitType
|
||||
report: BackendLimitType<BackendLimitReportOption>
|
||||
}
|
||||
|
||||
export interface BackendChallenge {
|
||||
nonce: string
|
||||
difficulty: number
|
||||
/** UNIX Timestamp */
|
||||
expires: number
|
||||
}
|
||||
|
||||
export interface BackendTag {
|
||||
id: string
|
||||
label: string
|
||||
usage: number
|
||||
}
|
||||
|
||||
export interface BackendArt {
|
||||
id: string
|
||||
created: string
|
||||
sticker: boolean
|
||||
audio: boolean
|
||||
framerate: number
|
||||
width: number
|
||||
height: number
|
||||
/** Float 0-1 */
|
||||
rating: number
|
||||
title: string
|
||||
tags: BackendTag[]
|
||||
}
|
||||
|
||||
export type UploadEventType =
|
||||
| UploadEventID
|
||||
| UploadEventError
|
||||
| UploadEventStep
|
||||
| UploadEventProgress
|
||||
| UploadEventFinish
|
||||
|
||||
interface UploadEventID {
|
||||
name: 'id'
|
||||
data: string
|
||||
}
|
||||
|
||||
interface UploadEventError {
|
||||
name: 'error'
|
||||
data: {
|
||||
code: number
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
interface UploadEventStep {
|
||||
name: 'step'
|
||||
data: {
|
||||
id: 'PROBE_QUEUE' | 'PROBE_START' | 'ENCODE_QUEUE' | 'ENCODE_START' | 'SERVER_FINALIZE'
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
interface UploadEventProgress {
|
||||
name: 'progress'
|
||||
data: {
|
||||
/** float string (e.g. 45.12) */
|
||||
percent: string
|
||||
}
|
||||
}
|
||||
|
||||
interface UploadEventFinish {
|
||||
name: 'finish'
|
||||
data: {
|
||||
id: string
|
||||
edit_token: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export const ScrollContext = createContext<HTMLElement | null>(null)
|
||||
export const useScrollRoot = () => useContext(ScrollContext)
|
||||
|
||||
// delete all the toastz
|
||||
export function toastNuke() {
|
||||
document.querySelectorAll('.layout-tooltip').forEach((e) => e.remove())
|
||||
}
|
||||
window.addEventListener('popstate', toastNuke)
|
||||
|
||||
// janky tooltips which are evil!1!
|
||||
export function toast(anchorId: string, message: string) {
|
||||
const anchor = document.getElementById(anchorId)
|
||||
if (!anchor) return
|
||||
|
||||
const dialog = anchor.closest('dialog') ?? document.body
|
||||
const parent = dialog.getBoundingClientRect()
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const elem = document.createElement('span')
|
||||
|
||||
// monospace font 4 da win
|
||||
const tooltipWidth = message.length * 8
|
||||
const clampedLeft = Math.max(
|
||||
tooltipWidth / 2 + 8,
|
||||
Math.min(rect.left + rect.width / 2, window.innerWidth - tooltipWidth / 2 - 8),
|
||||
)
|
||||
|
||||
elem.classList.add('layout-tooltip', 'animation-scroll-in')
|
||||
elem.textContent = message
|
||||
elem.style.cssText = `
|
||||
position: absolute;
|
||||
left: ${clampedLeft - parent.left}px;
|
||||
top: ${rect.top - parent.top - 8}px;
|
||||
transform: translateX(-50%) translateY(-100%);
|
||||
`
|
||||
|
||||
toastNuke()
|
||||
dialog.appendChild(elem)
|
||||
setTimeout(() => elem.remove(), 2000)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const validTagMatcher = new RegExp(/^[\p{L}\p{N}_]+$/u)
|
||||
|
||||
// Format user input for api requests, returns an empty string if invalid
|
||||
export function formatTagInput(str: string): string | false {
|
||||
const normal = str.trim().toLowerCase().replaceAll(/\s\s+/g, ' ').replaceAll(' ', '_')
|
||||
|
||||
if (!validTagMatcher.test(normal)) {
|
||||
return false
|
||||
}
|
||||
return normal
|
||||
}
|
||||
|
||||
// Format tag label for display in html
|
||||
export function formatTagTextContent(str: string): string {
|
||||
return str.trim().toUpperCase().replace('_', ' ')
|
||||
}
|
||||
|
||||
// Format tag usage for display in html
|
||||
export function formatTagUsage(num: number): string {
|
||||
return num.toLocaleString()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type MouseEvent } from 'react'
|
||||
|
||||
export const KEY_PREFIX_UPLOAD = 'upload_'
|
||||
|
||||
let route_index = 0
|
||||
let route_state: { path: string; with: any; keep: any }[] = [{ path: getWindowPath(), with: null, keep: null }]
|
||||
|
||||
window.addEventListener('popstate', (e) => {
|
||||
if (e.state?.route_index !== undefined) {
|
||||
route_index = e.state.route_index
|
||||
}
|
||||
})
|
||||
|
||||
export function getWindowPath() {
|
||||
return window.location.href.slice(window.location.origin.length)
|
||||
}
|
||||
|
||||
export function setTitle(title: string) {
|
||||
document.title = `${title} • gifuu`
|
||||
}
|
||||
|
||||
export function routeTo(path: string, withData?: any, keepData?: any) {
|
||||
route_state[route_index].keep = keepData ?? null
|
||||
route_state.splice(route_index + 1)
|
||||
route_state.push({ path, with: withData, keep: null })
|
||||
route_index++
|
||||
window.history.pushState({ route_index }, '', path)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
export function routeIntercept(event: MouseEvent<HTMLAnchorElement>, withData?: any, keepData?: any) {
|
||||
if (event.currentTarget instanceof HTMLAnchorElement) {
|
||||
event.preventDefault()
|
||||
routeTo(event.currentTarget.getAttribute('href') ?? '/', withData, keepData)
|
||||
}
|
||||
}
|
||||
|
||||
export function routeBack() {
|
||||
if (route_index <= 0) {
|
||||
routeTo('/')
|
||||
return
|
||||
}
|
||||
route_index--
|
||||
const entry = route_state[route_index]
|
||||
window.history.pushState({ route_index }, '', entry.path)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
export function routeBackURI() {
|
||||
return route_state[route_index - 1]?.path ?? '/'
|
||||
}
|
||||
|
||||
export function stateRecover<T>(): T | undefined {
|
||||
return (route_state[route_index]?.with ?? route_state[route_index]?.keep) as T
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: {
|
||||
track: (name: string, data?: Record<string, any>) => Promise<void>
|
||||
identify: (id: string, data?: Record<string, any>) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function wtEvent(eventName: string, data?: Record<string, any>) {
|
||||
console.log('[WATCHTOWER] Event:', eventName, data)
|
||||
window.umami?.track(eventName, data).catch((error) => {
|
||||
console.error('[WatchTower] Event Failed:', { eventName, data }, error)
|
||||
})
|
||||
}
|
||||
|
||||
// // Do not use, this goes against the privacy policy!
|
||||
// export function wtIdentify(who: string | number, data?: object) {
|
||||
// console.log('[WATCHTOWER] Identify:', who, data)
|
||||
// window.umami?.identify(String(who), data).catch((error) => {
|
||||
// console.error('[WatchTower] Identify Failed:', { who, data }, error)
|
||||
// })
|
||||
// }
|
||||
|
||||
// Not yet in service!
|
||||
// if (import.meta.env.PROD) {
|
||||
// const script = document.createElement('script')
|
||||
// script.defer = true
|
||||
// script.src = 'https://watchtower.pancakz.net/script.js'
|
||||
// script.dataset.websiteId = '...'
|
||||
// document.body.appendChild(script)
|
||||
// }
|
||||
Reference in New Issue
Block a user