TODO: 推翻整个项目重新建立根基
This commit is contained in:
5
client/utils/isMobileUI.ts
Normal file
5
client/utils/isMobileUI.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function isMobileUI() {
|
||||
const mobile = new URL(location.href).searchParams.get('mobile')
|
||||
if (mobile) return mobile == 'true'
|
||||
return /Mobi|Android|iPhone/i.test(navigator.userAgent)
|
||||
}
|
||||
52
client/utils/openImageViewer.ts
Normal file
52
client/utils/openImageViewer.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { $ } from 'mdui/jq'
|
||||
import 'pinch-zoom-element'
|
||||
|
||||
document.body.appendChild(new DOMParser().parseFromString(`
|
||||
<mdui-dialog id="image-viewer-dialog" fullscreen="fullscreen">
|
||||
<style>
|
||||
#image-viewer-dialog::part(panel) {
|
||||
background: rgba(0, 0, 0, 0) !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#image-viewer-dialog>mdui-button-icon[icon=close] {
|
||||
z-index: 114514;
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
color: #ffffff
|
||||
}
|
||||
|
||||
#image-viewer-dialog>mdui-button-icon[icon=open_in_new] {
|
||||
z-index: 114514;
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 65px;
|
||||
color: #ffffff
|
||||
}
|
||||
</style>
|
||||
<mdui-button-icon icon="open_in_new"
|
||||
onclick="window.open(document.querySelector('#image-viewer-dialog-inner > *').src, '_blank')">
|
||||
</mdui-button-icon>
|
||||
<mdui-button-icon icon="close" onclick="this.parentNode.open = false">
|
||||
</mdui-button-icon>
|
||||
<pinch-zoom id="image-viewer-dialog-inner" style="width: var(--whitesilk-window-width); height: var(--whitesilk-window-height);">
|
||||
</pinch-zoom>
|
||||
</mdui-dialog>
|
||||
`, 'text/html').body.firstChild as Node)
|
||||
|
||||
export default function openImageViewer(src: string) {
|
||||
$('#image-viewer-dialog-inner').empty()
|
||||
|
||||
const e = new Image()
|
||||
e.onload = () => ($('#image-viewer-dialog-inner').get(0) as any).scaleTo(0.1, {
|
||||
// Transform origin. Can be a number, or string percent, eg "50%"
|
||||
originX: '50%',
|
||||
originY: '50%',
|
||||
// Should the transform origin be relative to the container, or content?
|
||||
relativeTo: 'container',
|
||||
})
|
||||
e.src = src
|
||||
$('#image-viewer-dialog-inner').append(e)
|
||||
$('#image-viewer-dialog').attr('open', 'true')
|
||||
}
|
||||
34
client/utils/randomUUID.ts
Normal file
34
client/utils/randomUUID.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// https://www.xiabingbao.com/post/crypto/js-crypto-randomuuid-qxcuqj.html
|
||||
// 在此表示感謝
|
||||
|
||||
export default function randomUUID() {
|
||||
// crypto - 只支持在安全的上下文使用
|
||||
if (typeof crypto === 'object') {
|
||||
if (typeof crypto.randomUUID === 'function') {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
|
||||
// https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
|
||||
const callback = (c: string) => {
|
||||
const num = Number(c)
|
||||
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16)
|
||||
};
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback)
|
||||
}
|
||||
}
|
||||
// 隨機數 - fallback
|
||||
let timestamp = new Date().getTime()
|
||||
let perforNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
let random = Math.random() * 16
|
||||
if (timestamp > 0) {
|
||||
random = (timestamp + random) % 16 | 0
|
||||
timestamp = Math.floor(timestamp / 16)
|
||||
} else {
|
||||
random = (perforNow + random) % 16 | 0
|
||||
perforNow = Math.floor(perforNow / 16)
|
||||
}
|
||||
return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
89
client/utils/showSnackbar.ts
Normal file
89
client/utils/showSnackbar.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { snackbar as mduiSnackbar, Snackbar } from "mdui"
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Snackbar 出现的位置。默认为 `bottom`。可选值为:
|
||||
* * `top`:位于顶部,居中对齐
|
||||
* * `top-start`:位于顶部,左对齐
|
||||
* * `top-end`:位于顶部,右对齐
|
||||
* * `bottom`:位于底部,居中对齐
|
||||
* * `bottom-start`:位于底部,左对齐
|
||||
* * `bottom-end`:位于底部,右对齐
|
||||
*/
|
||||
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
|
||||
/**
|
||||
* 操作按钮的文本
|
||||
*/
|
||||
action?: string;
|
||||
/**
|
||||
* 是否在右侧显示关闭按钮
|
||||
*/
|
||||
closeable?: boolean;
|
||||
/**
|
||||
* 消息文本最多显示几行。默认不限制行数。可选值为
|
||||
* * `1`:消息文本最多显示一行
|
||||
* * `2`:消息文本最多显示两行
|
||||
*/
|
||||
messageLine?: 1 | 2;
|
||||
/**
|
||||
* 在多长时间后自动关闭(单位为毫秒)。设置为 0 时,不自动关闭。默认为 5 秒后自动关闭。
|
||||
*/
|
||||
autoCloseDelay?: number;
|
||||
/**
|
||||
* 点击或触摸 Snackbar 以外的区域时是否关闭 Snackbar
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
/**
|
||||
* 队列名称。
|
||||
* 默认不启用队列,在多次调用该函数时,将同时显示多个 snackbar。
|
||||
* 可在该参数中传入一个队列名称,具有相同队列名称的 snackbar 函数,将在上一个 snackbar 关闭后才打开下一个 snackbar。
|
||||
*/
|
||||
queue?: string;
|
||||
/**
|
||||
* 点击 Snackbar 时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onClick?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* 点击操作按钮时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* 默认点击后会关闭 snackbar;若返回值为 false,则不关闭 snackbar;若返回值为 promise,则将在 promise 被 resolve 后,关闭 snackbar。
|
||||
* @param snackbar
|
||||
*/
|
||||
onActionClick?: (snackbar: Snackbar) => void | boolean | Promise<void>;
|
||||
/**
|
||||
* Snackbar 开始显示时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onOpen?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* Snackbar 显示动画完成时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onOpened?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* Snackbar 开始隐藏时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onClose?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* Snackbar 隐藏动画完成时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onClosed?: (snackbar: Snackbar) => void;
|
||||
}
|
||||
|
||||
interface SnackbarOptions extends Options {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default function showSnackbar(opinions: SnackbarOptions) {
|
||||
opinions.autoCloseDelay == null && (opinions.autoCloseDelay = 3500)
|
||||
opinions.placement == null && (opinions.placement = 'top')
|
||||
return mduiSnackbar(opinions)
|
||||
}
|
||||
8
client/utils/useAsyncEffect.ts
Normal file
8
client/utils/useAsyncEffect.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
|
||||
export default function useAsyncEffect(func: Function, deps?: React.DependencyList) {
|
||||
React.useEffect(() => {
|
||||
func()
|
||||
// 警告: 不添加 deps 有可能導致無限執行
|
||||
}, deps || [])
|
||||
}
|
||||
8
client/utils/useEventListener.ts
Normal file
8
client/utils/useEventListener.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export default function useEventListener<T extends HTMLElement | null>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) {
|
||||
React.useEffect(() => {
|
||||
ref.current!.addEventListener(eventName, callback)
|
||||
return () => ref.current?.removeEventListener(eventName, callback)
|
||||
}, [ref, eventName, callback])
|
||||
}
|
||||
Reference in New Issue
Block a user