This commit is contained in:
2026-05-23 17:17:56 -07:00
commit 448f2e33ef
135 changed files with 11817 additions and 0 deletions
+138
View File
@@ -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
View File
@@ -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
}
}
+41
View File
@@ -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)
}
+21
View File
@@ -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()
}
+55
View File
@@ -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
}
+32
View File
@@ -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)
// }