Merge branch 'SteamOS' of https://github.com/NebulaZMG/NebulaBrowser into SteamOS
This commit is contained in:
+24
@@ -86,3 +86,27 @@ typings/
|
||||
site-history.json
|
||||
bookmarks.json
|
||||
bookmarks.backup.json
|
||||
|
||||
# AppImage / SteamOS
|
||||
squashfs-root/
|
||||
*.AppImage
|
||||
|
||||
# Electron build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
release/
|
||||
|
||||
# Native binaries
|
||||
nebula
|
||||
nebula.exe
|
||||
|
||||
# Node/Electron
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Build artifacts
|
||||
nebula-appdir/
|
||||
*.asar
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
Converting extracted AppImage (`squashfs-root`) into a distributable AppDir for Steam
|
||||
|
||||
If your environment lacks `rsync`, use `cp -a` to copy the extracted AppImage into a clean AppDir and prepare it for upload to Steam.
|
||||
|
||||
1) Copy the extracted AppImage to an AppDir folder
|
||||
```bash
|
||||
cp -a squashfs-root/ nebula-appdir
|
||||
```
|
||||
|
||||
2) Unpack `app.asar` to edit or include app sources (optional; requires `npx asar`)
|
||||
```bash
|
||||
cd nebula-appdir/resources
|
||||
npx asar extract app.asar app
|
||||
# keep a backup if you want
|
||||
mv app app.orig && rm app.asar
|
||||
cd ../../
|
||||
```
|
||||
|
||||
3) Add/verify launcher (we added `nebula-appdir/Nebula`):
|
||||
```bash
|
||||
chmod +x nebula-appdir/Nebula
|
||||
```
|
||||
Run locally:
|
||||
```bash
|
||||
cd nebula-appdir
|
||||
./Nebula
|
||||
```
|
||||
|
||||
4) Ensure binary & permissions are correct
|
||||
```bash
|
||||
chmod +x nebula-appdir/nebula
|
||||
```
|
||||
|
||||
5) Package or upload to Steam
|
||||
- Create a tarball to upload as game files, or upload the AppDir contents as the depot.
|
||||
```bash
|
||||
tar -czf nebula-appdir.tar.gz -C nebula-appdir .
|
||||
```
|
||||
- In Steamworks, set the launch command to `./Nebula` (or `./nebula`).
|
||||
|
||||
Notes
|
||||
- `--no-sandbox` reduces Chromium sandboxing; prefer fixing `chrome-sandbox` and enabling sandboxing when possible.
|
||||
- Using the AppDir avoids AppImage/FUSE dependency on target systems.
|
||||
- Test on a clean SteamOS/Deck image before publishing.
|
||||
|
||||
Big Picture auto-start (SteamOS Gaming Mode)
|
||||
- If Nebula is launched from SteamOS Gaming Mode, it will auto-start in Big Picture Mode.
|
||||
- To force/disable via Steam Launch Options: `--big-picture` or `--no-big-picture`.
|
||||
@@ -42,6 +42,14 @@ Nebula Browser includes a **Big Picture Mode** - a controller-friendly, console-
|
||||
### Automatic Detection
|
||||
If Nebula detects a Steam Deck-sized display (1280x800), it will suggest Big Picture Mode in settings.
|
||||
|
||||
### Auto-start in SteamOS Gaming Mode
|
||||
When Nebula is launched from SteamOS **Gaming Mode** (gamescope / Steam gamepad UI), it will automatically start in **Big Picture Mode**.
|
||||
|
||||
You can override this behavior:
|
||||
- Force Big Picture at launch: launch options `--big-picture` (or `--bigpicture`)
|
||||
- Disable Big Picture auto-start: launch options `--no-big-picture` (or `--no-bigpicture`)
|
||||
- Environment overrides: `NEBULA_BIG_PICTURE=1` / `NEBULA_NO_BIG_PICTURE=1`
|
||||
|
||||
## Navigation Sections
|
||||
|
||||
| Section | Description |
|
||||
|
||||
+16
-2
@@ -19,6 +19,15 @@ class GPUConfig {
|
||||
|
||||
// Start with conservative settings that usually work
|
||||
this.applyConservativeSettings();
|
||||
|
||||
// On Linux/SteamOS, force disable GPU and sandbox to ensure webview stability
|
||||
if (platform === 'linux') {
|
||||
console.log('Linux detected: Disabling GPU and enforcing no-sandbox');
|
||||
app.commandLine.appendSwitch('disable-gpu');
|
||||
app.commandLine.appendSwitch('no-sandbox');
|
||||
this.fallbackApplied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to enable GPU features progressively
|
||||
this.tryEnableGPU();
|
||||
@@ -43,8 +52,13 @@ class GPUConfig {
|
||||
// GPU acceleration switches
|
||||
app.commandLine.appendSwitch('ignore-gpu-blacklist');
|
||||
app.commandLine.appendSwitch('ignore-gpu-blocklist');
|
||||
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||
app.commandLine.appendSwitch('enable-zero-copy');
|
||||
|
||||
// On Linux/SteamOS, these aggressive flags can cause webview rendering issues (black screen)
|
||||
// We disable them for Linux to ensure stability
|
||||
if (process.platform !== 'linux') {
|
||||
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||
app.commandLine.appendSwitch('enable-zero-copy');
|
||||
}
|
||||
|
||||
// Video acceleration (usually safer than full GPU)
|
||||
app.commandLine.appendSwitch('enable-accelerated-video-decode');
|
||||
|
||||
@@ -72,6 +72,53 @@ ipcMain.removeHandler('window-close');
|
||||
// BIG PICTURE MODE - Steam Deck / Console UI
|
||||
// =============================================================================
|
||||
|
||||
function envTruthy(value) {
|
||||
if (value === undefined || value === null) return false;
|
||||
const s = String(value).trim().toLowerCase();
|
||||
return s === '1' || s === 'true' || s === 'yes' || s === 'on';
|
||||
}
|
||||
|
||||
function argvHasFlag(flag) {
|
||||
return process.argv.includes(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: detect Steam Deck / SteamOS Gaming Mode (gamescope) launches.
|
||||
*
|
||||
* This is intentionally conservative and only used for picking the *default*
|
||||
* startup UI. Users can override via CLI/env.
|
||||
*/
|
||||
function isGameModeEnvironment() {
|
||||
const env = process.env;
|
||||
|
||||
// Common Steam tenfoot / gamepad UI markers
|
||||
if (envTruthy(env.STEAM_GAMEPADUI)) return true;
|
||||
if (envTruthy(env.SteamTenfoot)) return true;
|
||||
if (envTruthy(env.STEAM_TENFOOT)) return true;
|
||||
|
||||
// SteamOS / gamescope compositor markers
|
||||
const currentDesktop = String(env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const sessionDesktop = String(env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
if (currentDesktop.includes('gamescope') || sessionDesktop.includes('gamescope')) return true;
|
||||
|
||||
if (env.GAMESCOPE_WSI || env.GAMESCOPE_SESSION || env.GAMESCOPE_FOCUSED_APP) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldStartInBigPictureMode() {
|
||||
// Explicit CLI overrides first
|
||||
if (argvHasFlag('--no-big-picture') || argvHasFlag('--no-bigpicture')) return false;
|
||||
if (argvHasFlag('--big-picture') || argvHasFlag('--bigpicture') || argvHasFlag('--tenfoot') || argvHasFlag('--game-mode')) return true;
|
||||
|
||||
// Explicit env overrides
|
||||
if (envTruthy(process.env.NEBULA_NO_BIG_PICTURE) || envTruthy(process.env.NEBULA_NO_BIGPICTURE)) return false;
|
||||
if (envTruthy(process.env.NEBULA_BIG_PICTURE) || envTruthy(process.env.NEBULA_BIGPICTURE) || envTruthy(process.env.NEBULA_GAME_MODE)) return true;
|
||||
|
||||
// Auto-detect SteamOS Gaming Mode
|
||||
return isGameModeEnvironment();
|
||||
}
|
||||
|
||||
// Steam Deck screen dimensions: 1280x800
|
||||
const STEAM_DECK_WIDTH = 1280;
|
||||
const STEAM_DECK_HEIGHT = 800;
|
||||
@@ -503,7 +550,17 @@ function configureSessionsAsync() {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const t0 = performance.now();
|
||||
createWindow();
|
||||
|
||||
// If launched via SteamOS Gaming Mode / gamepad UI, default to Big Picture Mode.
|
||||
// Desktop launches remain unchanged.
|
||||
const startInBigPicture = shouldStartInBigPictureMode();
|
||||
if (startInBigPicture) {
|
||||
console.log('[Startup] Detected game mode launch; starting in Big Picture Mode');
|
||||
createBigPictureWindow();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
|
||||
// Initialize user plugins after app ready
|
||||
try {
|
||||
pluginManager.ensureUserPluginsDir();
|
||||
@@ -512,7 +569,7 @@ app.whenReady().then(() => {
|
||||
} catch (e) {
|
||||
console.error('[Plugins] initialization error:', e);
|
||||
}
|
||||
console.log('[Startup] createWindow invoked in', (performance.now() - t0).toFixed(1), 'ms after app.whenReady');
|
||||
console.log('[Startup] initial window created (', startInBigPicture ? 'bigpicture' : 'desktop', ') in', (performance.now() - t0).toFixed(1), 'ms after app.whenReady');
|
||||
|
||||
// Handle GPU process crashes (still register early)
|
||||
app.on('gpu-process-crashed', (event, killed) => {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble nebula-appdir from extracted squashfs-root
|
||||
set -euo pipefail
|
||||
SRC="${1:-squashfs-root}"
|
||||
DEST="${2:-nebula-appdir}"
|
||||
|
||||
if [ ! -d "$SRC" ]; then
|
||||
echo "Source $SRC not found. Extract the AppImage first (./dist/Nebula-*.AppImage --appimage-extract)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy extracted contents into DEST
|
||||
mkdir -p "$DEST"
|
||||
cp -a "$SRC/." "$DEST/"
|
||||
|
||||
# Ensure launcher/binary exist
|
||||
if [ -f "$DEST/run-nebula.sh" ]; then
|
||||
mv "$DEST/run-nebula.sh" "$DEST/Nebula" 2>/dev/null || true
|
||||
fi
|
||||
chmod +x "$DEST/Nebula" || true
|
||||
|
||||
# Ensure directories for icons and desktop entries
|
||||
mkdir -p "$DEST/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$DEST/usr/share/applications"
|
||||
|
||||
# Copy icon if present at top level of extracted AppImage
|
||||
if [ -f "$SRC/nebula.png" ]; then
|
||||
cp "$SRC/nebula.png" "$DEST/usr/share/icons/hicolor/256x256/apps/nebula.png"
|
||||
fi
|
||||
|
||||
# Also embed project icon if present in repo assets
|
||||
PROJECT_ICON="$(cd "$(dirname "$0")" && pwd)/assets/images/Logos/Nebula-Favicon.png"
|
||||
if [ -f "$PROJECT_ICON" ]; then
|
||||
echo "Embedding project icon into AppDir: $PROJECT_ICON"
|
||||
cp "$PROJECT_ICON" "$DEST/usr/share/icons/hicolor/256x256/apps/nebula.png"
|
||||
fi
|
||||
|
||||
# Install desktop file into AppDir
|
||||
if [ -f "$DEST/nebula.desktop" ]; then
|
||||
cp "$DEST/nebula.desktop" "$DEST/usr/share/applications/nebula.desktop"
|
||||
else
|
||||
cat > "$DEST/usr/share/applications/nebula.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Nebula
|
||||
Comment=Nebula Browser
|
||||
Exec=./Nebula %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=nebula
|
||||
Categories=Network;WebBrowser;
|
||||
StartupWMClass=Nebula
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Fix permissions
|
||||
chmod -R a+r "$DEST/usr/share/icons/hicolor/256x256/apps" || true
|
||||
chmod +x "$DEST/Nebula" || true
|
||||
|
||||
echo "AppDir assembled at $DEST. Run with: $DEST/Nebula"
|
||||
@@ -5,6 +5,8 @@
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"start:dev": "electron . --no-sandbox --disable-gpu",
|
||||
"start:linux": "electron . --no-sandbox",
|
||||
"dist": "electron-builder",
|
||||
"run": "electron ."
|
||||
},
|
||||
|
||||
+27
@@ -1,5 +1,11 @@
|
||||
// preload.js - Optimized version
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
let pathModule;
|
||||
try {
|
||||
pathModule = require('path');
|
||||
} catch (err) {
|
||||
pathModule = null;
|
||||
}
|
||||
|
||||
// Cache DOM references for performance
|
||||
let domReady = false;
|
||||
@@ -75,6 +81,27 @@ const electronAPI = {
|
||||
saveImageFromNet: async (url) => ipcRenderer.invoke('save-image-from-url', { url })
|
||||
};
|
||||
|
||||
// Provide absolute path to the renderer preload for webview guests so
|
||||
// webview `preload` attributes use an absolute, resolvable path on all platforms.
|
||||
const webviewPreloadAbsolutePath = pathModule ? pathModule.join(__dirname, 'preload.js') : null;
|
||||
electronAPI.getWebviewPreloadPath = () => webviewPreloadAbsolutePath;
|
||||
|
||||
// Fixup any static <webview preload="..."> attributes in the DOM early so
|
||||
// guests receive an absolute path instead of a relative one that may fail
|
||||
// to resolve inside the guest process.
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
if (webviewPreloadAbsolutePath) {
|
||||
const els = document.querySelectorAll('webview[preload]');
|
||||
for (const el of els) {
|
||||
try { el.setAttribute('preload', webviewPreloadAbsolutePath); } catch {};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// non-fatal
|
||||
}
|
||||
});
|
||||
|
||||
// Cache for bookmarks to reduce IPC calls
|
||||
let bookmarksCache = null;
|
||||
let bookmarksCacheTime = 0;
|
||||
|
||||
+78
-19
@@ -594,34 +594,93 @@ function goForward() {
|
||||
// =============================================================================
|
||||
|
||||
function initGamepadSupport() {
|
||||
if (!navigator.getGamepads) {
|
||||
console.warn('[BigPicture] Gamepad API not available in this environment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: On Linux (and some controllers like handheld integrated gamepads),
|
||||
// the `gamepadconnected` event may not fire until the first button press,
|
||||
// or at all. We rely on continuous polling for robustness.
|
||||
window.addEventListener('gamepadconnected', (e) => {
|
||||
console.log('[BigPicture] Gamepad connected:', e.gamepad.id);
|
||||
state.gamepadConnected = true;
|
||||
state.gamepadIndex = e.gamepad.index;
|
||||
showToast('Controller connected');
|
||||
console.log('[BigPicture] Gamepad connected:', e.gamepad?.id || 'unknown');
|
||||
// Prefer the first connected controller as the active one.
|
||||
if (state.gamepadIndex === null) {
|
||||
state.gamepadConnected = true;
|
||||
state.gamepadIndex = e.gamepad.index;
|
||||
showToast('Controller connected');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
window.addEventListener('gamepaddisconnected', (e) => {
|
||||
console.log('[BigPicture] Gamepad disconnected');
|
||||
state.gamepadConnected = false;
|
||||
state.gamepadIndex = null;
|
||||
showToast('Controller disconnected');
|
||||
console.log('[BigPicture] Gamepad disconnected:', e.gamepad?.id || 'unknown');
|
||||
// If the active controller disconnected, clear it; polling will auto-select another.
|
||||
if (state.gamepadIndex === e.gamepad.index) {
|
||||
state.gamepadConnected = false;
|
||||
state.gamepadIndex = null;
|
||||
showToast('Controller disconnected');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Initial scan (covers controllers that are already connected at load).
|
||||
refreshActiveGamepad(true);
|
||||
|
||||
// Start polling for gamepad input
|
||||
requestAnimationFrame(pollGamepad);
|
||||
}
|
||||
|
||||
function pollGamepad() {
|
||||
if (state.gamepadConnected && state.gamepadIndex !== null) {
|
||||
const gamepads = navigator.getGamepads();
|
||||
const gamepad = gamepads[state.gamepadIndex];
|
||||
|
||||
if (gamepad) {
|
||||
handleGamepadInput(gamepad);
|
||||
}
|
||||
function getFirstConnectedGamepad(gamepads) {
|
||||
if (!gamepads) return null;
|
||||
for (let i = 0; i < gamepads.length; i++) {
|
||||
const gp = gamepads[i];
|
||||
if (gp) return gp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function refreshActiveGamepad(isInitial = false) {
|
||||
const gamepads = navigator.getGamepads();
|
||||
|
||||
// If we have an index, verify it still points to a real gamepad.
|
||||
let active = null;
|
||||
if (state.gamepadIndex !== null) {
|
||||
active = gamepads[state.gamepadIndex] || null;
|
||||
}
|
||||
|
||||
// Fallback: pick the first connected controller.
|
||||
if (!active) {
|
||||
active = getFirstConnectedGamepad(gamepads);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
const changed = !state.gamepadConnected || state.gamepadIndex !== active.index;
|
||||
state.gamepadConnected = true;
|
||||
state.gamepadIndex = active.index;
|
||||
if (changed && !isInitial) {
|
||||
console.log('[BigPicture] Active gamepad selected:', active.id);
|
||||
showToast('Controller connected');
|
||||
}
|
||||
} else {
|
||||
if (state.gamepadConnected) {
|
||||
state.gamepadConnected = false;
|
||||
state.gamepadIndex = null;
|
||||
if (!isInitial) {
|
||||
showToast('Controller disconnected');
|
||||
}
|
||||
}
|
||||
state.gamepadConnected = false;
|
||||
state.gamepadIndex = null;
|
||||
}
|
||||
|
||||
return { gamepads, active };
|
||||
}
|
||||
|
||||
function pollGamepad() {
|
||||
const { active } = refreshActiveGamepad(false);
|
||||
if (active) {
|
||||
handleGamepadInput(active);
|
||||
}
|
||||
|
||||
requestAnimationFrame(pollGamepad);
|
||||
}
|
||||
|
||||
|
||||
+18
-2
@@ -368,7 +368,16 @@ function createTab(inputUrl) {
|
||||
webview.src = resolvedUrl;
|
||||
webview.setAttribute('allowpopups', '');
|
||||
webview.setAttribute('partition', 'persist:main');
|
||||
webview.setAttribute('preload', '../preload.js');
|
||||
// Use absolute preload path provided by the main-window preload to ensure
|
||||
// the guest process can resolve the file (important on Linux/SteamOS).
|
||||
try {
|
||||
const preloadPath = (window.electronAPI && typeof window.electronAPI.getWebviewPreloadPath === 'function')
|
||||
? window.electronAPI.getWebviewPreloadPath()
|
||||
: '../preload.js';
|
||||
webview.setAttribute('preload', preloadPath);
|
||||
} catch (e) {
|
||||
webview.setAttribute('preload', '../preload.js');
|
||||
}
|
||||
// Add attributes needed for Google OAuth and sign-in flows
|
||||
webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true');
|
||||
try {
|
||||
@@ -667,7 +676,14 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
|
||||
webview.src = resolvedUrl;
|
||||
webview.setAttribute('allowpopups', '');
|
||||
webview.setAttribute('partition', 'persist:main');
|
||||
webview.setAttribute('preload', '../preload.js');
|
||||
try {
|
||||
const preloadPath = (window.electronAPI && typeof window.electronAPI.getWebviewPreloadPath === 'function')
|
||||
? window.electronAPI.getWebviewPreloadPath()
|
||||
: '../preload.js';
|
||||
webview.setAttribute('preload', preloadPath);
|
||||
} catch (e) {
|
||||
webview.setAttribute('preload', '../preload.js');
|
||||
}
|
||||
// Add attributes needed for Google OAuth and sign-in flows
|
||||
webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true');
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user