chore: merge declarative effects into frontend contract flow

This commit is contained in:
Tunglies
2026-10-03 18:44:55 +08:00
48 changed files with 497 additions and 420 deletions
+60
View File
@@ -68,6 +68,66 @@ const resolveNoticeMessage = (
const i18n = notice.i18n
if (!i18n) return bound(notice.message)
const detail = i18n.params?.message
const existingCore =
notice.code === 'SERVICE_SIDECAR_FAILED' && typeof detail === 'string'
? /process verge-mihomo(?:-alpha)?(?:\.exe)? \(PID (\d+)\) is still running; refusing a second core\s*$/.exec(
detail,
)
: null
if (existingCore && typeof detail === 'string') {
const permissionRejected = detail.startsWith('Service core rejected:')
return (
<>
{permissionRejected && (
<Box sx={{ mb: 1 }}>
{t(
'settings.feedback.notifications.clashService.permissionRejectedReason',
)}{' '}
<Link
href={SERVICE_PERMISSION_GUIDE}
color="inherit"
underline="always"
onClick={(event) => {
event.preventDefault()
void openExternalUrl(SERVICE_PERMISSION_GUIDE).catch(
showNotice.error,
)
}}
>
{t(
'settings.feedback.notifications.clashService.permissionRepairGuide',
)}
</Link>
</Box>
)}
{t('settings.feedback.errors.clashService.coreAlreadyRunning', {
pid: existingCore[1],
})}
<Box component="details" sx={{ mt: 1 }}>
<Box component="summary" sx={{ cursor: 'pointer' }}>
{t('shared.feedback.errors.details')}
</Box>
<Box
component="pre"
sx={{
m: 0,
mt: 1,
maxHeight: 160,
overflow: 'auto',
whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere',
userSelect: 'text',
fontSize: 'inherit',
}}
>
{detail}
</Box>
</Box>
</>
)
}
if (i18n.key === SERVICE_PERMISSION_NOTICE) {
return (
<>
+43 -75
View File
@@ -13,7 +13,6 @@ const upLineWidth = 4
const downLineAlpha = 1
const downLineWidth = 4
const sampleIntervalMs = 1000
const frameIntervalMs = 1000 / 15
const animationDurationMs = sampleIntervalMs
const zeroTraffic: Traffic = { up: 0, down: 0, upTotal: 0, downTotal: 0 }
@@ -36,7 +35,7 @@ type TrafficValueKey = 'up' | 'down'
* draw the traffic graph
*/
export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
const countRef = useRef(0)
const animationStartRef = useRef<number | null>(null)
const styleRef = useRef(true)
const listRef = useRef<Traffic[]>(createDefaultList())
const canvasRef = useRef<HTMLCanvasElement>(null!)
@@ -69,7 +68,6 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
if (shouldAppend) {
if (list.length > maxPoint + 2) list.shift()
list.push(data)
countRef.current = 0
requestDrawRef.current(true)
}
@@ -84,11 +82,8 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
}, [])
useEffect(() => {
let raf = 0
let frameTimer: ReturnType<typeof setTimeout> | null = null
let animation: Animation | null = null
let resizeObserver: ResizeObserver | null = null
let animationStart = 0
let lastFrameTime = 0
const canvas = canvasRef.current!
if (!canvas) return
@@ -102,25 +97,13 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
const upLineColor = secondary.main || '#9c27b0'
const downLineColor = primary.main || '#5b5c9d'
const cancelPendingDraw = () => {
if (frameTimer !== null) {
clearTimeout(frameTimer)
frameTimer = null
}
if (raf) {
cancelAnimationFrame(raf)
raf = 0
}
}
const drawGraph = (offset = countRef.current) => {
const drawGraph = () => {
const list = listRef.current
const lineStyle = styleRef.current
const width = canvas.width
const height = canvas.height
const dx = width / maxPoint
const dx = width / (maxPoint + 1)
const dy = height / 7
const l1 = dy
const l2 = dy * 4
@@ -142,17 +125,17 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
const drawBezier = (list: Traffic[], valueKey: TrafficValueKey) => {
if (list.length === 0) return
const firstX = (dx * -1 - offset + 3) | 0
const firstX = (dx * -1 + 3) | 0
const firstY = countY(list[0]?.[valueKey] ?? 0)
context.moveTo(firstX, firstY)
for (let i = 1; i < list.length; i++) {
const p1x = (dx * (i - 1) - offset + 3) | 0
const p1x = (dx * (i - 1) + 3) | 0
const p1y = countY(list[i]?.[valueKey] ?? 0)
const hasNext = i + 1 < list.length
const p2x = hasNext ? (dx * i - offset + 3) | 0 : p1x
const p2x = hasNext ? (dx * i + 3) | 0 : p1x
const p2y = hasNext ? countY(list[i + 1]?.[valueKey] ?? 0) : p1y
context.quadraticCurveTo(p1x, p1y, (p1x + p2x) / 2, (p1y + p2y) / 2)
@@ -162,13 +145,10 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
const drawLine = (list: Traffic[], valueKey: TrafficValueKey) => {
if (list.length === 0) return
context.moveTo((dx * -1 - offset) | 0, countY(list[0]?.[valueKey] ?? 0))
context.moveTo((dx * -1) | 0, countY(list[0]?.[valueKey] ?? 0))
for (let i = 1; i < list.length; i++) {
context.lineTo(
(dx * (i - 1) - offset) | 0,
countY(list[i]?.[valueKey] ?? 0),
)
context.lineTo((dx * (i - 1)) | 0, countY(list[i]?.[valueKey] ?? 0))
}
}
@@ -211,51 +191,28 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
context.closePath()
}
const drawAnimatedFrame = (timestamp: number) => {
raf = 0
const timeSinceLastFrame = timestamp - lastFrameTime
if (timeSinceLastFrame < frameIntervalMs) {
frameTimer = setTimeout(() => {
frameTimer = null
raf = requestAnimationFrame(drawAnimatedFrame)
}, frameIntervalMs - timeSinceLastFrame)
return
}
lastFrameTime = timestamp
const dx = canvas.width / maxPoint
const progress = Math.min(
(timestamp - animationStart) / animationDurationMs,
1,
)
const offset = progress * dx
countRef.current = offset
drawGraph(offset)
if (progress < 1) {
raf = requestAnimationFrame(drawAnimatedFrame)
return
}
countRef.current = dx
}
const requestDraw = (animate = false) => {
cancelPendingDraw()
if (animate) animationStartRef.current = performance.now()
animation?.cancel()
drawGraph()
if (!animate) {
raf = requestAnimationFrame(() => {
raf = 0
drawGraph()
})
return
}
animationStart = performance.now()
lastFrameTime = animationStart - frameIntervalMs
raf = requestAnimationFrame(drawAnimatedFrame)
const elapsed =
animationStartRef.current === null
? 0
: Math.min(
performance.now() - animationStartRef.current,
animationDurationMs,
)
const step = canvas.getBoundingClientRect().width / (maxPoint + 1)
animation = canvas.animate(
[
{ transform: 'translateX(0)' },
{ transform: `translateX(${-step}px)` },
],
{ duration: animationDurationMs, easing: 'linear', fill: 'forwards' },
)
animation.currentTime = elapsed
if (animationStartRef.current === null) animation.pause()
}
requestDrawRef.current = requestDraw
@@ -263,7 +220,7 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(() => requestDraw(false))
resizeObserver.observe(canvas)
resizeObserver.observe(canvas.parentElement!)
}
return () => {
@@ -271,9 +228,20 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
requestDrawRef.current = () => {}
}
resizeObserver?.disconnect()
cancelPendingDraw()
animation?.cancel()
}
}, [palette])
return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />
return (
<div style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
<canvas
ref={canvasRef}
width={310}
style={{
width: `${((maxPoint + 1) / maxPoint) * 100}%`,
height: '100%',
}}
/>
</div>
)
}
+4 -4
View File
@@ -43,7 +43,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [open, setOpen] = useState(false)
const [values, setValues] = useState({
stack: 'mixed',
stack: 'mips',
device: OS === 'macos' ? 'utun1024' : 'Mihomo',
autoRoute: true,
routeExcludeAddress: '',
@@ -73,7 +73,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
const computedAutoRedirect =
OS === 'linux' ? (nextAutoRoute ? rawAutoRedirect : false) : false
setValues({
stack: clash?.tun.stack ?? 'gvisor',
stack: clash?.tun.stack ?? 'mips',
device: clash?.tun.device ?? (OS === 'macos' ? 'utun1024' : 'Mihomo'),
autoRoute: nextAutoRoute,
routeExcludeAddress: (clash?.tun['route-exclude-address'] ?? []).join(
@@ -152,7 +152,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
size="small"
onClick={async () => {
const tun: IConfigData['tun'] = {
stack: 'gvisor',
stack: 'mips',
device: OS === 'macos' ? 'utun1024' : 'Mihomo',
'auto-route': true,
...(OS === 'linux'
@@ -167,7 +167,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
mtu: 1500,
}
setValues({
stack: 'gvisor',
stack: 'mips',
device: OS === 'macos' ? 'utun1024' : 'Mihomo',
autoRoute: true,
routeExcludeAddress: '',