Download Directory
diff --git a/apps/web/src/store/slices/uiSlice.ts b/apps/web/src/store/slices/uiSlice.ts
index f603715..f495a95 100644
--- a/apps/web/src/store/slices/uiSlice.ts
+++ b/apps/web/src/store/slices/uiSlice.ts
@@ -7,6 +7,8 @@ type UIActions = {
setStickyPlatform: (platform: string) => void;
setStickyRegion: (region: string) => void;
setTheme: (theme: "light" | "dark") => void;
+ setBigPictureMode: (enabled: boolean) => void;
+ setLaunchInBigPicture: (enabled: boolean) => void;
};
export type UIStore = UIState & UIActions;
@@ -42,7 +44,9 @@ const initialState: UIState = {
gridColumns: 3,
stickyPlatform: "",
stickyRegion: "",
- theme: getInitialTheme()
+ theme: getInitialTheme(),
+ bigPictureMode: false,
+ launchInBigPicture: false
};
export const useUIStore = create()(
@@ -53,7 +57,9 @@ export const useUIStore = create()(
setGridColumns: (columns) => set({ gridColumns: columns }),
setStickyPlatform: (platform) => set({ stickyPlatform: platform }),
setStickyRegion: (region) => set({ stickyRegion: region }),
- setTheme: (theme) => set({ theme })
+ setTheme: (theme) => set({ theme }),
+ setBigPictureMode: (enabled) => set({ bigPictureMode: enabled }),
+ setLaunchInBigPicture: (enabled) => set({ launchInBigPicture: enabled })
}),
{
name: "crocdesk-ui-storage",
@@ -61,7 +67,9 @@ export const useUIStore = create()(
stickyPlatform: state.stickyPlatform,
stickyRegion: state.stickyRegion,
gridColumns: state.gridColumns,
- theme: state.theme
+ theme: state.theme,
+ bigPictureMode: state.bigPictureMode,
+ launchInBigPicture: state.launchInBigPicture
})
}
)
diff --git a/apps/web/src/store/types.ts b/apps/web/src/store/types.ts
index 5e17435..d4df04f 100644
--- a/apps/web/src/store/types.ts
+++ b/apps/web/src/store/types.ts
@@ -33,6 +33,8 @@ export type UIState = {
stickyPlatform: string;
stickyRegion: string;
theme: "light" | "dark";
+ bigPictureMode: boolean;
+ launchInBigPicture: boolean;
};
diff --git a/apps/web/src/styles/big-picture.css b/apps/web/src/styles/big-picture.css
new file mode 100644
index 0000000..9881d8a
--- /dev/null
+++ b/apps/web/src/styles/big-picture.css
@@ -0,0 +1,343 @@
+/* Big Picture Mode Styles */
+
+.big-picture-mode {
+ position: fixed;
+ inset: 0;
+ background: linear-gradient(135deg, #0a0e27 0%, #1a1d3a 100%);
+ color: #ffffff;
+ display: grid;
+ grid-template-columns: 300px 1fr;
+ grid-template-rows: 1fr auto;
+ font-family: "Space Grotesk", sans-serif;
+ overflow: hidden;
+ z-index: 9999;
+}
+
+/* Sidebar Navigation */
+.bp-sidebar {
+ grid-row: 1 / -1;
+ background: linear-gradient(180deg, rgba(20, 30, 60, 0.9) 0%, rgba(10, 15, 35, 0.9) 100%);
+ backdrop-filter: blur(10px);
+ padding: 40px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ border-right: 2px solid rgba(61, 184, 117, 0.3);
+ box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
+}
+
+.bp-logo {
+ font-size: 48px;
+ font-weight: 700;
+ text-align: center;
+ background: linear-gradient(135deg, #3db875 0%, #2d8659 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-shadow: 0 0 30px rgba(61, 184, 117, 0.5);
+}
+
+.bp-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ flex: 1;
+}
+
+.bp-nav-item {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ padding: 24px 28px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 3px solid transparent;
+ border-radius: 12px;
+ color: #ffffff;
+ font-size: 28px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-align: left;
+}
+
+.bp-nav-item:hover {
+ background: rgba(61, 184, 117, 0.2);
+ border-color: rgba(61, 184, 117, 0.4);
+ transform: translateX(8px);
+}
+
+.bp-nav-item.focused {
+ background: linear-gradient(135deg, rgba(61, 184, 117, 0.3) 0%, rgba(45, 134, 89, 0.3) 100%);
+ border-color: #3db875;
+ box-shadow: 0 0 30px rgba(61, 184, 117, 0.6), inset 0 0 20px rgba(61, 184, 117, 0.2);
+ transform: translateX(12px) scale(1.05);
+}
+
+.bp-nav-icon {
+ font-size: 36px;
+ width: 48px;
+ text-align: center;
+}
+
+.bp-nav-label {
+ flex: 1;
+}
+
+/* Content Area */
+.bp-content {
+ padding: 40px 60px;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.bp-header {
+ margin-bottom: 40px;
+}
+
+.bp-title {
+ font-size: 64px;
+ font-weight: 700;
+ color: #ffffff;
+ text-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
+ margin: 0;
+}
+
+/* Game Grid */
+.bp-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 32px;
+ padding-bottom: 40px;
+}
+
+.bp-game-card {
+ background: rgba(255, 255, 255, 0.08);
+ border: 4px solid transparent;
+ border-radius: 16px;
+ overflow: hidden;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ aspect-ratio: 3 / 4;
+ display: flex;
+ flex-direction: column;
+}
+
+.bp-game-card:hover {
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(61, 184, 117, 0.5);
+ transform: translateY(-8px);
+}
+
+.bp-game-card.focused {
+ background: rgba(61, 184, 117, 0.2);
+ border-color: #3db875;
+ box-shadow: 0 0 40px rgba(61, 184, 117, 0.8), inset 0 0 30px rgba(61, 184, 117, 0.2);
+ transform: translateY(-12px) scale(1.08);
+}
+
+.bp-game-cover {
+ flex: 1;
+ position: relative;
+ overflow: hidden;
+ background: linear-gradient(135deg, rgba(61, 184, 117, 0.1) 0%, rgba(45, 134, 89, 0.1) 100%);
+}
+
+.bp-game-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.bp-game-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 72px;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.3);
+ background: linear-gradient(135deg, rgba(20, 30, 60, 0.8) 0%, rgba(10, 15, 35, 0.8) 100%);
+}
+
+.bp-game-info {
+ padding: 20px;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.bp-game-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.bp-game-platform {
+ font-size: 18px;
+ color: rgba(255, 255, 255, 0.7);
+ font-weight: 500;
+}
+
+/* Loading & Empty States */
+.bp-loading,
+.bp-empty,
+.bp-section-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 40px;
+ text-align: center;
+}
+
+.bp-loading {
+ font-size: 32px;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.bp-empty p {
+ font-size: 36px;
+ margin: 0 0 20px 0;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.bp-hint {
+ font-size: 24px;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.bp-section-placeholder {
+ font-size: 36px;
+ color: rgba(255, 255, 255, 0.6);
+ min-height: 400px;
+}
+
+/* Welcome Screen */
+.bp-welcome {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 60px 40px;
+}
+
+.bp-welcome h2 {
+ font-size: 48px;
+ font-weight: 700;
+ color: #3db875;
+ margin: 0 0 40px 0;
+ text-shadow: 0 0 20px rgba(61, 184, 117, 0.5);
+}
+
+.bp-welcome p {
+ font-size: 28px;
+ color: rgba(255, 255, 255, 0.8);
+ margin: 0 0 30px 0;
+ line-height: 1.6;
+}
+
+.bp-controls {
+ list-style: none;
+ padding: 0;
+ margin: 40px 0 0 0;
+}
+
+.bp-controls li {
+ font-size: 26px;
+ color: rgba(255, 255, 255, 0.9);
+ padding: 16px 0;
+ border-bottom: 2px solid rgba(255, 255, 255, 0.1);
+}
+
+.bp-controls li:last-child {
+ border-bottom: none;
+}
+
+.bp-controls strong {
+ color: #3db875;
+ font-weight: 600;
+ margin-right: 12px;
+}
+
+/* Footer */
+.bp-footer {
+ grid-column: 2;
+ padding: 24px 60px;
+ background: rgba(10, 15, 35, 0.8);
+ border-top: 2px solid rgba(61, 184, 117, 0.3);
+}
+
+.bp-hint-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 40px;
+}
+
+.bp-hint-bar .bp-hint {
+ font-size: 20px;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+/* Scrollbar Styling */
+.bp-content::-webkit-scrollbar {
+ width: 12px;
+}
+
+.bp-content::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.bp-content::-webkit-scrollbar-thumb {
+ background: rgba(61, 184, 117, 0.5);
+ border-radius: 6px;
+}
+
+.bp-content::-webkit-scrollbar-thumb:hover {
+ background: rgba(61, 184, 117, 0.7);
+}
+
+/* Responsive adjustments for 4K */
+@media (min-width: 2560px) {
+ .big-picture-mode {
+ grid-template-columns: 400px 1fr;
+ }
+
+ .bp-logo {
+ font-size: 64px;
+ }
+
+ .bp-nav-item {
+ padding: 32px 36px;
+ font-size: 36px;
+ }
+
+ .bp-nav-icon {
+ font-size: 48px;
+ }
+
+ .bp-title {
+ font-size: 96px;
+ }
+
+ .bp-grid {
+ grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
+ gap: 48px;
+ }
+
+ .bp-game-title {
+ font-size: 32px;
+ }
+
+ .bp-game-platform {
+ font-size: 24px;
+ }
+}
+
+/* Safe area margins for TV overscan */
+@media (display-mode: fullscreen) {
+ .big-picture-mode {
+ padding: 3vh 3vw;
+ }
+}
From b489554a06e262d57eeae916fc6bed17e0a05169 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 28 Dec 2025 16:20:01 +0000
Subject: [PATCH 03/10] Add tests for Big Picture Mode and finalize
implementation
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
.../pages/__tests__/BigPicturePage.spec.tsx | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 apps/web/src/pages/__tests__/BigPicturePage.spec.tsx
diff --git a/apps/web/src/pages/__tests__/BigPicturePage.spec.tsx b/apps/web/src/pages/__tests__/BigPicturePage.spec.tsx
new file mode 100644
index 0000000..21361c8
--- /dev/null
+++ b/apps/web/src/pages/__tests__/BigPicturePage.spec.tsx
@@ -0,0 +1,93 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import BigPicturePage from '../BigPicturePage';
+
+// Mock the useUIStore hook
+vi.mock('../../store', () => ({
+ useUIStore: vi.fn(() => ({
+ setBigPictureMode: vi.fn()
+ }))
+}));
+
+// Mock the API module
+vi.mock('../../lib/api', () => ({
+ apiGet: vi.fn(() => Promise.resolve([]))
+}));
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+describe('BigPicturePage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders the Big Picture welcome screen', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getAllByText('Welcome to Big Picture Mode')[0]).toBeInTheDocument();
+ });
+
+ it('renders navigation menu with all sections', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getAllByText('Home')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('Library')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('Search')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('Downloads')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('Settings')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('Exit')[0]).toBeInTheDocument();
+ });
+
+ it('displays controller instructions', () => {
+ render(
+
+
+
+ );
+
+ const dpadInstructions = screen.getAllByText(/D-Pad \/ Arrows:/);
+ const selectInstructions = screen.getAllByText(/A \/ Enter:/);
+ const backInstructions = screen.getAllByText(/B \/ Escape:/);
+
+ expect(dpadInstructions.length).toBeGreaterThan(0);
+ expect(selectInstructions.length).toBeGreaterThan(0);
+ expect(backInstructions.length).toBeGreaterThan(0);
+ });
+
+ it('renders the Jacare logo', () => {
+ render(
+
+
+
+ );
+
+ const logos = screen.getAllByText('Jacare');
+ expect(logos.length).toBeGreaterThan(0);
+ });
+});
From 2ee14a6e255a638208edfb2ad627e3ba4c14b932 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 28 Dec 2025 16:23:11 +0000
Subject: [PATCH 04/10] Fix code review issues - add constants and imports
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
apps/web/src/hooks/useGamepad.ts | 9 +++++----
apps/web/src/pages/BigPicturePage.tsx | 8 ++++++--
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/apps/web/src/hooks/useGamepad.ts b/apps/web/src/hooks/useGamepad.ts
index a7dbcfb..724f565 100644
--- a/apps/web/src/hooks/useGamepad.ts
+++ b/apps/web/src/hooks/useGamepad.ts
@@ -8,6 +8,10 @@ export type GamepadState = {
axes: { leftX: number; leftY: number; rightX: number; rightY: number };
};
+// Constants
+const AXIS_THRESHOLD = 0.5;
+const NAVIGATE_COOLDOWN_MS = 200; // Cooldown between navigation events to prevent double-input
+
const BUTTON_MAP: Record = {
0: "A", // Cross (PS) / A (Xbox)
1: "B", // Circle (PS) / B (Xbox)
@@ -27,8 +31,6 @@ const BUTTON_MAP: Record = {
15: "DPAD_RIGHT"
};
-const AXIS_THRESHOLD = 0.5;
-
export function useGamepad() {
const [gamepadState, setGamepadState] = useState({
connected: false,
@@ -125,7 +127,6 @@ export function useGamepadNavigation(onNavigate: (direction: "up" | "down" | "le
const gamepadState = useGamepad();
const prevButtonsRef = useRef>({} as Record);
const lastNavigateTimeRef = useRef(0);
- const NAVIGATE_COOLDOWN = 200; // ms between navigation events
useEffect(() => {
const now = Date.now();
@@ -137,7 +138,7 @@ export function useGamepadNavigation(onNavigate: (direction: "up" | "down" | "le
};
// Navigation with cooldown
- if (now - lastNavigateTimeRef.current > NAVIGATE_COOLDOWN) {
+ if (now - lastNavigateTimeRef.current > NAVIGATE_COOLDOWN_MS) {
if (buttons.DPAD_UP) {
onNavigate("up");
lastNavigateTimeRef.current = now;
diff --git a/apps/web/src/pages/BigPicturePage.tsx b/apps/web/src/pages/BigPicturePage.tsx
index dca7bf4..d54bbf1 100644
--- a/apps/web/src/pages/BigPicturePage.tsx
+++ b/apps/web/src/pages/BigPicturePage.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from "react";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "../lib/api";
import { useUIStore } from "../store";
@@ -9,8 +9,12 @@ import "../styles/big-picture.css";
type NavSection = "home" | "library" | "search" | "downloads" | "settings" | "exit";
+// Grid configuration
+const ITEMS_PER_ROW = 4; // Matches the CSS grid-template-columns for Big Picture
+
export default function BigPicturePage() {
const navigate = useNavigate();
+ const location = useLocation();
const setBigPictureMode = useUIStore((state) => state.setBigPictureMode);
const [activeSection, setActiveSection] = useState("home");
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -65,7 +69,7 @@ export default function BigPicturePage() {
}
} else {
// Navigate content grid
- const itemsPerRow = 4;
+ const itemsPerRow = ITEMS_PER_ROW;
const totalItems = libraryItems.length;
if (direction === "left") {
From 3e7f08d975fc37af2d3e75562a4d9cd1205bfc26 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 28 Dec 2025 16:34:22 +0000
Subject: [PATCH 05/10] Fix linting issues - add location to useCallback deps
and wrap libraryItems in useMemo
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
apps/web/src/pages/BigPicturePage.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/web/src/pages/BigPicturePage.tsx b/apps/web/src/pages/BigPicturePage.tsx
index d54bbf1..d68bda0 100644
--- a/apps/web/src/pages/BigPicturePage.tsx
+++ b/apps/web/src/pages/BigPicturePage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useRef, useCallback } from "react";
+import { useEffect, useState, useRef, useCallback, useMemo } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "../lib/api";
@@ -27,7 +27,7 @@ export default function BigPicturePage() {
queryFn: () => apiGet("/library/items")
});
- const libraryItems = libraryQuery.data || [];
+ const libraryItems = useMemo(() => libraryQuery.data || [], [libraryQuery.data]);
// Enable fullscreen on mount if running in Electron
useEffect(() => {
@@ -105,7 +105,7 @@ export default function BigPicturePage() {
navigate(`/library/item?id=${item.id}`, { state: { backgroundLocation: location } });
}
}
- }, [isSidebarFocused, activeSection, selectedIndex, libraryItems, exitBigPicture, navigate]);
+ }, [isSidebarFocused, activeSection, selectedIndex, libraryItems, exitBigPicture, navigate, location]);
// Handle back
const handleBack = useCallback(() => {
From 83b579565e434b0d247b70c23b3cb175a3ce73f9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 28 Dec 2025 16:40:11 +0000
Subject: [PATCH 06/10] Add coherent animations and e2e test coverage for Big
Picture mode
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
apps/web/src/pages/BigPicturePage.tsx | 2 +-
apps/web/src/styles/big-picture.css | 84 +++++++++++-
tests/e2e/big-picture.spec.ts | 179 ++++++++++++++++++++++++++
3 files changed, 262 insertions(+), 3 deletions(-)
create mode 100644 tests/e2e/big-picture.spec.ts
diff --git a/apps/web/src/pages/BigPicturePage.tsx b/apps/web/src/pages/BigPicturePage.tsx
index d68bda0..39ca800 100644
--- a/apps/web/src/pages/BigPicturePage.tsx
+++ b/apps/web/src/pages/BigPicturePage.tsx
@@ -230,7 +230,7 @@ export default function BigPicturePage() {