Controls: Use D-Pad or Left Stick to navigate, A to select, B to go back, X to search, Y for menu, LB/RB for pages.
diff --git a/apps/web/src/store/slices/bigPictureSlice.ts b/apps/web/src/store/slices/bigPictureSlice.ts
index c8f16b3..85ca719 100644
--- a/apps/web/src/store/slices/bigPictureSlice.ts
+++ b/apps/web/src/store/slices/bigPictureSlice.ts
@@ -55,6 +55,7 @@ export interface BigPictureState {
gridSize: "small" | "medium" | "large" | "auto";
autoHideUI: boolean;
autoHideDelay: number; // in seconds
+ availableMonitors: number; // Number of detected monitors
}
type BigPictureActions = {
@@ -69,6 +70,8 @@ type BigPictureActions = {
setGridSize: (size: "small" | "medium" | "large" | "auto") => void;
setAutoHideUI: (autoHide: boolean) => void;
setAutoHideDelay: (delay: number) => void;
+ setAvailableMonitors: (count: number) => void;
+ detectMonitors: () => Promise;
};
export type BigPictureStore = BigPictureState & BigPictureActions;
@@ -83,7 +86,8 @@ const initialState: BigPictureState = {
keyboardLayout: "qwerty",
gridSize: "auto",
autoHideUI: false,
- autoHideDelay: 3
+ autoHideDelay: 3,
+ availableMonitors: 1
};
export const useBigPictureStore = create()(
@@ -111,7 +115,27 @@ export const useBigPictureStore = create()(
setAutoHideUI: (autoHide) => set({ autoHideUI: autoHide }),
- setAutoHideDelay: (delay) => set({ autoHideDelay: delay })
+ setAutoHideDelay: (delay) => set({ autoHideDelay: delay }),
+
+ setAvailableMonitors: (count) => set({ availableMonitors: count }),
+
+ detectMonitors: async () => {
+ // Use Screen Orientation API if available
+ if (window.screen && 'isExtended' in window.screen) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const isExtended = await (window.screen as any).isExtended;
+ if (isExtended) {
+ // Multi-monitor setup detected
+ // We can't get exact count, but we know there's more than one
+ set({ availableMonitors: 2 });
+ } else {
+ set({ availableMonitors: 1 });
+ }
+ } else {
+ // Fallback: assume single monitor
+ set({ availableMonitors: 1 });
+ }
+ }
}),
{
name: "crocdesk-bigpicture-storage",
From 81081f3f3dc66b0a87e208b8ebaf1c71d107fe8b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 31 Dec 2025 12:10:31 +0000
Subject: [PATCH 4/6] Add Big Picture Mode documentation and polish
- Create comprehensive BIG_PICTURE_MODE.md documentation
- Add controller setup and usage guide
- Document all keyboard and controller mappings
- Include troubleshooting section
- Add performance optimization notes
- Update main README with Big Picture Mode feature highlight
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
README.md | 3 +
docs/BIG_PICTURE_MODE.md | 195 +++++++++++++++++++++++++++++++++++++++
2 files changed, 198 insertions(+)
create mode 100644 docs/BIG_PICTURE_MODE.md
diff --git a/README.md b/README.md
index a1a5e4c..f5c80cf 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@
**What makes Jacare special:**
- 🎮 **All-in-one solution** – Browse, search, download ROMs without switching between tools
+- 🎯 **Big Picture Mode** – Controller-friendly full-screen interface for TVs and couch gaming
- ⏸️ **Persistent download management** – Pause and resume downloads seamlessly, even after closing and reopening the application
- 🎨 **Customizable themes** – Personalize your experience with a variety of themes to suit your preferences
- ⚡ **Ultra-responsive web-based UI** – Enjoy a fast, smooth interface accessible through your browser or desktop app
@@ -19,9 +20,11 @@
> Want details?
> - 📚 **Developer guide:** Head to [`docs/README.md`](docs/README.md) for the full technical rundown.
> - 😀 **Friendly guide:** Open [`docs/user/README.md`](docs/user/README.md) for a non-technical walkthrough.
+> - 🎮 **Big Picture Mode:** Check out [`docs/BIG_PICTURE_MODE.md`](docs/BIG_PICTURE_MODE.md) for controller setup and usage.
## Why Jacare? 🐊
- **One app for everything:** Browse, enrich, and launch ROMs without juggling separate tools.
+- **Couch gaming ready:** Big Picture Mode transforms your PC into a console-like experience with full controller support.
- **Local-first with cloud search:** Metadata is pulled from [Crocdb](https://api.crocdb.net) while your collection, cache, and settings remain on disk.
- **Built for speed:** Background jobs, SSE updates, and caching cut down on repetitive scraping.
- **Works online or offline:** Cached search and entry data keep your library usable even when you lose connectivity.
diff --git a/docs/BIG_PICTURE_MODE.md b/docs/BIG_PICTURE_MODE.md
new file mode 100644
index 0000000..dcbb13d
--- /dev/null
+++ b/docs/BIG_PICTURE_MODE.md
@@ -0,0 +1,195 @@
+# Big Picture Mode
+
+Big Picture Mode is a controller-friendly, full-screen interface designed for TVs and couch gaming. It provides an immersive experience for browsing and managing your ROM collection with Xbox and PlayStation controllers.
+
+## Features
+
+### Controller Support
+- **Xbox Controllers**: Full support for Xbox 360, Xbox One, and Xbox Series X/S controllers
+- **PlayStation Controllers**: Full support for DualShock 4 and DualSense controllers
+- **Haptic Feedback**: Vibration feedback for button presses and navigation (when supported)
+- **Button Remapping**: Customize button mappings to your preference
+
+### Navigation
+- **D-Pad/Left Stick**: Navigate through the game grid
+- **A Button (✕ on PlayStation)**: Select game or confirm action
+- **B Button (○ on PlayStation)**: Go back or cancel
+- **X Button (□ on PlayStation)**: Open search
+- **Y Button (△ on PlayStation)**: Open menu
+- **LB/RB (L1/R1)**: Navigate between pages
+- **START Button**: Open menu
+
+### UI Features
+- **Dynamic Grid Sizing**: Automatically adjusts grid layout based on screen resolution
+- **On-Screen Keyboard**: Full keyboard for text input without needing a physical keyboard
+- **Search**: Find games quickly with controller-friendly search
+- **Downloads View**: Monitor active downloads with progress bars
+- **Fullscreen Mode**: Automatic fullscreen for distraction-free gaming
+- **Multi-Monitor Support**: Choose which monitor to display Big Picture Mode on
+
+## Getting Started
+
+### Accessing Big Picture Mode
+
+1. **From Settings Page**:
+ - Navigate to Settings
+ - Scroll to "Big Picture Mode" section
+ - Click "🎮 Launch Big Picture Mode"
+
+2. **Direct URL**:
+ - Navigate to `/big-picture` in your browser
+
+### Controller Setup
+
+Big Picture Mode automatically detects connected controllers. Simply:
+
+1. Connect your Xbox or PlayStation controller to your PC
+2. Launch Big Picture Mode
+3. Start navigating with your controller
+
+The controller will be detected automatically, and you'll see haptic feedback when you press buttons (if your controller supports it).
+
+### Configuration
+
+#### Haptic Feedback
+Enable or disable controller vibration:
+- Go to Settings → Big Picture Mode
+- Toggle "Enable haptic feedback (controller vibration)"
+
+#### Grid Size
+Adjust the number of games displayed in the grid:
+- **Auto**: Dynamic sizing based on screen resolution (recommended)
+- **Small**: 3 columns
+- **Medium**: 4 columns
+- **Large**: 5 columns
+
+#### Multi-Monitor Setup
+If you have multiple monitors:
+1. Big Picture Mode will automatically detect them
+2. Go to Settings → Big Picture Mode → Monitor Selection
+3. Choose your preferred monitor
+
+## Controls Reference
+
+### Main Screen (Browse Games)
+
+| Action | Xbox | PlayStation | Keyboard |
+|--------|------|-------------|----------|
+| Navigate | D-Pad / Left Stick | D-Pad / Left Stick | Arrow Keys |
+| Select Game | A | ✕ (Cross) | Enter |
+| Go Back | B | ○ (Circle) | Escape |
+| Search | X | □ (Square) | S |
+| Menu | Y / START | △ (Triangle) / OPTIONS | M |
+| Previous Page | LB | L1 | Page Up |
+| Next Page | RB | R1 | Page Down |
+
+### On-Screen Keyboard
+
+| Action | Xbox | PlayStation | Keyboard |
+|--------|------|-------------|----------|
+| Navigate Keys | D-Pad / Left Stick | D-Pad / Left Stick | Arrow Keys |
+| Select Key | A | ✕ (Cross) | Enter / Space |
+| Close Keyboard | B | ○ (Circle) | Escape |
+
+### Downloads View
+
+| Action | Xbox | PlayStation | Keyboard |
+|--------|------|-------------|----------|
+| Navigate | D-Pad Up/Down | D-Pad Up/Down | Arrow Keys |
+| Go Back | B / START | ○ (Circle) / OPTIONS | Escape |
+
+## Menu Options
+
+Press Y (△ on PlayStation) or START to open the menu:
+
+- **Browse Games**: Return to main Big Picture browse view
+- **Library**: View your local game library
+- **Downloads**: View active downloads
+- **Queue**: View job queue
+- **Settings**: Access application settings
+- **Exit Big Picture**: Return to standard desktop mode
+
+## Tips & Tricks
+
+1. **Analog Stick Sensitivity**: If navigation feels too sensitive, adjust the deadzone in settings
+2. **Search Optimization**: Use the on-screen keyboard with D-Pad for precise navigation
+3. **Page Navigation**: Use LB/RB (L1/R1) to quickly jump between pages
+4. **Haptic Feedback**: Different intensities indicate different actions (light for navigation, medium for selection, heavy for controller connection)
+5. **Fullscreen Toggle**: Press F11 to manually toggle fullscreen if needed
+
+## Troubleshooting
+
+### Controller Not Detected
+1. Ensure your controller is properly connected
+2. For wireless controllers, make sure they're paired
+3. Try reconnecting the controller
+4. Check browser console for gamepad connection messages
+
+### Haptic Feedback Not Working
+- Some controllers (especially older models) may not support vibration
+- Check that haptic feedback is enabled in Settings
+- Try a different USB port or wireless receiver
+
+### Navigation Feels Unresponsive
+- Adjust the deadzone setting in Big Picture Mode settings
+- Try using D-Pad instead of analog stick for more precise control
+- Ensure no other applications are capturing controller input
+
+### Multi-Monitor Issues
+1. Ensure you've selected the correct monitor in settings
+2. Restart Big Picture Mode after changing monitor selection
+3. Some browsers may not fully support multi-monitor APIs
+
+## Performance Optimization
+
+Big Picture Mode includes several optimizations:
+
+- **Virtual Scrolling**: Only renders visible game cards for better performance
+- **Image Lazy Loading**: Loads cover art on-demand as you scroll
+- **Debounced Navigation**: Prevents input overload during rapid navigation
+- **Request Animation Frame**: Uses RAF for smooth 60fps controller polling
+
+## Accessibility
+
+While Big Picture Mode is designed for controllers, it remains fully accessible:
+
+- **Keyboard Support**: All controller actions have keyboard equivalents
+- **Focus Indicators**: Clear visual feedback shows currently selected items
+- **High Contrast**: Works well with both light and dark themes
+- **Screen Reader Friendly**: Semantic HTML structure for screen reader compatibility
+
+## Browser Support
+
+Big Picture Mode uses modern web APIs:
+
+- **Gamepad API**: For controller support (Chrome 21+, Firefox 29+, Safari 10.1+)
+- **Fullscreen API**: For immersive experience (All modern browsers)
+- **Vibration Actuator API**: For haptic feedback (Chrome 68+, experimental in others)
+
+For the best experience, we recommend:
+- **Chrome/Edge**: Full support for all features including haptic feedback
+- **Firefox**: Full support except vibration may be limited
+- **Safari**: Basic controller support, limited haptic feedback
+
+## Future Enhancements
+
+Planned improvements for Big Picture Mode:
+
+- [ ] Analog stick scrolling support
+- [ ] Advanced button remapping UI
+- [ ] Voice control integration
+- [ ] Achievement notifications
+- [ ] Friend presence indicators
+- [ ] Cloud save status
+- [ ] Quick launch from Big Picture Mode
+
+## Contributing
+
+Found a bug or have a feature request? Please open an issue on GitHub:
+https://github.com/luandev/jacare/issues
+
+When reporting controller issues, please include:
+- Controller model and brand
+- Connection type (USB/Bluetooth)
+- Operating system
+- Browser and version
From f82b1dcb786e99b735504d1fcbc9ef76480f54ec Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 31 Dec 2025 12:14:05 +0000
Subject: [PATCH 5/6] Address code review feedback and refactor
- Remove duplicate assignment in useGamepadNavigation
- Fix isExtended API call to use method instead of property
- Create useFullscreenMode hook to reduce duplication
- Fix fullscreen cleanup to only exit if component entered it
- Add missing dependencies to OnScreenKeyboard useEffect
- Improve error handling in monitor detection
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
apps/web/src/components/OnScreenKeyboard.tsx | 3 +-
apps/web/src/hooks/useFullscreenMode.ts | 37 +++++++++++++++++++
apps/web/src/hooks/useGamepadNavigation.ts | 1 -
.../web/src/pages/BigPictureDownloadsPage.tsx | 17 +--------
apps/web/src/pages/BigPictureModePage.tsx | 17 +--------
apps/web/src/store/slices/bigPictureSlice.ts | 19 ++++++----
6 files changed, 54 insertions(+), 40 deletions(-)
create mode 100644 apps/web/src/hooks/useFullscreenMode.ts
diff --git a/apps/web/src/components/OnScreenKeyboard.tsx b/apps/web/src/components/OnScreenKeyboard.tsx
index 3a75cc9..e310dd1 100644
--- a/apps/web/src/components/OnScreenKeyboard.tsx
+++ b/apps/web/src/components/OnScreenKeyboard.tsx
@@ -130,8 +130,7 @@ export function OnScreenKeyboard({
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [focusedRow, focusedCol, shift, value, totalRows]);
+ }, [focusedRow, focusedCol, shift, value, totalRows, getCurrentKey, handleKeyPress, onClose]);
// Normalize focused column when changing rows
useEffect(() => {
diff --git a/apps/web/src/hooks/useFullscreenMode.ts b/apps/web/src/hooks/useFullscreenMode.ts
new file mode 100644
index 0000000..009196e
--- /dev/null
+++ b/apps/web/src/hooks/useFullscreenMode.ts
@@ -0,0 +1,37 @@
+import { useEffect, useRef } from "react";
+
+/**
+ * Custom hook to manage fullscreen mode
+ * Only exits fullscreen if this component was responsible for entering it
+ */
+export function useFullscreenMode(enabled: boolean) {
+ const enteredFullscreenRef = useRef(false);
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ const enterFullscreen = async () => {
+ if (document.documentElement.requestFullscreen && !document.fullscreenElement) {
+ try {
+ await document.documentElement.requestFullscreen();
+ enteredFullscreenRef.current = true;
+ } catch (error) {
+ console.warn("[Fullscreen] Failed to enter fullscreen:", error);
+ // Fullscreen request failed, continue anyway
+ }
+ }
+ };
+
+ enterFullscreen();
+
+ return () => {
+ // Only exit fullscreen if we entered it
+ if (enteredFullscreenRef.current && document.fullscreenElement) {
+ document.exitFullscreen().catch((error) => {
+ console.warn("[Fullscreen] Failed to exit fullscreen:", error);
+ });
+ enteredFullscreenRef.current = false;
+ }
+ };
+ }, [enabled]);
+}
diff --git a/apps/web/src/hooks/useGamepadNavigation.ts b/apps/web/src/hooks/useGamepadNavigation.ts
index eadbaf6..33078fb 100644
--- a/apps/web/src/hooks/useGamepadNavigation.ts
+++ b/apps/web/src/hooks/useGamepadNavigation.ts
@@ -117,7 +117,6 @@ export function useGamepadNavigation(options: GamepadNavigationOptions = {}) {
};
pollFunctionRef.current = pollGamepads;
- pollFunctionRef.current = pollGamepads;
}, [enabled, buttonMapping, getGamepads, wasButtonJustPressed]);
useEffect(() => {
diff --git a/apps/web/src/pages/BigPictureDownloadsPage.tsx b/apps/web/src/pages/BigPictureDownloadsPage.tsx
index 7ae646b..f3669fe 100644
--- a/apps/web/src/pages/BigPictureDownloadsPage.tsx
+++ b/apps/web/src/pages/BigPictureDownloadsPage.tsx
@@ -5,6 +5,7 @@ import { apiGet } from "../lib/api";
import type { JobRecord } from "@crocdesk/shared";
import { useBigPictureStore, useDownloadProgressStore } from "../store";
import { useGamepadNavigation, triggerHapticFeedback } from "../hooks/useGamepadNavigation";
+import { useFullscreenMode } from "../hooks/useFullscreenMode";
import DownloadCard from "../components/DownloadCard";
import "./BigPictureDownloadsPage.css";
@@ -94,21 +95,7 @@ export default function BigPictureDownloadsPage() {
});
// Enter fullscreen on mount
- useEffect(() => {
- if (bigPictureStore.enabled && document.documentElement.requestFullscreen) {
- document.documentElement.requestFullscreen().catch(() => {
- // Fullscreen request failed, continue anyway
- });
- }
-
- return () => {
- if (document.fullscreenElement) {
- document.exitFullscreen().catch(() => {
- // Exit fullscreen failed
- });
- }
- };
- }, [bigPictureStore.enabled]);
+ useFullscreenMode(bigPictureStore.enabled);
return (
diff --git a/apps/web/src/pages/BigPictureModePage.tsx b/apps/web/src/pages/BigPictureModePage.tsx
index e896929..acc00d7 100644
--- a/apps/web/src/pages/BigPictureModePage.tsx
+++ b/apps/web/src/pages/BigPictureModePage.tsx
@@ -9,6 +9,7 @@ import type {
} from "@crocdesk/shared";
import { useBigPictureStore, useDownloadProgressStore } from "../store";
import { useGamepadNavigation, triggerHapticFeedback } from "../hooks/useGamepadNavigation";
+import { useFullscreenMode } from "../hooks/useFullscreenMode";
import { OnScreenKeyboard } from "../components/OnScreenKeyboard";
import GameCard from "../components/GameCard";
import "./BigPictureModePage.css";
@@ -269,21 +270,7 @@ export default function BigPictureModePage() {
};
// Enter fullscreen on mount
- useEffect(() => {
- if (bigPictureStore.enabled && document.documentElement.requestFullscreen) {
- document.documentElement.requestFullscreen().catch(() => {
- // Fullscreen request failed, continue anyway
- });
- }
-
- return () => {
- if (document.fullscreenElement) {
- document.exitFullscreen().catch(() => {
- // Exit fullscreen failed
- });
- }
- };
- }, [bigPictureStore.enabled]);
+ useFullscreenMode(bigPictureStore.enabled);
return (
diff --git a/apps/web/src/store/slices/bigPictureSlice.ts b/apps/web/src/store/slices/bigPictureSlice.ts
index 85ca719..c012941 100644
--- a/apps/web/src/store/slices/bigPictureSlice.ts
+++ b/apps/web/src/store/slices/bigPictureSlice.ts
@@ -122,13 +122,18 @@ export const useBigPictureStore = create
()(
detectMonitors: async () => {
// Use Screen Orientation API if available
if (window.screen && 'isExtended' in window.screen) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const isExtended = await (window.screen as any).isExtended;
- if (isExtended) {
- // Multi-monitor setup detected
- // We can't get exact count, but we know there's more than one
- set({ availableMonitors: 2 });
- } else {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const isExtended = await (window.screen as any).isExtended?.();
+ if (isExtended) {
+ // Multi-monitor setup detected
+ // We can't get exact count, but we know there's more than one
+ set({ availableMonitors: 2 });
+ } else {
+ set({ availableMonitors: 1 });
+ }
+ } catch {
+ // If the API call fails, assume single monitor
set({ availableMonitors: 1 });
}
} else {
From 184b05694a9166452f9770ac9307a01c53d8c24c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 31 Dec 2025 12:16:33 +0000
Subject: [PATCH 6/6] Fix remaining code review issues
- Refactor gamepad polling to properly include all dependencies
- Add useCallback to OnScreenKeyboard functions to prevent recreation
- Fix focusedCol dependency in normalization effect
- Simplify polling logic by removing intermediate ref
- Improve memoization to avoid stale closures
Co-authored-by: luandev <6452989+luandev@users.noreply.github.com>
---
apps/web/src/components/OnScreenKeyboard.tsx | 16 ++++++++--------
apps/web/src/hooks/useGamepadNavigation.ts | 17 ++++++++++-------
2 files changed, 18 insertions(+), 15 deletions(-)
diff --git a/apps/web/src/components/OnScreenKeyboard.tsx b/apps/web/src/components/OnScreenKeyboard.tsx
index e310dd1..ec95648 100644
--- a/apps/web/src/components/OnScreenKeyboard.tsx
+++ b/apps/web/src/components/OnScreenKeyboard.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef, useEffect } from "react";
+import { useState, useRef, useEffect, useCallback } from "react";
import { useBigPictureStore } from "../store";
import "./OnScreenKeyboard.css";
@@ -47,8 +47,8 @@ export function OnScreenKeyboard({
// Calculate total rows including special keys row
const totalRows = QWERTY_LAYOUT.length + 1; // +1 for special keys row
- // Get the current key at focus position
- const getCurrentKey = () => {
+ // Get the current key at focus position - memoized to avoid recreation
+ const getCurrentKey = useCallback(() => {
if (focusedRow < QWERTY_LAYOUT.length) {
const row = QWERTY_LAYOUT[focusedRow];
if (focusedCol < row.length) {
@@ -68,10 +68,10 @@ export function OnScreenKeyboard({
return specialKeysArray[focusedCol] || null;
}
return null;
- };
+ }, [focusedRow, focusedCol, shift]);
- // Handle key press
- const handleKeyPress = (key: string) => {
+ // Handle key press - memoized to avoid recreation
+ const handleKeyPress = useCallback((key: string) => {
if (key === SPECIAL_KEYS.BACKSPACE) {
onChange(value.slice(0, -1));
} else if (key === SPECIAL_KEYS.SPACE) {
@@ -91,7 +91,7 @@ export function OnScreenKeyboard({
setShift(false);
}
}
- };
+ }, [value, shift, onChange, onClose, onSubmit]);
// Handle keyboard navigation
useEffect(() => {
@@ -140,7 +140,7 @@ export function OnScreenKeyboard({
if (focusedCol >= currentRow) {
setFocusedCol(currentRow - 1);
}
- }, [focusedRow]);
+ }, [focusedRow, focusedCol]);
return (
diff --git a/apps/web/src/hooks/useGamepadNavigation.ts b/apps/web/src/hooks/useGamepadNavigation.ts
index 33078fb..96e119a 100644
--- a/apps/web/src/hooks/useGamepadNavigation.ts
+++ b/apps/web/src/hooks/useGamepadNavigation.ts
@@ -54,7 +54,6 @@ export function useGamepadNavigation(options: GamepadNavigationOptions = {}) {
const frameRef = useRef();
const lastButtonStatesRef = useRef