Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ export function App(): JSX.Element {
const [queuePosition, setQueuePosition] = useState<number | undefined>();
const [navbarActiveSession, setNavbarActiveSession] = useState<ActiveSessionInfo | null>(null);
const [isResumingNavbarSession, setIsResumingNavbarSession] = useState(false);
const [isTerminatingNavbarSession, setIsTerminatingNavbarSession] = useState(false);
const [launchError, setLaunchError] = useState<LaunchErrorState | null>(null);
const [queueModalGame, setQueueModalGame] = useState<GameInfo | null>(null);
const [queueModalData, setQueueModalData] = useState<PrintedWasteQueueData | null>(null);
Expand Down Expand Up @@ -2716,6 +2717,40 @@ export function App(): JSX.Element {
streamStatus,
]);

const handleTerminateNavbarSession = useCallback(async () => {
if (!navbarActiveSession || isTerminatingNavbarSession || isResumingNavbarSession) {
return;
}
const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken;
if (!token) {
return;
}

setIsTerminatingNavbarSession(true);
try {
await window.openNow.stopSession({
token,
streamingBaseUrl: effectiveStreamingBaseUrl,
serverIp: navbarActiveSession.serverIp,
zone: "prod",
sessionId: navbarActiveSession.sessionId,
});
setNavbarActiveSession(null);
void refreshNavbarActiveSession();
} catch (error) {
console.error("Terminate remote session failed:", error);
} finally {
setIsTerminatingNavbarSession(false);
}
}, [
authSession,
effectiveStreamingBaseUrl,
isResumingNavbarSession,
isTerminatingNavbarSession,
navbarActiveSession,
refreshNavbarActiveSession,
]);

// Stop stream handler
const handleStopStream = useCallback(async () => {
try {
Expand Down Expand Up @@ -3336,9 +3371,13 @@ export function App(): JSX.Element {
activeSession={navbarActiveSession}
activeSessionGameTitle={activeSessionGameTitle}
isResumingSession={isResumingNavbarSession}
isTerminatingSession={isTerminatingNavbarSession}
onResumeSession={() => {
void handleResumeFromNavbar();
}}
onTerminateSession={() => {
void handleTerminateNavbarSession();
}}
onLogout={handleLogout}
/>
)}
Expand Down
60 changes: 42 additions & 18 deletions opennow-stable/src/renderer/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ActiveSessionInfo, AuthUser, SubscriptionInfo } from "@shared/gfn";
import { House, Library, Settings, User, LogOut, Zap, Timer, HardDrive, X, Loader2, PlayCircle } from "lucide-react";
import { House, Library, Settings, User, LogOut, Zap, Timer, HardDrive, X, Loader2, PlayCircle, Power } from "lucide-react";
import { useEffect, useState, type JSX } from "react";
import { createPortal } from "react-dom";

Expand All @@ -11,7 +11,9 @@ interface NavbarProps {
activeSession: ActiveSessionInfo | null;
activeSessionGameTitle: string | null;
isResumingSession: boolean;
isTerminatingSession: boolean;
onResumeSession: () => void;
onTerminateSession: () => void;
onLogout: () => void;
}

Expand All @@ -32,7 +34,9 @@ export function Navbar({
activeSession,
activeSessionGameTitle,
isResumingSession,
isTerminatingSession,
onResumeSession,
onTerminateSession,
onLogout,
}: NavbarProps): JSX.Element {
const [modalType, setModalType] = useState<NavbarModalType>(null);
Expand Down Expand Up @@ -273,23 +277,43 @@ export function Navbar({

<div className="navbar-right">
{activeSession && (
<button
type="button"
className={`navbar-session-resume${isResumingSession ? " is-loading" : ""}`}
title={
activeSession.serverIp
? activeSessionTitle
? `Resume active cloud session: ${activeSessionTitle}`
: "Resume active cloud session"
: "Active session found (missing server address)"
}
onClick={onResumeSession}
disabled={isResumingSession || !activeSession.serverIp}
>
{isResumingSession ? <Loader2 size={14} className="navbar-session-resume-spin" /> : <PlayCircle size={14} />}
<span className="navbar-session-resume-text">Resume</span>
{activeSessionTitle && <span className="navbar-session-resume-game">{activeSessionTitle}</span>}
</button>
<div className="navbar-active-session">
<button
type="button"
className={`navbar-session-resume${isResumingSession ? " is-loading" : ""}`}
title={
activeSession.serverIp
? activeSessionTitle
? `Resume active cloud session: ${activeSessionTitle}`
: "Resume active cloud session"
: "Active session found (missing server address)"
}
onClick={onResumeSession}
disabled={isResumingSession || isTerminatingSession || !activeSession.serverIp}
>
{isResumingSession ? <Loader2 size={14} className="navbar-session-resume-spin" /> : <PlayCircle size={14} />}
<span className="navbar-session-resume-text">Resume</span>
{activeSessionTitle && <span className="navbar-session-resume-game">{activeSessionTitle}</span>}
</button>
<button
type="button"
className={`navbar-session-terminate${isTerminatingSession ? " is-loading" : ""}`}
title={
activeSessionTitle
? `End cloud session: ${activeSessionTitle}`
: "End active cloud session"
}
onClick={onTerminateSession}
disabled={isResumingSession || isTerminatingSession}
aria-label="End active cloud session"
>
{isTerminatingSession ? (
<Loader2 size={14} className="navbar-session-terminate-spin" />
) : (
<Power size={14} strokeWidth={2.5} />
)}
</button>
</div>
)}
{(timeLabel || storageLabel) && (
<div className="navbar-subscription" aria-label="Subscription details">
Expand Down
65 changes: 65 additions & 0 deletions opennow-stable/src/renderer/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,66 @@ body,
z-index: 2;
}

.navbar-active-session {
display: inline-flex;
align-items: center;
gap: 6px;
}

.navbar-session-terminate {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
flex: 0 0 auto;
padding: 0;
border: 1px solid color-mix(in srgb, #f87171 42%, var(--panel-border));
border-radius: var(--r-full);
background:
linear-gradient(160deg, rgba(248, 113, 113, 0.22), rgba(248, 113, 113, 0.06)),
linear-gradient(180deg, rgba(28, 15, 15, 0.95), rgba(14, 10, 10, 0.92));
box-shadow:
0 4px 14px rgba(248, 113, 113, 0.12),
0 0 0 1px rgba(248, 113, 113, 0.12) inset;
color: #fecaca;
cursor: pointer;
transition:
transform var(--t-fast),
border-color var(--t-fast),
box-shadow var(--t-fast),
filter var(--t-fast);
}

.navbar-session-terminate:hover:not(:disabled) {
transform: translateY(-1px);
border-color: color-mix(in srgb, #f87171 72%, var(--panel-border));
box-shadow:
0 8px 22px rgba(248, 113, 113, 0.2),
0 0 0 1px rgba(248, 113, 113, 0.22) inset;
}

.navbar-session-terminate:focus-visible {
outline: none;
box-shadow:
0 0 0 2px rgba(248, 113, 113, 0.28),
0 8px 22px rgba(248, 113, 113, 0.2);
}

.navbar-session-terminate:disabled {
opacity: 0.55;
cursor: not-allowed;
filter: grayscale(0.15);
}

.navbar-session-terminate-spin {
animation: spin 1s linear infinite;
}

.navbar-session-terminate.is-loading {
animation: none;
}

.navbar-session-resume {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -871,6 +931,11 @@ body.controller-mode {
font-size: 0.84rem;
}

.controller-mode .navbar-session-terminate {
width: 42px;
height: 42px;
}

.controller-mode .navbar-link {
padding: 8px 14px;
}
Expand Down
Loading