mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-27 10:01:28 +02:00
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com>
117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
|
|
export function useAccentTrigger() {
|
|
const clicksRef = useRef(0);
|
|
const lastClickRef = useRef(0);
|
|
|
|
return useCallback(() => {
|
|
const now = performance.now();
|
|
if (now - lastClickRef.current > 400) {
|
|
clicksRef.current = 0;
|
|
}
|
|
lastClickRef.current = now;
|
|
clicksRef.current += 1;
|
|
if (clicksRef.current >= 10) {
|
|
clicksRef.current = 0;
|
|
triggerAccent();
|
|
}
|
|
}, []);
|
|
}
|
|
|
|
function triggerAccent() {
|
|
if (document.getElementById("nb-accent-root")) return;
|
|
|
|
const container = document.createElement("div");
|
|
container.id = "nb-accent-root";
|
|
document.body.appendChild(container);
|
|
const root = createRoot(container);
|
|
|
|
const cleanup = () => {
|
|
root.unmount();
|
|
container.remove();
|
|
};
|
|
|
|
root.render(<Accent onDone={cleanup} />);
|
|
}
|
|
|
|
function Accent({ onDone }: Readonly<{ onDone: () => void }>) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const raf = requestAnimationFrame(() => setVisible(true));
|
|
return () => cancelAnimationFrame(raf);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const resize = () => {
|
|
canvas.width = window.innerWidth * dpr;
|
|
canvas.height = window.innerHeight * dpr;
|
|
canvas.style.width = `${window.innerWidth}px`;
|
|
canvas.style.height = `${window.innerHeight}px`;
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
};
|
|
resize();
|
|
window.addEventListener("resize", resize);
|
|
|
|
const chars = "TEAMNETBIRD";
|
|
const fontSize = 16;
|
|
const columns = Math.floor(window.innerWidth / fontSize);
|
|
const drops = Array.from({ length: columns }, () => Math.random() * -50);
|
|
|
|
let raf = 0;
|
|
let last = 0;
|
|
const draw = (t: number) => {
|
|
if (t - last > 50) {
|
|
last = t;
|
|
|
|
ctx.globalCompositeOperation = "destination-out";
|
|
ctx.fillStyle = "rgba(0, 0, 0, 0.12)";
|
|
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
|
|
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.font = `${fontSize}px ui-monospace, monospace`;
|
|
ctx.fillStyle = "#f68330";
|
|
|
|
for (let i = 0; i < drops.length; i++) {
|
|
const ch = chars[Math.floor(Math.random() * chars.length)];
|
|
const y = drops[i] * fontSize;
|
|
ctx.fillText(ch, i * fontSize, y);
|
|
if (y > window.innerHeight && Math.random() > 0.975) {
|
|
drops[i] = 0;
|
|
}
|
|
drops[i]++;
|
|
}
|
|
}
|
|
raf = requestAnimationFrame(draw);
|
|
};
|
|
raf = requestAnimationFrame(draw);
|
|
|
|
const timeout = globalThis.setTimeout(() => {
|
|
setVisible(false);
|
|
globalThis.setTimeout(onDone, 500);
|
|
}, 9000);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
globalThis.clearTimeout(timeout);
|
|
window.removeEventListener("resize", resize);
|
|
};
|
|
}, [onDone]);
|
|
|
|
return (
|
|
<div
|
|
className={`pointer-events-none fixed inset-0 z-50 bg-black/5 transition-opacity duration-500 ${visible ? "opacity-100" : "opacity-0"}`}
|
|
>
|
|
<canvas ref={canvasRef} className={"block"} />
|
|
</div>
|
|
);
|
|
}
|