update build UI AU
This commit is contained in:
parent
b50dc96ce2
commit
f1de8cb627
|
|
@ -40,8 +40,13 @@ export default function FaceBracket({ box, transform, progress }: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const armPx = Math.max(14, Math.min(40, width * 1000 * 0.18));
|
// Arm length tính theo % cạnh bracket để scale đúng trên mọi kích thước Card
|
||||||
const cornerSize = { width: armPx, height: armPx } as const;
|
// (mobile ~350px, desktop ~1000px). Sàn tối thiểu 10px để không tàng hình
|
||||||
|
// khi mặt detect rất nhỏ; trần 36px để khi mặt to không bị quá dày.
|
||||||
|
const cornerSize = {
|
||||||
|
width: "min(36px, max(10px, 22%))",
|
||||||
|
height: "min(36px, max(10px, 22%))",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const cornerBase =
|
const cornerBase =
|
||||||
"absolute border-emerald-400/90 transition-opacity duration-150";
|
"absolute border-emerald-400/90 transition-opacity duration-150";
|
||||||
|
|
|
||||||
|
|
@ -113,10 +113,14 @@ export default function TabFeatures({ inline = false }: { inline?: boolean }) {
|
||||||
|
|
||||||
// Stable-face auto trigger: Main bumps autoCheckinTick when a face has been
|
// Stable-face auto trigger: Main bumps autoCheckinTick when a face has been
|
||||||
// present for 2s, and we fire the same checkin path used by the manual
|
// present for 2s, and we fire the same checkin path used by the manual
|
||||||
// button. Skip the initial 0 tick on mount.
|
// button. Khởi tạo null để mỗi lần remount (vd: resize đổi layout) không
|
||||||
const lastHandledTick = useRef(0);
|
// bắn nhầm tick cũ — lần chạy đầu chỉ "ghi nhớ" tick hiện tại rồi return.
|
||||||
|
const lastHandledTick = useRef<number | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoCheckinTick === 0) return;
|
if (lastHandledTick.current === null) {
|
||||||
|
lastHandledTick.current = autoCheckinTick;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (autoCheckinTick === lastHandledTick.current) return;
|
if (autoCheckinTick === lastHandledTick.current) return;
|
||||||
lastHandledTick.current = autoCheckinTick;
|
lastHandledTick.current = autoCheckinTick;
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -17,9 +16,28 @@ import RightSlidebar from "./components/right-slidebar";
|
||||||
import TabFeatures from "./components/tab-features";
|
import TabFeatures from "./components/tab-features";
|
||||||
import { LogList } from "./components/tab-log";
|
import { LogList } from "./components/tab-log";
|
||||||
|
|
||||||
|
const DESKTOP_MQ = "(min-width: 1024px)";
|
||||||
|
const matchDesktop = () =>
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window.matchMedia(DESKTOP_MQ).matches
|
||||||
|
: true;
|
||||||
|
|
||||||
export default function Main() {
|
export default function Main() {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isDesktop, setIsDesktop] = useState(matchDesktop);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(matchDesktop);
|
||||||
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false);
|
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const mq = window.matchMedia(DESKTOP_MQ);
|
||||||
|
const handler = (e: MediaQueryListEvent) => {
|
||||||
|
setIsDesktop(e.matches);
|
||||||
|
// Resize xuống tablet/mobile thì đóng luôn drawer logs (tắt auto-open).
|
||||||
|
if (!e.matches) setIsSidebarOpen(false);
|
||||||
|
};
|
||||||
|
mq.addEventListener("change", handler);
|
||||||
|
return () => mq.removeEventListener("change", handler);
|
||||||
|
}, []);
|
||||||
// const { currentUser, setCurrentUser } = useUserStore();
|
// const { currentUser, setCurrentUser } = useUserStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -36,6 +54,37 @@ export default function Main() {
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Kích thước thực của khung hiển thị video bên trong Card sau khi
|
||||||
|
// letterbox theo aspect-ratio nguồn (1280x720 = 16:9). FaceBracket dùng %
|
||||||
|
// của container này nên bám đúng vùng video, không lệch ra vùng đen.
|
||||||
|
const [videoFrame, setVideoFrame] = useState<{ w: number; h: number }>({
|
||||||
|
w: 0,
|
||||||
|
h: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = cardRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const VIDEO_ASPECT = 16 / 9;
|
||||||
|
const ro = new ResizeObserver(([entry]) => {
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
if (!width || !height) return;
|
||||||
|
const cardAspect = width / height;
|
||||||
|
let w: number, h: number;
|
||||||
|
if (cardAspect >= VIDEO_ASPECT) {
|
||||||
|
h = height;
|
||||||
|
w = height * VIDEO_ASPECT;
|
||||||
|
} else {
|
||||||
|
w = width;
|
||||||
|
h = width / VIDEO_ASPECT;
|
||||||
|
}
|
||||||
|
setVideoFrame({ w: Math.round(w), h: Math.round(h) });
|
||||||
|
});
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onStableFace = useCallback(() => {
|
const onStableFace = useCallback(() => {
|
||||||
// Fire the same path as the "Điểm Danh Ngay" button via the store trigger,
|
// Fire the same path as the "Điểm Danh Ngay" button via the store trigger,
|
||||||
|
|
@ -126,31 +175,42 @@ export default function Main() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="h-full flex flex-col p-2 sm:p-4 lg:p-6 gap-2 lg:gap-0">
|
<div className="h-full flex flex-col p-2 sm:p-4 lg:p-6 gap-2 lg:gap-0">
|
||||||
{/* Mobile/tablet: Buttons at top */}
|
{/* Mobile/tablet: Buttons at top — conditional render để tránh
|
||||||
<div className="lg:hidden">
|
mount 2 instance TabFeatures cùng lúc (duplicate auto-check). */}
|
||||||
<TabFeatures inline />
|
{!isDesktop && <TabFeatures inline />}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Video Feed */}
|
{/* Video Feed */}
|
||||||
<Card className="flex-1 min-h-0 overflow-hidden bg-black relative group">
|
<Card className="flex-1 min-h-0 overflow-hidden bg-black relative group">
|
||||||
|
<div ref={cardRef} className="absolute inset-0">
|
||||||
|
{/* Khung video centered theo aspect nguồn — đặt video + bracket
|
||||||
|
chung trong inner container để toạ độ % của bracket bám
|
||||||
|
đúng vùng video, không tràn ra letterbox đen. */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 m-auto"
|
||||||
|
style={{
|
||||||
|
width: videoFrame.w || "100%",
|
||||||
|
height: videoFrame.h || "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
className="w-full h-full object-contain lg:object-cover will-change-transform"
|
className="w-full h-full object-cover will-change-transform"
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: "0 0",
|
transformOrigin: "0 0",
|
||||||
transform: `translate3d(${faceZoom.translateX}%, ${faceZoom.translateY}%, 0) scale(${faceZoom.scale})`,
|
transform: `translate3d(${faceZoom.translateX}%, ${faceZoom.translateY}%, 0) scale(${faceZoom.scale})`,
|
||||||
backfaceVisibility: "hidden",
|
backfaceVisibility: "hidden",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 border-4 border-blue-500/30 pointer-events-none" />
|
|
||||||
|
|
||||||
<FaceBracket
|
<FaceBracket
|
||||||
box={faceBox}
|
box={faceBox}
|
||||||
transform={faceZoom}
|
transform={faceZoom}
|
||||||
progress={isAutoChecking ? faceStability : 0}
|
progress={isAutoChecking ? faceStability : 0}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 border-4 border-blue-500/30 pointer-events-none" />
|
||||||
|
|
||||||
{/* Camera shutter flash — keyed so each fire replays the animation. */}
|
{/* Camera shutter flash — keyed so each fire replays the animation. */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
@ -232,15 +292,16 @@ export default function Main() {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Desktop only: buttons overlaid at bottom of video */}
|
{/* Desktop only: buttons overlaid at bottom of video */}
|
||||||
<div className="hidden lg:block">
|
{isDesktop && <TabFeatures />}
|
||||||
<TabFeatures />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Mobile/tablet: Log at bottom */}
|
{/* Mobile/tablet: Log at bottom */}
|
||||||
<div className="lg:hidden h-44 border rounded-lg bg-white overflow-hidden">
|
{!isDesktop && (
|
||||||
|
<div className="h-44 border rounded-lg bg-white overflow-hidden">
|
||||||
<LogList />
|
<LogList />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue