Skip to content
Merged
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
80 changes: 78 additions & 2 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, JSX } from "react";
import { createPortal } from "react-dom";

import type {
ActiveSessionInfo,
Expand Down Expand Up @@ -877,6 +878,7 @@ export function App(): JSX.Element {
const [navbarActiveSession, setNavbarActiveSession] = useState<ActiveSessionInfo | null>(null);
const [isResumingNavbarSession, setIsResumingNavbarSession] = useState(false);
const [isTerminatingNavbarSession, setIsTerminatingNavbarSession] = useState(false);
const [logoutConfirmOpen, setLogoutConfirmOpen] = 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 @@ -2397,8 +2399,8 @@ export function App(): JSX.Element {
}
}, [applyCatalogBrowseResult, applyVariantSelections, loadSubscriptionInfo, providerIdpId, catalogFilterKey, catalogSelectedSortId]);

// Logout handler
const handleLogout = useCallback(async () => {
const confirmLogout = useCallback(async () => {
setLogoutConfirmOpen(false);
await window.openNow.logout();
setAuthSession(null);
setGames([]);
Expand All @@ -2419,6 +2421,11 @@ export function App(): JSX.Element {
setSelectedGameId("");
}, [resetLaunchRuntime]);

// Logout handler
const handleLogout = useCallback(() => {
setLogoutConfirmOpen(true);
}, []);

// Load games handler
const loadGames = useCallback(async (targetSource: GameSource) => {
setIsLoadingGames(true);
Expand Down Expand Up @@ -2913,6 +2920,74 @@ export function App(): JSX.Element {
setQueueModalData(null);
}, []);

useEffect(() => {
if (!logoutConfirmOpen) return;

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setLogoutConfirmOpen(false);
}
if (event.key === "Enter") {
event.preventDefault();
void confirmLogout();
}
};

window.addEventListener("keydown", handleKeyDown);
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";

return () => {
window.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = previousOverflow;
};
}, [confirmLogout, logoutConfirmOpen]);

const logoutConfirmModal = logoutConfirmOpen && typeof document !== "undefined"
? createPortal(
<div className="logout-confirm" role="dialog" aria-modal="true" aria-label="Log out confirmation">
<button
type="button"
className="logout-confirm-backdrop"
onClick={() => setLogoutConfirmOpen(false)}
aria-label="Cancel log out"
/>
<div className="logout-confirm-card">
<div className="logout-confirm-kicker">Session</div>
<h3 className="logout-confirm-title">Log out of OpenNOW?</h3>
<p className="logout-confirm-text">
You&apos;re about to sign out of this device and return to guest mode.
</p>
<p className="logout-confirm-subtext">
Your cloud session data stays on the service. This just clears your local app session.
</p>
<div className="logout-confirm-actions">
<button
type="button"
className="logout-confirm-btn logout-confirm-btn-cancel"
onClick={() => setLogoutConfirmOpen(false)}
>
Stay Signed In
</button>
<button
type="button"
className="logout-confirm-btn logout-confirm-btn-confirm"
onClick={() => {
void confirmLogout();
}}
>
Log Out
</button>
</div>
<div className="logout-confirm-hint">
<kbd>Enter</kbd> confirm · <kbd>Esc</kbd> cancel
</div>
</div>
</div>,
document.body,
)
: null;

const handleResumeFromNavbar = useCallback(async () => {
if (
!selectedProvider
Expand Down Expand Up @@ -3785,6 +3860,7 @@ export function App(): JSX.Element {
</div>
)}

{logoutConfirmModal}
{queueModalGame && streamStatus === "idle" && (
<QueueServerSelectModal
game={queueModalGame}
Expand Down
159 changes: 159 additions & 0 deletions opennow-stable/src/renderer/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,165 @@ body,
font-size: 0.78rem;
}

.logout-confirm {
position: fixed;
inset: 0;
z-index: 3250;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}

.logout-confirm-backdrop {
position: absolute;
inset: 0;
border: none;
background:
radial-gradient(circle at 18% 18%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 38%),
radial-gradient(circle at 82% 76%, rgba(248, 113, 113, 0.16), transparent 40%),
linear-gradient(180deg, rgba(8, 10, 12, 0.74), rgba(5, 6, 8, 0.82));
backdrop-filter: blur(14px) saturate(135%);
cursor: pointer;
}

.logout-confirm-card {
position: relative;
z-index: 1;
width: min(480px, calc(100vw - 32px));
padding: 22px;
border-radius: 18px;
border: 1px solid color-mix(in srgb, var(--panel-border-solid) 70%, rgba(255, 255, 255, 0.08));
background:
linear-gradient(180deg, rgba(19, 22, 25, 0.94), rgba(10, 12, 14, 0.96)),
rgba(12, 14, 16, 0.94);
box-shadow:
0 30px 90px rgba(0, 0, 0, 0.56),
0 0 0 1px rgba(255, 255, 255, 0.03) inset,
0 0 0 1px color-mix(in srgb, var(--error) 18%, transparent);
backdrop-filter: blur(24px) saturate(140%);
overflow: hidden;
animation: fade-in 170ms var(--ease);
}

.logout-confirm-card::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 3px;
background: linear-gradient(90deg, var(--accent), var(--error), #f59e0b);
opacity: 0.95;
}

.logout-confirm-kicker {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
background: color-mix(in srgb, var(--error) 14%, rgba(255, 255, 255, 0.03));
border: 1px solid color-mix(in srgb, var(--error) 34%, transparent);
color: #ffd4d4;
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}

.logout-confirm-title {
margin: 16px 0 0;
font-size: 1.45rem;
line-height: 1.1;
font-weight: 800;
color: var(--ink);
}

.logout-confirm-text {
margin: 12px 0 0;
font-size: 0.97rem;
line-height: 1.55;
color: var(--ink-soft);
}

.logout-confirm-subtext {
margin: 8px 0 0;
font-size: 0.83rem;
line-height: 1.5;
color: var(--ink-muted);
}

.logout-confirm-actions {
display: flex;
gap: 10px;
margin-top: 18px;
}

.logout-confirm-btn {
flex: 1;
min-height: 44px;
border-radius: 12px;
border: 1px solid var(--panel-border);
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
letter-spacing: 0.01em;
cursor: pointer;
transition:
transform var(--t-fast),
border-color var(--t-fast),
background var(--t-fast),
color var(--t-fast),
box-shadow var(--t-fast);
}

.logout-confirm-btn:hover {
transform: translateY(-1px);
}

.logout-confirm-btn:active {
transform: translateY(0);
}

.logout-confirm-btn-cancel {
background: rgba(255, 255, 255, 0.03);
color: var(--ink);
}

.logout-confirm-btn-cancel:hover {
border-color: var(--panel-border-solid);
background: rgba(255, 255, 255, 0.07);
}

.logout-confirm-btn-confirm {
border-color: color-mix(in srgb, var(--error) 62%, transparent);
background:
linear-gradient(135deg, color-mix(in srgb, var(--error) 78%, #7f1d1d), color-mix(in srgb, var(--error) 92%, #f97316));
color: #fff5f5;
box-shadow: 0 12px 28px rgba(248, 113, 113, 0.22);
}

.logout-confirm-btn-confirm:hover {
border-color: color-mix(in srgb, var(--error) 85%, transparent);
box-shadow: 0 16px 32px rgba(248, 113, 113, 0.28);
}

.logout-confirm-hint {
margin-top: 14px;
color: var(--ink-muted);
font-size: 0.73rem;
}

.logout-confirm-hint kbd {
display: inline-block;
margin: 0 2px;
padding: 2px 6px;
border-radius: 5px;
border: 1px solid var(--panel-border-solid);
background: rgba(255, 255, 255, 0.04);
color: var(--ink-soft);
font-size: 0.69rem;
}

.navbar-modal-backdrop {
position: fixed;
inset: 0;
Expand Down
Loading