Add SessionInfoFloat component to SessionPage and enhance session management in server

- Introduced SessionInfoFloat component to display session details when a session is active.
- Updated session server logic to automatically transition sessions to "ending" status when their end time is reached.
- Improved process termination handling with enhanced error checking and fallback mechanisms.
- Refactored application stop logic to handle multiple processes more efficiently and added timeout management for process termination.
This commit is contained in:
2025-12-10 20:12:32 +05:00
parent e8b8eca0d6
commit a80544c936
6 changed files with 330 additions and 28 deletions
@@ -0,0 +1,173 @@
import { useEffect, useState, useRef } from "react";
import type { Session } from "../../types/Session";
import clsx from "clsx";
interface SessionInfoFloatProps {
session: Session;
}
function SessionInfoFloat({ session }: SessionInfoFloatProps) {
const [timeRemaining, setTimeRemaining] = useState<string>("");
const [isVisible, setIsVisible] = useState<boolean>(true);
const [isAnimating, setIsAnimating] = useState<boolean>(false);
const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Таймер обратного отсчета
useEffect(() => {
const calculateTimeRemaining = () => {
if (!session.endAt) {
setTimeRemaining("--:--");
return;
}
const now = new Date().getTime();
const endTime = new Date(session.endAt).getTime();
const diff = endTime - now;
if (diff <= 0) {
setTimeRemaining("00:00");
return;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 0) {
setTimeRemaining(
`${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
);
} else {
setTimeRemaining(
`${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}`
);
}
};
calculateTimeRemaining();
const interval = setInterval(calculateTimeRemaining, 1000);
return () => clearInterval(interval);
}, [session.endAt]);
// Автоматическое скрытие через 3 секунды
useEffect(() => {
hideTimeoutRef.current = setTimeout(() => {
setIsAnimating(true);
setIsVisible(false);
// Разблокируем после завершения анимации (500ms)
animationTimeoutRef.current = setTimeout(() => {
setIsAnimating(false);
}, 500);
}, 3000);
return () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
if (animationTimeoutRef.current) {
clearTimeout(animationTimeoutRef.current);
}
};
}, []);
// Обработчики наведения на компонент
const handleComponentMouseEnter = () => {
// Игнорируем события во время анимации
if (isAnimating) return;
// Отменяем таймер скрытия если он есть
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
// Показываем компонент если он скрыт
if (!isVisible) {
setIsAnimating(true);
setIsVisible(true);
// Разблокируем после завершения анимации (500ms)
animationTimeoutRef.current = setTimeout(() => {
setIsAnimating(false);
}, 300);
}
};
const handleComponentMouseLeave = () => {
// Игнорируем события во время анимации
if (isAnimating) return;
// Скрываем компонент сразу при уходе курсора
setIsAnimating(true);
setIsVisible(false);
// Разблокируем после завершения анимации (300ms)
animationTimeoutRef.current = setTimeout(() => {
setIsAnimating(false);
}, 300);
};
return (
<div
className={clsx(
"flex flex-col items-center relative transition-all duration-300 ease-out",
"2xl:py-[1.111vw] py-4", // Padding сверху вместо top в родителе
!isVisible && "-translate-y-[calc(100%-max(1.667vw,24px))]"
)}
onMouseEnter={handleComponentMouseEnter}
onMouseLeave={handleComponentMouseLeave}
>
{/* Main Container with shadow - animates in/out */}
<div
className="flex flex-row items-center 2xl:gap-[1.111vw] gap-4 2xl:p-[0.556vw] p-2 bg-white 2xl:rounded-[1.389vw] rounded-[20px] transition-opacity duration-500 ease-out 2xl:mb-[0.556vw] mb-2"
style={{
boxShadow:
"0px 4px 40px 0px rgba(15, 16, 17, 0.1), 0px 2px 2px 0px rgba(0, 0, 0, 0.06)",
// opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? "auto" : "none",
}}
>
{/* Left Section: Image + Text */}
<div className="flex items-center 2xl:gap-[0.556vw] gap-2">
{/* App Icon/Image - 32x32px */}
<div className="flex justify-center items-center flex-shrink-0 2xl:w-[2.222vw] 2xl:h-[2.222vw] w-8 h-8 2xl:p-[0.556vw] p-2 bg-[#F0F0F0] 2xl:rounded-[0.833vw] rounded-xl">
<div className="w-full h-full bg-gradient-to-br from-[#7B60F3] to-[#5B40D3] rounded-sm flex items-center justify-center text-white font-semibold 2xl:text-[0.833vw] text-xs">
{session.app?.title?.charAt(0).toUpperCase() || "A"}
</div>
</div>
{/* Text Column */}
<div className="flex flex-col justify-center 2xl:gap-[0.139vw] gap-0.5">
<p className="caption-xs font-medium text-[#7D7D7D] leading-[110%]">
Проект
</p>
<p className="text-s font-normal text-[#141414] leading-[115%]">
{session.app?.title || "Приложение"}
</p>
</div>
</div>
{/* Right Section: Timer */}
<div className="flex justify-center items-center 2xl:gap-[0.556vw] gap-2 2xl:px-[0.833vw] 2xl:py-[0.556vw] px-3 py-2 bg-[#F3F3F3] 2xl:rounded-[0.833vw] rounded-xl 2xl:w-[3.889vw] w-14 2xl:h-[2.222vw] h-8">
<span className="caption-s font-medium text-[#141414] tabular-nums leading-[115%]">
{timeRemaining}
</span>
</div>
</div>
{/* Bottom Indicator - Always visible, triggers show on hover */}
<div
className="2xl:w-[4.444vw] w-16 2xl:h-[0.139vw] h-0.5 bg-white/55 2xl:rounded-[1.667vw] rounded-3xl cursor-pointer transition-all duration-200 hover:bg-white/80"
/>
</div>
);
}
export default SessionInfoFloat;
+8
View File
@@ -31,6 +31,7 @@ import { useMe } from "../hooks/useAuth";
import QuitSessionModal from "../components/modals/QuitSessionModal";
import useModalStore from "../store/modalStore";
import toast from "react-hot-toast";
import SessionInfoFloat from "../components/ui/SessionInfoFloat";
function SessionPage() {
const { setPopup, popupType } = usePopupStore();
@@ -192,6 +193,13 @@ function SessionPage() {
"overflow-hidden relative order-3 w-screen bg-black h-dvh touch-none max-2xl:flex max-2xl:portrait:items-center"
)}
>
{/* Session Info Float Menu */}
{session.status === "started" && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 z-[160]">
<SessionInfoFloat session={session} />
</div>
)}
{/* Pixel Streaming - показывается только когда сессия активна */}
{session.status === "started" &&
session.mode === "stream" &&
+1
View File
@@ -36,6 +36,7 @@ function TestPage() {
json: {
// appId: "60b0a46a-15f6-40db-8f1e-a288c78f8631",
appId: "1e762115-8bab-4d41-9bec-55c4a4ad3fbe",
// appId: "1b0416fc-fb32-44d9-9c92-20c3e9fe069f",
mode: "stream",
tier: "demo",
},