diff --git a/README.md b/README.md deleted file mode 100644 index e40b6be..0000000 --- a/README.md +++ /dev/null @@ -1,184 +0,0 @@ -# NEBULA BROWSER - -*A controller-first browser, now re-focused as a Linux-first project* - ---- - -### Limited Development โ€ข Project in Maintenance Mode - -Nebula Browser is no longer under active, full-time development. -The project is maintained on an occasional basis, with updates made when time and interest allow. - ---- - -# Nebula - -![Nebula Logo](assets/images/Logos/Nebula-Logo.svg) - -Nebula is a customizable and privacy-focused web browser built with Electron. It is designed to be lightweight, secure, and user-friendly, with a strong emphasis on **controller-first and keyboard-driven interaction**, performance, and privacy. - -While originally conceived for SteamOS and the Steam Deck, Nebula has since evolved into a **Linux-first experimental browser**, aimed at alternative input setups, handheld PCs, accessibility use cases, and living room environments. - ---- - -## Project Status - -**Status:** Maintenance Mode -**Maintenance:** Occasional updates -**Development:** No active roadmap -**Focus:** Linux-first (desktop, handhelds, and alternative input setups) - -Nebula is not a primary or actively scheduled project. Updates may occur sporadically and are not guaranteed. The repository remains open for use, modification, and experimentation under the MIT license. - -This repository reflects a stable snapshot of the project, with incremental improvements added when appropriate. - ---- - -## Why the Project Changed Direction - -Nebula was originally created with a very specific goal: to be a **controller-first browser designed to live inside the Steam ecosystem**, especially for Steam Deck and SteamOS users who wanted a seamless web experience without relying on desktop mode, keyboards, or external workarounds. - -During the Steam review process, Valve determined that Nebula does not fit within Steamโ€™s allowed categories for non-game software. As a result, the browser could not be distributed on the Steam Store. - -At the time, Steam distribution was considered a core pillar of the project, and development was paused as the original vision could no longer be fulfilled in its intended form. - -Since then, community feedback has highlighted broader interest in Nebulaโ€™s **input model and interaction design**, beyond Steam or SteamOS alone. Because of this, Nebula has been re-contextualized as a Linux-first project rather than a Steam-native application. - ---- - -## What This Means Now - -* Nebula is **not abandoned** -* It is **no longer a main or priority project** -* Development happens **occasionally and without a fixed schedule** -* The project is no longer tied to Steam or SteamOS -* Linux desktop users, handhelds, and alternative input setups are the primary audience - -Nebula exists as an ongoing experiment in controller- and keyboard-driven web navigation. It may evolve further, remain stable, or inspire forks and derivative projects. - ---- - -## Distribution - -Nebula may be distributed outside of Steam through platforms such as: - -* GitHub (source and releases) -* itch.io -* Flatpak / Flathub - -Availability and packaging may change over time and are not guaranteed. - -Official releases for Nebula will be published on itch.io; Steam distribution -is not available due to the Steam review outcome described above. - ---- - -## Licensing - -Nebula Browser is licensed under the MIT License. -You are free to use, modify, and build upon the project. - - - -## Features - -* **Privacy Control:** Easily clear your browsing data (history, cookies, cache, local storage, and more). -* **Tab Management:** Open new tabs, pop a tab out into a new window, and manage them efficiently. -* **Bookmarks:** Save your favorite sites with automatic backup on save. -* **History:** Keeps track of your browsing and search history with one-click clear. -* **Downloads Manager:** Track downloads, pause/resume/cancel, and open or reveal completed files. -* **Context Menu:** Native rightโ€‘click menu with Back/Forward/Reload, open/download links, image actions, and Inspect Element. -* **Auth Compatibility:** Improved OAuth/SSO & WebAuthn support (popup windows enabled where needed). -* **Performance Monitoring:** Built-in tools to monitor app performance and force GC when needed. -* **GPU Acceleration Control:** Diagnostics and safe fallbacks to troubleshoot rendering issues. -* **Themes & Customization:** Built-in themes and live editor to craft your own. -* **Plugins:** Extend Nebula with custom or community plugins via a simple plugin API. -* **Cross-Platform:** Runs on Windows, macOS, and Linux. - -[**Learn more about Nebula's features.**](documentation/FEATURES.md) - -## Getting Started - -### Prerequisites - -* [Node.js](https://nodejs.org/) installed. - -### Installation - -1. Clone the repository: - ```sh - git clone https://github.com/Bobbybear007/NebulaBrowser.git - ``` -2. Navigate to the project directory: - ```sh - cd NebulaBrowser - ``` -3. Install dependencies: - ```sh - npm install - ``` - -### Running the Application - -To start the browser, run the following command: - -```sh -npm start -``` - -## Building the Application - -To build the application for your platform, run: - -```sh -npm run dist -``` - -This will create a distributable file in the `dist` directory. - -Tip (Windows): If you encounter GPU issues, try starting with `start-gpu-safe.bat` to launch in a safer rendering mode. - -## Project Structure - -An overview of the project's structure. For a more detailed explanation, please see the [Project Structure documentation](documentation/PROJECT_STRUCTURE.md). - -- `main.js`: The main entry point for the Electron application. -- `renderer/`: Contains all the front-end files. -- `preload.js`: Bridges the main and renderer processes. -- `performance-monitor.js`: Module for monitoring performance. -- `gpu-config.js` & `gpu-fallback.js`: Modules for managing GPU settings. -- `assets/`: Contains static assets. -- `documentation/`: Contains additional documentation. -- `plugins/`: Sample plugins and scaffolding for developing your own. - -## Core Concepts - -Nebula is built on several core concepts that are essential to understanding how it works. For a deeper dive, read the [Core Concepts documentation](documentation/CORE_CONCEPTS.md). - -- **Main and Renderer Processes** -- **Inter-Process Communication (IPC)** -- **Performance and GPU Management** - -## Contributing - -Contributions are welcome! Please read our [contributing guidelines](documentation/CONTRIBUTING.md) to get started. - -## Technologies Used - -* [Electron](https://www.electronjs.org/) -* HTML, CSS, JavaScript - - - - -## Documentation - -* [MIT License](documentation/MIT.md) -* [GPU Fix](documentation/GPU-FIX-README.md) -* [Features](documentation/FEATURES.md) -* [Customization](documentation/Customization.md) -* [Project Structure](documentation/PROJECT_STRUCTURE.md) -* [Core Concepts](documentation/CORE_CONCEPTS.md) -* [Contributing Guide](documentation/CONTRIBUTING.md) -* [OAuth Debug](documentation/oauth-debug.md) -* [Plugins Guide](README-PLUGINS.md) diff --git a/appdir-example/Nebula b/appdir-example/Nebula deleted file mode 100755 index b4fc8ec..0000000 --- a/appdir-example/Nebula +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env bash -# Run Nebula from an extracted AppImage folder with safe LD/XDG paths and Linux flags. -# This repo can end up with either: -# - nebula-appdir/squashfs-root/nebula (common after copying squashfs-root into nebula-appdir) -# - nebula-appdir/nebula (if you copied the extracted contents directly into nebula-appdir) -# -# PORTABLE MODE: User data (cookies, history, bookmarks) is stored in usr/data/ alongside the app. -# This keeps all data self-contained and portable on Linux. -set -euo pipefail - -SELF_DIR="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)" - -if [ -x "$SELF_DIR/nebula-appdir/nebula" ]; then - APPROOT="$SELF_DIR/nebula-appdir" - BIN="$APPROOT/nebula" -elif [ -x "$SELF_DIR/squashfs-root/nebula" ]; then - APPROOT="$SELF_DIR/squashfs-root" - BIN="$APPROOT/nebula" -elif [ -x "$SELF_DIR/nebula" ]; then - APPROOT="$SELF_DIR" - BIN="$APPROOT/nebula" -else - echo "Nebula binary not found. Expected either:" - echo "- $SELF_DIR/nebula-appdir/nebula" - echo "- $SELF_DIR/squashfs-root/nebula" - echo "- $SELF_DIR/nebula" - exit 1 -fi - -# --- PORTABLE DATA CONFIGURATION --- -# Store user data (cookies, history, bookmarks, etc.) in a local folder -# Data is stored with secure permissions (700 for dirs, 600 for files) -PORTABLE_DATA_DIR="$SELF_DIR/usr/data" -export NEBULA_PORTABLE=1 -export NEBULA_PORTABLE_PATH="$PORTABLE_DATA_DIR" - -# Create portable data directory with secure permissions if it doesn't exist -if [ ! -d "$PORTABLE_DATA_DIR" ]; then - mkdir -p "$PORTABLE_DATA_DIR" - chmod 700 "$PORTABLE_DATA_DIR" -fi - -export LD_LIBRARY_PATH="$APPROOT/usr/lib:${LD_LIBRARY_PATH:-}" -export XDG_DATA_DIRS="$APPROOT/usr/share:${XDG_DATA_DIRS:-/usr/share}" - -# ============================================================================= -# STEAM INPUT CONFIGURATION - Always set these to prevent controller emulation -# ============================================================================= -# These variables tell Steam's input layer that this app handles controller -# input natively and should NOT have mouse/keyboard emulation applied. -# Set unconditionally since Software apps may not receive Steam env vars. - -# Disable Steam's virtual gamepad layer - CRITICAL for native controller support -export SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 -export STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 - -# Allow raw gamepad access -export SDL_GAMECONTROLLER_IGNORE_DEVICES="" - -# Allow background gamepad events (useful when app doesn't have focus) -export SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1 - -# Hint to SDL that we're using gamepads natively -export SDL_GAMECONTROLLERCONFIG="${SDL_GAMECONTROLLERCONFIG:-}" - -# Steam can inject its overlay via LD_PRELOAD (gameoverlayrenderer.so). -# In some setups this breaks Chromium/Electron gamepad input. When we detect -# a Steam-launched session, strip the overlay preload but keep other entries. -# Also configure additional Steam-specific settings. -if [ -n "${SteamAppId:-}" ] || [ -n "${STEAM_APP_ID:-}" ] || [ -n "${STEAM_COMPAT_CLIENT_INSTALL_PATH:-}" ] || [ -n "${STEAM_COMPAT_DATA_PATH:-}" ]; then - # Enable Big Picture Mode for controller-friendly UI when launched from Steam - export NEBULA_BIG_PICTURE="${NEBULA_BIG_PICTURE:-1}" - - # Enable GPU acceleration on Linux - export NEBULA_GPU_ALLOW_LINUX=1 - - if [ -n "${LD_PRELOAD:-}" ]; then - CLEANED_LD_PRELOAD="" - IFS=':' read -r -a _preload_parts <<< "$LD_PRELOAD" - for _p in "${_preload_parts[@]}"; do - [ -z "$_p" ] && continue - case "$_p" in - *gameoverlayrenderer.so*) - ;; - *) - if [ -z "$CLEANED_LD_PRELOAD" ]; then - CLEANED_LD_PRELOAD="$_p" - else - CLEANED_LD_PRELOAD="$CLEANED_LD_PRELOAD:$_p" - fi - ;; - esac - done - if [ -z "$CLEANED_LD_PRELOAD" ]; then - unset LD_PRELOAD - else - export LD_PRELOAD="$CLEANED_LD_PRELOAD" - fi - fi -fi - -GPU_ARGS=() - -# Optional GPU tuning profiles strike a balance between enabling hardware decode -# and keeping the launcher safe on systems with fragile Mesa/VA-API stacks. -# Default to enabling GPU tweaks on Linux unless explicitly disabled. -export NEBULA_GPU_PROFILE="${NEBULA_GPU_PROFILE:-}" -GPU_GL_MODE="${NEBULA_GPU_GL:-}" -export NEBULA_GPU_TWEAKS="${NEBULA_GPU_TWEAKS:-1}" - -if [ "${NEBULA_GPU_TWEAKS}" = "1" ] && [ -z "$NEBULA_GPU_PROFILE" ]; then - export NEBULA_GPU_PROFILE=vaapi -fi - -case "$NEBULA_GPU_PROFILE" in - vaapi) - GPU_ARGS+=(--ignore-gpu-blocklist) - GPU_ARGS+=(--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseSkiaRenderer,VaapiVideoDecoderLinuxGL,VaapiVideoDecoderLinuxZeroCopyGL) - GPU_ARGS+=(--enable-zero-copy) - GPU_ARGS+=(--enable-gpu-rasterization) - GPU_ARGS+=(--enable-accelerated-video-decode) - GPU_ARGS+=(--enable-native-gpu-memory-buffers) - if [ -z "${LIBVA_DRIVER_NAME:-}" ]; then - export LIBVA_DRIVER_NAME=radeonsi - fi - ;; - angle) - GPU_ARGS+=(--ignore-gpu-blocklist) - GPU_ARGS+=(--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseSkiaRenderer,VaapiVideoDecoderLinuxGL) - GPU_ARGS+=(--enable-zero-copy) - GPU_ARGS+=(--enable-gpu-rasterization) - GPU_ARGS+=(--enable-native-gpu-memory-buffers) - GPU_ARGS+=(--enable-accelerated-video-decode) - if [ -z "${LIBVA_DRIVER_NAME:-}" ]; then - export LIBVA_DRIVER_NAME=radeonsi - fi - if [ -z "${AMD_VULKAN_ICD:-}" ]; then - export AMD_VULKAN_ICD=RADV - fi - ;; - amd-handheld) - GPU_ARGS+=(--ignore-gpu-blocklist) - GPU_ARGS+=(--use-angle=vulkan) - AMD_HANDHELD_FEATURES="VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseSkiaRenderer,VaapiVideoDecoderLinuxGL,VaapiVideoDecoderLinuxZeroCopyGL" - if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then - AMD_HANDHELD_FEATURES="UseOzonePlatform,WaylandWindowDecorations,${AMD_HANDHELD_FEATURES}" - GPU_ARGS+=(--ozone-platform-hint=auto) - GPU_ARGS+=(--ozone-platform=wayland) - fi - GPU_ARGS+=(--enable-features="$AMD_HANDHELD_FEATURES") - GPU_ARGS+=(--enable-zero-copy) - GPU_ARGS+=(--enable-gpu-rasterization) - if [ -z "${LIBVA_DRIVER_NAME:-}" ]; then - export LIBVA_DRIVER_NAME=radeonsi - fi - if [ -z "${MESA_VK_WSI_PRESENT_MODE:-}" ]; then - export MESA_VK_WSI_PRESENT_MODE=mailbox - fi - ;; - software) - GPU_ARGS+=(--disable-gpu) - ;; - "") - ;; - *) - echo "Warning: Unknown NEBULA_GPU_PROFILE '$NEBULA_GPU_PROFILE'" >&2 - ;; -esac - -if [ -n "$GPU_GL_MODE" ]; then - # Map common GL mode names to Electron-compatible values - case "$GPU_GL_MODE" in - egl|egl-angle) GPU_GL_MODE="angle" ;; - desktop) GPU_GL_MODE="desktop" ;; - esac - GPU_ARGS+=(--use-gl="$GPU_GL_MODE") -fi - -if [ -n "${NEBULA_GPU_EXTRA_ARGS:-}" ]; then - # shellcheck disable=SC2086 # word splitting is intentional for custom flag tokens - read -r -a _nebula_gpu_extra <<< "${NEBULA_GPU_EXTRA_ARGS}" - GPU_ARGS+=("${_nebula_gpu_extra[@]}") -fi - -exec "$BIN" --no-sandbox "${GPU_ARGS[@]}" "$@" diff --git a/appdir-example/nebula.desktop b/appdir-example/nebula.desktop deleted file mode 100755 index 6c57a27..0000000 --- a/appdir-example/nebula.desktop +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env xdg-open -[Desktop Entry] -Name=Nebula -Comment=Nebula Browser (Portable Mode - data stored locally) -Exec=env NEBULA_GPU_TWEAKS=1 /home/deck/Documents/Repos/NebulaBrowser/nebula-appdir/Nebula %U -Terminal=false -Type=Application -Icon=nebula -Categories=Network;WebBrowser; -StartupWMClass=Nebula diff --git a/appdir-example/run-nebula.sh b/appdir-example/run-nebula.sh deleted file mode 100755 index f80df49..0000000 --- a/appdir-example/run-nebula.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Run Nebula with portable data storage -# User data (cookies, history, bookmarks) is stored in usr/user-data/ alongside the app. -set -e - -HERE="$(cd "$(dirname "$0")" && pwd)" - -export APPDIR="$HERE" -export PATH="$HERE/usr/bin:$PATH" -export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib64:$LD_LIBRARY_PATH" - -# --- PORTABLE DATA CONFIGURATION --- -# Store user data in a local folder for portable operation -PORTABLE_DATA_DIR="$HERE/usr/user-data" -export NEBULA_PORTABLE=1 -export NEBULA_PORTABLE_PATH="$PORTABLE_DATA_DIR" - -# Create portable data directory with secure permissions if it doesn't exist -if [ ! -d "$PORTABLE_DATA_DIR" ]; then - mkdir -p "$PORTABLE_DATA_DIR" - chmod 700 "$PORTABLE_DATA_DIR" -fi - -exec "$HERE/nebula-appdir/AppRun" diff --git a/appdir-example/steam_appid.txt b/appdir-example/steam_appid.txt deleted file mode 100644 index 557197f..0000000 --- a/appdir-example/steam_appid.txt +++ /dev/null @@ -1 +0,0 @@ -4290110 diff --git a/appdir-example/usr/share/applications/nebula.desktop b/appdir-example/usr/share/applications/nebula.desktop deleted file mode 100644 index c4c2a63..0000000 --- a/appdir-example/usr/share/applications/nebula.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=Nebula -Comment=Nebula Browser -Exec=./Nebula %U -Terminal=false -Type=Application -Icon=nebula -Categories=Network;WebBrowser; -StartupWMClass=Nebula diff --git a/assets/images/Logos/Nebula-Favicon.icns b/assets/images/Logos/Nebula-Favicon.icns deleted file mode 100644 index 62ac037..0000000 Binary files a/assets/images/Logos/Nebula-Favicon.icns and /dev/null differ diff --git a/assets/images/Logos/Nebula-Favicon.ico b/assets/images/Logos/Nebula-Favicon.ico deleted file mode 100644 index deaf8c3..0000000 Binary files a/assets/images/Logos/Nebula-Favicon.ico and /dev/null differ diff --git a/assets/images/Logos/Nebula-Favicon.png b/assets/images/Logos/Nebula-Favicon.png deleted file mode 100644 index 2f58b1f..0000000 Binary files a/assets/images/Logos/Nebula-Favicon.png and /dev/null differ diff --git a/assets/images/Logos/Nebula-Icon.svg b/assets/images/Logos/Nebula-Icon.svg deleted file mode 100644 index 43ae00b..0000000 --- a/assets/images/Logos/Nebula-Icon.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/images/Logos/Nebula-Logo.png b/assets/images/Logos/Nebula-Logo.png deleted file mode 100644 index 6acfb51..0000000 Binary files a/assets/images/Logos/Nebula-Logo.png and /dev/null differ diff --git a/assets/images/Logos/Nebula-Logo.svg b/assets/images/Logos/Nebula-Logo.svg deleted file mode 100644 index b19f223..0000000 --- a/assets/images/Logos/Nebula-Logo.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/images/Logos/Thumbs.db b/assets/images/Logos/Thumbs.db deleted file mode 100644 index 5c05497..0000000 Binary files a/assets/images/Logos/Thumbs.db and /dev/null differ diff --git a/assets/images/SteamOS.png b/assets/images/SteamOS.png deleted file mode 100644 index 285eed7..0000000 Binary files a/assets/images/SteamOS.png and /dev/null differ diff --git a/assets/images/SteamStore/Nebula_Header_Capsual.png b/assets/images/SteamStore/Nebula_Header_Capsual.png deleted file mode 100644 index c354c16..0000000 Binary files a/assets/images/SteamStore/Nebula_Header_Capsual.png and /dev/null differ diff --git a/assets/images/Thumbs.db b/assets/images/Thumbs.db deleted file mode 100644 index 4901c60..0000000 Binary files a/assets/images/Thumbs.db and /dev/null differ diff --git a/assets/images/fonts/InterVariable.ttf b/assets/images/fonts/InterVariable.ttf deleted file mode 100644 index 4ab79e0..0000000 Binary files a/assets/images/fonts/InterVariable.ttf and /dev/null differ diff --git a/assets/images/icons/Bing.svg b/assets/images/icons/Bing.svg deleted file mode 100644 index 460382b..0000000 --- a/assets/images/icons/Bing.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/images/icons/duckduckgo.svg b/assets/images/icons/duckduckgo.svg deleted file mode 100644 index 8215a91..0000000 --- a/assets/images/icons/duckduckgo.svg +++ /dev/null @@ -1 +0,0 @@ -duckduckgo \ No newline at end of file diff --git a/assets/images/icons/google.svg b/assets/images/icons/google.svg deleted file mode 100644 index 3ffa2aa..0000000 --- a/assets/images/icons/google.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/bookmarks.backup.json b/bookmarks.backup.json deleted file mode 100644 index c44dc44..0000000 --- a/bookmarks.backup.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - -] \ No newline at end of file diff --git a/bookmarks.json b/bookmarks.json deleted file mode 100644 index 0637a08..0000000 --- a/bookmarks.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/documentation/BIG_PICTURE_MODE.md b/documentation/BIG_PICTURE_MODE.md deleted file mode 100644 index a585940..0000000 --- a/documentation/BIG_PICTURE_MODE.md +++ /dev/null @@ -1,168 +0,0 @@ -# Big Picture Mode - Steam Deck & Controller UI - -Nebula Browser includes a **Big Picture Mode** - a controller-friendly, console-style interface designed for Steam Deck, handheld devices, and living room setups. - -## ๐Ÿš€ Single Window Architecture - -Big Picture Mode now opens **in the main window** instead of a separate window. This design: -- **Keeps resources low** - No extra Electron window process -- **Prevents SteamOS conflicts** - When auto-launching in Gaming Mode, Steam won't create a desktop mode alongside, preventing Steam from overriding controls to emulate keyboard/mouse -- **Seamless switching** - Navigate between Desktop and Big Picture modes smoothly - -## โš ๏ธ Steam Deck: Disabling Mouse Emulation - -If Steam is emulating mouse/keyboard input with the joysticks (overriding native controller support), you need to configure Steam Input: - -### Quick Fix - Steam Launch Options - -Add this to your Steam launch options (Right-click โ†’ Properties โ†’ Launch Options): - -```bash -SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% --big-picture -``` - -### Recommended Fix - Disable Steam Input Per-Game - -1. In Steam, right-click **Nebula** โ†’ **Properties** โ†’ **Controller** -2. Set **"Override for Nebula"** to **"Disable Steam Input"** -3. This completely disables Steam's input emulation for this app - -### Using the Steam Deck Launcher Script - -For the easiest setup, use the included launcher script: - -1. Set Steam launch options to: `./start-steamdeck.sh` - -The script automatically sets all necessary environment variables. - -See [README-STEAM.md](../README-STEAM.md) for detailed troubleshooting. - ---- - -## Features - -### ๐ŸŽฎ Controller Support -- **Full gamepad navigation** - Use D-pad or left stick to navigate -- **Button mapping**: - - **A / Cross** - Select/Activate - - **B / Circle** - Go Back - - **Y / Triangle** - Quick Search - - **Start** - Toggle Settings -- **Audio feedback** for navigation - -### ๐Ÿ“ฑ Optimized for Steam Deck -- **1280x800 native resolution** support -- Automatic detection of Steam Deck screens -- Large touch-friendly UI elements -- Fullscreen immersive experience - -### ๐ŸŽจ Modern Console-Style UI -- Inspired by Steam OS Big Picture and Xbox Dashboard -- Smooth animations and transitions -- Glowing focus indicators -- Dark theme optimized for OLED displays - -### โŒจ๏ธ On-Screen Keyboard -- Built-in virtual keyboard for controller input -- URL and search input support -- Special keys for common domains (.com, .org, etc.) - -## How to Access - -### From Desktop Mode -1. **Menu Button (โ˜ฐ)** โ†’ Click **"๐ŸŽฎ Big Picture Mode"** -2. **Settings** โ†’ **General** โ†’ Click **"Launch Big Picture Mode"** - -### Keyboard Shortcut -- Press `F11` while in Big Picture Mode to toggle fullscreen - -### 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 | -|---------|-------------| -| **Home** | Quick access sites, search, and recent browsing | -| **Bookmarks** | Your saved websites in a tile grid | -| **History** | Recently visited sites | -| **Downloads** | Downloaded files | -| **NeBot AI** | Launch the AI assistant | -| **Settings** | Theme, privacy, and display options | - -## Controller Button Reference - -| Button | Action | -|--------|--------| -| D-Pad / Left Stick | Navigate between elements | -| A / Cross | Select focused element | -| B / Circle | Go back / Close menu | -| Y / Triangle | Open search (on-screen keyboard) | -| Start | Open/Close settings | -| LB/RB | Scroll horizontally | - -## Exiting Big Picture Mode - -- Press the **Exit** button in the top-right corner -- Go to **Settings** โ†’ **Desktop Mode** -- Press `Escape` key multiple times - -## Technical Details - -### Files -- `renderer/bigpicture.html` - Main HTML structure -- `renderer/bigpicture.css` - Console-optimized styles -- `renderer/bigpicture.js` - Controller handling and navigation - -### Screen Detection -Big Picture Mode is suggested for displays matching: -- Steam Deck resolution: 1280ร—800 -- Screens smaller than 1366px width -- 16:10 or 16:9 aspect ratios - -### API -```javascript -// Check if Big Picture Mode is recommended -const suggested = await window.bigPictureAPI.isSuggested(); - -// Get screen information -const info = await window.bigPictureAPI.getScreenInfo(); - -// Check if currently in Big Picture Mode -const isActive = await window.bigPictureAPI.isActive(); - -// Launch Big Picture Mode (navigates main window) -await window.bigPictureAPI.launch(); - -// Exit Big Picture Mode (navigates back to desktop UI) -await window.bigPictureAPI.exit(); -``` - -## Customization - -The Big Picture Mode respects your theme settings. Colors are applied from your selected theme: -- Background colors -- Accent and primary colors -- Text colors - -## Known Limitations - -- Some complex web forms may be difficult to navigate with controller only -- Video players use native controls -- Right-click context menus require mouse/touch - -## Future Improvements - -- [ ] Rumble/haptic feedback for compatible controllers -- [ ] Voice search integration with NeBot -- [ ] Picture-in-picture mode for videos -- [ ] Game overlay mode -- [ ] Custom controller mappings diff --git a/documentation/CODE_OF_CONDUCT.md b/documentation/CODE_OF_CONDUCT.md deleted file mode 100644 index b976c63..0000000 --- a/documentation/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,119 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that are compassionate, direct, and respectful. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards and -will take appropriate and fair corrective action in response to any behavior that -they deem inappropriate, threatening, offensive, or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interaction in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/documentation/CONTRIBUTING.md b/documentation/CONTRIBUTING.md deleted file mode 100644 index f11c257..0000000 --- a/documentation/CONTRIBUTING.md +++ /dev/null @@ -1,65 +0,0 @@ -# Contributing to Nebula - -First off, thank you for considering contributing to Nebula! It's people like you that make open source such a great community. - -## How Can I Contribute? - -### Reporting Bugs - -- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Bobbybear007/NebulaBrowser/issues). -- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Bobbybear007/NebulaBrowser/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. - -### Suggesting Enhancements - -- Open a new issue to discuss your enhancement. Please provide a clear description of the enhancement and its potential benefits. - -### Pull Requests - -1. **Fork the repository** to your own GitHub account. -2. **Clone the project** to your machine. -3. **Create a new branch** for your changes: - ```sh - git checkout -b feature/your-feature-name - ``` -4. **Make your changes** and commit them with a clear, descriptive commit message: - ```sh - git commit -m "Add some feature" - ``` -5. **Push your branch** to your fork: - ```sh - git push origin feature/your-feature-name - ``` -6. **Open a pull request** to the `main` branch of the original repository. Provide a clear title and description for your pull request, explaining the changes you've made. - -## Styleguides - -### Git Commit Messages - -- Use the present tense ("Add feature" not "Added feature"). -- Use the imperative mood ("Move cursor to..." not "Moves cursor to..."). -- Limit the first line to 72 characters or less. -- Reference issues and pull requests liberally after the first line. - -### Code of Conduct - -We have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors are expected to follow. Please make sure you are familiar with its contents. - -### JavaScript Styleguide - -- All JavaScript must adhere to [StandardJS](https://standardjs.com/). This helps us maintain a consistent coding style. -- Use soft-tabs with a two-space indent. -- Prefer single quotes `'` over double quotes `"`. -- No semicolons. -- For more details, please refer to the [StandardJS rules](https://standardjs.com/rules.html). - -### CSS Styleguide - -- Follow a BEM-like naming convention for classes (`block__element--modifier`). -- Use soft-tabs with a two-space indent. -- Write selectors and their properties on separate lines. -- Organize properties logically (e.g., positioning, box model, typography, visual). -- Use `rem` for font sizes and `px` for borders. -- Use `===` and `!==` instead of `==` and `!=` for comparisons. -- Always declare variables with `const` or `let` instead of `var`. -- Use arrow functions instead of `function` where appropriate. -- Prefer template literals over string concatenation. diff --git a/documentation/CORE_CONCEPTS.md b/documentation/CORE_CONCEPTS.md deleted file mode 100644 index 96d90e7..0000000 --- a/documentation/CORE_CONCEPTS.md +++ /dev/null @@ -1,32 +0,0 @@ -# Core Concepts - -This document explains the core architectural concepts of the Nebula browser. - -### Main and Renderer Processes - -Electron applications have two types of processes: the **main process** and one or more **renderer processes**. - -- **Main Process**: The main process, which runs the `main.js` script, is the entry point of the application. It runs in a Node.js environment, meaning it has access to all Node.js APIs like `fs` for file system access and `ipcMain` for communication. It is responsible for creating and managing `BrowserWindow` instances, which are the application's windows. - -- **Renderer Process**: Each `BrowserWindow` runs its own renderer process. The renderer process is responsible for rendering web contentโ€”in Nebula's case, the browser's user interface (UI) built with HTML, CSS, and JavaScript. The renderer process does not have direct access to Node.js APIs for security reasons. - -### Inter-Process Communication (IPC) - -Since the main and renderer processes are separate, they need a way to communicate. This is done through Inter-Process Communication (IPC). - -- **`ipcMain` and `ipcRenderer`**: Electron provides the `ipcMain` and `ipcRenderer` modules for this purpose. The main process listens for messages using `ipcMain.handle`, and the renderer sends messages using `ipcRenderer.invoke`. - - Nebula uses IPC for features like downloads control, bookmarks/history management, performance reports, GPU diagnostics, and window controls. - -- **Context Bridge and Preload Script**: To securely expose APIs from the main process to the renderer process, Electron uses a **preload script** and the **context bridge**. The `preload.js` script runs in a special environment that has access to both the `window` object of the renderer process and Node.js APIs. The `contextBridge` is used to expose specific functions from the preload script to the renderer process, ensuring that the renderer process cannot access powerful Node.js APIs directly. - -### Performance and GPU Management - -- **Performance Monitoring**: The `performance-monitor.js` module helps track the application's performance by monitoring metrics like memory usage and page load times. This is essential for identifying and addressing performance bottlenecks. - -- **GPU Configuration**: The `gpu-config.js` and `gpu-fallback.js` modules manage GPU acceleration. Electron uses the system's GPU to render content, which can significantly improve performance. However, GPU drivers can sometimes be a source of instability. These modules allow Nebula to check the GPU status and apply fallbacks (like disabling hardware acceleration) if issues are detected, ensuring a more stable experience. - -### Authentication & User Agent Strategy - -- **Auth Flow Compatibility**: Nebula allows popup windows for http/https to preserve OAuth/SSO flows without stripping POST bodies. -- **WebAuthn**: Platform authenticator features are enabled where supported, with diagnostics logged to help troubleshoot availability. -- **User Agent**: The default Electron token is removed from the UA string to improve site compatibility while appending a `Nebula/` marker. diff --git a/documentation/Customization.md b/documentation/Customization.md deleted file mode 100644 index 5f07135..0000000 --- a/documentation/Customization.md +++ /dev/null @@ -1,68 +0,0 @@ -# Nebula Browser Themes - -This directory contains theme files for the Nebula Browser customization system. - -## Theme Structure - -Each theme is a JSON file with the following structure: - -```json -{ - "name": "Theme Name", - "colors": { - "bg": "#121418", - "darkBlue": "#0B1C2B", - "darkPurple": "#1B1035", - "primary": "#7B2EFF", - "accent": "#00C6FF", - "text": "#E0E0E0" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #121418 0%, #1B1035 100%)", - "version": "1.0", - "description": "Theme description" -} -``` - -## Color Properties - -- `bg`: Main background color -- `darkBlue`: Secondary dark blue accent -- `darkPurple`: Secondary dark purple accent -- `primary`: Primary accent color (used for buttons, logos) -- `accent`: Secondary accent color (used for highlights) -- `text`: Main text color - -## Layout Options - -- `centered`: Default centered layout -- `sidebar`: Sidebar navigation layout -- `compact`: Compact view layout - -## Directories - -- `/downloaded/`: Themes downloaded from the community -- `/user/`: User-created custom themes - -## Usage - -1. **Import Theme**: Go to Settings > Customization > Import Theme -2. **Export Theme**: Create your custom theme and export it -3. **Share Themes**: Share your exported .json files with other users - -## Creating Custom Themes - -1. Go to Settings > Browser Customization -2. Adjust colors and settings using the controls -3. Use the live preview to see changes -4. Save as custom theme or export to share - -## Community Themes - -Place downloaded community themes in the `/downloaded/` folder. The browser will automatically detect and make them available in the theme selector. - -## Non-Destructive Design - -All theme changes are stored separately and can be reset to default at any time. Your customizations never modify the original browser files. diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md deleted file mode 100644 index b545d71..0000000 --- a/documentation/FEATURES.md +++ /dev/null @@ -1,96 +0,0 @@ -# Features - -This document provides a detailed overview of the features available in Nebula. - -### Privacy Control - -Nebula is designed with your privacy in mind. You have granular control over your browsing data. - -- **Clear Browsing Data:** You can easily clear your browsing history, cookies, cache, and local storage. This can be done from the settings page. -- **No Tracking by Default:** Nebula does not collect any personal data for tracking or advertising purposes. - -### Tab Management - -Efficiently manage your browsing session with Nebula's tab management features. - -- **New Tabs:** Open new tabs to browse multiple websites at once. -- **Tab Controls:** Each tab has standard controls for closing. -- **Open in New Window:** You can pop a tab out into its own separate window. - -### Bookmarks - -Save and access your favorite websites with ease. - -- **Add Bookmarks:** Save the current page to your bookmarks. -- **View Bookmarks:** Access your saved bookmarks from the bookmarks bar or a dedicated page. - -### History - -Nebula keeps a record of your browsing and search history to help you find your way back to previously visited sites. - -- **Site History:** A list of all the websites you have visited. -- **Search History:** A list of all the searches you have made. -- **Clear History:** You can clear your history at any time from the settings page. - -### Downloads Manager - -Reliably download files with progress and controls. - -- **Progress & State:** Each download shows received/total bytes and status. -- **Pause/Resume/Cancel:** Control active downloads where the server supports resuming. -- **Open or Reveal:** Open downloaded files directly or show them in your file manager. -- **Safe Filenames:** Files are saved to your OS Downloads folder with automatic de-duplication. - -### Performance Monitoring - -Nebula includes built-in tools to help you monitor the browser's performance. - -- **Performance Report:** View a detailed report of performance metrics, including page load times and memory usage. -- **Force Garbage Collection:** Manually trigger garbage collection to free up memory. - -### GPU Acceleration Control - -For advanced users, Nebula provides tools to manage GPU acceleration. - -- **GPU Diagnostics:** View detailed information about your system's GPU and its status. -- **GPU Fallback:** If you experience rendering issues, you can apply a GPU fallback to use a more stable rendering path. This can help resolve visual glitches or crashes. - -### Native Context Menu - -Nebula provides a native right-click menu across pages and webviews. - -- **Navigation:** Back, Forward, Reload. -- **Links:** Open in new tab, Download link, Open externally, Copy address. -- **Images:** Open in new tab, Save image as, Copy image address. -- **Editing:** Undo/Redo, Cut/Copy/Paste, Select All when applicable. -- **Developer:** Inspect Element (opens DevTools docked by default). - -### Authentication & Web Compatibility - -- **OAuth/SSO Friendly:** Popup windows are allowed for http/https to support common login flows. -- **WebAuthn Diagnostics:** Platform authenticator features are enabled where supported and logged for troubleshooting. -- **Sturdy Navigation:** Login POST navigations are not intercepted by the main process. - -### Custom Themes & Customization - -Nebula offers extensive customization options to personalize your browsing experience. - -- **Theme System:** Choose from built-in themes (default, forest, ocean, sunset, cyberpunk, midnight-rose, arctic-ice, cherry-blossom, cosmic-purple, emerald-dream, mocha-coffee, lavender-fields) or create your own custom themes. -- **Live Theme Editor:** Modify colors, gradients, and layout options with real-time preview in the settings. -- **Import/Export Themes:** Share custom themes with the community or use themes created by other users. -- **Non-Destructive Design:** All customizations are stored separately and can be reset to default at any time. -- **Layout Options:** Switch between centered, sidebar, and compact view layouts. -- **Custom Branding:** Personalize the browser title and logo visibility. - -For detailed information about creating and managing themes, see the [Customization Guide](Customization.md). - -### Cross-Platform - -Nebula is built with Electron, allowing it to run on multiple operating systems. - -- **Windows, macOS, and Linux:** Enjoy a consistent browsing experience across different platforms. - -### Miscellaneous - -- **User Agent Strategy:** Electron token is removed from the default user agent to improve site compatibility while appending a `Nebula/x.y.z` marker. You can opt-in to include the Electron token by setting `NEBULA_DEBUG_ELECTRON_UA=1`. -- **Open Local Files:** Use the file picker to open `file://` URLs directly. diff --git a/documentation/GPU-FIX-README.md b/documentation/GPU-FIX-README.md deleted file mode 100644 index 329b8d7..0000000 --- a/documentation/GPU-FIX-README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Nebula Browser - GPU Error 18 Fix & Performance Optimizations - -## Problem Solved โœ… -**Error 18** - GPU process launch failure has been resolved. The browser now starts successfully and uses the best available rendering method. - -## What Was Fixed - -### 1. GPU Configuration System -- **New GPU Config Manager**: Created `gpu-config.js` that intelligently handles GPU setup -- **Automatic Fallback**: If GPU fails, automatically switches to software rendering -- **Progressive Enhancement**: Tries GPU acceleration first, falls back gracefully -- **No More Crashes**: Error 18 eliminated through proper GPU process handling - -### 2. Command Line Optimizations -**Essential Fixes:** -```javascript -app.commandLine.appendSwitch('no-sandbox'); -app.commandLine.appendSwitch('disable-dev-shm-usage'); -app.commandLine.appendSwitch('disable-gpu-sandbox'); -``` - -**Performance Improvements:** -```javascript -app.commandLine.appendSwitch('disable-background-timer-throttling'); -app.commandLine.appendSwitch('disable-renderer-backgrounding'); -app.commandLine.appendSwitch('max_old_space_size', '4096'); -``` - -### 3. Smart GPU Detection -The browser now: -- โœ… Detects GPU capabilities at startup -- โœ… Provides clear status information -- โœ… Offers recommendations for improvements -- โœ… Gracefully handles GPU failures - -## Performance Improvements Applied - -### 1. Memory Management -- **Debounced History Recording**: Reduces file I/O operations -- **Async File Operations**: Prevents UI blocking -- **Garbage Collection**: Manual GC triggering available -- **Memory Monitoring**: Built-in performance tracking - -### 2. Rendering Optimizations -- **Hardware Acceleration**: When available, uses GPU for better performance -- **Software Fallback**: Stable rendering when GPU isn't available -- **CSS Optimizations**: Hardware-accelerated animations and scrolling -- **Efficient Paint Management**: Reduced repaints and reflows - -### 3. Caching & Network -- **Request Caching**: HTTP cache headers for faster loading -- **Resource Preloading**: Critical resources loaded early -- **QUIC Protocol**: Faster network connections -- **localStorage Optimization**: Efficient bookmark and history management - -## Current Status - -### GPU Status: -- **Hardware Acceleration**: โŒ Not available on this system -- **Software Rendering**: โœ… Working perfectly -- **Stability**: โœ… No crashes, no Error 18 -- **Performance**: โœ… Optimized for software rendering - -### Browser Performance: -- **Startup Time**: โšก Significantly improved -- **Memory Usage**: ๐Ÿ“‰ Reduced and monitored -- **Responsiveness**: โœ… Smooth UI interactions -- **Stability**: โœ… Robust error handling - -## Diagnostic Tools Added - -### 1. GPU Diagnostics Page -Location: `renderer/gpu-diagnostics.html` -- Real-time GPU status monitoring -- WebGL and Canvas 2D testing -- Performance metrics -- Manual fallback controls - -### 2. Performance Monitor -- Memory usage tracking -- CPU monitoring -- Load time analysis -- Automatic reporting every 5 minutes - -### 3. Startup Script -Location: `start-gpu-safe.bat` -- Multiple GPU configuration options -- Debug mode with verbose logging -- Administrator privilege checking - -## Usage Instructions - -### Normal Startup: -```bash -npm start -``` - -### Diagnostic Startup: -```bash -start-gpu-safe.bat -``` - -### Check GPU Status: -1. Open browser -2. Navigate to GPU diagnostics page -3. View real-time status and recommendations - -## Why GPU Might Be Disabled - -Common reasons for GPU acceleration being unavailable: -1. **Outdated Drivers**: Graphics drivers need updating -2. **Hardware Limitations**: Older or integrated graphics -3. **Windows Settings**: Hardware acceleration disabled in system -4. **Virtual Environment**: Running in VM or remote desktop -5. **Security Software**: Antivirus blocking GPU access - -## Recommendations - -### For Better Performance: -1. **Update Graphics Drivers**: Check manufacturer website -2. **Windows Update**: Ensure system is up to date -3. **Hardware Acceleration**: Enable in Windows display settings -4. **Run as Administrator**: May help with GPU access -5. **Check Antivirus**: Temporarily disable to test - -### Current Configuration Works: -Even without GPU acceleration, the browser is now: -- โšก **Fast**: Software rendering optimized -- ๐Ÿ›ก๏ธ **Stable**: No crashes or errors -- ๐Ÿ”ง **Configurable**: Easy to adjust settings -- ๐Ÿ“Š **Monitored**: Performance tracking included - -## Files Modified/Added - -### Core Files: -- `main.js` - Enhanced GPU handling, performance optimizations -- `preload.js` - Improved API exposure with caching -- `performance-monitor.js` - System performance tracking - -### GPU Management: -- `gpu-config.js` - Intelligent GPU configuration -- `gpu-fallback.js` - Crash handling and fallbacks -- `start-gpu-safe.bat` - Diagnostic startup script - -### UI/CSS: -- `performance.css` - Hardware acceleration optimizations -- `gpu-diagnostics.html` - GPU status and testing page - -## Result: โœ… Problem Solved - -Your Nebula browser now: -1. **Starts without Error 18** โœ… -2. **Runs smoothly on your system** โœ… -3. **Uses optimal rendering method** โœ… -4. **Provides performance monitoring** โœ… -5. **Offers diagnostic tools** โœ… - -The browser is optimized to work great with or without GPU acceleration! diff --git a/documentation/MIT.md b/documentation/MIT.md deleted file mode 100644 index 4adb4f0..0000000 --- a/documentation/MIT.md +++ /dev/null @@ -1,31 +0,0 @@ -# MIT License - -The NebulaBrowser project is licensed under the MIT License. This license is a permissive open-source license that allows users to freely use, modify, distribute, and sublicense the software, provided that the original copyright notice and permission notice are included in all copies or substantial portions of the software. - -## Key Points of the MIT License - -1. **Permission to Use**: You are free to use this software for personal, educational, or commercial purposes without any restrictions. -2. **Modification and Distribution**: You can modify the source code and distribute your modified versions, as long as the original copyright notice is retained. -3. **No Warranty**: The software is provided "as is," without warranty of any kind. The authors are not liable for any damages or issues arising from the use of the software. - -## How It Applies to NebulaBrowser - -- **Open Contribution**: Developers are encouraged to contribute to the NebulaBrowser project. Contributions will also fall under the MIT License, ensuring that the project remains open and accessible to everyone. -- **Commercial Use**: Businesses can integrate NebulaBrowser into their products or services without needing to pay royalties or seek additional permissions. -- **Attribution**: Any distribution of NebulaBrowser or its derivatives must include the original copyright notice to acknowledge the work of the contributors. - -## Copyright Notice - -``` -Copyright (c) 2025 Zambazos Media Group - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` - -By using or contributing to NebulaBrowser, you agree to the terms of the MIT License. - -More on the MIT License [Here](https://tlo.mit.edu/understand-ip/exploring-mit-open-source-license-comprehensive-guide) \ No newline at end of file diff --git a/documentation/PROJECT_STRUCTURE.md b/documentation/PROJECT_STRUCTURE.md deleted file mode 100644 index 6cacbd2..0000000 --- a/documentation/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,40 +0,0 @@ -# Project Structure - -This document provides an in-depth look at the project's file and directory structure. - -- **`main.js`**: This is the heart of the Electron application. It's the main process script that controls the application's lifecycle, creates browser windows, and handles all interactions with the operating system. - - Manages native context menu, downloads, and OAuth/WebAuthn-friendly window behavior. - -- **`renderer/`**: This directory contains all the client-side code and assets for the browser's user interface (the renderer process). - - **`index.html`**: The main HTML file that serves as the container for the browser's UI, including the tab bar, address bar, and the webview for displaying web content. - - **`style.css`**: The primary stylesheet for the entire browser interface. - - **`script.js`**: The main JavaScript file for the renderer process. It handles all the user interactions within the browser window, such as creating new tabs, handling navigation, and communicating with the main process. - - **`home.html`**, **`home.css`**, **`home.js`**: These files define the content and behavior of the default home page (new tab page). - - **`settings.html`**, **`settings.css`**, **`settings.js`**: These files create the settings page, allowing users to configure the browser and manage their data. - - **`404.html`**, **`404.css`**: Files for the "page not found" error page. - - **`gpu-diagnostics.html`**: The page for displaying GPU information. - - **`performance.css`**: Styles for the performance monitoring page. - - **`icons.js`**, **`icons.json`**: Files related to managing icons within the UI. - - Other pages: downloads UI and settings integrate with main-process IPC. - -- **`preload.js`**: This script is a crucial part of Electron's security model. It runs in a privileged context before the renderer process's web page is loaded. It's used to selectively expose APIs from the main process to the renderer process via the `contextBridge`. - -- **`performance-monitor.js`**: A Node.js module that runs in the main process to track application performance metrics like memory usage and page load times. - -- **`gpu-config.js`** & **`gpu-fallback.js`**: These modules are responsible for managing GPU-related settings. `gpu-config.js` checks the system's GPU capabilities, and `gpu-fallback.js` provides mechanisms to disable or reduce GPU acceleration if problems are detected. - - `start-gpu-safe.bat` starts the app with safer GPU settings on Windows. - -- **`assets/`**: This directory holds all static assets. - - **`images/`**: Contains logos, icons, and other images used in the application. - - **`fonts/`**: Contains font files. - -- **`documentation/`**: Contains all supplementary documentation for the project. - -- **`*.json`**: Configuration and data files. - - **`package.json`**: Defines the project's metadata, dependencies, and scripts. - - **`bookmarks.json`**: Stores the user's bookmarks. - - **`site-history.json`**: Stores the user's browsing history. - - **`search-history.json`**: Stores the user's search history. - - **`bookmarks.backup.json`**: Auto-created backup of bookmarks on save. - -- **`start-gpu-safe.bat`**: A batch script for Windows users to start the application in a GPU-safe mode. diff --git a/documentation/UPLOAD-ITCH.md b/documentation/UPLOAD-ITCH.md deleted file mode 100644 index 5f9492f..0000000 --- a/documentation/UPLOAD-ITCH.md +++ /dev/null @@ -1,113 +0,0 @@ -# Uploading releases to itch.io using Butler - -This document explains how to prepare and upload Nebula releases to itch.io using Butler, covering macOS, Windows, and Linux. - -## Overview - -Butler (itch.io) is the recommended channel for distributing Nebula releases. The process is: - -1. Build your platform-specific artifact. -2. Install and authenticate `butler`. -3. Push the build to your `username/game:channel` with `butler push`. - -## Install Butler - -- macOS / Linux / Windows: Download the appropriate Butler binary from the official itch.io Butler releases (follow the official docs). Unpack, make executable, and place it on your `PATH`. - -Example (Linux/macOS): - -```bash -# after downloading 'butler' binary -chmod +x butler -sudo mv butler /usr/local/bin/ -``` - -On Windows, place `butler.exe` in a folder on `PATH` or use it directly from the build folder. - -## Authenticate - -Run: - -```bash -butler login -``` - -This opens a browser-based authentication flow. Verify with: - -```bash -butler whoami -``` - -If automation is required, consult the official Butler docs for API-key/token login options. - -## Prepare platform artifacts - -macOS -- Create a zip of your `.app` bundle (keep the `.app` as a top-level item inside the zip): - -```bash -ditto -c -k --sequesterRsrc --keepParent MyApp.app MyApp-mac.zip -``` - -Windows -- Zip the folder containing your `.exe` and runtime files, or create an installer and zip the installer. - -```powershell -Compress-Archive -Path .\build\MyApp\* -DestinationPath MyApp-windows.zip -``` - -Linux -- Create a tarball (or zip) of the Linux runtime files: - -```bash -tar -czf MyApp-linux.tar.gz -C build/linux . -``` - -Note: ensure the main binary has executable permissions before archiving. - -## Push to itch.io - -Basic command: - -```bash -butler push /: -``` - -Examples: - -```bash -# macOS build -butler push MyApp-mac.zip myuser/nebulabrowser:mac - -# Windows build -butler push MyApp-windows.zip myuser/nebulabrowser:windows - -# Linux build -butler push MyApp-linux.tar.gz myuser/nebulabrowser:linux -``` - -Set a release version for itch.io using `--userversion`: - -```bash -butler push MyApp-mac.zip myuser/nebulabrowser:mac --userversion 1.2.3 -``` - -## Recommended channel strategy - -- `stable` or `default` โ€” production releases -- `beta` โ€” pre-release testing -- Use platform-specific channels (e.g., `mac`, `windows`, `linux`) if you want separate channels per OS - -## Tips - -- Keep artifacts small and platform-specific to reduce download size. -- Verify the upload with `butler whoami` and by visiting your game page on itch.io. -- When testing on macOS, notarization and Gatekeeper may affect distribution; provide clear install instructions on your itch page. - -## Rollback - -Butler supports pushing to a channel multiple times; the latest pushed build becomes the current for that channel. To revert, push a previous artifact or use the itch.io web UI to select a previous build. - -## References - -Consult the official Butler documentation for advanced usage (credentials, automated CI uploads, delta uploads, and platform-specific packaging recommendations). diff --git a/documentation/archived/ELECTRON_UPGRADE_FIXES.md b/documentation/archived/ELECTRON_UPGRADE_FIXES.md deleted file mode 100644 index 8831828..0000000 --- a/documentation/archived/ELECTRON_UPGRADE_FIXES.md +++ /dev/null @@ -1,94 +0,0 @@ -# Electron Upgrade Feature - Bug Fixes - -## Problem Identified -The upgrade feature was downloading and installing new Electron versions successfully, but the app always showed the old version (1.0.0) after restart because: - -1. **Version Source Issue**: The app was reading `app.getVersion()` which gets the version from `package.json` at startup time -2. **Package.json Not Re-read**: Even after npm installed a new Electron version, the app didn't re-read the updated `package.json` -3. **Runtime Display**: The About tab showed the bundled Electron version (37.x) which is baked into the binary at build time - -## Solutions Implemented - -### 1. **New Helper Function: `getInstalledElectronVersion()`** -- Reads `package.json` directly every time it's called (not cached) -- Extracts the actual installed Electron version from `devDependencies` -- Handles both stable (`electron`) and nightly (`electron-nightly`) packages -- Strips version specifiers (^, ~, etc.) to get the clean version number -- Falls back to `app.getVersion()` if reading fails - -```javascript -function getInstalledElectronVersion() { - try { - const packageJsonPath = path.join(__dirname, 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - - const electronDep = packageJson.devDependencies?.electron; - const electronNightlyDep = packageJson.devDependencies?.['electron-nightly']; - - if (electronDep) { - return electronDep.replace(/^\D+/, ''); - } - if (electronNightlyDep) { - return electronNightlyDep.replace(/^\D+/, ''); - } - return app.getVersion(); - } catch (err) { - return app.getVersion(); - } -} -``` - -### 2. **Updated `get-electron-versions` Handler** -- Now uses `getInstalledElectronVersion()` instead of `app.getVersion()` -- Returns the actual installed version that was modified by npm -- Performs fresh version checks each time (no caching) - -### 3. **Improved `upgrade-electron` Handler** -- Increased `maxBuffer` to handle large npm output -- Added cleanup logic to remove the old Electron variant when switching types - - Removes `electron` when upgrading to `nightly` - - Removes `electron-nightly` when upgrading to `stable` -- Better error logging to debug npm failures -- Returns clearer messages about installation status - -### 4. **Enhanced UI/UX in settings.js** -- Added more descriptive status text ("Downloading and installing..." instead of just "Upgrading...") -- Disables all controls during upgrade to prevent multiple clicks -- Reduced restart delay from 2000ms to 1500ms for faster feedback -- Better error handling with proper cleanup of disabled states - -## How It Works Now - -1. **User clicks "Check for Updates"** - - Queries npm registry for latest version - - Uses `getInstalledElectronVersion()` to read current version from `package.json` - - Compares versions and shows if update is available - -2. **User clicks "Upgrade Electron"** - - Confirms action - - Runs `npm install --save-dev electron@latest` (or `electron-nightly@latest`) - - npm downloads and installs new version - - Handler removes the other Electron variant from `package.json` if needed - - Shows success message - -3. **App Restarts** - - Uses `app.relaunch()` and `app.quit()` - - When app relaunches, it: - - Loads new Electron binary from `node_modules` - - Runs new Electron version - - Settings page shows correct new version on next check - -## Testing Recommendations - -1. Test upgrading from stable to nightly version -2. Test upgrading from nightly back to stable -3. Verify version display updates after restart -4. Check that old variant is removed from `package.json` -5. Verify app runs stably with new Electron version - -## Notes for Future Development - -- The About tab displays `process.versions.electron` which is the bundled Chromium version, not the Electron framework version -- The Electron version we display in the upgrade section comes from `package.json` which is the actual framework version -- When building with electron-builder, the bundled version becomes fixed until next rebuild -- For development/testing, the upgrade feature reads live from `package.json` diff --git a/documentation/archived/GPU-FIX-README.md b/documentation/archived/GPU-FIX-README.md deleted file mode 100644 index fef0e51..0000000 --- a/documentation/archived/GPU-FIX-README.md +++ /dev/null @@ -1,3 +0,0 @@ -# GPU-FIX-README (archived) - -This file was present at repository root and has been archived here for reference. The canonical GPU fix documentation lives under `documentation/`. diff --git a/documentation/archived/README-PLUGINS.md b/documentation/archived/README-PLUGINS.md deleted file mode 100644 index 512ebbd..0000000 --- a/documentation/archived/README-PLUGINS.md +++ /dev/null @@ -1,98 +0,0 @@ -# Nebula Plugins (Early Preview) - -This document explains how to build simple plugins for Nebula. The initial API is intentionally small and will grow with feedback. - -## Overview - -- Plugins live under either of these folders: - - App folder: `/plugins//` - - User folder: `%APPDATA%/Nebula/plugins//` (Windows) โ€“ preferred for user-installed plugins. -- Each plugin has a `plugin.json` manifest. Optional `main.js` runs in the main process. Optional `renderer-preload.js` runs in the renderer preload context and can expose safe APIs via `contextBridge`. -- Plugins are loaded on app start. Toggle a plugin by setting `"enabled": false` in its manifest. - -## Manifest (plugin.json) - -Example: - -```json -{ - "id": "my-plugin", - "name": "My Plugin", - "version": "0.1.0", - "description": "What it does", - "main": "main.js", - "rendererPreload": "renderer-preload.js", - "categories": ["Search", "Productivity"], - "authors": ["Jane Doe", { "name": "Acme Labs", "email": "oss@acme.example" }], - "enabled": true -} -``` - -Fields: -- id: Unique id. Defaults to folder name if omitted. -- main: Optional entry for main process integration. -- rendererPreload: Optional file injected into the preload. Use it to expose a safe surface to the page. -- categories: Optional string or array of strings used for organizing/filtering plugins in UI and APIs. Example: ["AI", "Utilities"]. -- authors: Optional string or array of strings/objects describing authors. Objects support { name, email, url }. In APIs/UI, names are displayed. -- enabled: Defaults to true. - -## Main process API (activate) - -If `main` is present, export an `activate(ctx)` function. The `ctx` contains: -- Electron: `app`, `BrowserWindow`, `ipcMain`, `session`, `Menu`, `dialog`, `shell` -- paths: `{ appPath, userData, pluginDir }` -- log/warn/error: prefix logs with your plugin id -- on(event, cb): subscribe to lifecycle events (experimental) -- registerIPC(channel, handler): quickly expose an `ipcMain.handle` -- registerWebRequest(filter, listener): attach `session.webRequest.onBeforeRequest` - -Example: - -module.exports.activate = (ctx) => { - ctx.log('hello'); - ctx.registerIPC('my-plugin:do', async (_evt, payload) => ({ ok: true })); - ctx.registerWebRequest({ urls: ['*://*/*'] }, (details) => ({ cancel: false })); -}; - -## Renderer preload API - -If `rendererPreload` is present, it will be `require()`-d from the app preload. You can use `contextBridge` to expose a safe surface to the page: - -const { contextBridge, ipcRenderer } = require('electron'); -contextBridge.exposeInMainWorld('myPlugin', { - hello: () => ipcRenderer.invoke('my-plugin:do'), -}); - -Your exposed API will be available on `window.myPlugin` in `renderer/` code (e.g., `script.js`). - -## Sample plugin - -A working sample is included at `plugins/sample-hello/`: -- Adds menu item "Say Hello (Sample Plugin)" under Help. -- Exposes `window.sampleHello.ping()` and `window.sampleHello.onHello(cb)`. - -Try it from the DevTools console: - -await window.sampleHello.ping(); -window.sampleHello.onHello((m) => console.log('got hello', m)); - -Click Help -> Say Hello (Sample Plugin) to see the message delivered to the page. - -## Loading order and safety - -- Plugins load after the app is ready. Renderer preloads run after Nebula's own preload has exposed its APIs. -- Context isolation stays enabled. Only data explicitly exposed via `contextBridge` is available to pages. -- Avoid long blocking work in plugin activation. - -## Debugging - -- See logs with a `[Plugin:]` prefix in the app console. -- Temporarily disable a plugin by setting `enabled: false` in `plugin.json`. - -## Roadmap - -This is a first pass. Planned next: -- Enable plugin settings UI -- Hot reload/reload button -- More lifecycle hooks (tab events, context menu contributions) -- Theming hooks diff --git a/documentation/archived/README-STEAM.md b/documentation/archived/README-STEAM.md deleted file mode 100644 index dfffd61..0000000 --- a/documentation/archived/README-STEAM.md +++ /dev/null @@ -1,238 +0,0 @@ -Converting extracted AppImage (`squashfs-root`) into a distributable AppDir for Steam - -> Note: Nebula will not be distributed on the Steam Store. This document is -> kept for reference and for users who run Nebula via Steam as a non-Steam -> shortcut. Official distribution will be handled via itch.io and other -> non-Steam channels. - -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`. - ---- - -## Built-in Controller Support (Steam Deck / Game Mode) - -Nebula has **native gamepad support** that signals to Steam that the application is consuming controller input. This prevents Steam from applying Desktop mouse/keyboard emulation when running in Game Mode. - -### How It Works - -Steam Deck only stops applying Desktop mouse emulation when: -1. The application actively reads controller/gamepad input, OR -2. Steam Input is enabled (which requires explicit configuration) - -If an app does not read controller input at all, Steam assumes the user needs mouse emulation. - -Nebula solves this by: -1. **Preload Gamepad Handler**: The preload script (`preload.js`) continuously polls `navigator.getGamepads()` from the moment any window loads. This signals to Steam that the app is consuming gamepad events and should not apply mouse emulation. -2. **Big Picture Mode**: Full controller-friendly UI with: - - D-pad / Left stick: Navigate menus - - A button: Select/activate - - B button: Back - - X button: Backspace (in keyboard) - - Y button: Space / Open search - - LB/RB: Navigate webview history - - Right stick: Virtual cursor (in browse mode) - - Triggers: Left/right click (in browse mode) - - Start: Toggle settings/sidebar - - Select: Toggle fullscreen webview - -### Gamepad API (for Developers) - -The gamepad handler exposes an API via `window.gamepadAPI`. - -```javascript -// Check if gamepad handler is initialized -if (gamepadAPI.isAvailable()) { - console.log('Gamepad handler is running'); -} - -// Check if a gamepad is connected -if (gamepadAPI.isConnected()) { - console.log('Gamepad connected!'); -} - -// Get list of connected gamepads -const gamepads = gamepadAPI.getConnected(); -// Returns: [{ id, index, mapping, buttons, axes }, ...] -console.log(gamepads); - -// Get active gamepad's current state (buttons and axes) -const active = gamepadAPI.getActive(); -if (active) { - console.log('Active gamepad:', active.id); - console.log('Buttons:', active.buttons); - console.log('Axes:', active.axes); -} - -// Get handler state for debugging -const state = gamepadAPI.getState(); -console.log('Handler state:', state); -// Returns: { initialized, connectedCount, activeGamepadIndex, isPolling } - -// Listen for gamepad events (via CustomEvent on window) -window.addEventListener('nebula-gamepad-button', (e) => { - const { button, pressed, value } = e.detail; - console.log(`Button ${button}: ${pressed ? 'pressed' : 'released'}`); -}); - -window.addEventListener('nebula-gamepad-connect', (e) => { - console.log('Gamepad connected:', e.detail.id); -}); - -window.addEventListener('nebula-gamepad-disconnect', (e) => { - console.log('Gamepad disconnected:', e.detail.id); -}); - -window.addEventListener('nebula-gamepad-axis', (e) => { - const { axis, value } = e.detail; - console.log(`Axis ${axis}: ${value}`); -}); - -// Enable debug logging -gamepadAPI.setDebug(true); -``` - -### Troubleshooting - -If Steam is still applying mouse emulation: - -1. **Configure Steam Input per-game** (most reliable fix): - - **Windows / Desktop Steam UI**: - - Library โ†’ right-click Nebula โ†’ Properties โ†’ **Controller** - - Set **"Override for Nebula"** to **"Disable Steam Input"** - - **Steam Deck / SteamOS Gaming Mode**: - - Open Nebula โ†’ press the Steam button โ†’ **Controller Settings** (or the controller icon) - - Set the layout to a **Gamepad** template (not โ€œKeyboard/Mouseโ€), or disable Steam Input if the toggle is available - - If you **donโ€™t see a Controller tab** (common when the Steam entry is treated as an โ€œapplication/toolโ€): - - Use **Big Picture / Gaming Mode** and edit the **Controller Layout** for that specific entry. - - Or change Steamโ€™s global Desktop Layout: Steam โ†’ Settings โ†’ Controller โ†’ **Desktop Layout** โ†’ pick a gamepad-focused template or remove mouse/keyboard bindings. - -2. **Verify gamepad polling is active**: Open DevTools (F12) and run `gamepadAPI.getState()` - check that `isPolling` is `true` -3. **Check gamepad connection**: Run `gamepadAPI.getConnected()` to see detected gamepads -4. **Press a button first**: On Linux, the `gamepadconnected` event may not fire until the first button press -5. **Enable debug mode**: Run `gamepadAPI.setDebug(true)` to see detailed logs -6. **Restart the app**: Close Nebula completely and relaunch from Steam - -### Steam Launch Options - -#### Windows - -The `VAR=value %command%` syntax does **not** work on Windows. Use the Steam UI instead: - -1. **Library** โ†’ right-click Nebula โ†’ **Properties** โ†’ **Controller** โ†’ set to **"Disable Steam Input"** -2. If no Controller tab exists, open Steam in **Big Picture Mode** โ†’ Nebula โ†’ **Manage Game** (gear) โ†’ **Controller Options** โ†’ **Disable Steam Input** - -If you must use launch options on Windows, use this wrapper syntax: -```bat -cmd /c "set SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 && %command%" -``` - -#### Linux / SteamOS / Steam Deck - -Add these to your Steam launch options (Right-click game โ†’ Properties โ†’ Launch Options): - -```bash -# Disable Steam Input completely (recommended for native controller support) -SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 %command% - -# Force native gamepad without Steam's emulation layer -STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% - -# Combined - full native controller mode with Big Picture UI -SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% --big-picture - -# If you need to debug controller issues -SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 %command% --big-picture 2>&1 | tee ~/nebula-debug.log -``` - -### Steam Deck Recommended Setup - -For the best experience on Steam Deck: - -1. **Add Nebula as a Non-Steam Game** (if not using Steamworks version) -2. **Controller Settings**: - - Right-click Nebula โ†’ Properties โ†’ Controller - - Set to **"Disable Steam Input"** -3. **Launch Options**: - ``` - SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% --big-picture - ``` -4. **Shortcuts** (optional): - - Configure gamepad shortcuts in Steam for Steam button actions (screenshots, etc.) - -### Why This Is Needed - -Steam Deck / SteamOS Game Mode applies "Desktop Configuration" mouse/keyboard emulation to apps that don't appear to handle controller input. Even though Nebula polls `navigator.getGamepads()` continuously, Steam's input layer initializes before the app can signal its intent. - -The solution is two-fold: -1. **Environment variables** (`SDL_GAMECONTROLLER_*`) signal to Steam's SDL-based input layer early -2. **Steam Input settings** ("Disable Steam Input") bypasses the emulation entirely - -### Shipping Defaults (Steamworks โ€œSoftware/Appโ€ limitation) - -If your Steamworks package is categorized as **Software/Application**, Steamworks may not expose per-title Steam Input configuration the way it does for games. - -In that case: -- You generally **cannot force a global Steam Input toggle** for all users from Steamworks. -- The practical, shippable approach is to (a) **consume controller input natively** (Nebula does this via early Gamepad API polling) so Steam Deck/Game Mode backs off Desktop emulation, and (b) provide user-facing guidance for disabling Steam Input / choosing a Gamepad layout. - -If you need Steam Input defaults controlled centrally, the usual path is to ask Valve Partner Support to enable the relevant Steam Input configuration for your App ID, or to re-categorize the title where appropriate. - -### Force Big Picture Mode - -```bash -# Via command line -./Nebula --big-picture - -# Via environment -NEBULA_BIG_PICTURE=1 ./Nebula - -# Disable Big Picture Mode -./Nebula --no-big-picture -NEBULA_NO_BIG_PICTURE=1 ./Nebula -``` diff --git a/documentation/archived/README-linux-upload.md b/documentation/archived/README-linux-upload.md deleted file mode 100644 index c2bed17..0000000 --- a/documentation/archived/README-linux-upload.md +++ /dev/null @@ -1,212 +0,0 @@ -# Linux / SteamOS Build Upload Guide (SteamCMD) - -> Note: Nebula will not be distributed on the Steam Store. This Steam upload -> guide is retained for historical/reference purposes only. Official releases -> will be published on itch.io and other non-Steam channels. - -This guide explains how to upload the **Linux / SteamOS** build of Nebula Browser to Steam using **SteamCMD**. It is tailored to the current project layout on Steam Deck / Linux. - ---- - -## App & Depot IDs - -* **App ID:** 4290110 -* **Windows Depot:** 4290111 -* **Linux / SteamOS Depot:** 4290112 - -This guide covers **Linux only (4290112)**. - ---- - -## Current Folder Layout - -Home directory layout expected by this guide: - -``` -/home/deck/ -โ”œโ”€โ”€ steamcmd/ -โ”‚ โ””โ”€โ”€ steamcmd.sh -โ””โ”€โ”€ steam_build/ - โ”œโ”€โ”€ Nebula_SteamOS/ - โ”‚ โ”œโ”€โ”€ nebula-browser - โ”‚ โ””โ”€โ”€ ...other runtime files... - โ”œโ”€โ”€ output/ - โ””โ”€โ”€ app_4290110.vdf -``` - ---- - -## Step 1: Verify Executable Permissions - -The main Linux binary **must** be executable. - -```bash -cd ~/steam_build/Nebula_SteamOS -ls -la -``` - -Identify the main binary (for example `nebula-browser`) and run: - -```bash -chmod +x nebula-browser -``` - -Optional quick test: - -```bash -./nebula-browser -``` - ---- - -## Step 2: App Build VDF Configuration - -Ensure `app_4290110.vdf` exists at: - -``` -/home/deck/steam_build/app_4290110.vdf -``` - -Its contents should be **exactly**: - -``` -"AppBuild" -{ - "AppID" "4290110" - "Desc" "Nebula SteamOS build" - "BuildOutput" "output/" - "ContentRoot" "." - "Preview" "0" - - "Depots" - { - "4290112" - { - "FileMapping" - { - "LocalPath" "Nebula_SteamOS/*" - "DepotPath" "." - "recursive" "1" - } - } - } -} -``` - -Notes: - -* `ContentRoot "."` maps to `/home/deck/steam_build` -* `Nebula_SteamOS/*` uploads everything inside that folder -* Depot **4290112** is the Linux / SteamOS depot - ---- - -## Step 3: Launch SteamCMD - -```bash -cd ~/steamcmd -./steamcmd.sh -``` - -You should now see: - -``` -Steam> -``` - ---- - -## Step 4: Log In - -Inside SteamCMD: - -``` -login YOUR_STEAM_USERNAME -``` - -If Steam Guard is enabled, enter the code when prompted. - ---- - -## Step 5: Upload the Linux Build - -Inside SteamCMD, run **with full path** (no `~`): - -``` -run_app_build /home/deck/steam_build/app_4290110.vdf -``` - -Expected behavior: - -* Content scan starts -* Files upload to depot 4290112 -* A **BuildID** is printed on success - ---- - -## Step 6: Exit SteamCMD - -``` -quit -``` - ---- - -## Step 7: Assign Build to a Branch (Required) - -After upload completes: - -1. Open **Steamworks** -2. Go to **Nebula Browser โ†’ Builds** -3. Find the new Build ID -4. Assign it to a branch (`internal`, `beta`, or `default`) - -If this step is skipped, Steam Deck installs **0 bytes** even though upload succeeded. - ---- - -## Common Issues - -### App build file does not exist - -* SteamCMD does **not** expand `~` -* Always use `/home/deck/...` absolute paths - -### 0 bytes uploaded - -* `LocalPath` is wrong -* Build not assigned to a branch -* Executable missing or filtered out - -### Build installs but does not launch - -* Binary not executable -* Missing runtime libraries -* Incorrect launch configuration in Steamworks - ---- - -## Logs - -If something fails, check: - -```bash -tail -n 200 /home/deck/.local/share/Steam/logs/stderr.txt -tail -n 200 /home/deck/.local/share/Steam/logs/bootstrap_log.txt -``` - ---- - -## Notes for Steam Deck - -* Test in **Desktop Mode** first -* Then test in **Gaming Mode** -* Steam Input and sandboxing differ between modes -* Avoid absolute paths inside the app - ---- - -## Status - -This process uploads **Linux / SteamOS depot 4290112 only**. -Windows builds should be uploaded separately using depot **4290111**. diff --git a/documentation/archived/TYPING_ANIMATION_DEMO.md b/documentation/archived/TYPING_ANIMATION_DEMO.md deleted file mode 100644 index 4dde636..0000000 --- a/documentation/archived/TYPING_ANIMATION_DEMO.md +++ /dev/null @@ -1,133 +0,0 @@ -# Nebot Typing Animation Feature - -## Overview -Added a realistic typing animation to the Nebot chat interface that makes AI responses appear character by character, similar to ChatGPT and other modern AI chat interfaces. - -## Features Added - -### 1. **Typing Animation** -- Characters appear one by one instead of instantly -- Smooth, natural typing rhythm -- Configurable typing speed -- Blinking cursor indicator during typing - -### 2. **Settings Integration** -- **Enable/Disable Toggle**: Users can turn typing animation on/off -- **Speed Control**: Adjustable from 10-200 characters per second -- **Live Preview**: Speed indicator updates in real-time -- **Persistent Settings**: Preferences are saved and restored - -### 3. **Smart Behavior** -- **Queue Management**: Handles fast token streams efficiently -- **Graceful Fallback**: Falls back to instant display if disabled -- **Markdown Rendering**: Waits for typing to complete before rendering markdown -- **Auto-scroll**: Maintains scroll position during animation - -## Technical Implementation - -### Code Changes Made: - -#### 1. **page.js** - Main Logic -```javascript -// Typing animation state -let typingQueue = []; -let isTyping = false; -let typingSpeed = 25; // milliseconds per character -let typingEnabled = true; // can be toggled in settings - -function startTypingAnimation(element) { - if (isTyping || typingQueue.length === 0) return; - - isTyping = true; - element.classList.add('typing'); - - function typeNext() { - if (typingQueue.length === 0) { - isTyping = false; - element.classList.remove('typing'); - return; - } - - const char = typingQueue.shift(); - element.textContent += char; - els.messages.scrollTop = els.messages.scrollHeight; - - setTimeout(typeNext, typingSpeed); - } - - typeNext(); -} -``` - -#### 2. **page.css** - Visual Effects -```css -/* Typing animation cursor */ -.markdown.typing:after { - content: "โ–‹"; - color: var(--accent); - animation: blink 1s infinite; - margin-left: 1px; -} - -@keyframes blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } -} -``` - -#### 3. **Settings UI** - User Controls -- Checkbox to enable/disable typing animation -- Range slider for speed control (10-200 chars/sec) -- Real-time speed display -- Proper styling for form elements - -## User Experience - -### Before: -- Text appeared instantly when AI responded -- No visual feedback during response generation -- Less engaging interaction - -### After: -- Smooth character-by-character reveal -- Blinking cursor shows active typing -- Configurable speed for user preference -- More engaging, human-like interaction - -## Usage Instructions - -1. **Open Nebot**: Navigate to the Nebot page in Nebula Browser -2. **Start a Chat**: Send a message to begin conversation -3. **Watch the Animation**: AI responses will type out naturally -4. **Customize Settings**: - - Click the โš™ Settings button - - Toggle "Enable typing animation" - - Adjust typing speed with the slider - - Save changes - -## Performance Considerations - -- **Efficient Queuing**: Uses character queue to handle fast token streams -- **Memory Friendly**: Minimal memory overhead -- **Responsive**: Maintains smooth UI during animation -- **Interruptible**: Can be disabled without restart - -## Future Enhancements - -Potential improvements could include: -- Variable speed based on punctuation (pause at periods) -- Sound effects for typing -- Different animation styles -- Per-conversation speed settings -- Typing speed based on message length - -## Testing - -To test the feature: -1. Start Nebula Browser (`npm start`) -2. Navigate to Nebot page -3. Send a message and observe the typing animation -4. Try different speed settings in the settings panel -5. Toggle the feature on/off to compare experiences - -The typing animation enhances the user experience by making AI interactions feel more natural and engaging, similar to popular chat interfaces like ChatGPT. diff --git a/documentation/oauth-debug.md b/documentation/oauth-debug.md deleted file mode 100644 index 33547d2..0000000 --- a/documentation/oauth-debug.md +++ /dev/null @@ -1,53 +0,0 @@ -# Google OAuth Sign-in Debug Guide - -## Changes Made to Fix Google Sign-in Issues - -### 1. User Agent Strategy -- Nebula removes the default Electron token from the UA and appends `Nebula/` for better compatibility while still identifying the app. -- The UA is applied at the session level (main/default sessions) so all tabs/webviews inherit it. -- To debug with Electron visible in UA, set environment variable `NEBULA_DEBUG_ELECTRON_UA=1` before launch. - -### 2. Webview and Window Behavior -- Webviews inherit secure defaults from `webPreferences`. -- Popup windows opened by sites (e.g., OAuth) are allowed for `http`/`https` URLs to preserve login flows. - -### 3. Session Configuration for OAuth -- Configured session permissions for OAuth compatibility. -- Added cookie change monitoring for Google domains. -- Enhanced request headers (Accept-Language, Accept) and `Referrer-Policy` for OAuth endpoints. - -### 4. Unified Session Partitioning -- The main window uses partition `persist:main`, and sessions are configured consistently so auth/session state is shared across tabs. - -## Testing Google Sign-in - -1. **Open the browser** (already running) -2. **Navigate to** any Google service (Gmail, YouTube, Drive, etc.) -3. **Click Sign In** - you should now see the Google account picker -4. **Select your account** - should take you to password/2FA screen -5. **Complete sign-in** - should successfully sign you in - -Note: POST-based navigations are not blocked or intercepted by the main process to avoid stripping request bodies. - -## Debug Information - -If issues persist, check the Console (F12) for: -- Cookie changes for Google domains -- OAuth redirect flows -- JavaScript errors - -## Common OAuth Issues Fixed - -- โœ… Missing User Agent (Google blocks unidentified browsers) -- โœ… Third-party cookie restrictions -- โœ… Session isolation between tabs -- โœ… Missing referrer policies -- โœ… Popup blocking for OAuth flows - -## What Should Work Now - -- Google account picker should appear -- Password entry screens should load -- Two-factor authentication should work -- OAuth redirects should complete properly -- Session should persist across tabs diff --git a/gamepad-handler.js b/gamepad-handler.js deleted file mode 100644 index 747ad72..0000000 --- a/gamepad-handler.js +++ /dev/null @@ -1,453 +0,0 @@ -/** - * Nebula Browser - Global Gamepad Input Handler (Standalone Reference) - * - * NOTE: This is a standalone reference implementation. The actual gamepad handler - * used by Nebula is integrated directly into preload.js for proper context isolation - * compatibility. This file is kept for reference and potential future use. - * - * This module actively polls and consumes gamepad input from the Gamepad API. - * This is CRITICAL for Steam Deck/SteamOS Game Mode: - * - * Steam only stops applying Desktop mouse emulation when: - * - The application actively reads controller/gamepad input, OR - * - Steam Input is enabled (which requires explicit configuration) - * - * If the app does not read controller input at all, Steam assumes the user - * needs mouse emulation. By continuously polling navigator.getGamepads(), - * Steam recognizes that the app is consuming gamepad events and backs off - * the Desktop mouse emulation layer. - * - * This module should be loaded as early as possible in the renderer process. - */ - -(function() { - 'use strict'; - - // Prevent double initialization - if (window.__nebulaGamepadHandler) { - return; - } - - const CONFIG = { - // Polling rate in ms (60fps = ~16ms, we use requestAnimationFrame) - POLL_INTERVAL: 16, - - // Deadzone for analog sticks - STICK_DEADZONE: 0.15, - TRIGGER_DEADZONE: 0.1, - - // Enable debug logging - DEBUG: false, - }; - - // Global state - const state = { - initialized: false, - gamepads: {}, - connectedCount: 0, - activeGamepadIndex: null, - lastPollTime: 0, - rafId: null, - - // Button states for edge detection - buttonStates: {}, - - // Callbacks for interested listeners - listeners: { - connect: [], - disconnect: [], - button: [], - axis: [], - input: [], // Any input (for keeping the polling "active") - }, - }; - - // Debug logger - const log = (...args) => { - if (CONFIG.DEBUG) { - console.log('[NebulaGamepad]', ...args); - } - }; - - /** - * Initialize the gamepad handler. - * This should be called as early as possible. - */ - function init() { - if (state.initialized) { - log('Already initialized'); - return; - } - - if (typeof navigator === 'undefined' || !navigator.getGamepads) { - console.warn('[NebulaGamepad] Gamepad API not available'); - return; - } - - log('Initializing gamepad handler'); - - // Listen for connect/disconnect events - window.addEventListener('gamepadconnected', handleGamepadConnected); - window.addEventListener('gamepaddisconnected', handleGamepadDisconnected); - - // Do an initial scan for already-connected gamepads - // (important for Steam Deck where the controller is always connected) - scanGamepads(); - - // Start the polling loop immediately - // This is the KEY part: continuously polling getGamepads() signals to Steam - // that we're actively consuming gamepad input - startPolling(); - - state.initialized = true; - - log('Gamepad handler initialized'); - - // Expose debug info - if (CONFIG.DEBUG) { - window.__nebulaGamepadDebug = { - state, - getActiveGamepad, - getConnectedGamepads, - }; - } - } - - /** - * Handle gamepad connection event - */ - function handleGamepadConnected(event) { - const gamepad = event.gamepad; - log('Gamepad connected:', gamepad.index, gamepad.id); - - state.gamepads[gamepad.index] = { - id: gamepad.id, - index: gamepad.index, - connected: true, - mapping: gamepad.mapping, - timestamp: Date.now(), - }; - state.connectedCount++; - - // Set as active if we don't have one - if (state.activeGamepadIndex === null) { - state.activeGamepadIndex = gamepad.index; - log('Set active gamepad:', gamepad.index); - } - - // Initialize button states for this gamepad - state.buttonStates[gamepad.index] = {}; - - // Notify listeners - emitEvent('connect', { gamepad, index: gamepad.index, id: gamepad.id }); - } - - /** - * Handle gamepad disconnection event - */ - function handleGamepadDisconnected(event) { - const gamepad = event.gamepad; - log('Gamepad disconnected:', gamepad.index, gamepad.id); - - if (state.gamepads[gamepad.index]) { - state.gamepads[gamepad.index].connected = false; - delete state.gamepads[gamepad.index]; - state.connectedCount--; - } - - // Clear button states - delete state.buttonStates[gamepad.index]; - - // If this was the active gamepad, find another - if (state.activeGamepadIndex === gamepad.index) { - state.activeGamepadIndex = null; - - // Try to find another connected gamepad - const gamepads = navigator.getGamepads(); - for (let i = 0; i < gamepads.length; i++) { - if (gamepads[i]) { - state.activeGamepadIndex = i; - log('Switched active gamepad to:', i); - break; - } - } - } - - // Notify listeners - emitEvent('disconnect', { index: gamepad.index, id: gamepad.id }); - } - - /** - * Scan for already-connected gamepads - * This is important because on Linux/Steam Deck, the gamepadconnected event - * may not fire until the first button press - */ - function scanGamepads() { - const gamepads = navigator.getGamepads(); - - for (let i = 0; i < gamepads.length; i++) { - const gamepad = gamepads[i]; - if (gamepad && !state.gamepads[gamepad.index]) { - log('Found pre-connected gamepad:', gamepad.index, gamepad.id); - - state.gamepads[gamepad.index] = { - id: gamepad.id, - index: gamepad.index, - connected: true, - mapping: gamepad.mapping, - timestamp: Date.now(), - }; - state.connectedCount++; - - if (state.activeGamepadIndex === null) { - state.activeGamepadIndex = gamepad.index; - } - - state.buttonStates[gamepad.index] = {}; - } - } - } - - /** - * Start the gamepad polling loop - * Uses requestAnimationFrame for efficient, consistent polling - */ - function startPolling() { - if (state.rafId !== null) { - return; // Already polling - } - - function pollLoop(timestamp) { - state.lastPollTime = timestamp; - - // CRITICAL: This call to getGamepads() is what tells Steam we're - // actively consuming gamepad input - const gamepads = navigator.getGamepads(); - - // Process input from all connected gamepads - for (let i = 0; i < gamepads.length; i++) { - const gamepad = gamepads[i]; - if (gamepad) { - processGamepadInput(gamepad); - } - } - - // Also do periodic scans for newly connected gamepads - // (handles edge case where event doesn't fire) - if (timestamp % 1000 < 20) { - scanGamepads(); - } - - // Continue polling - state.rafId = requestAnimationFrame(pollLoop); - } - - state.rafId = requestAnimationFrame(pollLoop); - log('Started gamepad polling'); - } - - /** - * Stop the polling loop (called on page unload) - */ - function stopPolling() { - if (state.rafId !== null) { - cancelAnimationFrame(state.rafId); - state.rafId = null; - log('Stopped gamepad polling'); - } - } - - /** - * Process input from a gamepad - */ - function processGamepadInput(gamepad) { - const index = gamepad.index; - const buttonState = state.buttonStates[index] || {}; - let hasInput = false; - - // Process buttons - for (let i = 0; i < gamepad.buttons.length; i++) { - const button = gamepad.buttons[i]; - const wasPressed = buttonState[`b${i}`] || false; - const isPressed = button.pressed || button.value > 0.5; - - if (isPressed !== wasPressed) { - buttonState[`b${i}`] = isPressed; - hasInput = true; - - emitEvent('button', { - gamepad, - index, - button: i, - pressed: isPressed, - value: button.value, - }); - - log(`Button ${i}: ${isPressed ? 'pressed' : 'released'}`); - } - } - - // Process axes (analog sticks, triggers) - for (let i = 0; i < gamepad.axes.length; i++) { - const value = gamepad.axes[i]; - const prevValue = buttonState[`a${i}`] || 0; - - // Only emit if there's significant change - if (Math.abs(value - prevValue) > 0.01) { - buttonState[`a${i}`] = value; - - // Check if beyond deadzone - if (Math.abs(value) > CONFIG.STICK_DEADZONE) { - hasInput = true; - - emitEvent('axis', { - gamepad, - index, - axis: i, - value, - }); - } - } - } - - state.buttonStates[index] = buttonState; - - // Emit generic input event if any input detected - if (hasInput) { - emitEvent('input', { gamepad, index }); - } - } - - /** - * Emit an event to registered listeners - */ - function emitEvent(type, data) { - const listeners = state.listeners[type] || []; - for (const listener of listeners) { - try { - listener(data); - } catch (err) { - console.error('[NebulaGamepad] Listener error:', err); - } - } - } - - /** - * Register a listener for gamepad events - * @param {string} type - Event type: 'connect', 'disconnect', 'button', 'axis', 'input' - * @param {function} callback - Callback function - * @returns {function} Unsubscribe function - */ - function on(type, callback) { - if (!state.listeners[type]) { - state.listeners[type] = []; - } - state.listeners[type].push(callback); - - return () => { - const idx = state.listeners[type].indexOf(callback); - if (idx !== -1) { - state.listeners[type].splice(idx, 1); - } - }; - } - - /** - * Get the currently active gamepad - * @returns {Gamepad|null} - */ - function getActiveGamepad() { - if (state.activeGamepadIndex === null) { - return null; - } - const gamepads = navigator.getGamepads(); - return gamepads[state.activeGamepadIndex] || null; - } - - /** - * Get all connected gamepads - * @returns {Gamepad[]} - */ - function getConnectedGamepads() { - const gamepads = navigator.getGamepads(); - return Array.from(gamepads).filter(gp => gp !== null); - } - - /** - * Check if any gamepad is connected - * @returns {boolean} - */ - function isGamepadConnected() { - return state.connectedCount > 0; - } - - /** - * Set the active gamepad by index - * @param {number} index - */ - function setActiveGamepad(index) { - const gamepads = navigator.getGamepads(); - if (gamepads[index]) { - state.activeGamepadIndex = index; - log('Active gamepad set to:', index); - return true; - } - return false; - } - - // Cleanup on page unload - window.addEventListener('beforeunload', () => { - stopPolling(); - window.removeEventListener('gamepadconnected', handleGamepadConnected); - window.removeEventListener('gamepaddisconnected', handleGamepadDisconnected); - }); - - // Pause polling when page is hidden to save resources - // but not for too long - we still want Steam to see we're active - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - // Continue polling but at a slower rate when hidden - // We don't stop entirely because Steam needs to see we're consuming input - log('Page hidden, continuing polling'); - } else { - log('Page visible'); - } - }); - - // Export the API - const gamepadHandler = { - init, - on, - getActiveGamepad, - getConnectedGamepads, - isGamepadConnected, - setActiveGamepad, - - // Expose state for debugging - get state() { - return { ...state, buttonStates: { ...state.buttonStates } }; - }, - - // Config - get config() { - return { ...CONFIG }; - }, - setDebug(enabled) { - CONFIG.DEBUG = !!enabled; - }, - }; - - // Mark as initialized and expose globally - window.__nebulaGamepadHandler = gamepadHandler; - - // Auto-initialize when DOM is ready (or immediately if already loaded) - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - // DOM already loaded, initialize immediately - init(); - } - - log('Gamepad handler module loaded'); - -})(); diff --git a/gpu-config.js b/gpu-config.js deleted file mode 100644 index bf76853..0000000 --- a/gpu-config.js +++ /dev/null @@ -1,152 +0,0 @@ -// gpu-config.js - Comprehensive GPU configuration manager -const { app } = require('electron'); - -function envTruthy(value) { - if (value === undefined || value === null) return false; - const normalized = String(value).trim().toLowerCase(); - return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on'; -} - -class GPUConfig { - constructor() { - this.isGPUSupported = false; - this.fallbackApplied = false; - } - - // Apply GPU configuration based on system capabilities - configure() { - console.log('Configuring GPU settings...'); - - // Try to detect if we're on a system that supports GPU acceleration - const platform = process.platform; - const arch = process.arch; - - console.log(`Platform: ${platform}, Architecture: ${arch}`); - - // Start with conservative settings that usually work - this.applyConservativeSettings(); - - if (platform === 'linux') { - const env = process.env; - const profile = String(env.NEBULA_GPU_PROFILE || '').toLowerCase(); - const forcedSoftware = envTruthy(env.NEBULA_GPU_FORCE_SOFTWARE) || profile === 'software'; - const optInRequested = envTruthy(env.NEBULA_GPU_TWEAKS) || envTruthy(env.NEBULA_GPU_ALLOW_LINUX) || envTruthy(env.NEBULA_GPU_FORCE_GPU) || (profile && profile !== 'software') || Boolean(env.NEBULA_GPU_GL) || Boolean(env.NEBULA_GPU_EXTRA_ARGS); - - if (forcedSoftware || !optInRequested) { - console.log('Linux detected: Disabling GPU (no opt-in overrides present) and enforcing no-sandbox'); - app.commandLine.appendSwitch('disable-gpu'); - app.commandLine.appendSwitch('no-sandbox'); - this.fallbackApplied = true; - return; - } - - console.log('Linux GPU opt-in detected: leaving GPU acceleration enabled for this session'); - } - - // Try to enable GPU features progressively - this.tryEnableGPU(); - } - - applyConservativeSettings() { - // Essential switches that usually don't cause issues - app.commandLine.appendSwitch('no-sandbox'); - app.commandLine.appendSwitch('disable-dev-shm-usage'); - app.commandLine.appendSwitch('disable-gpu-sandbox'); - - // Performance improvements that don't rely on GPU - app.commandLine.appendSwitch('disable-background-timer-throttling'); - app.commandLine.appendSwitch('disable-renderer-backgrounding'); - app.commandLine.appendSwitch('disable-backgrounding-occluded-windows'); - app.commandLine.appendSwitch('enable-quic'); - app.commandLine.appendSwitch('max_old_space_size', '4096'); - } - - tryEnableGPU() { - try { - // GPU acceleration switches - app.commandLine.appendSwitch('ignore-gpu-blacklist'); - app.commandLine.appendSwitch('ignore-gpu-blocklist'); - - // 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'); - app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode'); - - // Conservative feature enabling - app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecoder'); - - console.log('GPU acceleration switches applied'); - } catch (err) { - console.error('Error applying GPU switches:', err); - this.applyFallback(); - } - } - - applyFallback() { - console.log('Applying GPU fallback configuration...'); - - // Force software rendering if GPU fails - app.commandLine.appendSwitch('disable-gpu'); - app.commandLine.appendSwitch('disable-gpu-compositing'); - app.commandLine.appendSwitch('disable-software-rasterizer'); - - this.fallbackApplied = true; - this.isGPUSupported = false; - } - - // Check if GPU is working after app starts - async checkGPUStatus() { - try { - const gpuInfo = app.getGPUFeatureStatus(); - - // Check if any critical GPU features are enabled - const enabledFeatures = Object.entries(gpuInfo) - .filter(([key, value]) => !value.includes('disabled')) - .map(([key]) => key); - - this.isGPUSupported = enabledFeatures.length > 2; // At least some features working - - console.log('GPU Status Check:'); - console.log('- Enabled features:', enabledFeatures); - console.log('- GPU supported:', this.isGPUSupported); - - return { - isSupported: this.isGPUSupported, - enabledFeatures, - fullStatus: gpuInfo - }; - } catch (err) { - console.error('GPU status check failed:', err); - return { isSupported: false, error: err.message }; - } - } - - getRecommendations() { - const recommendations = []; - - if (!this.isGPUSupported) { - recommendations.push('GPU acceleration is not available on this system'); - recommendations.push('The browser will use software rendering (slower but stable)'); - recommendations.push('Consider updating your graphics drivers'); - recommendations.push('Check if your system supports hardware acceleration'); - } else { - recommendations.push('GPU acceleration is working'); - recommendations.push('Browser should have good performance'); - } - - if (this.fallbackApplied) { - recommendations.push('Fallback mode is active due to GPU issues'); - recommendations.push('Performance may be reduced but stability improved'); - } - - return recommendations; - } -} - -module.exports = GPUConfig; diff --git a/gpu-fallback.js b/gpu-fallback.js deleted file mode 100644 index a503257..0000000 --- a/gpu-fallback.js +++ /dev/null @@ -1,105 +0,0 @@ -// gpu-fallback.js - GPU error handling and fallback system -const { app } = require('electron'); - -class GPUFallback { - constructor() { - this.gpuEnabled = true; - this.fallbackLevel = 0; - this.maxFallbacks = 3; - } - - // Apply progressive GPU fallbacks - applyFallback(level = 0) { - console.log(`Applying GPU fallback level ${level}`); - - switch (level) { - case 0: - // Level 0: Conservative GPU settings (already applied in main.js) - break; - - case 1: - // Level 1: Disable hardware acceleration for some features - app.commandLine.appendSwitch('disable-accelerated-2d-canvas'); - app.commandLine.appendSwitch('disable-accelerated-jpeg-decoding'); - break; - - case 2: - // Level 2: Software rendering only - app.commandLine.appendSwitch('disable-gpu'); - app.commandLine.appendSwitch('disable-gpu-compositing'); - this.gpuEnabled = false; - break; - - case 3: - // Level 3: Most conservative settings - app.commandLine.appendSwitch('disable-gpu'); - app.commandLine.appendSwitch('disable-gpu-compositing'); - app.commandLine.appendSwitch('disable-software-rasterizer'); - app.commandLine.appendSwitch('disable-2d-canvas-image-chromium'); - this.gpuEnabled = false; - break; - - default: - console.warn('Maximum fallback level reached'); - } - - this.fallbackLevel = level; - } - - // Check if GPU is working properly - async checkGPUStatus() { - try { - const gpuInfo = app.getGPUFeatureStatus(); - - // Check for critical GPU failures - const criticalFeatures = ['gpu_compositing', 'webgl', 'webgl2']; - const failures = criticalFeatures.filter(feature => - gpuInfo[feature] && gpuInfo[feature].includes('disabled') - ); - - if (failures.length > 0) { - console.warn('GPU features disabled:', failures); - return false; - } - - return true; - } catch (err) { - console.error('GPU status check failed:', err); - return false; - } - } - - // Handle GPU process crashes - setupCrashHandling() { - let crashCount = 0; - - app.on('gpu-process-crashed', (event, killed) => { - crashCount++; - console.error(`GPU process crashed (count: ${crashCount}), killed: ${killed}`); - - if (crashCount >= 3 && this.fallbackLevel < this.maxFallbacks) { - console.log('Too many GPU crashes, applying fallback...'); - this.applyFallback(this.fallbackLevel + 1); - - // Restart the app if needed - if (!killed) { - setTimeout(() => { - app.relaunch(); - app.exit(); - }, 1000); - } - } - }); - } - - // Get current GPU status - getStatus() { - return { - gpuEnabled: this.gpuEnabled, - fallbackLevel: this.fallbackLevel, - isHardwareAccelerated: this.fallbackLevel < 2 - }; - } -} - -module.exports = GPUFallback; diff --git a/main.js b/main.js deleted file mode 100644 index f47c678..0000000 --- a/main.js +++ /dev/null @@ -1,3056 +0,0 @@ -// ============================================================================= -// STEAM DECK / STEAMOS CONTROLLER INPUT FIX -// ============================================================================= -// These environment variables MUST be set before Electron/Chromium initializes. -// They signal to Steam's input layer that this application handles its own -// controller input and should NOT have mouse/keyboard emulation applied. -// -// Without these, Steam assumes the app needs Desktop mouse emulation when running -// in Game Mode, which overrides the app's native gamepad support. -// ============================================================================= - -// Tell SDL (and by extension Steam Input) that this app uses the gamepad API -// SDL_GAMECONTROLLERCONFIG is used by SDL to know about controllers -process.env.SDL_GAMECONTROLLERCONFIG = process.env.SDL_GAMECONTROLLERCONFIG || ''; - -// Signal that this app handles gamepad input natively -// This prevents Steam from applying mouse emulation in Game Mode -// IMPORTANT: set to 0 to avoid Steam's virtual gamepad layer when possible. -// Forcing this to 1 can keep Steam virtualization/emulation active. -process.env.SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD = - process.env.SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD ?? '0'; - -// Prevent Steam from remapping the controller to keyboard/mouse -// Setting to '1' tells Steam we want raw controller access -process.env.SDL_GAMECONTROLLER_IGNORE_DEVICES = ''; - -// Disable Steam's overlay input hooks for this process if possible -process.env.SteamNoOverlayUIDrawing = process.env.SteamNoOverlayUIDrawing || '0'; - -// Tell Steam Input we're a native controller app -// When STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD is 0, Steam won't virtualize the gamepad -process.env.STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD = - process.env.STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD ?? '0'; - -// Hint that this is a game/controller-focused app -process.env.SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS = '1'; - -// ============================================================================= -// STEAMWORKS API INTEGRATION -// ============================================================================= -// Initialize Steam API to properly signal to Steam that this app handles -// controller input natively. This is more reliable than environment variables -// alone for disabling Steam Input's mouse/keyboard emulation. -// -// NOTE: Since Nebula is categorized as Software (not a Game), we can't configure -// Steam Input settings in the Steamworks dashboard. Instead, we initialize the -// Steam Input API directly to signal native controller handling. -// ============================================================================= - -let steamworksClient = null; -let steamworksInitialized = false; -let steamInput = null; -let steamworksModule = null; -let steamCallbacksInterval = null; - -function initializeSteamworks() { - try { - const steamworks = require('steamworks.js'); - steamworksModule = steamworks; - - // Initialize with Nebula's Steam App ID - steamworksClient = steamworks.init(4290110); - steamworksInitialized = true; - - // Log successful initialization - const playerName = steamworksClient.localplayer.getName(); - console.log(`[Steamworks] Initialized successfully for user: ${playerName}`); - - // Initialize Steam Input API - this tells Steam we handle controllers natively - // and should prevent mouse/keyboard emulation in Game Mode - try { - steamInput = steamworksClient.input; - if (steamInput) { - console.log('[Steamworks] Steam Input API available - native controller mode enabled'); - - // Explicitly initialize Steam Input. - // Also ensure Steam callbacks are pumped; Steamworks features (including input) - // depend on runCallbacks being called regularly. - try { - if (typeof steamInput.init === 'function') { - steamInput.init(); - } - } catch (initErr) { - console.log('[Steamworks] Steam Input init failed:', initErr.message); - } - - if (!steamCallbacksInterval && typeof steamworks.runCallbacks === 'function') { - steamCallbacksInterval = setInterval(() => { - try { - steamworks.runCallbacks(); - } catch { - // Ignore callback pump errors to avoid crashing the app. - } - }, 100); - } - - // Try to get connected controllers to verify input is working - try { - const controllers = steamInput.getControllers(); - if (controllers && controllers.length > 0) { - console.log(`[Steamworks] Found ${controllers.length} connected controller(s)`); - } - } catch (inputErr) { - // Controller enumeration may not be available, that's OK - } - } - } catch (inputErr) { - console.log('[Steamworks] Steam Input API not fully available:', inputErr.message); - } - - return true; - } catch (e) { - // Not running through Steam, or steamworks.js not available - // This is fine - app works without Steam API - if (e.code === 'MODULE_NOT_FOUND') { - console.log('[Steamworks] steamworks.js not installed - running without Steam API'); - } else if (e.message && e.message.includes('Steam client')) { - console.log('[Steamworks] Steam client not running - running without Steam API'); - } else { - console.log('[Steamworks] Failed to initialize:', e.message || e); - } - return false; - } -} - -// Initialize Steamworks early (before app.ready) -// This is critical for Steam Input to recognize native controller support -initializeSteamworks(); - -const { app, BrowserWindow, BrowserView, ipcMain, session, screen, shell, dialog, Menu, clipboard, webContents } = require('electron'); - -// Cleanup Steam callback pump on exit -app.once('before-quit', () => { - if (steamCallbacksInterval) { - clearInterval(steamCallbacksInterval); - steamCallbacksInterval = null; - } - try { - steamInput?.shutdown?.(); - } catch {} -}); -const { autoUpdater } = require('electron-updater'); -const { pathToFileURL } = require('url'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const { spawn } = require('child_process'); -const PerformanceMonitor = require('./performance-monitor'); -const GPUFallback = require('./gpu-fallback'); -const GPUConfig = require('./gpu-config'); -const PluginManager = require('./plugin-manager'); -const portableData = require('./portable-data'); - -// Windows: set explicit AppUserModelID to ensure proper default-app registration -// and notification branding. -if (process.platform === 'win32') { - try { - app.setAppUserModelId('com.andrewzambazos.nebula'); - } catch {} -} - -// --- Single instance + protocol URL handling --- -let pendingOpenUrl = null; - -function extractUrlFromArgv(argv = []) { - return argv.find(arg => /^https?:\/\//i.test(arg)); -} - -function openUrlInExistingWindow(targetUrl) { - if (!targetUrl) return false; - const windows = BrowserWindow.getAllWindows(); - const mainWindow = windows.find(w => { - try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; } - }); - - if (mainWindow) { - try { mainWindow.show(); } catch {} - try { mainWindow.focus(); } catch {} - try { - mainWindow.webContents.send('open-url-new-tab', targetUrl); - return true; - } catch {} - try { - mainWindow.webContents.send('open-url', targetUrl); - return true; - } catch {} - } - - pendingOpenUrl = targetUrl; - return false; -} - -const gotSingleInstanceLock = app.requestSingleInstanceLock(); -if (!gotSingleInstanceLock) { - app.quit(); -} else { - app.on('second-instance', (_event, argv) => { - const url = extractUrlFromArgv(argv); - if (url) { - openUrlInExistingWindow(url); - return; - } - const windows = BrowserWindow.getAllWindows(); - const mainWindow = windows.find(w => { - try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; } - }); - if (mainWindow) { - try { mainWindow.show(); } catch {} - try { mainWindow.focus(); } catch {} - } - }); -} - -app.on('open-url', (event, url) => { - event.preventDefault(); - openUrlInExistingWindow(url); -}); - -// Capture protocol URL if the app was launched with one -const initialProtocolUrl = extractUrlFromArgv(process.argv); -if (initialProtocolUrl) { - pendingOpenUrl = initialProtocolUrl; -} - -// Initialize performance monitoring and GPU management -const perfMonitor = new PerformanceMonitor(); -const gpuFallback = new GPUFallback(); -const gpuConfig = new GPUConfig(); -const pluginManager = new PluginManager(); - -// ============================================================================= -// DESKTOP MODE: BrowserView tab management -// ============================================================================= -const desktopViewStateByWindowId = new Map(); -const desktopViewByWebContentsId = new Map(); -const menuPopupByWindowId = new Map(); -const MENU_POPUP_SIZE = { width: 240, height: 240 }; - -const SCROLL_NORMALIZATION_CSS = ` - *, *::before, *::after { scroll-behavior: auto !important; } - html, body { scroll-behavior: auto !important; } -`; - -const SCROLL_NORMALIZATION_JS = ` -(function() { - if (window.__nebulaScrollNormalized) return; - window.__nebulaScrollNormalized = true; - const SCROLL_SPEED = 100; - document.addEventListener('wheel', function(e) { - if (e.ctrlKey || e.metaKey || e.altKey) return; - let target = e.target; - let scrollable = null; - while (target && target !== document.body && target !== document.documentElement) { - const style = window.getComputedStyle(target); - const overflowY = style.overflowY; - const overflowX = style.overflowX; - if ((overflowY === 'auto' || overflowY === 'scroll') && target.scrollHeight > target.clientHeight) { scrollable = target; break; } - if ((overflowX === 'auto' || overflowX === 'scroll') && target.scrollWidth > target.clientWidth && e.shiftKey) { scrollable = target; break; } - target = target.parentElement; - } - if (!scrollable) scrollable = document.scrollingElement || document.documentElement || document.body; - let deltaY = e.deltaY; - let deltaX = e.deltaX; - if (e.deltaMode === 1) { - deltaY *= SCROLL_SPEED; deltaX *= SCROLL_SPEED; - } else if (e.deltaMode === 2) { - deltaY *= window.innerHeight; deltaX *= window.innerWidth; - } else { - const sign = deltaY > 0 ? 1 : -1; - deltaY = sign * Math.min(Math.abs(deltaY), SCROLL_SPEED * 3); - const signX = deltaX > 0 ? 1 : -1; - deltaX = signX * Math.min(Math.abs(deltaX), SCROLL_SPEED * 3); - } - e.preventDefault(); - scrollable.scrollBy({ top: deltaY, left: e.shiftKey ? deltaX : 0, behavior: 'auto' }); - }, { passive: false, capture: true }); -})(); -`; - -function getDesktopViewState(win) { - if (!win) return null; - let state = desktopViewStateByWindowId.get(win.id); - if (!state) { - state = { - views: new Map(), // tabId -> BrowserView - activeTabId: null, - bounds: null - }; - desktopViewStateByWindowId.set(win.id, state); - } - return state; -} - -function createMenuPopupWindow(parentWin) { - const menuWin = new BrowserWindow({ - modal: false, - frame: false, - transparent: true, - resizable: false, - show: false, - alwaysOnTop: true, - skipTaskbar: true, - focusable: false, - fullscreenable: false, - hasShadow: true, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - nodeIntegration: false, - contextIsolation: true, - sandbox: false, - partition: 'persist:main' - } - }); - - menuWin.setMenu(null); - try { menuWin.setAlwaysOnTop(true, 'pop-up-menu'); } catch {} - try { menuWin.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } catch {} - - const hideMenu = () => { - if (!menuWin.isDestroyed()) menuWin.hide(); - }; - - menuWin.on('blur', hideMenu); - parentWin.on('move', hideMenu); - parentWin.on('resize', hideMenu); - - menuWin.on('closed', () => { - try { menuPopupByWindowId.delete(parentWin.id); } catch {} - try { parentWin.removeListener('move', hideMenu); } catch {} - try { parentWin.removeListener('resize', hideMenu); } catch {} - }); - - menuWin.loadFile(path.join(__dirname, 'renderer', 'menu-popup.html')); - return menuWin; -} - -function positionMenuPopup(parentWin, menuWin, anchorRect) { - if (!parentWin || !menuWin) return; - - const width = MENU_POPUP_SIZE.width; - const height = MENU_POPUP_SIZE.height; - - const parentBounds = parentWin.getBounds(); - const contentBounds = parentWin.getContentBounds(); - - const rect = anchorRect && Number.isFinite(anchorRect.x) && Number.isFinite(anchorRect.y) - ? anchorRect - : null; - - let x; - let y; - - if (rect) { - x = Math.round(contentBounds.x + rect.x + rect.width - width); - y = Math.round(contentBounds.y + rect.y + rect.height + 6); - } else { - x = Math.round(parentBounds.x + parentBounds.width - width - 12); - y = Math.round(parentBounds.y + 52); - } - - const display = screen.getDisplayNearestPoint({ x, y }); - const workArea = display?.workArea || { x: 0, y: 0, width: 1920, height: 1080 }; - - if (x < workArea.x) x = workArea.x + 6; - if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width - 6; - if (y < workArea.y) y = workArea.y + 6; - if (y + height > workArea.y + workArea.height) { - const aboveY = rect - ? Math.round(contentBounds.y + rect.y - height - 6) - : Math.round(parentBounds.y + parentBounds.height - height - 12); - if (aboveY >= workArea.y) { - y = aboveY; - } else { - y = workArea.y + workArea.height - height - 6; - } - } - - menuWin.setBounds({ x, y, width, height }, false); -} - -function getOwnerWindowForContents(contents) { - if (!contents) return null; - try { - if (contents.hostWebContents) { - return BrowserWindow.fromWebContents(contents.hostWebContents); - } - } catch {} - try { - const maybeWin = BrowserWindow.fromWebContents(contents); - if (maybeWin) return maybeWin; - } catch {} - const mapped = desktopViewByWebContentsId.get(contents.id); - return mapped?.win || null; -} - -function getActiveDesktopViewWebContents(win) { - const state = getDesktopViewState(win); - if (!state || !state.activeTabId) return null; - const view = state.views.get(state.activeTabId); - return view?.webContents || null; -} - -function sendBrowserViewEvent(win, payload) { - try { - if (win && !win.isDestroyed()) { - win.webContents.send('browserview-event', payload); - } - } catch {} -} - -function createBrowserViewForTab(win, tabId, url) { - const state = getDesktopViewState(win); - if (!state) return null; - if (state.views.has(tabId)) return state.views.get(tabId); - - const view = new BrowserView({ - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - nodeIntegration: false, - contextIsolation: true, - partition: 'persist:main', - sandbox: false, - webSecurity: true, - allowRunningInsecureContent: false, - nativeWindowOpen: false, - additionalArguments: [`--nebula-tab-id=${tabId}`] - } - }); - - try { - if (!process.env.NEBULA_DEBUG_ELECTRON_UA) { - view.webContents.setUserAgent(app.userAgentFallback || computeBaseUA()); - } - } catch {} - - state.views.set(tabId, view); - desktopViewByWebContentsId.set(view.webContents.id, { win, tabId, view }); - - view.webContents.on('page-title-updated', (_e, title) => { - sendBrowserViewEvent(win, { tabId, type: 'page-title-updated', title }); - }); - - view.webContents.on('destroyed', () => { - try { desktopViewByWebContentsId.delete(view.webContents.id); } catch {} - try { state.views.delete(tabId); } catch {} - if (state.activeTabId === tabId) state.activeTabId = null; - }); - - view.webContents.on('page-favicon-updated', (_e, favicons) => { - sendBrowserViewEvent(win, { tabId, type: 'page-favicon-updated', favicons }); - }); - - view.webContents.on('did-navigate', (_e, url) => { - sendBrowserViewEvent(win, { tabId, type: 'did-navigate', url }); - }); - - view.webContents.on('did-navigate-in-page', (_e, url) => { - sendBrowserViewEvent(win, { tabId, type: 'did-navigate-in-page', url }); - }); - - view.webContents.on('did-finish-load', () => { - sendBrowserViewEvent(win, { tabId, type: 'did-finish-load' }); - }); - - view.webContents.on('did-fail-load', (_e, errorCode, errorDescription, validatedURL, isMainFrame) => { - sendBrowserViewEvent(win, { - tabId, - type: 'did-fail-load', - errorCode, - errorDescription, - validatedURL, - isMainFrame - }); - }); - - view.webContents.on('dom-ready', () => { - try { view.webContents.insertCSS(SCROLL_NORMALIZATION_CSS); } catch {} - try { view.webContents.executeJavaScript(SCROLL_NORMALIZATION_JS, true); } catch {} - sendBrowserViewEvent(win, { tabId, type: 'dom-ready' }); - }); - - view.webContents.on('focus', () => { - sendBrowserViewEvent(win, { tabId, type: 'focus' }); - }); - - // Route window.open() calls to tabs unless OAuth allowlist matched - view.webContents.setWindowOpenHandler((details) => { - const { url: targetUrl } = details; - if (!/^https?:\/\//i.test(targetUrl)) return { action: 'deny' }; - const oauthDomains = [ - 'accounts.google.com', - 'login.microsoftonline.com', - 'appleid.apple.com', - 'github.com/login', - 'auth0.com', - 'okta.com', - 'login.live.com', - 'facebook.com/dialog', - 'api.twitter.com/oauth', - 'discord.com/oauth2' - ]; - const isOAuthDomain = oauthDomains.some(domain => targetUrl.toLowerCase().includes(domain.toLowerCase())); - if (isOAuthDomain) return { action: 'allow' }; - try { win.webContents.send('open-url-new-tab', targetUrl); } catch {} - return { action: 'deny' }; - }); - - if (url) { - try { view.webContents.loadURL(url); } catch {} - } - - return view; -} - -function setActiveBrowserView(win, tabId) { - const state = getDesktopViewState(win); - if (!state) return null; - const view = state.views.get(tabId); - if (!view) return null; - - state.activeTabId = tabId; - try { - win.setBrowserView(view); - if (state.bounds) { - view.setBounds(state.bounds); - } - view.setAutoResize({ width: true, height: true }); - view.webContents.focus(); - } catch {} - return view; -} - -function destroyBrowserView(win, tabId) { - const state = getDesktopViewState(win); - if (!state) return false; - const view = state.views.get(tabId); - if (!view) return false; - try { - if (state.activeTabId === tabId) { - try { win.setBrowserView(null); } catch {} - state.activeTabId = null; - } - state.views.delete(tabId); - desktopViewByWebContentsId.delete(view.webContents.id); - try { view.webContents.destroy(); } catch {} - } catch {} - return true; -} - -function getZoomTargetForEvent(event) { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return null; - const parentWin = typeof win.getParentWindow === 'function' ? win.getParentWindow() : null; - if (parentWin && !parentWin.isDestroyed?.()) { - if (parentWin.__nebulaMode === 'desktop') { - return getActiveDesktopViewWebContents(parentWin) || parentWin.webContents; - } - return parentWin.webContents; - } - if (win.__nebulaMode === 'desktop') { - return getActiveDesktopViewWebContents(win) || win.webContents; - } - return win.webContents; -} - -// ============================================================================= -// FIRST-TIME SETUP UTILITIES -// ============================================================================= - -/** - * Check if this is the first run of the application - */ -function getOnboardingFilePath() { - try { - const portablePath = portableData.getDataFilePath?.('first-run.json'); - if (portablePath) return portablePath; - } catch {} - return path.join(app.getPath('userData'), 'first-run.json'); -} - -function migrateFirstRunFile() { - const newPath = getOnboardingFilePath(); - const legacyPath = path.join(__dirname, 'first-run.json'); - if (newPath === legacyPath) return; - try { - if (!fs.existsSync(newPath) && fs.existsSync(legacyPath)) { - const data = fs.readFileSync(legacyPath, 'utf8'); - fs.writeFileSync(newPath, data); - try { fs.unlinkSync(legacyPath); } catch {} - console.log('[FirstRun] Migrated first-run.json to user data path'); - } - } catch (err) { - console.error('[FirstRun] Error migrating first-run.json:', err); - } -} - -function isFirstRun() { - migrateFirstRunFile(); - const firstRunPath = getOnboardingFilePath(); - try { - if (fs.existsSync(firstRunPath)) { - const data = JSON.parse(fs.readFileSync(firstRunPath, 'utf8')); - return !data.completed; - } - return true; // File doesn't exist, so it's first run - } catch (err) { - console.error('[FirstRun] Error checking first-run status:', err); - return true; // Assume first run on error - } -} - -/** - * Get first-run data - */ -function getFirstRunData() { - migrateFirstRunFile(); - const firstRunPath = getOnboardingFilePath(); - try { - if (fs.existsSync(firstRunPath)) { - return JSON.parse(fs.readFileSync(firstRunPath, 'utf8')); - } - return null; - } catch (err) { - console.error('[FirstRun] Error reading first-run data:', err); - return null; - } -} - -/** - * Complete first-run setup and save preferences - */ -async function completeFirstRun(preferences = {}) { - migrateFirstRunFile(); - const firstRunPath = getOnboardingFilePath(); - const data = { - completed: true, - skipped: preferences.skipped || false, - selectedThemeId: preferences.selectedTheme || 'default', - defaultBrowserAttempted: preferences.defaultBrowserSet || false, - defaultBrowserSet: preferences.defaultBrowserSet || false, - completedAt: new Date().toISOString() - }; - - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(firstRunPath, JSON.stringify(data, null, 2)); - } else { - await fs.promises.writeFile(firstRunPath, JSON.stringify(data, null, 2)); - } - console.log('[FirstRun] First-run setup completed:', data); - return true; - } catch (err) { - console.error('[FirstRun] Error saving first-run data:', err); - return false; - } -} - -/** - * Check if Nebula is set as the default browser - */ -function getProtocolClientArgs() { - if (process.platform === 'win32' && process.defaultApp) { - const appPath = path.resolve(process.argv[1]); - return { exe: process.execPath, args: [appPath] }; - } - return null; -} - -function isDefaultBrowser() { - try { - const protocolArgs = getProtocolClientArgs(); - if (protocolArgs) { - return app.isDefaultProtocolClient('http', protocolArgs.exe, protocolArgs.args) - && app.isDefaultProtocolClient('https', protocolArgs.exe, protocolArgs.args); - } - return app.isDefaultProtocolClient('http') && app.isDefaultProtocolClient('https'); - } catch (err) { - console.error('[DefaultBrowser] Error checking default browser status:', err); - return false; - } -} - -/** - * Set Nebula as the default browser - */ -function setAsDefaultBrowser() { - try { - const protocolArgs = getProtocolClientArgs(); - const httpResult = protocolArgs - ? app.setAsDefaultProtocolClient('http', protocolArgs.exe, protocolArgs.args) - : app.setAsDefaultProtocolClient('http'); - const httpsResult = protocolArgs - ? app.setAsDefaultProtocolClient('https', protocolArgs.exe, protocolArgs.args) - : app.setAsDefaultProtocolClient('https'); - const htmlResult = protocolArgs - ? app.setAsDefaultProtocolClient('html', protocolArgs.exe, protocolArgs.args) - : app.setAsDefaultProtocolClient('html'); - - const success = httpResult && httpsResult; - const needsUserAction = success && !isDefaultBrowser(); - - console.log('[DefaultBrowser] Set as default:', { httpResult, httpsResult, htmlResult, needsUserAction }); - return { success, needsUserAction }; - } catch (err) { - console.error('[DefaultBrowser] Error setting as default browser:', err); - return { success: false, needsUserAction: false, error: err.message }; - } -} - -function openDefaultBrowserSettings() { - try { - if (process.platform === 'win32') { - return shell.openExternal('ms-settings:defaultapps'); - } - if (process.platform === 'darwin') { - return shell.openExternal('x-apple.systempreferences:com.apple.preference.general?DefaultWebBrowser'); - } - } catch (err) { - console.warn('[DefaultBrowser] Failed to open system settings:', err.message || err); - } - return false; -} - -// ============================================================================= - -// Initialize portable data paths BEFORE app.ready (must be done early) -// This enables portable mode on all platforms (Windows, macOS, Linux) -// Data is stored in 'user-data' folder within the application directory -portableData.initialize(); - -/** - * Get the path for a user data file (bookmarks, history, etc.) - * Uses portable path when in portable mode, otherwise uses __dirname - * @param {string} filename - The filename (e.g., 'bookmarks.json') - * @returns {string} The full path to the file - */ -function getDataFilePath(filename) { - const portablePath = portableData.getDataFilePath(filename); - if (portablePath) { - return portablePath; - } - return path.join(__dirname, filename); -} - -/** - * Get the directory path for user data files - * Uses portable path when in portable mode, otherwise uses __dirname - * @returns {string} The directory path - */ -function getDataDirPath() { - if (portableData.isPortableMode()) { - const portablePath = portableData.getPortableDataPath(); - if (portablePath) { - return portablePath; - } - } - return __dirname; -} - -// Try to enable WebAuthn/platform authenticator features early. -// This helps Chromium expose platform authenticators (Touch ID / built-in) where supported. -try { - app.commandLine.appendSwitch('enable-experimental-web-platform-features'); - // Add common WebAuthn-related feature flags. These are safe attempts to enable platform - // authenticators and related WebAuthn plumbing in embedded Chromium builds. - app.commandLine.appendSwitch('enable-features', 'WebAuthn,WebAuthnNestedAssertions,WebAuthnCable'); -} catch (e) { - // Non-fatal: some environments may not allow commandLine changes at this time. -} - -// ============================================================================= -// GAMEPAD / CONTROLLER CHROMIUM FLAGS -// ============================================================================= -// Enable native gamepad support in Chromium - helps with Steam Deck compatibility -try { - // Enable raw gamepad access (bypasses Steam's virtualization when possible) - app.commandLine.appendSwitch('enable-gamepad-extensions'); - - // Ensure the Gamepad API is enabled and working - app.commandLine.appendSwitch('enable-blink-features', 'GamepadExtensions'); - - // On Linux/Steam Deck, this can help with gamepad detection - if (process.platform === 'linux') { - // Disable Chromium's sandbox for gamepad access if having issues - // (Only needed in some SteamOS configurations) - // app.commandLine.appendSwitch('no-sandbox'); - - // Use the system's gamepad config rather than Chromium's built-in - app.commandLine.appendSwitch('enable-features', 'WebGamepad'); - } -} catch (e) { - console.warn('[Gamepad] Failed to set Chromium gamepad flags:', e.message); -} - -// Configure GPU settings before app is ready -gpuConfig.configure(); - -// Set a custom application name -app.setName('Nebula'); - -// --- Custom User Agent (hide Electron token & brand as Nebula) --- -// Many sites rely on UA sniffing. Default Electron UA contains 'Electron/x.y.z' which -// makes detection sites label the app as an Electron application. We construct a -// Chromeโ€‘compatible UA string without the Electron token, appending a Nebula marker. -// NOTE: Keep the Chrome and Safari tokens for maximum compatibility. -// If you ever need to temporarily reveal Electron for debugging, set NEBULA_DEBUG_ELECTRON_UA=1. -const chromeVersion = process.versions.chrome; // matches bundled Chromium -const nebulaVersion = app.getVersion(); -function computeBaseUA() { - let platformPart; - if (process.platform === 'win32') { - // Use generic Windows 10 token; detailed build numbers rarely needed and can cause UA entropy issues. - platformPart = 'Windows NT 10.0; Win64; x64'; - } else if (process.platform === 'darwin') { - // A neutral modern macOS token; avoid exposing real minor version for stability. - platformPart = 'Macintosh; Intel Mac OS X 10_15_7'; - } else { - platformPart = 'X11; Linux x86_64'; - } - return `Mozilla/5.0 (${platformPart}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36 Nebula/${nebulaVersion}`; -} - -if (!process.env.NEBULA_DEBUG_ELECTRON_UA) { - // Set a fallback UA so any new sessions inherit it automatically. - try { app.userAgentFallback = computeBaseUA(); } catch {} -} - -// Setup GPU crash handling -gpuFallback.setupCrashHandling(); - -// --- clear any prior registrations to prevent duplicateโ€handler errors --- -ipcMain.removeHandler('window-minimize'); -ipcMain.removeHandler('window-maximize'); -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; -const HANDHELD_THRESHOLD = 1366; // Consider screens smaller than this as "handheld" - -// Track if main window is currently in Big Picture Mode (no separate window anymore) -let isInBigPictureMode = false; - -/** - * Check if the current display is likely a Steam Deck or similar handheld - */ -function isSteamDeckDisplay() { - const primaryDisplay = screen.getPrimaryDisplay(); - const { width, height } = primaryDisplay.size; - - // Check for Steam Deck resolution or similar small screens - const isSteamDeckRes = width === STEAM_DECK_WIDTH && height === STEAM_DECK_HEIGHT; - const isSmallScreen = width <= HANDHELD_THRESHOLD; - - // Also check for certain aspect ratios common in handhelds (16:10, 16:9) - const aspectRatio = width / height; - const isHandheldAspect = aspectRatio >= 1.5 && aspectRatio <= 1.8; - - return isSteamDeckRes || (isSmallScreen && isHandheldAspect); -} - -/** - * Get screen info for UI decisions - */ -function getScreenInfo() { - const primaryDisplay = screen.getPrimaryDisplay(); - const { width, height } = primaryDisplay.size; - const { scaleFactor } = primaryDisplay; - - return { - width, - height, - scaleFactor, - isSteamDeck: width === STEAM_DECK_WIDTH && height === STEAM_DECK_HEIGHT, - isSmallScreen: width <= HANDHELD_THRESHOLD, - aspectRatio: width / height, - suggestBigPicture: isSteamDeckDisplay() - }; -} - -/** - * Launch Big Picture Mode in the main window (no separate window) - * This keeps resources low and prevents SteamOS from creating desktop mode alongside. - */ -function launchBigPictureMode() { - const windows = BrowserWindow.getAllWindows(); - // Prefer the top-level app window (menu popup is a child window) - const mainWindow = windows.find(w => { - try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; } - }); - - if (!mainWindow || mainWindow.isDestroyed()) { - console.warn('[BigPicture] No main window available'); - return null; - } - - if (isInBigPictureMode) { - console.log('[BigPicture] Already in Big Picture Mode'); - mainWindow.focus(); - return mainWindow; - } - - isInBigPictureMode = true; - - // Switch mode and ensure any active BrowserView is detached so it can't cover the UI. - try { mainWindow.__nebulaMode = 'bigpicture'; } catch {} - try { mainWindow.setBrowserView(null); } catch {} - - // Enter fullscreen for Big Picture experience - mainWindow.setFullScreen(true); - mainWindow.setTitle('Nebula - Big Picture Mode'); - - // Navigate to Big Picture UI - mainWindow.loadFile('renderer/bigpicture.html'); - - console.log('[BigPicture] Launched in main window'); - return mainWindow; -} - -/** - * Exit Big Picture Mode and return to desktop UI in the same window - */ -function exitBigPictureMode() { - const windows = BrowserWindow.getAllWindows(); - // Prefer the top-level app window (menu popup is a child window) - const mainWindow = windows.find(w => { - try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; } - }); - - if (!mainWindow || mainWindow.isDestroyed()) { - console.warn('[BigPicture] No main window to exit from'); - return; - } - - if (!isInBigPictureMode) { - console.log('[BigPicture] Not in Big Picture Mode'); - return; - } - - isInBigPictureMode = false; - - // Restore desktop mode and (after the UI reloads) reattach the active BrowserView. - try { mainWindow.__nebulaMode = 'desktop'; } catch {} - - // Exit fullscreen and restore normal window - mainWindow.setFullScreen(false); - mainWindow.setTitle('Nebula'); - - // Navigate back to desktop UI - mainWindow.loadFile('renderer/index.html'); - - try { - mainWindow.webContents.once('did-finish-load', () => { - try { - const state = getDesktopViewState(mainWindow); - const tabId = state?.activeTabId; - const view = tabId ? state?.views?.get(tabId) : null; - if (view) { - try { mainWindow.setBrowserView(view); } catch {} - try { if (state.bounds) view.setBounds(state.bounds); } catch {} - try { view.setAutoResize({ width: true, height: true }); } catch {} - } - } catch {} - }); - } catch {} - - // Maximize on Windows after exiting fullscreen - if (process.platform === 'win32') { - setTimeout(() => { - try { mainWindow.maximize(); } catch {} - }, 100); - } - - console.log('[BigPicture] Exited to desktop mode'); -} - -// IPC handlers for Big Picture Mode -ipcMain.handle('get-screen-info', () => getScreenInfo()); - -ipcMain.handle('launch-bigpicture', () => { - launchBigPictureMode(); - return { success: true }; -}); - -ipcMain.handle('exit-bigpicture', () => { - exitBigPictureMode(); - return { success: true }; -}); - -ipcMain.handle('is-bigpicture-suggested', () => { - return isSteamDeckDisplay(); -}); - -// Check if currently in Big Picture Mode -ipcMain.handle('is-in-bigpicture', () => { - return isInBigPictureMode; -}); - -ipcMain.on('exit-bigpicture', () => { - exitBigPictureMode(); -}); - -// IPC handler for sending mouse input events to webviews (used by Big Picture Mode) -ipcMain.handle('webview-send-input-event', async (event, { webContentsId, inputEvent }) => { - try { - const { webContents: webContentsModule } = require('electron'); - const targetWebContents = webContentsModule.fromId(webContentsId); - if (targetWebContents && !targetWebContents.isDestroyed()) { - targetWebContents.sendInputEvent(inputEvent); - return { success: true }; - } - return { success: false, error: 'WebContents not found' }; - } catch (err) { - console.error('[Main] webview-send-input-event error:', err); - return { success: false, error: err.message }; - } -}); - -// ============================================================================= - - -function createWindow(startUrl, bigPictureMode = false) { - // Capture highโ€‘resolution startup timing markers - const perfMarks = { createWindow_called: performance.now() }; - - // Track Big Picture Mode state if starting in that mode - if (bigPictureMode) { - isInBigPictureMode = true; - } - - // Get the available screen size (avoid full workArea allocation jank by starting slightly smaller then maximizing later if desired) - const { width, height } = screen.getPrimaryDisplay().workAreaSize; - const initialWidth = Math.min(width, Math.round(width * 0.9)); - const initialHeight = Math.min(height, Math.round(height * 0.9)); - - // Window is created hidden; we only show after first meaningful paint to avoid OSโ€‘level pointer jank while Chromium initializes - let windowOptions = { - width: bigPictureMode ? width : initialWidth, - height: bigPictureMode ? height : initialHeight, - show: false, - useContentSize: true, - backgroundColor: bigPictureMode ? '#0a0a0f' : '#121212', // Big Picture uses darker bg - resizable: true, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - nodeIntegration: false, // Security & performance improvement - contextIsolation: true, - webviewTag: true, - enableRemoteModule: false, // Deprecated and slow - nodeIntegrationInSubFrames: false, // Security & performance - nativeWindowOpen: false, - spellcheck: false, - webSecurity: true, - allowRunningInsecureContent: false, - experimentalFeatures: false, - offscreen: false, - enableWebSQL: false, - plugins: false, - backgroundThrottling: false, // keep UI responsive during early load - // OAuth compatibility settings - partition: 'persist:main', - sandbox: false - }, - fullscreen: bigPictureMode, // Start in fullscreen for Big Picture Mode - autoHideMenuBar: true, - icon: process.platform === 'darwin' - ? path.join(__dirname, 'assets/images/Logos/Nebula-Favicon.icns') - : path.join(__dirname, 'assets/images/Logos/Nebula-favicon.png'), - title: 'Nebula' - }; - - if (process.platform === 'darwin') { - // Use a hidden/transparent title bar on macOS so we can render a - // custom, sleeker header in the renderer while still supporting - // native traffic-light placement. The renderer will expose a - // draggable region via CSS (-webkit-app-region: drag). - Object.assign(windowOptions, { - frame: true, - titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: 15, y: 20 }, - // Transparent background so renderer chrome blends with content. - backgroundColor: '#00000000', - transparent: true, - }); - } else if (process.platform === 'win32') { - // Use frameless window on Windows with custom title bar controls - // rendered in the tab strip area (Firefox-style). - Object.assign(windowOptions, { - frame: false, - backgroundColor: '#0b0d10', - }); - } else if (process.platform === 'linux') { - // On Linux, use a frameless window so only the in-app controls are shown. - Object.assign(windowOptions, { - frame: false, - backgroundColor: '#0b0d10', - skipTaskbar: true - }); - } else { - windowOptions.frame = true; - } - - const win = new BrowserWindow(windowOptions); - win.__nebulaMode = bigPictureMode ? 'bigpicture' : 'desktop'; - win.on('closed', () => { - const state = desktopViewStateByWindowId.get(win.id); - if (state) { - for (const view of state.views.values()) { - try { desktopViewByWebContentsId.delete(view.webContents.id); } catch {} - try { view.webContents.destroy(); } catch {} - } - desktopViewStateByWindowId.delete(win.id); - } - }); - perfMarks.browserWindow_instantiated = performance.now(); - - // Intercept window.open() requests and route them into the existing window as a new tab - // instead of spawning separate BrowserWindows. We allow a small list of specific OAuth - // domains to open real popups if the flow depends on window.opener relationships. - // Everything else becomes a new tab. - win.webContents.setWindowOpenHandler((details) => { - const { url } = details; - if (!/^https?:\/\//i.test(url)) return { action: 'deny' }; - // OAuth / SSO allowlist - only allow specific authentication provider domains - // Be restrictive to prevent normal links from opening in new windows - const oauthDomains = [ - 'accounts.google.com', - 'login.microsoftonline.com', - 'appleid.apple.com', - 'github.com/login', - 'auth0.com', - 'okta.com', - 'login.live.com', - 'facebook.com/dialog', - 'api.twitter.com/oauth', - 'discord.com/oauth2' - ]; - const isOAuthDomain = oauthDomains.some(domain => url.toLowerCase().includes(domain.toLowerCase())); - if (isOAuthDomain) { - return { action: 'allow' }; // preserve popup semantics for complex auth flows - } - // Forward to renderer to open as tab - try { win.webContents.send('open-url-new-tab', url); } catch {} - return { action: 'deny' }; - }); - - // IMPORTANT: Do NOT intercept 'will-navigate' with preventDefault() because - // that strips POST bodies (turning logins into GET requests). Let Chromium - // perform the navigation normally. If you need to observe navigations, add - // a listener without calling preventDefault(). - // (Previous code here was causing login forms to fail.) - - // Remove deprecated 'new-window' handler that forcibly loaded targets in the - // same window; this also broke some auth popup flows. setWindowOpenHandler - // above now governs popup behavior. - - // ensure all embedded tags behave predictably without heavy injections - win.webContents.on('did-attach-webview', (event, webviewContents) => { - // Route window.open() calls to tabs unless OAuth allowlist matched - webviewContents.setWindowOpenHandler((details) => { - const { url } = details; - if (!/^https?:\/\//i.test(url)) return { action: 'deny' }; - // OAuth / SSO allowlist - only allow specific authentication provider domains - const oauthDomains = [ - 'accounts.google.com', - 'login.microsoftonline.com', - 'appleid.apple.com', - 'github.com/login', - 'auth0.com', - 'okta.com', - 'login.live.com', - 'facebook.com/dialog', - 'api.twitter.com/oauth', - 'discord.com/oauth2' - ]; - const isOAuthDomain = oauthDomains.some(domain => url.toLowerCase().includes(domain.toLowerCase())); - if (isOAuthDomain) { - return { action: 'allow' }; // keep popup for auth - } - // Send to main window's webContents to open a new tab - try { - win.webContents.send('open-url-new-tab', url); - } catch {} - return { action: 'deny' }; - }); - }); - - // Load appropriate UI based on mode (Big Picture or Desktop) - // Check for first-run and load setup page if needed - if (bigPictureMode) { - win.loadFile('renderer/bigpicture.html'); - win.setTitle('Nebula - Big Picture Mode'); - } else { - // Check if this is the first run (only for desktop mode) - const firstRun = isFirstRun(); - if (firstRun) { - console.log('[Startup] First run detected, loading setup page'); - win.loadFile('renderer/setup.html'); - win.setTitle('Welcome to Nebula'); - } else { - win.loadFile('renderer/index.html'); - } - } - perfMarks.loadFile_issued = performance.now(); - - // if caller passed in a URL, forward it to the renderer after load - if (startUrl) { - win.webContents.once('did-finish-load', () => { - win.webContents.send('open-url', startUrl); - }); - } - - // Set default zoom to 100% - const zoomFactor = 1.0; - const loadStartTime = Date.now(); - // Show window ASAP after first paint for perceived performance - let shown = false; - const showNow = (reason) => { - if (shown) return; - shown = true; - win.show(); - if (process.platform === 'win32') { - // Defer maximize to next frame to avoid large-surface first paint cost - setTimeout(() => { - try { win.maximize(); } catch {} - }, 16); - } - console.log(`[Startup] Window shown (${reason}) in ${(performance.now() - perfMarks.createWindow_called).toFixed(1)}ms`); - }; - - win.webContents.once('ready-to-show', () => showNow('ready-to-show')); - // Fallback in case ready-to-show is delayed - setTimeout(() => showNow('timeout-fallback'), 4000); - - win.webContents.on('did-finish-load', () => { - win.webContents.setZoomFactor(zoomFactor); - const loadTime = Date.now() - loadStartTime; - perfMonitor.trackLoadTime(win.webContents.getURL(), loadTime); - perfMarks.did_finish_load = performance.now(); - - // Defer heavier, nonโ€‘critical tasks to next idle slice to keep UI smooth - setTimeout(() => { - // Kick off GPU status check here (was earlier) to avoid competing with first paint - gpuConfig.checkGPUStatus() - .then(gpuStatus => { - console.log('[Deferred] GPU Configuration Results:'); - console.log('- GPU Status:', gpuStatus); - console.log('- Recommendations:', gpuConfig.getRecommendations()); - }) - .catch(err => console.error('[Deferred] GPU status check failed:', err)); - - // Start performance monitoring after initial load - perfMonitor.start(); - }, 300); - // Diagnostic: check WebAuthn / platform authenticator availability in renderer - try { - win.webContents.executeJavaScript(`(async function(){ - const out = { hasNavigator: !!window.navigator, hasCredentials: !!navigator.credentials, hasCreate: !!(navigator.credentials && navigator.credentials.create), hasGet: !!(navigator.credentials && navigator.credentials.get) }; - try { - if (window.PublicKeyCredential) { - out.PublicKeyCredential = true; - out.isUserVerifyingPlatformAuthenticatorAvailable = typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function' ? await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() : 'unknown'; - } else { - out.PublicKeyCredential = false; - } - } catch (e) { out.webauthnError = String(e); } - return out; - })()`) - .then(result => { - console.log('[WebAuthn Diagnostic] renderer report:', result); - }).catch(err => { - console.error('[WebAuthn Diagnostic] executeJavaScript failed:', err); - }); - } catch (e) { - console.warn('WebAuthn diagnostic injection skipped:', e); - } - - // After the first load, let plugins know a window exists - try { pluginManager.emit('window-created', win); } catch {} - }); - - // Renderer manages history; no main-process recording here -} - -// This method will be called when Electron has finished initialization -// Configure sessions asynchronously (non-blocking for window creation) -function configureSessionsAsync() { - const sessionsToConfigure = [session.fromPartition('persist:main'), session.defaultSession]; - try { - for (const ses of sessionsToConfigure) { - if (!ses) continue; - ses.setPermissionRequestHandler((webContents, permission, callback) => { - if (['notifications', 'geolocation', 'camera', 'microphone'].includes(permission)) { - callback(false); - } else { - callback(true); - } - }); - try { - let realUA = ses.getUserAgent(); - // If Electron token present and we're not in debug mode, recompute using base builder. - if (!process.env.NEBULA_DEBUG_ELECTRON_UA) { - const hasElectron = /Electron\//i.test(realUA); - if (hasElectron || !/Nebula\//.test(realUA)) { - realUA = app.userAgentFallback || computeBaseUA(); - ses.setUserAgent(realUA); - } - } else { - // Debug mode: just append Nebula tag if missing (keeps Electron segment visible) - if (realUA && !/Nebula\//.test(realUA)) { - ses.setUserAgent(realUA + ' Nebula/' + app.getVersion()); - } - } - } catch (e) { - console.warn('Failed to read real user agent, keeping default:', e); - } - ses.cookies.on('changed', (event, cookie, cause, removed) => { - if (cookie.domain && (cookie.domain.includes('google') || cookie.domain.includes('accounts'))) { - console.log(`Cookie ${removed ? 'removed' : 'added'}: ${cookie.name} for ${cookie.domain}`); - } - }); - ses.webRequest.onBeforeSendHeaders((details, callback) => { - const headers = details.requestHeaders; - if (details.url.includes('accounts.google.com') || details.url.includes('oauth')) { - headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'; - headers['Accept'] = headers['Accept'] || 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'; - } - if (!headers['Accept-Language'] && !headers['accept-language']) { - headers['Accept-Language'] = 'en-US,en;q=0.9'; - } - callback({ requestHeaders: headers }); - }); - } - console.log('Session configured successfully for OAuth compatibility'); - } catch (err) { - console.error('Session setup error:', err); - } -} - -app.whenReady().then(() => { - const t0 = performance.now(); - - // If launched via SteamOS Gaming Mode / gamepad UI, default to Big Picture Mode. - // Desktop launches remain unchanged. Big Picture now opens in main window to keep resources low. - const startUrl = pendingOpenUrl; - pendingOpenUrl = null; - - const startInBigPicture = startUrl ? false : shouldStartInBigPictureMode(); - if (startInBigPicture) { - console.log('[Startup] Detected game mode launch; starting in Big Picture Mode (in main window)'); - createWindow(null, true); // Pass bigPictureMode flag - } else { - createWindow(startUrl || null, false); - } - - // Initialize user plugins after app ready - try { - pluginManager.ensureUserPluginsDir(); - pluginManager.loadAll(); - pluginManager.emit('app-ready'); - } catch (e) { - console.error('[Plugins] initialization error:', e); - } - 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) => { - console.warn('GPU process crashed, killed:', killed); - if (!killed) { - console.log('Attempting to recover GPU process...'); - } - }); - - // Defer session configuration to microtask/next tick (already inexpensive) โ€“ keep explicit - setImmediate(configureSessionsAsync); - - // Register download handlers for common sessions - try { - const mainSes = session.fromPartition('persist:main'); - const defSes = session.defaultSession; - if (mainSes) registerDownloadHandling(mainSes); - if (defSes && defSes !== mainSes) registerDownloadHandling(defSes); - // Allow plugins to attach webRequest hooks - if (mainSes) pluginManager.applyWebRequestHandlers(mainSes); - if (defSes) pluginManager.applyWebRequestHandlers(defSes); - pluginManager.emit('session-configured', { mainSes, defSes }); - } catch (e) { - console.warn('Failed to register download handlers:', e); - } - - if (process.platform === 'darwin') { - app.dock.setIcon(path.join(__dirname, 'assets/images/Logos/Nebula-Icon.icns')); - } - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); - }); - - // --- Auto-Updater Setup --- - // Configure auto-updater logging - try { - autoUpdater.logger = require('electron-updater').autoUpdater.logger; - if (autoUpdater.logger && autoUpdater.logger.transports && autoUpdater.logger.transports.file) { - autoUpdater.logger.transports.file.level = 'info'; - } - } catch (err) { - console.log('[AutoUpdater] Could not configure logger:', err.message); - } - - // Check for updates after a short delay to not block startup - setTimeout(() => { - autoUpdater.checkForUpdatesAndNotify().catch(err => { - console.log('[AutoUpdater] Update check failed:', err.message); - }); - }, 3000); - - // Auto-updater event handlers - autoUpdater.on('checking-for-update', () => { - console.log('[AutoUpdater] Checking for updates...'); - broadcastToAll('update-status', { status: 'checking' }); - }); - - autoUpdater.on('update-available', (info) => { - console.log('[AutoUpdater] Update available:', info.version); - broadcastToAll('update-status', { status: 'available', version: info.version }); - }); - - autoUpdater.on('update-not-available', (info) => { - console.log('[AutoUpdater] No update available. Current version:', app.getVersion()); - broadcastToAll('update-status', { status: 'not-available', currentVersion: app.getVersion() }); - }); - - autoUpdater.on('download-progress', (progress) => { - console.log(`[AutoUpdater] Download progress: ${progress.percent.toFixed(1)}%`); - broadcastToAll('update-status', { status: 'downloading', progress: progress.percent }); - }); - - autoUpdater.on('update-downloaded', (info) => { - console.log('[AutoUpdater] Update downloaded:', info.version); - broadcastToAll('update-status', { status: 'downloaded', version: info.version }); - // Optionally prompt user to restart - dialog.showMessageBox({ - type: 'info', - title: 'Update Ready', - message: `Nebula ${info.version} has been downloaded.`, - detail: 'The update will be installed when you restart the app.', - buttons: ['Restart Now', 'Later'] - }).then(result => { - if (result.response === 0) { - autoUpdater.quitAndInstall(); - } - }); - }); - - autoUpdater.on('error', (err) => { - console.error('[AutoUpdater] Error:', err.message); - broadcastToAll('update-status', { status: 'error', message: err.message }); - }); -}); - -// Quit when all windows are closed. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') app.quit(); -}); - -// ipcMain handlers - -// --- Auto-Update IPC handlers --- -ipcMain.handle('check-for-updates', async () => { - try { - const result = await autoUpdater.checkForUpdates(); - return { success: true, updateInfo: result?.updateInfo }; - } catch (err) { - return { success: false, error: err.message }; - } -}); - -ipcMain.handle('download-update', async () => { - try { - await autoUpdater.downloadUpdate(); - return { success: true }; - } catch (err) { - return { success: false, error: err.message }; - } -}); - -ipcMain.handle('install-update', () => { - autoUpdater.quitAndInstall(); -}); - -ipcMain.handle('get-app-version', () => { - return app.getVersion(); -}); - -ipcMain.handle('get-app-info', () => { - return { - version: app.getVersion(), - electron: process.versions.electron, - chrome: process.versions.chrome, - node: process.versions.node, - v8: process.versions.v8, - platform: process.platform, - arch: process.arch, - isPackaged: app.isPackaged, - isDevelopment: !app.isPackaged - }; -}); - -// --- First-Time Setup IPC handlers --- -ipcMain.handle('is-first-run', () => { - return isFirstRun(); -}); - -ipcMain.handle('get-first-run-data', () => { - return getFirstRunData(); -}); - -ipcMain.handle('complete-first-run', async (event, preferences) => { - try { - const success = await completeFirstRun(preferences); - return { success }; - } catch (err) { - console.error('[FirstRun] Error in IPC handler:', err); - return { success: false, error: err.message }; - } -}); - -ipcMain.handle('get-all-themes', () => { - try { - const ThemeManager = require('./theme-manager.js'); - const manager = new ThemeManager(); - const themes = manager.getAllThemes(); - const defaultThemeCount = Object.keys(themes.default || {}).length; - const userThemeCount = Object.keys(themes.user || {}).length; - const downloadedThemeCount = Object.keys(themes.downloaded || {}).length; - console.log('[Themes] Loaded themes:', { - default: defaultThemeCount, - user: userThemeCount, - downloaded: downloadedThemeCount - }); - return themes; - } catch (err) { - console.error('[Themes] Error loading themes:', err); - return { default: { default: { name: 'Default', colors: {} } } }; - } -}); - -ipcMain.handle('apply-theme', async (event, themeId) => { - try { - // The theme will be applied in the renderer - // Here we just save the preference - console.log('[Themes] Theme selected:', themeId); - return { success: true }; - } catch (err) { - console.error('[Themes] Error applying theme:', err); - return { success: false, error: err.message }; - } -}); - -ipcMain.handle('is-default-browser', () => { - return isDefaultBrowser(); -}); - -ipcMain.handle('set-as-default-browser', () => { - try { - const result = setAsDefaultBrowser(); - return result; - } catch (err) { - console.error('[DefaultBrowser] Error in IPC handler:', err); - return { success: false, error: err.message }; - } -}); - -ipcMain.handle('open-default-browser-settings', () => { - try { - const result = openDefaultBrowserSettings(); - return { success: !!result }; - } catch (err) { - console.error('[DefaultBrowser] Error opening system settings:', err); - return { success: false, error: err.message }; - } -}); - -// --- window control handlers (only registered once now) -ipcMain.handle('window-minimize', event => { - BrowserWindow.fromWebContents(event.sender).minimize(); -}); -ipcMain.handle('window-maximize', event => { - const w = BrowserWindow.fromWebContents(event.sender); - w.isMaximized() ? w.unmaximize() : w.maximize(); -}); -ipcMain.handle('window-close', event => { - BrowserWindow.fromWebContents(event.sender).close(); -}); -ipcMain.handle('window-is-maximized', event => { - return BrowserWindow.fromWebContents(event.sender).isMaximized(); -}); - -// Add site and search history IPC handlers -// Site history is now handled via localStorage in the renderer -// But keep these handlers for compatibility and potential future use -ipcMain.handle('load-site-history', async () => { - const filePath = getDataFilePath('site-history.json'); - try { - const data = await fs.promises.readFile(filePath, 'utf-8'); - return JSON.parse(data); - } catch (err) { - return []; - } -}); - -ipcMain.handle('save-site-history', async (event, history) => { - const filePath = getDataFilePath('site-history.json'); - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(filePath, JSON.stringify(history, null, 2)); - } else { - await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2)); - } - return true; - } catch (err) { - return false; - } -}); - -ipcMain.handle('clear-site-history', async () => { - const filePath = getDataFilePath('site-history.json'); - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(filePath, JSON.stringify([], null, 2)); - } else { - await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); - } - return true; - } catch (err) { - return false; - } -}); - -ipcMain.handle('load-search-history', async () => { - const filePath = getDataFilePath('search-history.json'); - try { - const data = await fs.promises.readFile(filePath, 'utf-8'); - return JSON.parse(data); - } catch (err) { - return []; - } -}); - -ipcMain.handle('save-search-history', async (event, history) => { - const filePath = getDataFilePath('search-history.json'); - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(filePath, JSON.stringify(history, null, 2)); - } else { - await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2)); - } - return true; - } catch (err) { - return false; - } -}); - -// debug: log defaultโ€homepage changes from renderer -ipcMain.on('homepage-changed', (event, url) => { - console.log('[MAIN] homepage-changed โ†’', url); -}); - -// Handle theme changes - broadcast to all windows -ipcMain.on('theme-changed', (event, theme) => { - console.log('[MAIN] theme-changed โ†’', theme?.name || 'unknown'); - // Broadcast theme change to all browser windows - BrowserWindow.getAllWindows().forEach(win => { - if (win.webContents && win.webContents.id !== event.sender.id) { - win.webContents.send('theme-changed', theme); - } - }); -}); - -// Handle display scale changes -ipcMain.on('set-display-scale', (event, scale) => { - console.log('[MAIN] set-display-scale โ†’', scale); - try { - // Get the webcontents from the event (will be bigPictureWindow) - const wc = event.sender; - if (wc && typeof wc.setZoomFactor === 'function') { - const zoomFactor = Math.max(0.5, Math.min(3, scale / 100)); - wc.setZoomFactor(zoomFactor); - console.log(`[MAIN] Applied zoom factor: ${zoomFactor} for scale ${scale}%`); - } - } catch (err) { - console.warn('[MAIN] Failed to apply display scale:', err); - } -}); - -// Bookmark management -ipcMain.handle('load-bookmarks', async () => { - try { - const bookmarksPath = getDataFilePath('bookmarks.json'); - try { - await fs.promises.access(bookmarksPath); - } catch { - console.log('No bookmarks file found, starting with empty array'); - return []; - } - const data = await fs.promises.readFile(bookmarksPath, 'utf8'); - const bookmarks = JSON.parse(data); - console.log(`Loaded ${bookmarks.length} bookmarks from file`); - return bookmarks; - } catch (error) { - console.error('Error loading bookmarks:', error); - // Try to create a backup if the file is corrupted - const bookmarksPath = getDataFilePath('bookmarks.json'); - const backupPath = getDataFilePath(`bookmarks.backup.${Date.now()}.json`); - try { - await fs.promises.copyFile(bookmarksPath, backupPath); - console.log(`Corrupted bookmarks file backed up to: ${backupPath}`); - } catch (backupError) { - console.error('Failed to create backup:', backupError); - } - return []; - } -}); - -ipcMain.handle('save-bookmarks', async (event, bookmarks) => { - try { - const bookmarksPath = getDataFilePath('bookmarks.json'); - try { - await fs.promises.access(bookmarksPath); - const backupPath = getDataFilePath('bookmarks.backup.json'); - await fs.promises.copyFile(bookmarksPath, backupPath); - } catch {} - // Use secure file writing in portable mode - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(bookmarksPath, JSON.stringify(bookmarks, null, 2)); - } else { - await fs.promises.writeFile(bookmarksPath, JSON.stringify(bookmarks, null, 2)); - } - console.log(`Saved ${bookmarks.length} bookmarks to file`); - return true; - } catch (error) { - console.error('Error saving bookmarks:', error); - return false; - } -}); - -ipcMain.handle('clear-browser-data', async () => { - try { - const sessionsToClear = [session.defaultSession, session.fromPartition('persist:main')]; - - for (const ses of sessionsToClear) { - if (!ses) continue; - // Clear all common site storage types - await ses.clearStorageData({ - storages: [ - 'cookies', - 'localstorage', - 'indexdb', - 'filesystem', - 'websql', - 'serviceworkers', - 'caches', - 'shadercache', - 'appcache' - ], - }); - // Clear caches and auth - await ses.clearCache(); - await ses.clearAuthCache(); - } - - // Also reset on-disk history JSON files managed by the app - const siteHistoryPath = getDataFilePath('site-history.json'); - const searchHistoryPath = getDataFilePath('search-history.json'); - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(siteHistoryPath, JSON.stringify([], null, 2)); - } else { - await fs.promises.writeFile(siteHistoryPath, JSON.stringify([], null, 2)); - } - } catch {} - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(searchHistoryPath, JSON.stringify([], null, 2)); - } else { - await fs.promises.writeFile(searchHistoryPath, JSON.stringify([], null, 2)); - } - } catch {} - - return true; // Indicate success - } catch (error) { - console.error('Failed to clear browser data:', error); - return false; // Indicate failure - } -}); - -// Optional: standalone clear for search history JSON -ipcMain.handle('clear-search-history', async () => { - const filePath = getDataFilePath('search-history.json'); - try { - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(filePath, JSON.stringify([], null, 2)); - } else { - await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); - } - return true; - } catch (err) { - return false; - } -}); - -ipcMain.handle('get-zoom-factor', event => { - const wc = getZoomTargetForEvent(event); - return wc ? wc.getZoomFactor() : 1.0; -}); - -ipcMain.handle('zoom-in', event => { - const wc = getZoomTargetForEvent(event); - if (!wc) return 1.0; - const current = wc.getZoomFactor(); - const z = Math.min(current + 0.1, 3); - wc.setZoomFactor(z); - return z; -}); - - -ipcMain.handle('zoom-out', event => { - const wc = getZoomTargetForEvent(event); - if (!wc) return 1.0; - const current = wc.getZoomFactor(); - const z = Math.max(current - 0.1, 0.25); - wc.setZoomFactor(z); - return z; -}); - -ipcMain.handle('get-display-scale', async (event) => { - // Try to read from localStorage data (user data path) - const userDataPath = app.getPath('userData'); - const storageFile = path.join(userDataPath, 'localStorage'); - - try { - // Try to get from electron store or persistent storage - // For now, we'll just return a default and let the app set it - // The display scale is stored in localStorage on the client side - return 100; // Default to 100% - } catch (err) { - return 100; // Default to 100% - } -}); - -ipcMain.handle('set-zoom-factor', (event, zoomFactor) => { - const wc = getZoomTargetForEvent(event); - if (wc && typeof wc.setZoomFactor === 'function') { - wc.setZoomFactor(zoomFactor); - return true; - } - return false; -}); - -// allow renderer to pop a tab into its own window -ipcMain.handle('open-tab-in-new-window', (event, url) => { - createWindow(url); -}); - -ipcMain.handle('save-site-history-entry', async (event, url) => { - const filePath = getDataFilePath('site-history.json'); - try { - let data = []; - try { - const raw = await fs.promises.readFile(filePath, 'utf8'); - data = JSON.parse(raw); - } catch {} - // Remove if already exists to avoid duplicates - data = data.filter(item => item !== url); - // Add to beginning and clamp size - data.unshift(url); - if (data.length > 100) data = data.slice(0, 100); - if (portableData.isPortableMode()) { - await portableData.writeSecureFileAsync(filePath, JSON.stringify(data, null, 2)); - } else { - await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); - } - return true; - } catch (err) { - console.error('[MAIN] Error saving site history entry:', err); - return false; - } -}); - -// Add performance monitoring IPC handlers -ipcMain.handle('get-performance-report', () => { - return perfMonitor.getReport(); -}); - -ipcMain.handle('force-gc', () => { - perfMonitor.forceGC(); - return true; -}); - -// GPU diagnostics handler -ipcMain.handle('get-gpu-info', async () => { - try { - const gpuStatus = await gpuConfig.checkGPUStatus(); - const fallbackStatus = gpuFallback.getStatus(); - const recommendations = gpuConfig.getRecommendations(); - - return { - ...gpuStatus, - fallbackStatus: fallbackStatus, - recommendations: recommendations, - isOptimized: gpuStatus.isSupported && !fallbackStatus.fallbackLevel - }; - } catch (err) { - console.error('Error getting GPU info:', err); - return { error: err.message, isSupported: false }; - } -}); - -// Force GPU fallback handler -ipcMain.handle('apply-gpu-fallback', (event, level) => { - try { - gpuFallback.applyFallback(level); - return { success: true, level: level }; - } catch (err) { - console.error('Error applying GPU fallback:', err); - return { error: err.message }; - } -}); - -// About/info handler -ipcMain.handle('get-about-info', () => { - try { - return { - appName: app.getName(), - appVersion: app.getVersion(), - isPackaged: app.isPackaged, - appPath: app.getAppPath(), - userDataPath: app.getPath('userData'), - electronVersion: process.versions.electron, - chromeVersion: process.versions.chrome, - nodeVersion: process.versions.node, - v8Version: process.versions.v8, - platform: process.platform, - arch: process.arch, - osType: os.type(), - osRelease: os.release(), - cpu: os.cpus()?.[0]?.model || 'Unknown CPU', - totalMemGB: Math.round((os.totalmem() / (1024 ** 3)) * 10) / 10, - }; - } catch (err) { - console.error('Error building about info:', err); - return { error: err.message }; - } -}); - -// Toggle DevTools for the requesting window (main window webContents) -ipcMain.handle('open-devtools', (event) => { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return false; - const contents = win.__nebulaMode === 'desktop' - ? (getActiveDesktopViewWebContents(win) || win.webContents) - : win.webContents; - if (contents.isDevToolsOpened()) { - contents.closeDevTools(); - } else { - // Open docked inside the main window (bottom). Other options: 'right', 'undocked', 'detach' - contents.openDevTools({ mode: 'bottom' }); - } - return contents.isDevToolsOpened(); -}); - -// ============================================================================= -// BrowserView IPC (desktop mode tabs) -// ============================================================================= -ipcMain.handle('browserview-create', (event, { tabId, url }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return { success: false, error: 'no-window' }; - if (win.__nebulaMode !== 'desktop') return { success: false, error: 'not-desktop' }; - const view = createBrowserViewForTab(win, tabId, url); - return { success: !!view }; - } catch (err) { - return { success: false, error: err.message }; - } -}); - -ipcMain.handle('browserview-set-active', (event, { tabId }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return false; - if (win.__nebulaMode !== 'desktop') return false; - return !!setActiveBrowserView(win, tabId); - } catch { - return false; - } -}); - -ipcMain.handle('browserview-destroy', (event, { tabId }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return false; - if (win.__nebulaMode !== 'desktop') return false; - return destroyBrowserView(win, tabId); - } catch { - return false; - } -}); - -ipcMain.handle('browserview-load-url', (event, { tabId, url }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return false; - if (win.__nebulaMode !== 'desktop') return false; - const state = getDesktopViewState(win); - const view = state?.views.get(tabId); - if (!view) return false; - view.webContents.loadURL(url); - return true; - } catch { - return false; - } -}); - -ipcMain.handle('browserview-reload', (event, { tabId, ignoreCache }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return false; - if (win.__nebulaMode !== 'desktop') return false; - const state = getDesktopViewState(win); - const view = state?.views.get(tabId); - if (!view) return false; - if (ignoreCache && typeof view.webContents.reloadIgnoringCache === 'function') { - view.webContents.reloadIgnoringCache(); - } else { - view.webContents.reload(); - } - return true; - } catch { - return false; - } -}); - -ipcMain.handle('browserview-get-url', (event, { tabId }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return null; - if (win.__nebulaMode !== 'desktop') return null; - const state = getDesktopViewState(win); - const view = state?.views.get(tabId); - return view?.webContents.getURL() || null; - } catch { - return null; - } -}); - -ipcMain.handle('browserview-execute-js', async (event, { tabId, code }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return null; - if (win.__nebulaMode !== 'desktop') return null; - const state = getDesktopViewState(win); - const view = state?.views.get(tabId); - if (!view) return null; - return await view.webContents.executeJavaScript(code, true); - } catch { - return null; - } -}); - -ipcMain.handle('browserview-set-bounds', (event, bounds) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return false; - if (win.__nebulaMode !== 'desktop') return false; - const state = getDesktopViewState(win); - if (!state) return false; - const safeBounds = { - x: Math.max(0, Math.round(bounds?.x || 0)), - y: Math.max(0, Math.round(bounds?.y || 0)), - width: Math.max(0, Math.round(bounds?.width || 0)), - height: Math.max(0, Math.round(bounds?.height || 0)) - }; - state.bounds = safeBounds; - if (state.activeTabId) { - const view = state.views.get(state.activeTabId); - if (view) { - view.setBounds(safeBounds); - } - } - return true; - } catch { - return false; - } -}); - -// Native popup menu for the burger button โ€” renders above BrowserView on all platforms -ipcMain.handle('show-app-menu', (event, payload = {}) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return; - const zoomFactor = win.webContents.getZoomFactor?.() || 1; - const zoomLabel = `${Math.round(zoomFactor * 100)}%`; - const template = [ - { label: 'Settings', click: () => win.webContents.send('menu-command', { cmd: 'open-settings' }) }, - { type: 'separator' }, - { label: '\u{1F3AE} Big Picture Mode', click: () => win.webContents.send('menu-command', { cmd: 'big-picture' }) }, - { type: 'separator' }, - { label: 'Toggle Developer Tools', click: () => win.webContents.send('menu-command', { cmd: 'toggle-devtools' }) }, - { type: 'separator' }, - { label: `Zoom: ${zoomLabel}`, enabled: false }, - { label: 'Zoom In', accelerator: 'CmdOrCtrl+=', click: () => win.webContents.send('menu-command', { cmd: 'zoom-in' }) }, - { label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', click: () => win.webContents.send('menu-command', { cmd: 'zoom-out' }) }, - { type: 'separator' }, - { label: 'Hard Reload (Ignore Cache)', click: () => win.webContents.send('menu-command', { cmd: 'hard-reload' }) }, - { label: 'Reload Fresh (Add Cache-Buster)', click: () => win.webContents.send('menu-command', { cmd: 'fresh-reload' }) }, - ]; - const menu = Menu.buildFromTemplate(template); - menu.popup({ window: win, x: payload.x, y: payload.y }); - } catch (err) { - console.error('[show-app-menu] Error:', err); - } -}); - -// Overlay menu (to sit above BrowserView) โ€” kept for backwards compat but no longer primary -ipcMain.on('menu-popup-toggle', (event, payload = {}) => { - try { - const parentWin = BrowserWindow.fromWebContents(event.sender); - if (!parentWin) return; - - let menuWin = menuPopupByWindowId.get(parentWin.id); - if (!menuWin || menuWin.isDestroyed()) { - menuWin = createMenuPopupWindow(parentWin); - menuPopupByWindowId.set(parentWin.id, menuWin); - } - - if (menuWin.isVisible()) { - menuWin.hide(); - return; - } - - const anchorRect = payload?.anchorRect; - const anchorScreenPoint = payload?.anchorScreenPoint; - - if (anchorScreenPoint && Number.isFinite(anchorScreenPoint.x) && Number.isFinite(anchorScreenPoint.y)) { - const width = MENU_POPUP_SIZE.width; - const height = MENU_POPUP_SIZE.height; - let x = Math.round(anchorScreenPoint.x - width); - let y = Math.round(anchorScreenPoint.y + 6); - const display = screen.getDisplayNearestPoint({ x, y }); - const workArea = display?.workArea || { x: 0, y: 0, width: 1920, height: 1080 }; - if (x < workArea.x) x = workArea.x + 6; - if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width - 6; - if (y < workArea.y) y = workArea.y + 6; - if (y + height > workArea.y + workArea.height) y = workArea.y + workArea.height - height - 6; - menuWin.setBounds({ x, y, width, height }, false); - } else { - positionMenuPopup(parentWin, menuWin, anchorRect); - } - - const initPayload = { theme: payload.theme || null }; - const sendInit = () => { - try { menuWin.webContents.send('menu-popup-init', initPayload); } catch {} - }; - try { - if (menuWin.webContents.isLoadingMainFrame()) { - menuWin.webContents.once('did-finish-load', sendInit); - } else { - sendInit(); - } - } catch {} - - try { - if (typeof menuWin.showInactive === 'function') { - menuWin.showInactive(); - } else { - menuWin.show(); - } - } catch { - menuWin.show(); - } - } catch {} -}); - -ipcMain.on('menu-popup-hide', (event) => { - try { - const parentWin = BrowserWindow.fromWebContents(event.sender); - if (!parentWin) return; - const menuWin = menuPopupByWindowId.get(parentWin.id); - if (menuWin && !menuWin.isDestroyed()) menuWin.hide(); - } catch {} -}); - -ipcMain.on('menu-popup-command', (event, payload = {}) => { - try { - const menuWin = BrowserWindow.fromWebContents(event.sender); - const parentWin = menuWin?.getParentWindow(); - if (!parentWin || parentWin.isDestroyed()) return; - if (!payload?.cmd || payload.cmd === 'close') return; - parentWin.webContents.send('menu-command', payload); - } catch {} -}); - -ipcMain.on('browserview-broadcast', (event, { channel, args }) => { - try { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return; - if (win.__nebulaMode !== 'desktop') return; - const state = getDesktopViewState(win); - if (!state) return; - for (const view of state.views.values()) { - try { view.webContents.send(channel, ...(args || [])); } catch {} - } - } catch {} -}); - -ipcMain.on('browserview-host-message', (event, payload = {}) => { - try { - const { tabId, channel, args } = payload || {}; - console.log('[IPC Main] browserview-host-message received, tabId:', tabId, 'channel:', channel); - - let win = getOwnerWindowForContents(event.sender); - console.log('[IPC Main] getOwnerWindowForContents returned:', win ? 'window found' : 'null'); - - if (!win && tabId) { - console.log('[IPC Main] Trying to find window by tabId...'); - for (const candidate of BrowserWindow.getAllWindows()) { - const state = desktopViewStateByWindowId.get(candidate.id); - console.log('[IPC Main] Checking window', candidate.id, 'state:', state ? 'found' : 'null', 'has tabId:', state?.views?.has(tabId)); - if (state && state.views && state.views.has(tabId)) { - win = candidate; - console.log('[IPC Main] Found window by tabId'); - break; - } - } - } - - if (!win || win.isDestroyed()) { - console.log('[IPC Main] No valid window found, returning'); - return; - } - console.log('[IPC Main] Forwarding to renderer'); - win.webContents.send('browserview-host-message', { tabId, channel, args: args || [] }); - } catch (err) { - console.error('[IPC Main] Error:', err); - } -}); - -// Helper function to read package.json version -function getInstalledElectronVersion() { - try { - const packageJsonPath = path.join(__dirname, 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - - // Get the version from devDependencies - const electronDep = packageJson.devDependencies?.electron; - const electronNightlyDep = packageJson.devDependencies?.['electron-nightly']; - - if (electronDep) { - return electronDep.replace(/^\D+/, ''); // Remove ^ or ~ or other version specifiers - } - if (electronNightlyDep) { - return electronNightlyDep.replace(/^\D+/, ''); - } - - return app.getVersion(); - } catch (err) { - console.error('Error reading installed electron version:', err); - return app.getVersion(); - } -} - -// Electron version management handlers -ipcMain.handle('get-electron-versions', async (event, buildType = 'stable') => { - const https = require('https'); - - return new Promise((resolve) => { - let url; - - if (buildType === 'nightly') { - // Get latest nightly version from npm - url = 'https://registry.npmjs.org/electron-nightly/latest'; - } else { - // Get latest stable version from npm - url = 'https://registry.npmjs.org/electron/latest'; - } - - const request = https.get(url, (res) => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - try { - const packageInfo = JSON.parse(data); - // Get the actual installed version from package.json, not app.getVersion() - const installedVersion = getInstalledElectronVersion(); - resolve({ - available: packageInfo.version, - current: installedVersion, - buildType: buildType - }); - } catch (err) { - console.error('Failed to parse version info:', err); - resolve({ - available: null, - current: getInstalledElectronVersion(), - error: 'Failed to fetch version info' - }); - } - }); - }); - - request.on('error', (err) => { - console.error('Failed to fetch versions:', err); - resolve({ - available: null, - current: getInstalledElectronVersion(), - error: err.message - }); - }); - - request.setTimeout(5000, () => { - request.destroy(); - resolve({ - available: null, - current: getInstalledElectronVersion(), - error: 'Version check timed out' - }); - }); - }); -}); - -ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => { - const https = require('https'); - const { exec } = require('child_process'); - - console.log('[ELECTRON-UPGRADE] Checking environment...'); - console.log('[ELECTRON-UPGRADE] app.isPackaged:', app.isPackaged); - console.log('[ELECTRON-UPGRADE] __dirname:', __dirname); - console.log('[ELECTRON-UPGRADE] process.resourcesPath:', process.resourcesPath); - - // For packaged apps (like win-unpacked), we can't use npm - // This feature is only for development with `npm start` - // Steam users will get updates through Steam - - return new Promise((resolve) => { - resolve({ - success: false, - error: 'Electron updates are not available in packaged builds', - message: 'For Steam users: Updates are delivered through Steam.\n\nFor developers: Use "npm start" to enable Electron updates during development.' - }); - }); - - /* Keeping this code commented for future reference if needed - const packageName = buildType === 'nightly' ? 'electron-nightly' : 'electron'; - const packageJsonPath = path.join(__dirname, 'package.json'); - const nodeModulesPath = path.join(__dirname, 'node_modules'); - - return new Promise((resolve) => { - // Check if we're in a real development environment - if (app.isPackaged || !fs.existsSync(packageJsonPath) || !fs.existsSync(nodeModulesPath)) { - resolve({ - success: false, - error: 'Electron updates are only available in development mode', - message: 'Run the app with "npm start" to enable Electron updates.' - }); - return; - } - - // Run npm install to upgrade the package - const command = `npm install --save-dev ${packageName}@latest`; - - console.log('[ELECTRON-UPGRADE] Running command:', command); - console.log('[ELECTRON-UPGRADE] Working directory:', __dirname); - - exec(command, - { - cwd: __dirname, - maxBuffer: 10 * 1024 * 1024, - shell: true, - env: process.env - }, - (error, stdout, stderr) => { - if (error) { - console.error('[ELECTRON-UPGRADE] Upgrade failed:', error); - console.error('[ELECTRON-UPGRADE] stderr:', stderr); - - let errorMsg = error.message; - if (errorMsg.includes('ENOENT')) { - errorMsg = 'npm command not found. Please ensure Node.js and npm are installed.'; - } else if (errorMsg.includes('EACCES')) { - errorMsg = 'Permission denied. Try running as administrator.'; - } - - resolve({ - success: false, - error: errorMsg, - message: 'Failed to upgrade Electron' - }); - } else { - console.log('[ELECTRON-UPGRADE] Upgrade output:', stdout); - if (stderr) console.log('[ELECTRON-UPGRADE] stderr:', stderr); - - // Clean up alternate package - try { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (buildType === 'nightly' && packageJson.devDependencies?.electron) { - delete packageJson.devDependencies.electron; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); - } else if (buildType === 'stable' && packageJson.devDependencies?.['electron-nightly']) { - delete packageJson.devDependencies['electron-nightly']; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); - } - } catch (err) { - console.warn('[ELECTRON-UPGRADE] Could not clean up alternate package:', err); - } - - resolve({ - success: true, - message: 'Electron upgrade completed. Restarting application...' - }); - } - } - ); - }); - */ -}); - -ipcMain.handle('restart-app', async (event) => { - // Quit and relaunch the app - app.relaunch(); - app.quit(); -}); - -// Open local file dialog -> returns file:// URL (or null if cancelled) -ipcMain.handle('show-open-file-dialog', async () => { - try { - const result = await dialog.showOpenDialog({ - properties: ['openFile'], - filters: [ - { name: 'HTML Files', extensions: ['html', 'htm', 'xhtml'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - if (result.canceled || !result.filePaths || !result.filePaths.length) return null; - const filePath = result.filePaths[0]; - try { - return pathToFileURL(filePath).href; - } catch { - // Fallback manual conversion - let p = filePath.replace(/\\/g, '/'); - if (!p.startsWith('/')) p = '/' + p; // ensure leading slash for drive letters - return 'file://' + (p.startsWith('/') ? '/' : '') + p; // double slash safety - } - } catch (err) { - console.error('open-file dialog failed:', err); - return null; - } -}); - -// Helper to build and show a native context menu for a given webContents + params -function buildAndShowContextMenu(sender, params = {}) { - try { - const ownerWin = getOwnerWindowForContents(sender); - const embedder = ownerWin?.webContents || sender.hostWebContents || sender; - const template = []; - - template.push( - { label: 'Back', enabled: sender.canGoBack?.(), click: () => { try { sender.goBack(); } catch {} } }, - { label: 'Forward', enabled: sender.canGoForward?.(), click: () => { try { sender.goForward(); } catch {} } }, - { label: 'Reload', click: () => { try { sender.reload(); } catch {} } }, - { type: 'separator' } - ); - - // Link actions - const linkURL = params.linkURL && params.linkURL.startsWith('http') ? params.linkURL : undefined; - if (linkURL) { - template.push( - { label: 'Open Link in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-link-new-tab', url: linkURL }) }, - { label: 'Download Link', click: () => { - try { (sender.hostWebContents || sender).downloadURL(linkURL); } catch (e) { console.error('downloadURL failed:', e); } - } - }, - { label: 'Open Link Externally', click: () => shell.openExternal(linkURL).catch(()=>{}) }, - { label: 'Copy Link Address', click: () => clipboard.writeText(linkURL) }, - { type: 'separator' } - ); - } - - // Image actions - const imageURL = (params.mediaType === 'image' && params.srcURL) ? params.srcURL : (params.imgURL || undefined); - if (imageURL) { - template.push( - { label: 'Open Image in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-image-new-tab', url: imageURL }) }, - { label: 'Copy Image Address', click: () => clipboard.writeText(imageURL) }, - { label: 'Save Image As...', click: () => embedder.send('context-menu-command', { cmd: 'save-image', url: imageURL, mime: params.mediaType === 'image' ? params.mimeType : undefined }) }, - { type: 'separator' } - ); - } - - // Text / editable - if (params.isEditable) { - template.push( - { label: 'Undo', role: 'undo' }, - { label: 'Redo', role: 'redo' }, - { type: 'separator' }, - { label: 'Cut', role: 'cut' }, - { label: 'Copy', role: 'copy' }, - { label: 'Paste', role: 'paste' }, - { label: 'Select All', role: 'selectAll' }, - { type: 'separator' } - ); - } else if (params.selectionText) { - template.push( - { label: 'Copy', role: 'copy' }, - { label: 'Select All', role: 'selectAll' }, - { type: 'separator' } - ); - } - - template.push({ - label: 'Inspect Element', - click: () => { - try { - const inspectTarget = sender; - const inspectX = params.x ?? params.clientX ?? 0; - const inspectY = params.y ?? params.clientY ?? 0; - - // Open DevTools docked at bottom if not already open - if (!inspectTarget.isDevToolsOpened()) { - inspectTarget.openDevTools({ mode: 'bottom' }); - } - - // Inspect the element - setTimeout(() => { - try { - inspectTarget.inspectElement(inspectX, inspectY); - } catch (e) { - // Fallback: try on original sender - try { sender.inspectElement(inspectX, inspectY); } catch {} - } - }, 50); - } catch (err) { - console.error('Inspect Element failed:', err); - } - } - }); - - // Allow plugins to customize/append context menu - try { pluginManager.applyContextMenuContrib(template, params, sender); } catch {} - const menu = Menu.buildFromTemplate(template); - const win = ownerWin || BrowserWindow.fromWebContents(embedder); - if (win) menu.popup({ window: win }); - } catch (err) { - console.error('Failed to build context menu:', err); - } -} - -// IPC trigger (legacy / renderer-requested) -ipcMain.handle('show-context-menu', (event, params = {}) => { - buildAndShowContextMenu(event.sender, params); -}); - -// Plugins: expose renderer preload list -ipcMain.handle('plugins-get-renderer-preloads', () => { - try { return pluginManager.getRendererPreloads(); } catch { return []; } -}); - -// Plugins: expose registered internal pages (nebula://) -ipcMain.handle('plugins-get-pages', () => { - try { return pluginManager.getRendererPages(); } catch { return []; } -}); - -// Plugins: management IPC for settings UI -ipcMain.handle('plugins-list', () => pluginManager.discoverPlugins()); -ipcMain.handle('plugins-set-enabled', async (_e, { id, enabled }) => { - const ok = await pluginManager.setEnabled(id, enabled); - // Reload to apply enable/disable (requires app reload for renderer preloads) - pluginManager.reload(); - return ok; -}); -ipcMain.handle('plugins-reload', (_e, { id } = {}) => { - pluginManager.reload(id); - return true; -}); - -// Automatic native context menu for any webContents (windows + webviews) -app.on('web-contents-created', (event, contents) => { - contents.on('context-menu', (e, params) => { - buildAndShowContextMenu(contents, params); - }); - - // Emit to plugins - try { pluginManager.emit('web-contents-created', contents); } catch {} - - // On macOS, when a page (or a ) enters HTML fullscreen (e.g., YouTube video), - // also toggle the BrowserWindow into simple fullscreen so the content uses the whole - // screen and macOS traffic lights/titlebar are hidden. Revert when HTML fullscreen exits. - if (process.platform === 'darwin') { - const getOwningWindow = () => { - try { - const host = contents.hostWebContents || contents; - return BrowserWindow.fromWebContents(host) || null; - } catch { return null; } - }; - - contents.on('enter-html-full-screen', () => { - const win = getOwningWindow(); - if (!win) return; - win.__htmlFsDepth = (win.__htmlFsDepth || 0) + 1; - // If the window is already in native fullscreen (green button), don't switch modes - const alreadyNativeFs = typeof win.isFullScreen === 'function' && win.isFullScreen(); - if (!alreadyNativeFs && !win.isSimpleFullScreen?.()) { - try { win.setSimpleFullScreen?.(true); win.__htmlFsUsingSimple = true; } catch {} - } - }); - - contents.on('leave-html-full-screen', () => { - const win = getOwningWindow(); - if (!win) return; - win.__htmlFsDepth = Math.max(0, (win.__htmlFsDepth || 1) - 1); - if (win.__htmlFsDepth === 0 && win.__htmlFsUsingSimple) { - try { if (win.isSimpleFullScreen?.()) win.setSimpleFullScreen?.(false); } catch {} - win.__htmlFsUsingSimple = false; - } - }); - } -}); - -// --- Image save handlers --- -ipcMain.handle('save-image-from-dataurl', async (event, { suggestedName = 'image', dataUrl }) => { - try { - if (!dataUrl || !dataUrl.startsWith('data:')) return false; - const match = /^data:(.*?);base64,(.*)$/.exec(dataUrl); - if (!match) return false; - const mime = match[1] || 'application/octet-stream'; - const ext = (mime.split('/')[1] || 'png').split(';')[0]; - const buf = Buffer.from(match[2], 'base64'); - const win = BrowserWindow.fromWebContents(event.sender.hostWebContents || event.sender); - const { canceled, filePath } = await dialog.showSaveDialog(win, { defaultPath: `${suggestedName}.${ext}` }); - if (canceled || !filePath) return false; - await fs.promises.writeFile(filePath, buf); - return true; - } catch (err) { - console.error('save-image-from-dataurl failed:', err); - return false; - } -}); - -ipcMain.handle('save-image-from-url', async (event, { url }) => { - if (!url) return false; - const win = BrowserWindow.fromWebContents(event.sender.hostWebContents || event.sender); - try { - let dataBuf; - if (url.startsWith('http')) { - const res = await fetch(url); - if (!res.ok) throw new Error('HTTP '+res.status); - const arrayBuf = await res.arrayBuffer(); - dataBuf = Buffer.from(arrayBuf); - const ctype = res.headers.get('content-type') || 'application/octet-stream'; - const ext = (ctype.split('/')[1] || 'png').split(';')[0]; - const { canceled, filePath } = await dialog.showSaveDialog(win, { defaultPath: `image.${ext}` }); - if (canceled || !filePath) return false; - await fs.promises.writeFile(filePath, dataBuf); - return true; - } else if (url.startsWith('data:')) { - // Forward to dataURL handler path โ€“ easier to keep logic single - return ipcMain.emit('save-image-from-dataurl', event, { dataUrl: url }); - } else if (url.startsWith('file:')) { - // Copy file to chosen destination - const filePathSrc = new URL(url).pathname.replace(/^\//, ''); - const base = path.basename(filePathSrc); - const { canceled, filePath } = await dialog.showSaveDialog(win, { defaultPath: base }); - if (canceled || !filePath) return false; - await fs.promises.copyFile(filePathSrc, filePath); - return true; - } else { - return false; - } - } catch (err) { - console.error('save-image-from-url failed:', err); - return false; - } -}); - -// ========================= -// Download manager plumbing -// ========================= - -// In-memory download registry -const downloads = new Map(); // id -> { id, url, filename, savePath, totalBytes, receivedBytes, state, startedAt, mime, canResume, paused, scan? } - -function broadcastToAll(channel, payload) { - try { - for (const wc of webContents.getAllWebContents()) { - try { wc.send(channel, payload); } catch {} - } - } catch (e) { - // Fallback to windows only - for (const win of BrowserWindow.getAllWindows()) { - try { win.webContents.send(channel, payload); } catch {} - } - } -} - -function registerDownloadHandling(ses) { - if (!ses || ses.__nebulaDownloadsHooked) return; - ses.__nebulaDownloadsHooked = true; - ses.on('will-download', async (event, item, wc) => { - try { - // Build an id (prefer stable GUID if available) - const id = typeof item.getGUID === 'function' ? item.getGUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`; - item.__nebulaId = id; - const filename = item.getFilename(); - const mime = item.getMimeType?.() || 'application/octet-stream'; - const totalBytes = item.getTotalBytes(); - const url = item.getURL(); - - // Choose a default save path under user's Downloads, ensure unique to avoid overwrite - const defaultDir = app.getPath('downloads'); - const uniquePath = await computeUniqueSavePath(defaultDir, filename); - try { item.setSavePath(uniquePath); } catch {} - - const info = { - id, url, filename, - savePath: uniquePath, - totalBytes, - receivedBytes: 0, - state: 'in-progress', - startedAt: Date.now(), - mime, - canResume: false, - paused: false, - scan: { status: process.platform === 'win32' ? 'pending' : 'unavailable', engine: process.platform === 'win32' ? 'Windows Defender' : 'none' } - }; - downloads.set(id, { ...info, item }); - const payload = { ...info }; - broadcastToAll('downloads-started', payload); - - item.on('updated', (e, state) => { - const d = downloads.get(id); - if (!d) return; - d.receivedBytes = item.getReceivedBytes(); - d.canResume = !!item.canResume?.(); - d.paused = !!item.isPaused?.(); - d.state = state === 'interrupted' ? 'interrupted' : 'in-progress'; - downloads.set(id, d); - broadcastToAll('downloads-updated', { - id, - receivedBytes: d.receivedBytes, - totalBytes: d.totalBytes, - state: d.state, - canResume: d.canResume, - paused: d.paused - }); - }); - - item.once('done', async (e, state) => { - const d = downloads.get(id) || {}; - const finalState = state === 'completed' ? 'completed' : (state === 'cancelled' ? 'cancelled' : 'interrupted'); - const final = { - id, - url, - filename, - savePath: item.getSavePath?.() || d.savePath, - totalBytes: d.totalBytes || item.getTotalBytes?.() || 0, - receivedBytes: item.getReceivedBytes?.() || d.receivedBytes || 0, - state: finalState, - startedAt: d.startedAt || Date.now(), - endedAt: Date.now(), - mime, - scan: d.scan || { status: process.platform === 'win32' ? 'pending' : 'unavailable', engine: process.platform === 'win32' ? 'Windows Defender' : 'none' } - }; - // Store minimal object; drop live item ref - downloads.set(id, final); - broadcastToAll('downloads-done', final); - - // Kick off a malware scan on Windows if the download completed and path exists - if (finalState === 'completed' && final.savePath && process.platform === 'win32') { - try { - // Update to scanning state and broadcast - const cur = downloads.get(id) || final; - cur.scan = { ...(cur.scan || {}), status: 'scanning', engine: 'Windows Defender' }; - downloads.set(id, cur); - broadcastToAll('downloads-scan-started', { id, savePath: final.savePath }); - - const result = await scanFileForMalware(final.savePath); - const updated = downloads.get(id) || cur; - updated.scan = result; - downloads.set(id, updated); - broadcastToAll('downloads-scan-result', { id, scan: result }); - } catch (scanErr) { - const updated = downloads.get(id) || final; - updated.scan = { status: 'error', engine: 'Windows Defender', details: String(scanErr && scanErr.message || scanErr) }; - downloads.set(id, updated); - broadcastToAll('downloads-scan-result', { id, scan: updated.scan }); - } - } - }); - } catch (err) { - console.error('will-download handler error:', err); - } - }); -} - -async function computeUniqueSavePath(dir, baseName) { - try { - const target = path.join(dir, baseName); - try { - await fs.promises.access(target); - // Already exists, create a (n) suffix - const { name, ext } = splitNameExt(baseName); - for (let i = 1; i < 10000; i++) { - const candidate = path.join(dir, `${name} (${i})${ext}`); - try { await fs.promises.access(candidate); } catch { return candidate; } - } - // Fallback if too many - return path.join(dir, `${Date.now()}-${baseName}`); - } catch { - return target; // does not exist - } - } catch (e) { - // Fallback to temp directory - return path.join(app.getPath('downloads'), `${Date.now()}-${baseName}`); - } -} - -function splitNameExt(filename) { - const ext = path.extname(filename); - const name = filename.slice(0, filename.length - ext.length); - return { name, ext }; -} - -// IPC: list downloads -ipcMain.handle('downloads-get-all', () => { - return Array.from(downloads.values()).map(d => { - const { item, ...rest } = d; - if (item) { - return { - ...rest, - receivedBytes: item.getReceivedBytes?.() ?? rest.receivedBytes ?? 0, - totalBytes: item.getTotalBytes?.() ?? rest.totalBytes ?? 0, - state: rest.state || 'in-progress', - paused: item.isPaused?.() || false, - canResume: item.canResume?.() || false, - scan: rest.scan || { status: process.platform === 'win32' ? 'pending' : 'unavailable', engine: process.platform === 'win32' ? 'Windows Defender' : 'none' } - }; - } - return rest; - }); -}); - -// IPC: control a download (pause/resume/cancel/open/show) -ipcMain.handle('downloads-action', async (event, { id, action }) => { - const d = downloads.get(id); - if (!d) return false; - const item = d.item; - try { - switch (action) { - case 'pause': - if (item && !item.isPaused?.()) item.pause?.(); - return true; - case 'resume': - if (item && item.canResume?.()) item.resume?.(); - return true; - case 'cancel': - if (item && d.state === 'in-progress') item.cancel?.(); - return true; - case 'delete-file': { - if (d.savePath) { - try { - await fs.promises.unlink(d.savePath); - // Mark entry as deleted (custom state) and clear savePath - const updated = { ...d, state: d.state === 'completed' ? 'deleted' : d.state, savePath: null }; - downloads.set(id, updated); - broadcastToAll('downloads-updated', { id, state: updated.state, savePath: null }); - return true; - } catch (e) { - console.error('Failed to delete file:', e); - return false; - } - } - return false; - } - case 'rescan': { - if (d.savePath && process.platform === 'win32') { - try { - const cur = downloads.get(id) || d; - cur.scan = { status: 'scanning', engine: 'Windows Defender' }; - downloads.set(id, cur); - broadcastToAll('downloads-scan-started', { id, savePath: d.savePath }); - const result = await scanFileForMalware(d.savePath); - const updated = downloads.get(id) || cur; - updated.scan = result; - downloads.set(id, updated); - broadcastToAll('downloads-scan-result', { id, scan: result }); - return true; - } catch (e) { - console.error('Rescan failed:', e); - const updated = downloads.get(id) || d; - updated.scan = { status: 'error', engine: 'Windows Defender', details: String(e && e.message || e) }; - downloads.set(id, updated); - broadcastToAll('downloads-scan-result', { id, scan: updated.scan }); - return false; - } - } - return false; - } - case 'open-file': - if (d.savePath) { - await shell.openPath(d.savePath); - return true; - } - return false; - case 'show-in-folder': - if (d.savePath) { - shell.showItemInFolder(d.savePath); - return true; - } - return false; - default: - return false; - } - } catch (e) { - console.error('downloads-action error:', e); - return false; - } -}); - -// IPC: clear completed entries from the registry (keeps in-progress) -ipcMain.handle('downloads-clear-completed', () => { - for (const [id, d] of downloads.entries()) { - if (d.state === 'completed' || d.state === 'cancelled' || d.state === 'deleted') downloads.delete(id); - } - broadcastToAll('downloads-cleared'); - return true; -}); - -// --------------------------- -// Malware scan helpers (Windows Defender) -// --------------------------- -async function findDefenderMpCmdRun() { - if (process.platform !== 'win32') return null; - const candidates = []; - const programData = process.env['ProgramData']; - if (programData) { - const platformDir = path.join(programData, 'Microsoft', 'Windows Defender', 'Platform'); - try { - const entries = await fs.promises.readdir(platformDir, { withFileTypes: true }); - const versions = entries.filter(e => e.isDirectory()).map(e => e.name); - // Sort versions descending (simple lex sort approximates ok as versions are zero-padded; fallback to reverse chronological by stats) - versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' })); - for (const v of versions) { - candidates.push(path.join(platformDir, v, 'MpCmdRun.exe')); - } - } catch {} - } - const programFiles = process.env['ProgramFiles'] || 'C://Program Files'; - candidates.push(path.join(programFiles, 'Windows Defender', 'MpCmdRun.exe')); - candidates.push(path.join(programFiles, 'Microsoft Defender', 'MpCmdRun.exe')); - for (const c of candidates) { - try { - await fs.promises.access(c, fs.constants.X_OK | fs.constants.R_OK); - return c; - } catch {} - } - return null; -} - -async function scanFileForMalware(filePath) { - if (process.platform !== 'win32') { - return { status: 'unavailable', engine: 'none', details: 'Malware scanning is only available on Windows with Microsoft Defender.' }; - } - try { - // Ensure file exists - await fs.promises.access(filePath, fs.constants.R_OK); - } catch { - return { status: 'error', engine: 'Windows Defender', details: 'File not found for scanning.' }; - } - const exe = await findDefenderMpCmdRun(); - if (!exe) { - return { status: 'unavailable', engine: 'Windows Defender', details: 'Microsoft Defender command-line scanner not found.' }; - } - - return await new Promise((resolve) => { - const args = ['-Scan', '-ScanType', '3', '-File', filePath]; - let stdout = ''; - let stderr = ''; - const child = spawn(exe, args, { windowsHide: true }); - child.stdout.on('data', (d) => { stdout += d.toString(); }); - child.stderr.on('data', (d) => { stderr += d.toString(); }); - child.on('error', (err) => { - resolve({ status: 'error', engine: 'Windows Defender', details: 'Failed to run scanner: ' + String(err && err.message || err) }); - }); - child.on('close', (code) => { - const out = (stdout + '\n' + stderr).toLowerCase(); - // Heuristics: exit code 2 indicates threats found; also parse output - const infected = code === 2 || /threat|infected|malware|found\s*:\s*[1-9]/i.test(stdout) || /threat|infected|malware/.test(stderr); - if (infected) { - resolve({ status: 'infected', engine: 'Windows Defender', details: stdout || stderr, exitCode: code }); - } else if (code === 0 || /no threats/.test(out) || /found\s*:\s*0/.test(out)) { - resolve({ status: 'clean', engine: 'Windows Defender', details: stdout || 'No threats found.', exitCode: code }); - } else { - resolve({ status: 'error', engine: 'Windows Defender', details: (stdout || stderr || 'Unknown scan result') + ` (code ${code})`, exitCode: code }); - } - }); - }); -} diff --git a/make-appdir.sh b/make-appdir.sh deleted file mode 100755 index 582d348..0000000 --- a/make-appdir.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -# Assemble nebula-appdir from extracted squashfs-root -set -euo pipefail -SRC="${1:-squashfs-root}" -DEST="${2:-nebula-appdir}" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -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 (prefer extracted run-nebula.sh as fallback) -if [ -f "$DEST/run-nebula.sh" ] && [ ! -f "$DEST/Nebula" ]; 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 - -# Match appdir-example layout: extract app.asar to resources/app and keep app.asar.orig -if [ -f "$DEST/resources/app.asar" ]; then - if command -v npx &> /dev/null; then - echo "Extracting app.asar to resources/app (keeping app.asar.orig)" - (cd "$DEST/resources" && npx asar extract "app.asar" "app") - mv "$DEST/resources/app.asar" "$DEST/resources/app.asar.orig" 2>/dev/null || true - else - echo "Warning: npx not found; leaving app.asar in place." - fi -fi - -# Copy Linux launch wrappers if present in appdir-example -if [ -f "$SCRIPT_DIR/appdir-example/run-nebula.sh" ]; then - cp "$SCRIPT_DIR/appdir-example/run-nebula.sh" "$DEST/run-nebula.sh" - sed -i.bak 's|usr/data|usr/user-data|g' "$DEST/run-nebula.sh" && rm -f "$DEST/run-nebula.sh.bak" - chmod +x "$DEST/run-nebula.sh" || true -fi -if [ -f "$SCRIPT_DIR/appdir-example/steam_appid.txt" ]; then - cp "$SCRIPT_DIR/appdir-example/steam_appid.txt" "$DEST/steam_appid.txt" -fi -if [ -f "$SCRIPT_DIR/appdir-example/nebula.desktop" ]; then - cp "$SCRIPT_DIR/appdir-example/nebula.desktop" "$DEST/nebula.desktop" - cp "$SCRIPT_DIR/appdir-example/nebula.desktop" "$DEST/usr/share/applications/nebula.desktop" -fi -# Ensure root launchers exist (from example if needed) -if [ -f "$SCRIPT_DIR/appdir-example/Nebula-Desktop" ]; then - cp "$SCRIPT_DIR/appdir-example/Nebula-Desktop" "$DEST/Nebula-Desktop" - chmod +x "$DEST/Nebula-Desktop" || true -fi -if [ -f "$SCRIPT_DIR/appdir-example/Nebula-Controller" ]; then - cp "$SCRIPT_DIR/appdir-example/Nebula-Controller" "$DEST/Nebula-Controller" - chmod +x "$DEST/Nebula-Controller" || true -fi -# Fallback: create Nebula as symlink to Nebula-Desktop -if [ ! -f "$DEST/Nebula" ] && [ -f "$DEST/Nebula-Desktop" ]; then - ln -sf "Nebula-Desktop" "$DEST/Nebula" -fi - -# Fix permissions -chmod -R a+r "$DEST/usr/share/icons/hicolor/256x256/apps" || true -chmod +x "$DEST/Nebula" "$DEST/Nebula-Desktop" "$DEST/Nebula-Controller" || true -mkdir -p "$DEST/usr/user-data" -chmod 700 "$DEST/usr/user-data" || true - -echo "AppDir assembled at $DEST." -echo " Desktop mode: $DEST/Nebula-Desktop" -echo " Controller mode: $DEST/Nebula-Controller" -echo " Default: $DEST/Nebula (symlink to Nebula-Desktop)" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fb04bcd..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3449 +0,0 @@ -{ - "name": "nebula", - "version": "1.3.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nebula", - "version": "1.3.2", - "license": "ISC", - "dependencies": { - "dompurify": "^3.1.6", - "electron-updater": "^6.6.2", - "highlight.js": "^11.9.0", - "marked": "^12.0.2", - "steamworks.js": "^0.3.2" - }, - "devDependencies": { - "electron": "^40.0.0", - "electron-builder": "^23.0.0", - "electron-nightly": "^39.0.0-nightly.20250811" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/universal": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", - "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^1.1.0", - "asar": "^3.1.0", - "debug": "^4.3.1", - "dir-compare": "^2.4.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.3.tgz", - "integrity": "sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", - "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", - "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", - "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/universal": "1.2.1", - "@malept/flatpak-bundler": "^0.4.0", - "7zip-bin": "~5.1.1", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.4", - "ejs": "^3.1.7", - "electron-osx-sign": "^0.6.0", - "electron-publish": "23.6.0", - "form-data": "^4.0.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", - "isbinaryfile": "^4.0.10", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "minimatch": "^3.1.2", - "read-config-file": "6.2.0", - "sanitize-filename": "^1.6.3", - "semver": "^7.3.7", - "tar": "^6.1.11", - "temp-file": "^3.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/asar": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", - "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", - "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", - "dev": true, - "license": "MIT", - "dependencies": { - "chromium-pickle-js": "^0.2.0", - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - }, - "optionalDependencies": { - "@types/glob": "^7.1.1" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/bluebird-lst": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "^3.5.5" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true, - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", - "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "@types/fs-extra": "^9.0.11", - "7zip-bin": "~5.1.1", - "app-builder-bin": "4.0.0", - "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.1.1", - "chalk": "^4.1.1", - "cross-spawn": "^7.0.3", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-ci": "^3.0.0", - "js-yaml": "^4.1.0", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", - "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", - "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal": "1.0.0", - "colors": "1.0.3", - "commander": "2.9.0", - "minimatch": "3.0.4" - }, - "bin": { - "dircompare": "src/cli/dircompare.js" - } - }, - "node_modules/dir-compare/node_modules/commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-readlink": ">= 1.0.0" - }, - "engines": { - "node": ">= 0.6.x" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", - "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "23.6.0", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "fs-extra": "^10.0.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dompurify": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "40.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.0.0.tgz", - "integrity": "sha512-UyBy5yJ0/wm4gNugCtNPjvddjAknMTuXR2aCHioXicH7aKRKGDBPp4xqTEi/doVcB3R+MN3wfU9o8d/9pwgK2A==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^24.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", - "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs": "^17.0.1", - "app-builder-lib": "23.6.0", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "chalk": "^4.1.1", - "dmg-builder": "23.6.0", - "fs-extra": "^10.0.0", - "is-ci": "^3.0.0", - "lazy-val": "^1.0.5", - "read-config-file": "6.2.0", - "simple-update-notifier": "^1.0.7", - "yargs": "^17.5.1" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-nightly": { - "version": "39.0.0-nightly.20250902", - "resolved": "https://registry.npmjs.org/electron-nightly/-/electron-nightly-39.0.0-nightly.20250902.tgz", - "integrity": "sha512-dWlw0mv/I1n70wXMSdT2rYzRsR1EBDvOAk9b4R4Wm4s4SVTVc1HtuF47iGZTqG2NW9eqi2FGhkraIOABxzjNjw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-osx-sign": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", - "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", - "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "bluebird": "^3.5.0", - "compare-version": "^0.1.2", - "debug": "^2.6.8", - "isbinaryfile": "^3.0.2", - "minimist": "^1.2.0", - "plist": "^3.0.1" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/electron-osx-sign/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/electron-osx-sign/node_modules/isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-alloc": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/electron-osx-sign/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-publish": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", - "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "23.6.0", - "builder-util-runtime": "9.1.1", - "chalk": "^4.1.1", - "fs-extra": "^10.0.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-updater": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz", - "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==", - "license": "MIT", - "dependencies": { - "builder-util-runtime": "9.3.1", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "^7.6.3", - "tiny-typed-emitter": "^2.1.0" - } - }, - "node_modules/electron-updater/node_modules/builder-util-runtime": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", - "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/electron-updater/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-updater/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-updater/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron/node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/electron/node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/marked": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-config-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", - "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dotenv": "^9.0.2", - "dotenv-expand": "^5.1.0", - "js-yaml": "^4.1.0", - "json5": "^2.2.0", - "lazy-val": "^1.0.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "~7.0.0" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/steamworks.js": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/steamworks.js/-/steamworks.js-0.3.2.tgz", - "integrity": "sha512-iuZdAHBktF/Ov7hYFApmM3tGaIi24hrmE84yzbBtd+E+2UX1ccFgINm5GIiAq8AEIA+BmXUXE9ezzEsXoOm1UA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "format": "^0.2.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 7554c6e..0000000 --- a/package.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "nebula", - "productName": "Nebula", - "version": "1.3.2", - "main": "main.js", - "scripts": { - "start": "electron .", - "start:dev": "electron . --no-sandbox --disable-gpu", - "start:linux": "electron . --no-sandbox", - "dist": "electron-builder", - "run": "electron ." - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "dompurify": "^3.1.6", - "electron-updater": "^6.6.2", - "highlight.js": "^11.9.0", - "marked": "^12.0.2", - "steamworks.js": "^0.3.2" - }, - "devDependencies": { - "electron": "^40.0.0", - "electron-builder": "^23.0.0", - "electron-nightly": "^39.0.0-nightly.20250811" - }, - "build": { - "appId": "com.andrewzambazos.nebula", - "protocols": [ - { - "name": "Nebula", - "schemes": [ - "http", - "https" - ] - } - ], - "publish": [ - { - "provider": "github", - "owner": "Bobbybear007", - "repo": "NebulaBrowser" - } - ], - "files": [ - "**/*", - "!user-data/**", - "!*.backup.json" - ], - "extraResources": [], - "mac": { - "category": "public.app-category.productivity", - "icon": "assets/images/Logos/Nebula-Favicon.icns" - }, - "win": { - "icon": "assets/images/Logos/Nebula-Favicon.ico" - }, - "linux": { - "icon": "assets/images/Logos/Nebula-Favicon.png", - "target": [ - "AppImage" - ] - } - } -} diff --git a/performance-monitor.js b/performance-monitor.js deleted file mode 100644 index 1f1f8af..0000000 --- a/performance-monitor.js +++ /dev/null @@ -1,100 +0,0 @@ -// performance-monitor.js - Monitor and optimize browser performance -const { app } = require('electron'); - -class PerformanceMonitor { - constructor() { - this.metrics = { - memoryUsage: [], - cpuUsage: [], - loadTimes: [] - }; - this.startTime = Date.now(); - } - - // Monitor memory usage - trackMemory() { - const usage = process.memoryUsage(); - this.metrics.memoryUsage.push({ - timestamp: Date.now(), - rss: usage.rss, - heapUsed: usage.heapUsed, - heapTotal: usage.heapTotal, - external: usage.external - }); - - // Keep only last 100 measurements - if (this.metrics.memoryUsage.length > 100) { - this.metrics.memoryUsage.shift(); - } - } - - // Monitor CPU usage - trackCPU() { - const usage = process.cpuUsage(); - this.metrics.cpuUsage.push({ - timestamp: Date.now(), - user: usage.user, - system: usage.system - }); - - if (this.metrics.cpuUsage.length > 100) { - this.metrics.cpuUsage.shift(); - } - } - - // Track page load times - trackLoadTime(url, loadTime) { - this.metrics.loadTimes.push({ - timestamp: Date.now(), - url, - loadTime - }); - - if (this.metrics.loadTimes.length > 50) { - this.metrics.loadTimes.shift(); - } - } - - // Get performance report - getReport() { - const memAvg = this.metrics.memoryUsage.length > 0 - ? this.metrics.memoryUsage.reduce((sum, m) => sum + m.heapUsed, 0) / this.metrics.memoryUsage.length - : 0; - - const avgLoadTime = this.metrics.loadTimes.length > 0 - ? this.metrics.loadTimes.reduce((sum, l) => sum + l.loadTime, 0) / this.metrics.loadTimes.length - : 0; - - return { - uptime: Date.now() - this.startTime, - averageMemoryUsage: Math.round(memAvg / 1024 / 1024), // MB - averageLoadTime: Math.round(avgLoadTime), - totalPageLoads: this.metrics.loadTimes.length - }; - } - - // Start monitoring - start() { - // Monitor every 30 seconds - setInterval(() => { - this.trackMemory(); - this.trackCPU(); - }, 30000); - - // Log performance report every 5 minutes - setInterval(() => { - const report = this.getReport(); - console.log('Performance Report:', report); - }, 5 * 60 * 1000); - } - - // Force garbage collection if available - forceGC() { - if (global.gc) { - global.gc(); - console.log('Forced garbage collection'); - } - } -} - -module.exports = PerformanceMonitor; diff --git a/plugin-manager.js b/plugin-manager.js deleted file mode 100644 index a11fd13..0000000 --- a/plugin-manager.js +++ /dev/null @@ -1,285 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { pathToFileURL } = require('url'); -const { app, session, Menu, ipcMain, BrowserWindow, dialog, shell } = require('electron'); - -class PluginManager { - constructor() { - this.plugins = []; // { id, dir, manifest, mod, enabled } - this.rendererPreloads = []; // absolute file paths - this.rendererPages = []; // { id, file, pluginId } - this._listeners = { - 'app-ready': [], - 'window-created': [], - 'web-contents-created': [], - 'session-configured': [], - }; - this._webRequestHandlers = []; // { filter, listener } - this._contextMenuContribs = []; // [function(template, params, sender)] - } - - getPluginDirs() { - const appDir = path.join(app.getAppPath(), 'plugins'); - const userDir = path.join(app.getPath('userData'), 'plugins'); - return [appDir, userDir]; - } - - ensureUserPluginsDir() { - try { - const userDir = path.join(app.getPath('userData'), 'plugins'); - fs.mkdirSync(userDir, { recursive: true }); - return userDir; - } catch (_) { return null; } - } - - loadAll() { - this.plugins = []; - this.rendererPreloads = []; - this.rendererPages = []; - const dirs = this.getPluginDirs(); - for (const root of dirs) { - let entries = []; - try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; } - for (const ent of entries) { - if (!ent.isDirectory()) continue; - const dir = path.join(root, ent.name); - const manifestPath = path.join(dir, 'plugin.json'); - let manifest; - try { - manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - // Normalize optional fields - const cats = manifest.categories; - if (typeof cats === 'string') manifest.categories = [cats]; - else if (Array.isArray(cats)) manifest.categories = cats.filter(x => typeof x === 'string'); - else if (cats == null) manifest.categories = []; - - const au = manifest.authors; - if (typeof au === 'string') manifest.authors = [au]; - else if (Array.isArray(au)) manifest.authors = au.filter(x => (typeof x === 'string') || (x && typeof x === 'object' && typeof x.name === 'string')); - else if (au == null) manifest.authors = []; - } catch { continue; } - const enabled = manifest.enabled !== false; // default true - const id = manifest.id || ent.name; - const record = { id, dir, manifest, enabled, mod: null, mainPath: null }; - if (enabled) { - // Load main module if provided - if (manifest.main) { - const mainPath = path.join(dir, manifest.main); - try { - // eslint-disable-next-line import/no-dynamic-require, global-require - record.mod = require(mainPath); - record.mainPath = mainPath; - } catch (e) { - console.error(`[Plugins] Failed to load main for ${id}:`, e); - } - } - // Collect renderer preload if provided - if (manifest.rendererPreload) { - const rp = path.join(dir, manifest.rendererPreload); - try { - if (fs.existsSync(rp)) this.rendererPreloads.push(rp); - } catch {} - } - } - this.plugins.push(record); - } - } - // Activate plugins with activate(ctx) - for (const p of this.plugins) { - if (!p.enabled || !p.mod) continue; - try { - const ctx = this._buildContext(p); - if (typeof p.mod.activate === 'function') { - p.mod.activate(ctx); - } else if (typeof p.mod === 'function') { - // support default export as function(ctx) - p.mod(ctx); - } - } catch (e) { - console.error(`[Plugins] Error activating ${p.id}:`, e); - } - } - } - - _buildContext(plugin) { - const manager = this; - const logPrefix = `[Plugin:${plugin.id}]`; - return { - app, - BrowserWindow, - ipcMain, - session, - Menu, - dialog, - shell, - paths: { - appPath: app.getAppPath(), - userData: app.getPath('userData'), - pluginDir: plugin.dir, - }, - log: (...args) => console.log(logPrefix, ...args), - warn: (...args) => console.warn(logPrefix, ...args), - error: (...args) => console.error(logPrefix, ...args), - on: (evt, cb) => manager.on(evt, cb), - registerIPC: (channel, handler) => { - try { ipcMain.handle(channel, handler); } catch (e) { console.error(logPrefix, 'registerIPC failed', e); } - }, - registerWebRequest: (filter, listener) => { - try { manager._webRequestHandlers.push({ filter, listener }); } catch (e) { console.error(logPrefix, 'registerWebRequest failed', e); } - }, - contributeContextMenu: (contribFn) => { - try { manager._contextMenuContribs.push(contribFn); } catch (e) { console.error(logPrefix, 'contributeContextMenu failed', e); } - }, - // Register a dedicated internal page (shown via nebula://) - registerRendererPage: ({ id, html }) => { - try { - if (!id || !html) return; - let fileUrl = null; - try { fileUrl = pathToFileURL(html).href; } catch {} - manager.rendererPages.push({ id, file: html, fileUrl, pluginId: plugin.id }); - console.log('[Plugins] Registered page:', id, '->', html, 'fileUrl:', fileUrl); - manager.log('registered page:', id, '->', html); - } catch (e) { manager.error('registerRendererPage failed', e); } - } - }; - } - - getRendererPreloads() { - return Array.from(new Set(this.rendererPreloads)); - } - - getRendererPages() { - // Return a shallow copy so callers can't mutate internal array - return this.rendererPages.map(p => ({ ...p })); - } - - on(evt, cb) { - if (!this._listeners[evt]) this._listeners[evt] = []; - this._listeners[evt].push(cb); - } - - emit(evt, ...args) { - const list = this._listeners[evt] || []; - for (const cb of list) { - try { cb(...args); } catch (e) { console.error('[Plugins] listener error for', evt, e); } - } - } - - applyWebRequestHandlers(ses) { - try { - if (!ses || !ses.webRequest) return; - for (const { filter, listener } of this._webRequestHandlers) { - try { - ses.webRequest.onBeforeRequest(filter || {}, (details, callback) => { - try { - const res = listener(details); - if (res && typeof res === 'object') callback(res); else callback({ cancel: false }); - } catch (e) { - console.error('[Plugins] webRequest handler error:', e); - callback({ cancel: false }); - } - }); - } catch (e) { - console.error('[Plugins] Failed to attach webRequest handler:', e); - } - } - } catch (e) { - console.error('[Plugins] applyWebRequestHandlers error:', e); - } - } - - applyContextMenuContrib(template, params, sender) { - try { - for (const fn of this._contextMenuContribs) { - try { fn(template, params, sender); } catch (e) { console.error('[Plugins] context menu contrib error:', e); } - } - } catch (e) { console.error('[Plugins] applyContextMenuContrib error:', e); } - } - - getPluginsInfo() { - return this.plugins.map(p => ({ - id: p.id, - name: p.manifest.name || p.id, - version: p.manifest.version || '0.0.0', - description: p.manifest.description || '', - categories: Array.isArray(p.manifest.categories) ? p.manifest.categories : [], - authors: Array.isArray(p.manifest.authors) - ? p.manifest.authors.map(x => (typeof x === 'string' ? x : (x && x.name) || '')).filter(Boolean) - : [], - enabled: !!p.enabled, - hasMain: !!p.manifest.main, - hasRendererPreload: !!p.manifest.rendererPreload, - dir: p.dir - })); - } - - // Fast discovery that does not activate plugins; always shows disabled items - discoverPlugins() { - const out = []; - for (const root of this.getPluginDirs()) { - let entries = []; - try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; } - for (const ent of entries) { - if (!ent.isDirectory()) continue; - const dir = path.join(root, ent.name); - const manifestPath = path.join(dir, 'plugin.json'); - try { - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - const cats = manifest.categories; - const categories = typeof cats === 'string' ? [cats] : Array.isArray(cats) ? cats.filter(x => typeof x === 'string') : []; - const au = manifest.authors; - const authors = typeof au === 'string' - ? [au] - : Array.isArray(au) - ? au.map(x => (typeof x === 'string' ? x : (x && x.name) || null)).filter(Boolean) - : []; - out.push({ - id: manifest.id || ent.name, - name: manifest.name || ent.name, - version: manifest.version || '0.0.0', - description: manifest.description || '', - categories, - authors, - enabled: manifest.enabled !== false, - hasMain: !!manifest.main, - hasRendererPreload: !!manifest.rendererPreload, - dir - }); - } catch {} - } - } - return out; - } - - async setEnabled(id, enabled) { - const p = this.plugins.find(x => x.id === id) || null; - if (!p) throw new Error('Plugin not found: ' + id); - const manifestPath = path.join(p.dir, 'plugin.json'); - let manifest; - try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch (e) { throw new Error('Manifest read failed: ' + e.message); } - manifest.enabled = !!enabled; - await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); - return true; - } - - _clearRequireCache(p) { - try { - if (p && p.mainPath) { - const k = require.resolve(p.mainPath); - if (require.cache[k]) delete require.cache[k]; - } - } catch {} - } - - reload(id) { - if (id) { - const p = this.plugins.find(x => x.id === id); - if (p) this._clearRequireCache(p); - } else { - for (const p of this.plugins) this._clearRequireCache(p); - } - this.loadAll(); - } -} - -module.exports = PluginManager; diff --git a/portable-data.js b/portable-data.js deleted file mode 100644 index 057dc77..0000000 --- a/portable-data.js +++ /dev/null @@ -1,381 +0,0 @@ -/** - * Portable Data Manager for Nebula Browser - * - * Handles portable user data storage for all platforms (Windows, macOS, Linux). - * Data is stored in a 'user-data' folder within the application directory, - * keeping all user data local to the compiled project. - * - * Security considerations: - * - Data is stored with restricted permissions (0700 for directories, 0600 for files on Unix) - * - Path validation prevents directory traversal attacks - * - Portable mode is enabled by default on all platforms - * - Can be disabled via NEBULA_PORTABLE=0 environment variable - */ - -const { app } = require('electron'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -class PortableDataManager { - constructor() { - this._isPortable = null; - this._portableDataPath = null; - this._initialized = false; - } - - /** - * Get the application's root directory - * Works for both development and packaged builds - */ - _getAppRootDir() { - // In packaged app, process.resourcesPath points to resources folder - // We want the parent directory (the app folder itself) - if (app.isPackaged) { - // For packaged apps: - // - Windows: path\to\app\resources -> path\to\app - // - macOS: path/to/App.app/Contents/Resources -> path/to/App.app/Contents - // - Linux: path/to/app/resources -> path/to/app - const resourcesPath = process.resourcesPath; - - if (process.platform === 'darwin') { - // On macOS, go up two levels from Resources to get to App.app parent folder - // But we want to store data inside the .app bundle's Contents folder for portability - return path.dirname(resourcesPath); // Contents folder - } else { - // Windows and Linux: go up one level from resources - return path.dirname(resourcesPath); - } - } else { - // Development mode: use __dirname (the directory containing main.js) - return __dirname; - } - } - - /** - * Check if we're running in portable mode - * Portable mode is enabled by default on all platforms. - * - * Can be disabled by setting NEBULA_PORTABLE=0 or NEBULA_PORTABLE=false - * Can specify custom path via NEBULA_PORTABLE_PATH environment variable - */ - isPortableMode() { - if (this._isPortable !== null) { - return this._isPortable; - } - - // Check if NEBULA_PORTABLE is explicitly set to disable - const portableEnv = process.env.NEBULA_PORTABLE; - if (portableEnv !== undefined) { - const isDisabled = portableEnv === '0' || - portableEnv.toLowerCase() === 'false' || - portableEnv.toLowerCase() === 'no'; - - if (isDisabled) { - this._isPortable = false; - console.log('[Portable] Portable mode disabled via NEBULA_PORTABLE environment variable'); - return false; - } - } - - // Portable mode is enabled by default on all platforms - this._isPortable = true; - return this._isPortable; - } - - /** - * Get the portable data directory path - * Uses NEBULA_PORTABLE_PATH if set, otherwise creates app-local 'user-data'. - * Linux AppDir builds prefer 'usr/user-data' to keep writable data inside AppDir. - */ - getPortableDataPath() { - if (this._portableDataPath !== null) { - return this._portableDataPath; - } - - if (!this.isPortableMode()) { - this._portableDataPath = ''; - return ''; - } - - // First, check if custom path is provided via environment variable - const customPath = process.env.NEBULA_PORTABLE_PATH; - if (customPath) { - const resolvedPath = path.resolve(customPath); - if (this._isPathSafe(resolvedPath)) { - this._portableDataPath = resolvedPath; - console.log(`[Portable] Using custom portable path: ${resolvedPath}`); - return this._portableDataPath; - } else { - console.warn('[Portable] Custom path is unsafe, using default location'); - } - } - - // Default: app-local user data - // - Windows: beside the executable - // - macOS: inside .app Contents (portable bundle) - // - Linux AppDir: /usr/user-data - // - Other Linux/dev: beside the app root - const appRoot = this._getAppRootDir(); - let dataPath = path.join(appRoot, 'user-data'); - - if (process.platform === 'linux') { - const appDirUsr = path.join(appRoot, 'usr'); - try { - if (fs.existsSync(appDirUsr) && fs.statSync(appDirUsr).isDirectory()) { - dataPath = path.join(appDirUsr, 'user-data'); - } - } catch (err) { - console.warn('[Portable] Could not inspect Linux AppDir usr path, using app root user-data'); - } - } - - // Validate the path - if (this._isPathSafe(dataPath)) { - this._portableDataPath = dataPath; - console.log(`[Portable] Using portable data path: ${dataPath}`); - } else { - console.error('[Portable] Default path is unsafe, falling back to system default'); - this._portableDataPath = ''; - } - - return this._portableDataPath; - } - - /** - * Initialize portable data directory with secure permissions - * Must be called before app.ready event - */ - initialize() { - if (this._initialized) { - return true; - } - - if (!this.isPortableMode()) { - console.log('[Portable] Not in portable mode, using default paths'); - this._initialized = true; - return true; - } - - const dataPath = this.getPortableDataPath(); - if (!dataPath) { - console.warn('[Portable] No valid portable path, using default paths'); - this._initialized = true; - return false; - } - - try { - // Create the data directory with secure permissions (owner only: rwx) - this._ensureSecureDirectory(dataPath); - - // Create subdirectories for organized storage - // Note: Don't create 'Cache', 'Cookies', 'Network' - Electron manages these internally - const subdirs = ['Local Storage', 'Session Storage', 'IndexedDB']; - for (const subdir of subdirs) { - this._ensureSecureDirectory(path.join(dataPath, subdir)); - } - - // Set Electron's user data path to our portable location - // This must be done BEFORE app.ready event - app.setPath('userData', dataPath); - app.setPath('sessionData', dataPath); - - // Also redirect cache to be portable - const cachePath = path.join(dataPath, 'Cache'); - app.setPath('cache', cachePath); - - console.log(`[Portable] User data path set to: ${dataPath}`); - console.log(`[Portable] Cache path set to: ${cachePath}`); - - this._initialized = true; - return true; - } catch (err) { - console.error('[Portable] Failed to initialize portable data:', err); - this._initialized = true; - return false; - } - } - - /** - * Get the path for a data file (bookmarks, history, etc.) - * Returns portable path if in portable mode, otherwise returns __dirname path - */ - getDataFilePath(filename) { - // Validate filename to prevent directory traversal - if (!this._isFilenameSafe(filename)) { - throw new Error(`Invalid filename: ${filename}`); - } - - if (this.isPortableMode()) { - const portablePath = this.getPortableDataPath(); - if (portablePath) { - return path.join(portablePath, filename); - } - } - - // Fallback to __dirname (project directory) for non-portable or if portable not configured - // Note: In production, you might want to use app.getPath('userData') as fallback - return null; // Return null to indicate caller should use their default path - } - - /** - * Ensure a directory exists with secure permissions - * On Unix systems (macOS, Linux), applies restricted permissions (0700) - * On Windows, creates directory with default permissions (ACLs handle security) - */ - _ensureSecureDirectory(dirPath) { - if (!fs.existsSync(dirPath)) { - if (process.platform === 'win32') { - // Windows: create directory with default permissions - // Windows ACLs handle security through inheritance - fs.mkdirSync(dirPath, { recursive: true }); - } else { - // Unix (macOS, Linux): create with restricted permissions (owner only: rwx------) - fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 }); - } - console.log(`[Portable] Created secure directory: ${dirPath}`); - } else { - // Verify and fix permissions on existing directory (Unix only) - if (process.platform !== 'win32') { - try { - const stats = fs.statSync(dirPath); - if (stats.isDirectory()) { - // Set secure permissions - fs.chmodSync(dirPath, 0o700); - } - } catch (err) { - console.warn(`[Portable] Could not verify permissions for ${dirPath}:`, err.message); - } - } - } - } - - /** - * Write a file with secure permissions - * On Unix systems, applies restricted permissions (0600) - * On Windows, writes with default permissions - */ - writeSecureFile(filePath, data) { - // Ensure parent directory exists with secure permissions - const dir = path.dirname(filePath); - this._ensureSecureDirectory(dir); - - // Write file - if (process.platform === 'win32') { - fs.writeFileSync(filePath, data); - } else { - fs.writeFileSync(filePath, data, { mode: 0o600 }); - } - } - - /** - * Async version of secure file write - * On Unix systems, applies restricted permissions (0600) - * On Windows, writes with default permissions - */ - async writeSecureFileAsync(filePath, data) { - // Ensure parent directory exists with secure permissions - const dir = path.dirname(filePath); - this._ensureSecureDirectory(dir); - - // Write file with restricted permissions (owner only: rw------- on Unix) - if (process.platform === 'win32') { - await fs.promises.writeFile(filePath, data); - } else { - await fs.promises.writeFile(filePath, data, { mode: 0o600 }); - } - } - - /** - * Validate path safety (prevent directory traversal) - * Works across Windows, macOS, and Linux - */ - _isPathSafe(testPath) { - // Resolve to absolute path - const resolved = path.resolve(testPath); - - // Check for directory traversal patterns - if (resolved.includes('..')) { - return false; - } - - // Platform-specific system path checks - if (process.platform === 'win32') { - // Windows: block system directories - const dangerousWin = [ - 'C:\\Windows', - 'C:\\Program Files', - 'C:\\Program Files (x86)', - 'C:\\ProgramData' - ]; - const resolvedLower = resolved.toLowerCase(); - for (const pattern of dangerousWin) { - if (resolvedLower.startsWith(pattern.toLowerCase())) { - return false; - } - } - } else if (process.platform === 'darwin') { - // macOS: block system directories - const dangerousMac = ['/System', '/Library', '/usr', '/bin', '/sbin', '/etc', '/var']; - for (const pattern of dangerousMac) { - if (resolved.startsWith(pattern) && !resolved.includes('.app')) { - return false; - } - } - } else { - // Linux: block system directories - const dangerousLinux = ['~root', '/etc', '/var/run', '/proc', '/sys', '/dev']; - for (const pattern of dangerousLinux) { - if (resolved.includes(pattern)) { - return false; - } - } - - const systemPaths = ['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/boot', '/lib', '/lib64']; - for (const sysPath of systemPaths) { - if (resolved.startsWith(sysPath)) { - return false; - } - } - } - - return true; - } - - /** - * Validate filename safety - */ - _isFilenameSafe(filename) { - // Check for directory traversal - if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { - return false; - } - - // Check for hidden files that might be system files - if (filename.startsWith('.') && !filename.endsWith('.json')) { - return false; - } - - return true; - } - - /** - * Get status information for debugging - */ - getStatus() { - return { - isPortable: this.isPortableMode(), - portablePath: this.getPortableDataPath(), - appRootDir: this._getAppRootDir(), - initialized: this._initialized, - platform: process.platform, - isPackaged: app.isPackaged, - envPortable: process.env.NEBULA_PORTABLE, - envPath: process.env.NEBULA_PORTABLE_PATH - }; - } -} - -// Export singleton instance -const portableDataManager = new PortableDataManager(); -module.exports = portableDataManager; diff --git a/preload.js b/preload.js deleted file mode 100644 index 8eb4684..0000000 --- a/preload.js +++ /dev/null @@ -1,549 +0,0 @@ -// preload.js - Optimized version -const { contextBridge, ipcRenderer } = require('electron'); -let pathModule; -let fsModule; -try { - pathModule = require('path'); - fsModule = require('fs'); -} catch (err) { - pathModule = null; - fsModule = null; -} - -// BrowserView tab id (desktop mode) injected via additionalArguments -let nebulaTabId = null; -try { - const arg = (process?.argv || []).find(a => typeof a === 'string' && a.startsWith('--nebula-tab-id=')); - if (arg) nebulaTabId = arg.split('=')[1] || null; -} catch {} - -// ============================================================================= -// GAMEPAD HANDLER - Steam Deck / SteamOS Support -// ============================================================================= -// This is CRITICAL for Steam Deck Game Mode: Steam only stops applying -// Desktop mouse emulation when the app actively reads controller input. -// By continuously polling navigator.getGamepads(), Steam recognizes that -// the app is consuming gamepad events and backs off the mouse emulation layer. -// ============================================================================= - -const gamepadState = { - initialized: false, - gamepads: {}, - connectedCount: 0, - activeGamepadIndex: null, - rafId: null, - buttonStates: {}, - listeners: { connect: [], disconnect: [], button: [], axis: [], input: [] }, -}; - -const GAMEPAD_CONFIG = { - STICK_DEADZONE: 0.15, - DEBUG: false, -}; - -function gamepadLog(...args) { - if (GAMEPAD_CONFIG.DEBUG) { - console.log('[NebulaGamepad]', ...args); - } -} - -function initGamepadHandler() { - if (gamepadState.initialized) return; - - if (typeof navigator === 'undefined' || !navigator.getGamepads) { - console.warn('[NebulaGamepad] Gamepad API not available'); - return; - } - - gamepadLog('Initializing gamepad handler'); - - window.addEventListener('gamepadconnected', handleGamepadConnected); - window.addEventListener('gamepaddisconnected', handleGamepadDisconnected); - - // Initial scan for already-connected gamepads - scanGamepads(); - - // Start polling loop - this is what tells Steam we're consuming gamepad input - startGamepadPolling(); - - gamepadState.initialized = true; - console.log('[NebulaGamepad] Gamepad handler initialized - Steam will see controller input being consumed'); -} - -function handleGamepadConnected(event) { - const gamepad = event.gamepad; - gamepadLog('Gamepad connected:', gamepad.index, gamepad.id); - - gamepadState.gamepads[gamepad.index] = { - id: gamepad.id, - index: gamepad.index, - connected: true, - mapping: gamepad.mapping, - timestamp: Date.now(), - }; - gamepadState.connectedCount++; - - if (gamepadState.activeGamepadIndex === null) { - gamepadState.activeGamepadIndex = gamepad.index; - } - - gamepadState.buttonStates[gamepad.index] = {}; - emitGamepadEvent('connect', { gamepad, index: gamepad.index, id: gamepad.id }); -} - -function handleGamepadDisconnected(event) { - const gamepad = event.gamepad; - gamepadLog('Gamepad disconnected:', gamepad.index, gamepad.id); - - if (gamepadState.gamepads[gamepad.index]) { - delete gamepadState.gamepads[gamepad.index]; - gamepadState.connectedCount--; - } - - delete gamepadState.buttonStates[gamepad.index]; - - if (gamepadState.activeGamepadIndex === gamepad.index) { - gamepadState.activeGamepadIndex = null; - const gamepads = navigator.getGamepads(); - for (let i = 0; i < gamepads.length; i++) { - if (gamepads[i]) { - gamepadState.activeGamepadIndex = i; - break; - } - } - } - - emitGamepadEvent('disconnect', { index: gamepad.index, id: gamepad.id }); -} - -function scanGamepads() { - const gamepads = navigator.getGamepads(); - for (let i = 0; i < gamepads.length; i++) { - const gamepad = gamepads[i]; - if (gamepad && !gamepadState.gamepads[gamepad.index]) { - gamepadLog('Found pre-connected gamepad:', gamepad.index, gamepad.id); - gamepadState.gamepads[gamepad.index] = { - id: gamepad.id, - index: gamepad.index, - connected: true, - mapping: gamepad.mapping, - timestamp: Date.now(), - }; - gamepadState.connectedCount++; - if (gamepadState.activeGamepadIndex === null) { - gamepadState.activeGamepadIndex = gamepad.index; - } - gamepadState.buttonStates[gamepad.index] = {}; - } - } -} - -function startGamepadPolling() { - if (gamepadState.rafId !== null) return; - - function pollLoop(timestamp) { - // CRITICAL: This call to getGamepads() tells Steam we're consuming gamepad input - const gamepads = navigator.getGamepads(); - - for (let i = 0; i < gamepads.length; i++) { - const gamepad = gamepads[i]; - if (gamepad) { - processGamepadInput(gamepad); - } - } - - // Periodic scan for newly connected gamepads - if (timestamp % 1000 < 20) { - scanGamepads(); - } - - gamepadState.rafId = requestAnimationFrame(pollLoop); - } - - gamepadState.rafId = requestAnimationFrame(pollLoop); - gamepadLog('Started gamepad polling'); -} - -function processGamepadInput(gamepad) { - const index = gamepad.index; - const buttonState = gamepadState.buttonStates[index] || {}; - let hasInput = false; - - // Process buttons - for (let i = 0; i < gamepad.buttons.length; i++) { - const button = gamepad.buttons[i]; - const wasPressed = buttonState[`b${i}`] || false; - const isPressed = button.pressed || button.value > 0.5; - - if (isPressed !== wasPressed) { - buttonState[`b${i}`] = isPressed; - hasInput = true; - emitGamepadEvent('button', { gamepad, index, button: i, pressed: isPressed, value: button.value }); - } - } - - // Process axes - for (let i = 0; i < gamepad.axes.length; i++) { - const value = gamepad.axes[i]; - const prevValue = buttonState[`a${i}`] || 0; - - if (Math.abs(value - prevValue) > 0.01) { - buttonState[`a${i}`] = value; - if (Math.abs(value) > GAMEPAD_CONFIG.STICK_DEADZONE) { - hasInput = true; - emitGamepadEvent('axis', { gamepad, index, axis: i, value }); - } - } - } - - gamepadState.buttonStates[index] = buttonState; - - if (hasInput) { - emitGamepadEvent('input', { gamepad, index }); - } -} - -function emitGamepadEvent(type, data) { - // Dispatch as CustomEvent for renderer scripts to listen to - try { - window.dispatchEvent(new CustomEvent(`nebula-gamepad-${type}`, { detail: data })); - } catch (err) { - // Ignore errors if CustomEvent isn't available - } -} - -function getActiveGamepad() { - if (gamepadState.activeGamepadIndex === null) return null; - const gamepads = navigator.getGamepads(); - return gamepads[gamepadState.activeGamepadIndex] || null; -} - -function getConnectedGamepads() { - const gamepads = navigator.getGamepads(); - return Array.from(gamepads).filter(gp => gp !== null); -} - -// Cleanup on page unload -window.addEventListener('beforeunload', () => { - if (gamepadState.rafId !== null) { - cancelAnimationFrame(gamepadState.rafId); - gamepadState.rafId = null; - } -}); - -// ============================================================================= -// EARLY GAMEPAD INITIALIZATION - Critical for Steam Deck -// ============================================================================= -// Initialize gamepad polling as EARLY as possible to signal Steam Input -// that this app handles controller input natively. This MUST happen before -// Steam decides to apply mouse/keyboard emulation. -// -// We try to initialize immediately when preload runs, not waiting for DOMContentLoaded, -// because Steam's input layer makes decisions very early in the process lifecycle. -// ============================================================================= - -// Try immediate initialization (works in most Electron contexts) -try { - if (typeof navigator !== 'undefined' && navigator.getGamepads) { - // Start polling immediately - this is the key signal to Steam - initGamepadHandler(); - console.log('[NebulaGamepad] Early initialization successful - Steam should recognize controller input'); - } -} catch (e) { - // Will retry on DOMContentLoaded - console.log('[NebulaGamepad] Early init deferred, will retry on DOM ready'); -} - -// ============================================================================= -// DOM READY & INITIALIZATION -// ============================================================================= - -// Cache DOM references for performance -let domReady = false; -window.addEventListener('DOMContentLoaded', () => { - domReady = true; - console.log("Browser UI loaded."); - - // Re-initialize gamepad handler if early init failed - if (!gamepadState.initialized) { - initGamepadHandler(); - } -}); - -// Optimized API exposure with error handling and caching -const electronAPI = { - send: (ch, ...args) => { - try { - return ipcRenderer.send(ch, ...args); - } catch (err) { - console.error('IPC send error:', err); - } - }, - // Send message to embedding page (webview host) or to BrowserView host - sendToHost: (ch, ...args) => { - try { - // If running in BrowserView context, ALWAYS use browserview-host-message - if (nebulaTabId) { - return ipcRenderer.send('browserview-host-message', { tabId: nebulaTabId, channel: ch, args }); - } - // Otherwise try ipcRenderer.sendToHost (for webview contexts) - if (typeof ipcRenderer.sendToHost === 'function') { - return ipcRenderer.sendToHost(ch, ...args); - } - // Final fallback - return ipcRenderer.send(ch, ...args); - } catch (err) { - console.error('IPC sendToHost error:', err); - } - }, - invoke: (ch, ...args) => { - try { - return ipcRenderer.invoke(ch, ...args); - } catch (err) { - console.error('IPC invoke error:', err); - return Promise.reject(err); - } - }, - on: (ch, fn) => { - try { - return ipcRenderer.on(ch, (e, ...args) => fn(...args)); - } catch (err) { - console.error('IPC on error:', err); - } - }, - // Add removeListener for cleanup - removeListener: (ch, fn) => { - try { - return ipcRenderer.removeListener(ch, fn); - } catch (err) { - console.error('IPC removeListener error:', err); - } - }, - toggleDevTools: () => { - try { - return ipcRenderer.invoke('open-devtools'); - } catch (err) { - console.error('IPC open-devtools error:', err); - return Promise.reject(err); - } - }, - openLocalFile: async () => { - try { - return await ipcRenderer.invoke('show-open-file-dialog'); - } catch (err) { - console.error('IPC openLocalFile error:', err); - return null; - } - }, - showContextMenu: (params) => { - try { - return ipcRenderer.invoke('show-context-menu', params); - } catch (err) { - console.error('IPC showContextMenu error:', err); - } - }, - saveImageToDisk: async (suggestedName, dataUrl) => ipcRenderer.invoke('save-image-from-dataurl', { suggestedName, dataUrl }), - 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 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; -const CACHE_DURATION = 5000; // 5 seconds - -const bookmarksAPI = { - load: async () => { - const now = Date.now(); - if (bookmarksCache && (now - bookmarksCacheTime) < CACHE_DURATION) { - return bookmarksCache; - } - try { - bookmarksCache = await ipcRenderer.invoke('load-bookmarks'); - bookmarksCacheTime = now; - return bookmarksCache; - } catch (err) { - console.error('Bookmarks load error:', err); - return []; - } - }, - save: async (data) => { - try { - bookmarksCache = data; // Update cache immediately - bookmarksCacheTime = Date.now(); - return await ipcRenderer.invoke('save-bookmarks', data); - } catch (err) { - console.error('Bookmarks save error:', err); - return false; - } - } -}; - -// Expose APIs to main world -contextBridge.exposeInMainWorld('electronAPI', electronAPI); -contextBridge.exposeInMainWorld('bookmarksAPI', bookmarksAPI); - -// Gamepad API - Access to the gamepad handler running in the preload context -// The handler actively polls navigator.getGamepads() to signal to Steam that -// the app is consuming controller input (prevents mouse emulation on Steam Deck) -contextBridge.exposeInMainWorld('gamepadAPI', { - // Check if gamepad handler is initialized - isAvailable: () => gamepadState.initialized, - - // Check if any gamepad is connected - isConnected: () => gamepadState.connectedCount > 0, - - // Get connected gamepads info - getConnected: () => { - const gamepads = getConnectedGamepads(); - return gamepads.map(gp => ({ - id: gp.id, - index: gp.index, - mapping: gp.mapping, - buttons: gp.buttons.length, - axes: gp.axes.length, - })); - }, - - // Get the active gamepad's current state - getActive: () => { - const gp = getActiveGamepad(); - if (!gp) return null; - return { - id: gp.id, - index: gp.index, - mapping: gp.mapping, - buttons: Array.from(gp.buttons).map((b, i) => ({ index: i, pressed: b.pressed, value: b.value })), - axes: Array.from(gp.axes), - }; - }, - - // Enable debug mode - setDebug: (enabled) => { - GAMEPAD_CONFIG.DEBUG = !!enabled; - }, - - // Get handler state for debugging - getState: () => ({ - initialized: gamepadState.initialized, - connectedCount: gamepadState.connectedCount, - activeGamepadIndex: gamepadState.activeGamepadIndex, - isPolling: gamepadState.rafId !== null, - }), -}); - -// Minimal about API for settings page -contextBridge.exposeInMainWorld('aboutAPI', { - getInfo: () => ipcRenderer.invoke('get-about-info') -}); - -// Big Picture Mode API - Steam Deck / Console UI -// Note: Big Picture Mode now opens in the main window (not a separate window) to keep resources low -// and prevent SteamOS from creating desktop mode alongside when auto-launching. -contextBridge.exposeInMainWorld('bigPictureAPI', { - // Get screen info to determine if Big Picture Mode is recommended - getScreenInfo: () => ipcRenderer.invoke('get-screen-info'), - // Check if device is likely a Steam Deck or handheld - isSuggested: () => ipcRenderer.invoke('is-bigpicture-suggested'), - // Check if currently in Big Picture Mode - isActive: () => ipcRenderer.invoke('is-in-bigpicture'), - // Launch Big Picture Mode (navigates main window to Big Picture UI) - launch: () => ipcRenderer.invoke('launch-bigpicture'), - // Exit Big Picture Mode (navigates main window back to desktop UI) - exit: () => ipcRenderer.invoke('exit-bigpicture'), - // Navigate to URL (from Big Picture Mode) - navigate: (url) => ipcRenderer.send('bigpicture-navigate', url), - // Send input event to a webview (for virtual cursor clicks) - sendInputEvent: (webContentsId, inputEvent) => - ipcRenderer.invoke('webview-send-input-event', { webContentsId, inputEvent }) -}); - -// Relay context-menu commands from main to active renderer context (open new tabs etc.) -ipcRenderer.on('context-menu-command', (event, payload) => { - window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload })); -}); - -// Downloads API exposed to renderer -contextBridge.exposeInMainWorld('downloadsAPI', { - list: () => ipcRenderer.invoke('downloads-get-all'), - action: (id, action) => ipcRenderer.invoke('downloads-action', { id, action }), - clearCompleted: () => ipcRenderer.invoke('downloads-clear-completed'), - onStarted: (handler) => ipcRenderer.on('downloads-started', (_e, payload) => handler(payload)), - onUpdated: (handler) => ipcRenderer.on('downloads-updated', (_e, payload) => handler(payload)), - onDone: (handler) => ipcRenderer.on('downloads-done', (_e, payload) => handler(payload)), - onCleared: (handler) => ipcRenderer.on('downloads-cleared', handler), - onScanStarted: (handler) => ipcRenderer.on('downloads-scan-started', (_e, payload) => handler(payload)), - onScanResult: (handler) => ipcRenderer.on('downloads-scan-result', (_e, payload) => handler(payload)) -}); - -// Auto-Updater API exposed to renderer -contextBridge.exposeInMainWorld('updaterAPI', { - checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), - downloadUpdate: () => ipcRenderer.invoke('download-update'), - installUpdate: () => ipcRenderer.invoke('install-update'), - getAppVersion: () => ipcRenderer.invoke('get-app-version'), - onUpdateStatus: (handler) => ipcRenderer.on('update-status', (_e, payload) => handler(payload)) -}); - -// First-Time Setup API -contextBridge.exposeInMainWorld('api', { - // Check if this is the first run - isFirstRun: () => ipcRenderer.invoke('is-first-run'), - // Get all available themes - getAllThemes: () => ipcRenderer.invoke('get-all-themes'), - // Apply a theme - applyTheme: (themeId) => ipcRenderer.invoke('apply-theme', themeId), - // Check if Nebula is the default browser - isDefaultBrowser: () => ipcRenderer.invoke('is-default-browser'), - // Set Nebula as the default browser - setAsDefaultBrowser: () => ipcRenderer.invoke('set-as-default-browser'), - // Open OS default browser settings - openDefaultBrowserSettings: () => ipcRenderer.invoke('open-default-browser-settings'), - // Complete first-run setup - completeFirstRun: (data) => ipcRenderer.invoke('complete-first-run', data), - // Get first-run data - getFirstRunData: () => ipcRenderer.invoke('get-first-run-data') -}); - -// ---------------------------------------- -// Plugin renderer preloads -// ---------------------------------------- -// We request a list of absolute file paths from main and require() them here. -// Each file can optionally call contextBridge.exposeInMainWorld to add APIs. -(async () => { - try { - const preloads = await ipcRenderer.invoke('plugins-get-renderer-preloads'); - if (Array.isArray(preloads)) { - for (const p of preloads) { - try { - // eslint-disable-next-line global-require, import/no-dynamic-require - require(p); - } catch (e) { - console.error('[Plugins] Failed to load renderer preload:', p, e); - } - } - } - } catch (e) { - console.warn('[Plugins] No renderer preloads:', e); - } -})(); \ No newline at end of file diff --git a/renderer/404.css b/renderer/404.css deleted file mode 100644 index 5a08c72..0000000 --- a/renderer/404.css +++ /dev/null @@ -1,81 +0,0 @@ -/* Load InterVariable */ -@font-face { - font-family: 'InterVariable'; - src: url('../assets/images/fonts/InterVariable.ttf') format('truetype'); - font-weight: 100 900; - font-display: swap; -} - -:root { - --bg: #121418; - --dark-blue: #0B1C2B; - --dark-purple: #1B1035; - --primary: #7B2EFF; - --accent: #00C6FF; - --text: #E0E0E0; -} - -body { - background-color: var(--bg); - color: var(--text); - font-family: 'InterVariable', sans-serif; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - margin: 0; -} - -.container { - text-align: center; - background-color: var(--dark-purple); - padding: 2rem; - border-radius: 20px; - box-shadow: 0 0 12px rgba(0, 0, 0, 0.4); - width: 90%; - max-width: 500px; -} - -.error-icon { - font-size: 4rem; - margin-bottom: 1rem; - color: var(--accent); -} - -h1 { - margin: 0; - font-size: 1.8rem; - color: var(--primary); -} - -p { - margin: 0.5rem 0; - color: var(--text); -} - -.url-line { - font-style: italic; - color: var(--text); -} - -.actions { - margin-top: 1.5rem; - display: flex; - justify-content: center; - gap: 1rem; -} - -button { - padding: 0.6rem 1.2rem; - background-color: var(--primary); - color: var(--text); - border: none; - border-radius: 8px; - font-size: 1rem; - cursor: pointer; - transition: background 0.2s ease; -} - -button:hover { - background-color: var(--accent); -} diff --git a/renderer/404.html b/renderer/404.html deleted file mode 100644 index 5a1f1ad..0000000 --- a/renderer/404.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - -404 - Page Not Found - - - - -
-

- - Page Not Found 404 -

-

The page you're looking for doesn't exist or has been moved. You've warped into an unknown sector of the web.

-
-
    -
  • The URL might be typed incorrectly.
  • -
  • The page may have been removed or relocated.
  • -
  • The link you followed could be outdated or broken.
  • -
  • Try going back or navigate to the home page.
  • -
-
- - - -
-
Nebula Navigation Error
-
- - - diff --git a/renderer/bigpicture.css b/renderer/bigpicture.css deleted file mode 100644 index 1c185fb..0000000 --- a/renderer/bigpicture.css +++ /dev/null @@ -1,1966 +0,0 @@ -/* Big Picture Mode - Steam Deck / Console-style UI */ -/* Optimized for 1280x800 (Steam Deck) and controller navigation */ - -@font-face { - font-family: 'InterVariable'; - src: url('../assets/images/fonts/InterVariable.ttf') format('truetype'); - font-weight: 100 900; - font-display: swap; -} - -/* CSS Variables for theming */ -:root { - --bp-bg: #0a0a0f; - --bp-surface: #14141f; - --bp-surface-hover: #1e1e2d; - --bp-surface-active: #28283d; - --bp-primary: #7B2EFF; - --bp-primary-glow: rgba(123, 46, 255, 0.4); - --bp-accent: #00C6FF; - --bp-accent-glow: rgba(0, 198, 255, 0.3); - --bp-text: #ffffff; - --bp-text-muted: #8888a0; - --bp-text-dim: #555570; - --bp-border: #2a2a40; - --bp-success: #4ade80; - --bp-warning: #fbbf24; - --bp-danger: #ef4444; - - /* Focus ring for controller navigation */ - --bp-focus-ring: 0 0 0 3px var(--bp-primary), 0 0 30px var(--bp-primary-glow); - --bp-focus-ring-accent: 0 0 0 3px var(--bp-accent), 0 0 30px var(--bp-accent-glow); - - /* Spacing scaled for touch/controller */ - --bp-spacing-xs: 8px; - --bp-spacing-sm: 12px; - --bp-spacing-md: 20px; - --bp-spacing-lg: 32px; - --bp-spacing-xl: 48px; - - /* Border radius */ - --bp-radius-sm: 8px; - --bp-radius-md: 12px; - --bp-radius-lg: 16px; - --bp-radius-xl: 24px; - - /* Animation timing */ - --bp-transition-fast: 150ms ease; - --bp-transition-normal: 250ms ease; - --bp-transition-slow: 400ms ease; -} - -/* Base reset */ -*, *::before, *::after { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, body { - width: 100%; - height: 100%; - overflow: hidden; - font-family: 'InterVariable', 'Segoe UI', system-ui, -apple-system, sans-serif; - font-size: 18px; /* Larger base for readability on TV/handheld */ - line-height: 1.5; - color: var(--bp-text); - background: var(--bp-bg); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - cursor: none; /* Hide cursor for controller-only mode */ -} - -/* Show cursor when mouse moves */ -body.mouse-active { - cursor: auto; -} - -/* Main container */ -.bp-container { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; -} - -/* Animated background */ -.bp-background { - position: absolute; - inset: 0; - z-index: 0; - pointer-events: none; - overflow: hidden; -} - -.bg-gradient { - position: absolute; - inset: 0; - background: - radial-gradient(ellipse 120% 80% at 20% 10%, rgba(123, 46, 255, 0.15) 0%, transparent 50%), - radial-gradient(ellipse 100% 60% at 80% 90%, rgba(0, 198, 255, 0.1) 0%, transparent 40%), - linear-gradient(180deg, var(--bp-bg) 0%, #0d0d15 100%); -} - -.bg-particles { - position: absolute; - inset: 0; - background-image: - radial-gradient(2px 2px at 20% 30%, rgba(255,255,255,0.15), transparent), - radial-gradient(2px 2px at 40% 70%, rgba(255,255,255,0.1), transparent), - radial-gradient(1px 1px at 60% 20%, rgba(255,255,255,0.12), transparent), - radial-gradient(2px 2px at 80% 50%, rgba(255,255,255,0.08), transparent); - animation: particles-drift 60s linear infinite; -} - -@keyframes particles-drift { - 0% { transform: translateY(0); } - 100% { transform: translateY(-100px); } -} - -.bg-glow { - position: absolute; - width: 600px; - height: 600px; - border-radius: 50%; - background: radial-gradient(circle, var(--bp-primary-glow) 0%, transparent 70%); - filter: blur(80px); - opacity: 0.5; - animation: glow-pulse 8s ease-in-out infinite alternate; - top: -200px; - left: -100px; -} - -@keyframes glow-pulse { - 0% { transform: scale(1); opacity: 0.3; } - 100% { transform: scale(1.2); opacity: 0.5; } -} - -/* Header */ -.bp-header { - position: relative; - z-index: 100; - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--bp-spacing-sm) var(--bp-spacing-lg); - background: linear-gradient(180deg, rgba(10,10,15,0.95) 0%, rgba(10,10,15,0.8) 100%); - backdrop-filter: blur(20px); - border-bottom: 1px solid var(--bp-border); -} - -.header-left { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); -} - -.bp-logo { - width: 40px; - height: 40px; - filter: drop-shadow(0 0 10px var(--bp-primary-glow)); -} - -.bp-title { - font-size: 1.4rem; - font-weight: 700; - background: linear-gradient(135deg, var(--bp-text) 0%, var(--bp-accent) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.header-center { - position: absolute; - left: 50%; - transform: translateX(-50%); -} - -.clock-widget { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; -} - -.clock-widget .time { - font-size: 1.5rem; - font-weight: 600; - letter-spacing: 1px; -} - -.clock-widget .date { - font-size: 0.8rem; - color: var(--bp-text-muted); - text-transform: uppercase; - letter-spacing: 2px; -} - -.header-right { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); -} - -.status-icons { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); -} - -.status-icon { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: var(--bp-radius-sm); - background: var(--bp-surface); - color: var(--bp-text-muted); -} - -.status-icon .material-symbols-outlined { - font-size: 20px; -} - -.bp-exit-btn { - display: flex; - align-items: center; - gap: var(--bp-spacing-xs); - padding: var(--bp-spacing-xs) var(--bp-spacing-md); - background: var(--bp-surface); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-md); - color: var(--bp-text); - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.bp-exit-btn:hover, -.bp-exit-btn:focus, -.bp-exit-btn.focused { - background: var(--bp-surface-hover); - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - outline: none; -} - -.bp-exit-btn .material-symbols-outlined { - font-size: 18px; -} - -/* Main layout */ -.bp-main { - flex: 1; - display: flex; - overflow: hidden; - position: relative; - z-index: 1; -} - -/* Sidebar navigation */ -.bp-sidebar { - width: 220px; - min-width: 220px; - display: flex; - flex-direction: column; - justify-content: space-between; - padding: var(--bp-spacing-md); - background: rgba(20, 20, 31, 0.6); - backdrop-filter: blur(10px); - border-right: 1px solid var(--bp-border); -} - -.nav-items { - display: flex; - flex-direction: column; - gap: var(--bp-spacing-xs); -} - -.nav-item { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - padding: var(--bp-spacing-md) var(--bp-spacing-md); - background: transparent; - border: 2px solid transparent; - border-radius: var(--bp-radius-lg); - color: var(--bp-text-muted); - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all var(--bp-transition-fast); - text-align: left; - width: 100%; -} - -.nav-item .material-symbols-outlined { - font-size: 28px; - transition: transform var(--bp-transition-fast); -} - -.nav-item:hover { - background: var(--bp-surface); - color: var(--bp-text); -} - -.nav-item:focus, -.nav-item.focused { - outline: none; - background: var(--bp-surface-hover); - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - color: var(--bp-text); -} - -.nav-item:focus .material-symbols-outlined, -.nav-item.focused .material-symbols-outlined { - transform: scale(1.1); -} - -.nav-item.active { - background: linear-gradient(135deg, var(--bp-primary) 0%, #5a1fd4 100%); - border-color: var(--bp-primary); - color: var(--bp-text); - box-shadow: 0 4px 20px var(--bp-primary-glow); -} - -.nav-item.active .material-symbols-outlined { - color: var(--bp-text); -} - -/* Webview container for browsing */ -.webview-container { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - background: var(--bp-bg); - z-index: 2; -} - -.webview-container.hidden { - display: none; - pointer-events: none; -} - -.webview-container webview { - width: 100%; - height: 100%; - border: none; -} - -/* Content area */ -.bp-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: var(--bp-spacing-lg); - scroll-behavior: smooth; - position: relative; -} - -/* Custom scrollbar */ -.bp-content::-webkit-scrollbar { - width: 8px; -} - -.bp-content::-webkit-scrollbar-track { - background: var(--bp-surface); - border-radius: 4px; -} - -.bp-content::-webkit-scrollbar-thumb { - background: var(--bp-border); - border-radius: 4px; -} - -.bp-content::-webkit-scrollbar-thumb:hover { - background: var(--bp-primary); -} - -/* Sections */ -.bp-section { - display: none; - animation: fadeIn 0.3s ease; -} - -.bp-section.active { - display: flex; - flex-direction: column; -} - -/* Special layout for settings section */ -#section-settings.active { - display: flex; - flex-direction: column; - height: 100%; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.section-header { - margin-bottom: var(--bp-spacing-lg); -} - -.section-title { - font-size: 2rem; - font-weight: 700; - margin-bottom: var(--bp-spacing-xs); - background: linear-gradient(135deg, var(--bp-text) 0%, var(--bp-accent) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.section-subtitle { - font-size: 1rem; - color: var(--bp-text-muted); -} - -/* Section action buttons */ -.section-actions { - display: flex; - gap: var(--bp-spacing-md); - margin-bottom: var(--bp-spacing-lg); -} - -.action-btn { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); - padding: var(--bp-spacing-sm) var(--bp-spacing-md); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-md); - color: var(--bp-text-muted); - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.action-btn:hover { - background: var(--bp-surface-hover); - color: var(--bp-text); - border-color: var(--bp-text-dim); -} - -.action-btn:focus, -.action-btn.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - color: var(--bp-text); -} - -.action-btn .material-symbols-outlined { - font-size: 20px; -} - -.action-btn.danger:hover, -.action-btn.danger:focus, -.action-btn.danger.focused { - border-color: var(--bp-danger); - color: var(--bp-danger); -} - -.subsection-title { - font-size: 1.1rem; - font-weight: 600; - color: var(--bp-text-muted); - margin-bottom: var(--bp-spacing-md); - text-transform: uppercase; - letter-spacing: 1px; -} - -/* Search card */ -.search-card { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - padding: var(--bp-spacing-md) var(--bp-spacing-lg); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-xl); - margin-bottom: var(--bp-spacing-xl); - transition: all var(--bp-transition-fast); - cursor: pointer; -} - -.search-card:hover { - background: var(--bp-surface-hover); - border-color: var(--bp-text-dim); -} - -.search-card:focus, -.search-card:focus-within, -.search-card.focused { - outline: none; - border-color: var(--bp-accent); - box-shadow: var(--bp-focus-ring-accent); -} - -.search-icon { - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - border-radius: var(--bp-radius-md); - background: linear-gradient(135deg, var(--bp-accent) 0%, var(--bp-primary) 100%); -} - -.search-icon .material-symbols-outlined { - font-size: 28px; - color: var(--bp-text); -} - -.search-input { - flex: 1; - background: transparent; - border: none; - outline: none; - font-size: 1.2rem; - color: var(--bp-text); - caret-color: var(--bp-accent); -} - -.search-input::placeholder { - color: var(--bp-text-dim); -} - -.search-hint { - display: flex; - align-items: center; - gap: var(--bp-spacing-xs); - color: var(--bp-text-muted); - font-size: 0.9rem; -} - -/* Key hints */ -.key-hint { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 28px; - height: 28px; - padding: 0 8px; - background: var(--bp-primary); - border-radius: var(--bp-radius-sm); - font-size: 0.85rem; - font-weight: 700; - color: var(--bp-text); -} - -/* Tile grid */ -.tile-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: var(--bp-spacing-md); - margin-bottom: var(--bp-spacing-xl); -} - -.tile-grid.large { - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); -} - -.tile { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - aspect-ratio: 16 / 10; - padding: var(--bp-spacing-md); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-lg); - cursor: pointer; - transition: all var(--bp-transition-fast); - overflow: hidden; - position: relative; -} - -.tile::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(135deg, transparent 0%, rgba(123, 46, 255, 0.1) 100%); - opacity: 0; - transition: opacity var(--bp-transition-fast); -} - -.tile:hover { - background: var(--bp-surface-hover); - transform: scale(1.02); -} - -.tile:hover::before { - opacity: 1; -} - -.tile:focus, -.tile.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - transform: scale(1.02); -} - -.tile:focus::before, -.tile.focused::before { - opacity: 1; -} - -.tile-icon { - width: 64px; - height: 64px; - border-radius: var(--bp-radius-md); - background: var(--bp-surface-active); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: var(--bp-spacing-sm); - overflow: hidden; -} - -.tile-icon img, -.tile-favicon { - width: 40px; - height: 40px; - object-fit: contain; -} - -.tile-icon .material-symbols-outlined { - font-size: 36px; - color: var(--bp-accent); -} - -/* Bookmark tile specific styles */ -.bookmark-tile .tile-icon { - background: linear-gradient(135deg, var(--bp-surface-active) 0%, var(--bp-surface-hover) 100%); -} - -.tile-title { - font-size: 1rem; - font-weight: 600; - color: var(--bp-text); - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; -} - -.tile-url { - font-size: 0.8rem; - color: var(--bp-text-dim); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; -} - -/* Add tile button */ -.tile.add-tile { - border-style: dashed; - border-color: var(--bp-text-dim); -} - -.tile.add-tile .material-symbols-outlined { - font-size: 48px; - color: var(--bp-text-dim); -} - -.tile.add-tile:hover, -.tile.add-tile:focus, -.tile.add-tile.focused { - border-color: var(--bp-accent); - border-style: solid; -} - -.tile.add-tile:hover .material-symbols-outlined, -.tile.add-tile:focus .material-symbols-outlined, -.tile.add-tile.focused .material-symbols-outlined { - color: var(--bp-accent); -} - -/* Horizontal scroll */ -.horizontal-scroll { - display: flex; - gap: var(--bp-spacing-md); - overflow-x: auto; - padding-bottom: var(--bp-spacing-md); - scroll-snap-type: x mandatory; -} - -.horizontal-scroll::-webkit-scrollbar { - height: 6px; -} - -.horizontal-scroll::-webkit-scrollbar-track { - background: var(--bp-surface); - border-radius: 3px; -} - -.horizontal-scroll::-webkit-scrollbar-thumb { - background: var(--bp-border); - border-radius: 3px; -} - -.scroll-card { - flex: 0 0 280px; - scroll-snap-align: start; - display: flex; - flex-direction: column; - padding: var(--bp-spacing-md); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-lg); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.scroll-card:hover { - background: var(--bp-surface-hover); - transform: translateY(-4px); -} - -.scroll-card:focus, -.scroll-card.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - transform: translateY(-4px); -} - -.scroll-card-preview { - width: 100%; - aspect-ratio: 16 / 9; - background: var(--bp-surface-active); - border-radius: var(--bp-radius-sm); - margin-bottom: var(--bp-spacing-sm); - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; -} - -.scroll-card-preview img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.scroll-card-favicon { - width: 64px; - height: 64px; - object-fit: contain; -} - -.scroll-card-icon { - width: 100%; - height: 100%; -} - -.scroll-card-title { - font-size: 1rem; - font-weight: 600; - color: var(--bp-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.scroll-card-meta { - font-size: 0.85rem; - color: var(--bp-text-muted); -} - -/* List container */ -.list-container { - display: flex; - flex-direction: column; - gap: var(--bp-spacing-sm); -} - -.list-item { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - padding: var(--bp-spacing-md) var(--bp-spacing-lg); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-md); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.list-item:hover { - background: var(--bp-surface-hover); -} - -.list-item:focus, -.list-item.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); -} - -.list-item-icon { - width: 48px; - height: 48px; - border-radius: var(--bp-radius-sm); - background: var(--bp-surface-active); - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - flex-shrink: 0; -} - -.list-item-icon img, -.list-item-favicon { - width: 32px; - height: 32px; - object-fit: contain; -} - -.list-item-icon .material-symbols-outlined { - font-size: 24px; - color: var(--bp-text-muted); -} - -/* History item specific styles */ -.history-item:hover .list-item-icon { - background: var(--bp-surface-active); -} - -.list-item-content { - flex: 1; - min-width: 0; -} - -.list-item-title { - font-size: 1rem; - font-weight: 600; - color: var(--bp-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.list-item-meta { - font-size: 0.85rem; - color: var(--bp-text-muted); -} - -.list-item-action { - display: flex; - align-items: center; - gap: var(--bp-spacing-xs); -} - -/* Empty state */ -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--bp-spacing-xl); - color: var(--bp-text-dim); -} - -.empty-state.compact { - padding: var(--bp-spacing-lg); -} - -.empty-state .material-symbols-outlined { - font-size: 64px; - margin-bottom: var(--bp-spacing-md); - opacity: 0.5; -} - -.empty-state p { - font-size: 1.1rem; -} - -.empty-state .empty-hint { - font-size: 0.9rem; - margin-top: var(--bp-spacing-xs); - opacity: 0.7; -} - -/* NeBot section */ -.nebot-launch { - display: flex; - justify-content: center; -} - -.nebot-card { - display: flex; - align-items: center; - gap: var(--bp-spacing-lg); - padding: var(--bp-spacing-lg) var(--bp-spacing-xl); - background: linear-gradient(135deg, var(--bp-surface) 0%, var(--bp-surface-hover) 100%); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-xl); - cursor: pointer; - transition: all var(--bp-transition-fast); - max-width: 500px; - width: 100%; -} - -.nebot-card:hover { - border-color: var(--bp-accent); - transform: scale(1.02); -} - -.nebot-card:focus, -.nebot-card.focused { - outline: none; - border-color: var(--bp-accent); - box-shadow: var(--bp-focus-ring-accent); - transform: scale(1.02); -} - -.nebot-icon { - width: 72px; - height: 72px; - border-radius: var(--bp-radius-lg); - background: linear-gradient(135deg, var(--bp-accent) 0%, var(--bp-primary) 100%); - display: flex; - align-items: center; - justify-content: center; -} - -.nebot-icon .material-symbols-outlined { - font-size: 40px; - color: var(--bp-text); -} - -.nebot-info h3 { - font-size: 1.3rem; - font-weight: 600; - color: var(--bp-text); - margin-bottom: 4px; -} - -.nebot-info p { - font-size: 0.95rem; - color: var(--bp-text-muted); -} - -.nebot-action { - margin-left: auto; -} - -/* Settings grid (legacy) */ -.settings-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: var(--bp-spacing-md); -} - -.settings-card { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--bp-spacing-sm); - padding: var(--bp-spacing-lg); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-lg); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.settings-card:hover { - background: var(--bp-surface-hover); - transform: scale(1.02); -} - -.settings-card:focus, -.settings-card.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - transform: scale(1.02); -} - -.settings-card .material-symbols-outlined { - font-size: 40px; - color: var(--bp-accent); -} - -.settings-label { - font-size: 1rem; - font-weight: 600; - color: var(--bp-text); -} - -/* New Settings UI */ -.settings-tabs { - display: flex; - gap: var(--bp-spacing-sm); - margin-bottom: var(--bp-spacing-lg); - padding-bottom: var(--bp-spacing-md); - border-bottom: 1px solid var(--bp-border); - flex-wrap: wrap; -} - -.settings-tab { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); - padding: var(--bp-spacing-sm) var(--bp-spacing-md); - background: transparent; - border: 2px solid transparent; - border-radius: var(--bp-radius-md); - color: var(--bp-text-muted); - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.settings-tab:hover { - background: var(--bp-surface); - color: var(--bp-text); -} - -.settings-tab.active { - background: var(--bp-surface); - border-color: var(--bp-primary); - color: var(--bp-text); -} - -.settings-tab:focus, -.settings-tab.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); -} - -.settings-tab.focused { - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - background: var(--bp-surface); - color: var(--bp-text); -} - -.settings-tab .material-symbols-outlined { - font-size: 24px; -} - -.settings-panels { - flex: 1; - overflow-y: auto; -} - -.settings-panel { - display: none; - animation: fadeIn var(--bp-transition-normal); -} - -.settings-panel.active { - display: block; -} - -.settings-panel-title { - font-size: 1.25rem; - font-weight: 600; - color: var(--bp-text); - margin-bottom: var(--bp-spacing-md); -} - -/* Theme Grid */ -.theme-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: var(--bp-spacing-md); -} - -.theme-card { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--bp-spacing-sm); - padding: var(--bp-spacing-md); - background: var(--bp-surface); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-lg); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.theme-card:hover { - background: var(--bp-surface-hover); - transform: translateY(-2px); -} - -.theme-card:focus, -.theme-card.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); - transform: translateY(-2px) scale(1.02); -} - -.theme-card.active { - border-color: var(--bp-accent); - box-shadow: 0 0 0 2px var(--bp-accent); -} - -.theme-preview { - width: 100%; - height: 60px; - border-radius: var(--bp-radius-sm); - box-shadow: inset 0 0 0 1px rgba(255,255,255,0.1); -} - -.theme-name { - font-size: 0.9rem; - font-weight: 500; - color: var(--bp-text); -} - -/* Settings Options */ -.settings-option { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--bp-spacing-md); - background: var(--bp-surface); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-md); - margin-bottom: var(--bp-spacing-sm); -} - -.option-info { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); -} - -.option-info > .material-symbols-outlined { - font-size: 28px; - color: var(--bp-accent); -} - -.option-text { - display: flex; - flex-direction: column; - gap: 2px; -} - -.option-label { - font-size: 1rem; - font-weight: 600; - color: var(--bp-text); -} - -.option-description { - font-size: 0.85rem; - color: var(--bp-text-muted); -} - -.option-control { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); -} - -.scale-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: var(--bp-surface-hover); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-sm); - color: var(--bp-text); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.scale-btn:hover { - background: var(--bp-primary); - border-color: var(--bp-primary); -} - -.scale-btn:focus, -.scale-btn.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); -} - -.scale-value { - min-width: 60px; - text-align: center; - font-size: 1.1rem; - font-weight: 600; - color: var(--bp-text); -} - -.action-button { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); - padding: var(--bp-spacing-sm) var(--bp-spacing-md); - background: var(--bp-surface-hover); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-md); - color: var(--bp-text); - font-size: 0.95rem; - font-weight: 500; - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.action-button:hover { - background: var(--bp-primary); - border-color: var(--bp-primary); -} - -.action-button:focus, -.action-button.focused { - outline: none; - border-color: var(--bp-primary); - box-shadow: var(--bp-focus-ring); -} - -.action-button.danger:hover { - background: #dc3545; - border-color: #dc3545; -} - -/* About Panel */ -.about-info { - display: flex; - flex-direction: column; - gap: var(--bp-spacing-lg); -} - -.about-logo { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - padding: var(--bp-spacing-md); - background: var(--bp-surface); - border-radius: var(--bp-radius-lg); -} - -.about-logo-img { - width: 64px; - height: 64px; -} - -.about-title h3 { - font-size: 1.5rem; - font-weight: 600; - color: var(--bp-text); - margin: 0; -} - -.about-title span { - font-size: 0.9rem; - color: var(--bp-text-muted); -} - -.about-details { - display: flex; - flex-direction: column; - gap: var(--bp-spacing-xs); - padding: var(--bp-spacing-md); - background: var(--bp-surface); - border-radius: var(--bp-radius-md); -} - -.about-row { - display: flex; - justify-content: space-between; - padding: var(--bp-spacing-xs) 0; - border-bottom: 1px solid var(--bp-border); -} - -.about-row:last-child { - border-bottom: none; -} - -.about-label { - color: var(--bp-text-muted); - font-size: 0.9rem; -} - -.about-value { - color: var(--bp-text); - font-weight: 500; - font-size: 0.9rem; -} - -.about-actions { - display: flex; - gap: var(--bp-spacing-md); - flex-wrap: wrap; -} - -/* Footer controller hints */ -.bp-footer { - position: relative; - z-index: 100; - padding: var(--bp-spacing-sm) var(--bp-spacing-lg); - background: linear-gradient(0deg, rgba(10,10,15,0.95) 0%, rgba(10,10,15,0.8) 100%); - backdrop-filter: blur(20px); - border-top: 1px solid var(--bp-border); -} - -.controller-hints { - display: flex; - justify-content: center; - gap: var(--bp-spacing-xl); -} - -.hint { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); - color: var(--bp-text-muted); - font-size: 0.9rem; -} - -.controller-btn { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 32px; - height: 32px; - padding: 0 8px; - background: var(--bp-surface); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-sm); - font-size: 0.85rem; - font-weight: 700; - color: var(--bp-text); -} - -.controller-btn.a-btn { - background: #107c10; - border-color: #107c10; -} - -.controller-btn.b-btn { - background: #e81123; - border-color: #e81123; -} - -.controller-btn.y-btn { - background: #ffb900; - border-color: #ffb900; - color: #000; -} - -.controller-btn .material-symbols-outlined { - font-size: 18px; -} - -/* On-screen keyboard */ -.osk-overlay { - position: fixed; - inset: 0; - z-index: 1000; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(10px); - display: flex; - align-items: flex-end; - justify-content: center; - padding-bottom: var(--bp-spacing-xl); -} - -.osk-overlay.hidden { - display: none; -} - -.osk-container { - width: 100%; - max-width: 900px; - background: var(--bp-surface); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-xl) var(--bp-radius-xl) 0 0; - padding: var(--bp-spacing-lg); -} - -.osk-title { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - margin-bottom: var(--bp-spacing-md); - color: var(--bp-accent); - font-size: 1rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; -} - -.osk-title .material-symbols-outlined { - font-size: 1.3rem; -} - -.osk-header { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - margin-bottom: var(--bp-spacing-md); -} - -.osk-input-wrapper { - flex: 1; - position: relative; - display: flex; - align-items: center; - background: var(--bp-bg); - border: 2px solid var(--bp-accent); - border-radius: var(--bp-radius-md); - box-shadow: 0 0 20px var(--bp-accent-glow); - overflow: hidden; -} - -.osk-text-input { - flex: 1; - padding: var(--bp-spacing-md) var(--bp-spacing-lg); - background: transparent; - border: none; - font-size: 1.3rem; - color: var(--bp-text); - font-weight: 500; - letter-spacing: 0.5px; - outline: none; -} - -.osk-text-input::placeholder { - color: var(--bp-text-dim); -} - -/* Blinking cursor that follows text */ -.osk-cursor { - position: absolute; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 1.5em; - background: var(--bp-accent); - border-radius: 2px; - animation: blink-cursor 1s step-end infinite; - box-shadow: 0 0 8px var(--bp-accent); - pointer-events: none; - left: var(--bp-spacing-lg); -} - -/* Hidden element to measure text width */ -.osk-text-measure { - position: absolute; - visibility: hidden; - white-space: pre; - font-size: 1.3rem; - font-weight: 500; - letter-spacing: 0.5px; - font-family: inherit; -} - -@keyframes blink-cursor { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0; - } -} - -.osk-close { - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - background: var(--bp-surface-hover); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-md); - color: var(--bp-text); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.osk-close:hover, -.osk-close:focus, -.osk-close.focused { - background: var(--bp-danger); - border-color: var(--bp-danger); - outline: none; -} - -.osk-keyboard { - display: flex; - flex-direction: column; - gap: var(--bp-spacing-sm); - margin-bottom: var(--bp-spacing-md); -} - -.osk-row { - display: flex; - justify-content: center; - gap: var(--bp-spacing-sm); -} - -.osk-key { - display: flex; - align-items: center; - justify-content: center; - min-width: 64px; - height: 64px; - padding: 0 var(--bp-spacing-md); - background: var(--bp-surface-hover); - border: 3px solid var(--bp-border); - border-radius: var(--bp-radius-md); - font-size: 1.3rem; - font-weight: 700; - color: var(--bp-text); - cursor: pointer; - transition: all var(--bp-transition-fast); - text-transform: uppercase; -} - -.osk-key:hover { - background: var(--bp-surface-active); - transform: scale(1.05); -} - -.osk-key:focus, -.osk-key.focused { - outline: none; - border-color: var(--bp-accent); - box-shadow: 0 0 0 4px var(--bp-accent-glow), 0 0 20px var(--bp-accent-glow); - background: var(--bp-surface-active); - transform: scale(1.1); - z-index: 1; -} - -.osk-key.wide { - min-width: 120px; -} - -.osk-actions { - display: flex; - justify-content: center; - gap: var(--bp-spacing-md); - flex-wrap: wrap; -} - -.osk-action-btn { - display: flex; - align-items: center; - gap: var(--bp-spacing-sm); - padding: var(--bp-spacing-md) var(--bp-spacing-xl); - background: var(--bp-surface-hover); - border: 2px solid var(--bp-border); - border-radius: var(--bp-radius-md); - font-size: 1rem; - font-weight: 600; - color: var(--bp-text); - cursor: pointer; - transition: all var(--bp-transition-fast); -} - -.osk-action-btn .btn-hint { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 28px; - height: 28px; - padding: 0 6px; - background: var(--bp-primary); - border-radius: 6px; - font-size: 0.75rem; - font-weight: 700; - color: white; -} - -.osk-action-btn:hover, -.osk-action-btn:focus, -.osk-action-btn.focused { - background: var(--bp-surface-active); - outline: none; - border-color: var(--bp-accent); -} - -.osk-action-btn.primary { - background: linear-gradient(135deg, var(--bp-primary) 0%, #5a1fd4 100%); - border-color: var(--bp-primary); -} - -.osk-action-btn.primary:hover, -.osk-action-btn.primary:focus, -.osk-action-btn.primary.focused { - box-shadow: var(--bp-focus-ring); -} - -.osk-action-btn.primary .btn-hint { - background: rgba(255, 255, 255, 0.2); -} - -/* OSK hints bar */ -.osk-hints { - display: flex; - justify-content: center; - gap: var(--bp-spacing-lg); - margin-top: var(--bp-spacing-md); - padding-top: var(--bp-spacing-md); - border-top: 1px solid var(--bp-border); - color: var(--bp-text-muted); - font-size: 0.9rem; -} - -.osk-hints b { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 24px; - height: 24px; - padding: 0 6px; - margin-right: 4px; - background: var(--bp-surface-active); - border: 1px solid var(--bp-border); - border-radius: 4px; - font-size: 0.75rem; - font-weight: 700; - color: var(--bp-text); -} - -/* Context menu */ -.context-menu { - position: fixed; - z-index: 1001; - min-width: 200px; - background: var(--bp-surface); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-md); - padding: var(--bp-spacing-xs); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); -} - -.context-menu.hidden { - display: none; -} - -.context-item { - display: flex; - align-items: center; - gap: var(--bp-spacing-md); - width: 100%; - padding: var(--bp-spacing-md); - background: transparent; - border: 2px solid transparent; - border-radius: var(--bp-radius-sm); - font-size: 1rem; - color: var(--bp-text); - cursor: pointer; - transition: all var(--bp-transition-fast); - text-align: left; -} - -.context-item:hover { - background: var(--bp-surface-hover); -} - -.context-item:focus, -.context-item.focused { - outline: none; - border-color: var(--bp-primary); - background: var(--bp-surface-hover); -} - -.context-item .material-symbols-outlined { - font-size: 20px; - color: var(--bp-accent); -} - -/* Focus indicators for controller navigation */ -[data-focusable]:focus, -[data-focusable].focused { - outline: none; -} - -/* Quick access specific styles */ -.quick-access { - margin-bottom: var(--bp-spacing-xl); -} - -/* Recent sites specific styles */ -.recent-sites { - margin-bottom: var(--bp-spacing-lg); -} - -/* Responsive adjustments for Steam Deck (1280x800) */ -@media screen and (max-width: 1280px) and (max-height: 800px) { - html, body { - font-size: 16px; - } - - .bp-sidebar { - width: 180px; - min-width: 180px; - } - - .nav-item { - padding: var(--bp-spacing-sm) var(--bp-spacing-md); - } - - .nav-item .material-symbols-outlined { - font-size: 24px; - } - - .section-title { - font-size: 1.6rem; - } - - .tile-grid { - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - } - - .tile-icon { - width: 48px; - height: 48px; - } -} - -/* Even smaller screens */ -@media screen and (max-width: 960px) { - .bp-sidebar { - width: 80px; - min-width: 80px; - } - - .nav-label { - display: none; - } - - .nav-item { - justify-content: center; - } -} - -/* Fullscreen mode */ -body.fullscreen .bp-header, -body.fullscreen .bp-footer { - display: none; -} - -body.fullscreen .bp-main { - height: 100vh; -} - -/* Loading state */ -.loading { - display: flex; - align-items: center; - justify-content: center; - padding: var(--bp-spacing-xl); -} - -.loading::after { - content: ''; - width: 40px; - height: 40px; - border: 3px solid var(--bp-border); - border-top-color: var(--bp-primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Notification toast */ -.toast { - position: fixed; - bottom: 100px; - left: 50%; - transform: translateX(-50%); - padding: var(--bp-spacing-md) var(--bp-spacing-lg); - background: var(--bp-surface); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-md); - color: var(--bp-text); - font-size: 1rem; - z-index: 1002; - animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards; -} - -@keyframes toastIn { - from { opacity: 0; transform: translateX(-50%) translateY(20px); } - to { opacity: 1; transform: translateX(-50%) translateY(0); } -} - -@keyframes toastOut { - from { opacity: 1; transform: translateX(-50%) translateY(0); } - to { opacity: 0; transform: translateX(-50%) translateY(20px); } -} - -/* Virtual Cursor for controller-based web browsing */ -.virtual-cursor { - position: fixed; - z-index: 10000; - pointer-events: none; - transform: translate(-2px, -2px); - opacity: 0; - transition: opacity 0.2s ease; - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); -} - -.virtual-cursor.active { - opacity: 1; -} - -.virtual-cursor svg { - width: 28px; - height: 28px; - transition: transform 0.1s ease; -} - -.virtual-cursor.clicking svg { - transform: scale(0.85); -} - -.cursor-click-indicator { - position: absolute; - top: 4px; - left: 4px; - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--bp-primary); - opacity: 0; - transform: scale(0); - transition: all 0.15s ease; -} - -.virtual-cursor.clicking .cursor-click-indicator { - opacity: 0.5; - transform: scale(1.5); -} - -/* Cursor trail effect (optional visual enhancement) */ -.virtual-cursor::after { - content: ''; - position: absolute; - top: 6px; - left: 6px; - width: 8px; - height: 8px; - background: var(--bp-accent); - border-radius: 50%; - opacity: 0.6; - animation: cursorPulse 1.5s ease-in-out infinite; -} - -@keyframes cursorPulse { - 0%, 100% { transform: scale(1); opacity: 0.6; } - 50% { transform: scale(1.3); opacity: 0.3; } -} - -/* Cursor hint overlay when in webview */ -.cursor-controls-hint { - position: fixed; - bottom: 80px; - right: 20px; - background: rgba(20, 20, 31, 0.9); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-md); - padding: var(--bp-spacing-sm) var(--bp-spacing-md); - font-size: 0.8rem; - color: var(--bp-text-muted); - z-index: 9999; - display: flex; - flex-direction: column; - gap: 4px; -} - -.cursor-controls-hint .hint-row { - display: flex; - align-items: center; - gap: 8px; -} - -.cursor-controls-hint .hint-key { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 24px; - padding: 2px 6px; - background: var(--bp-surface-active); - border: 1px solid var(--bp-border); - border-radius: 4px; - font-weight: 600; - font-size: 0.7rem; - color: var(--bp-text); -} - -/* ============================================================================= - SIDEBAR HIDDEN STATE (Fullscreen webview mode) - ============================================================================= */ - -.bp-sidebar.sidebar-hidden { - transform: translateX(-100%); - opacity: 0; - pointer-events: none; - width: 0; - min-width: 0; - padding: 0; - border: none; - transition: all 0.3s ease-out; -} - -.bp-sidebar { - transition: all 0.3s ease-out; -} - -.bp-content.fullscreen { - margin-left: 0; - width: 100%; -} - -.bp-header.sidebar-hidden .header-left { - opacity: 0.5; - transform: scale(0.9); - transition: all 0.3s ease; -} - -/* Show sidebar toggle hint when in fullscreen */ -.bp-content.fullscreen::before { - content: 'โ˜ฐ Menu'; - position: fixed; - bottom: 20px; - left: 20px; - background: rgba(20, 20, 31, 0.8); - border: 1px solid var(--bp-border); - border-radius: var(--bp-radius-sm); - padding: 8px 12px; - font-size: 0.75rem; - color: var(--bp-text-muted); - z-index: 100; - opacity: 0.6; - transition: opacity 0.2s ease; -} - -.bp-content.fullscreen:hover::before { - opacity: 1; -} diff --git a/renderer/bigpicture.html b/renderer/bigpicture.html deleted file mode 100644 index b84937d..0000000 --- a/renderer/bigpicture.html +++ /dev/null @@ -1,529 +0,0 @@ - - - - - - Nebula - Big Picture Mode - - - - - - - - - - - -
- -
-
-
-
-
- - -
-
- - Nebula -
-
-
- --:-- - --- -
-
-
-
- - wifi - - - battery_full - -
- -
-
- - -
- - - - -
- - - - -
-
-

- Welcome back -

-

What would you like to browse today?

-
- - -
-
- search -
- -
- A Search -
-
- - -
-

Quick Access

-
- -
-
- - -
-

Continue Browsing

-
- -
-
-
- - -
- -
- - -
-
-

Bookmarks

-

Your saved websites

-
-
- - -
-
- -
-
- - -
-
-

History

-

Recently visited sites

-
-
- - -
-
- -
-
- - -
-
-

Downloads

-

Your downloaded files

-
-
-
- folder_open -

No recent downloads

-
-
-
- - -
-
-

NeBot AI Assistant

-

Your AI-powered browsing companion

-
-
-
-
- smart_toy -
-
-

Start Conversation

-

Ask questions, get summaries, and more

-
-
- A -
-
-
-
- - -
-
-

Settings

-

Configure your browser

-
- - -
- - - - -
- - -
- -
-

Theme Presets

-
- - - - - - - - - - - - -
-
- - -
-

Display Settings

-
-
- zoom_in -
- Display Scale - Adjust the default zoom level -
-
-
- - 100% - -
-
-
-
- desktop_windows -
- Exit Big Picture Mode - Return to standard desktop interface -
-
-
- -
-
-
- - -
-

Privacy & Data

-
-
- delete_sweep -
- Clear Browsing Data - Delete cookies, cache, and site data -
-
-
- -
-
-
-
- history -
- Clear History - Delete browsing history -
-
-
- -
-
-
-
- search_off -
- Clear Search History - Delete search query history -
-
-
- -
-
-
- - -
-

About Nebula Browser

-
- -
-
- Electron - -- -
-
- Chromium - -- -
-
- Node.js - -- -
-
- Platform - -- -
-
-
- - -
-
-
-
-
-
-
- - -
-
-
- - gamepad - - Navigate -
-
- A - Select -
-
- B - Back -
-
- Y - Search -
-
- โ˜ฐ - Menu -
-
-
- - - - - - -
- - - - diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js deleted file mode 100644 index ab66540..0000000 --- a/renderer/bigpicture.js +++ /dev/null @@ -1,3035 +0,0 @@ -/** - * Big Picture Mode - Controller-friendly UI for Steam Deck / Console - * Supports gamepad navigation, on-screen keyboard, and touch input - */ - -const ipcRenderer = window.electronAPI; - -// ============================================================================= -// SCROLL NORMALIZATION (consistent scroll speed across all sites) -// ============================================================================= - -const SCROLL_NORMALIZATION_CSS = ` - /* Disable smooth scrolling behavior that some sites force */ - *, *::before, *::after { - scroll-behavior: auto !important; - } - html, body { - scroll-behavior: auto !important; - } -`; - -const SCROLL_NORMALIZATION_JS = ` -(function() { - if (window.__nebulaScrollNormalized) return; - window.__nebulaScrollNormalized = true; - - // Consistent scroll amount in pixels per wheel delta unit - const SCROLL_SPEED = 100; - - // Intercept wheel events to normalize scroll speed - document.addEventListener('wheel', function(e) { - // Don't interfere if modifier keys are pressed (zoom, horizontal scroll, etc.) - if (e.ctrlKey || e.metaKey || e.altKey) return; - - // Get the scroll target - let target = e.target; - let scrollable = null; - - // Find the nearest scrollable element - while (target && target !== document.body && target !== document.documentElement) { - const style = window.getComputedStyle(target); - const overflowY = style.overflowY; - const overflowX = style.overflowX; - - if ((overflowY === 'auto' || overflowY === 'scroll') && target.scrollHeight > target.clientHeight) { - scrollable = target; - break; - } - if ((overflowX === 'auto' || overflowX === 'scroll') && target.scrollWidth > target.clientWidth && e.shiftKey) { - scrollable = target; - break; - } - target = target.parentElement; - } - - // If no scrollable container found, use the document - if (!scrollable) { - scrollable = document.scrollingElement || document.documentElement || document.body; - } - - // Calculate normalized scroll delta - // deltaMode: 0 = pixels, 1 = lines, 2 = pages - let deltaY = e.deltaY; - let deltaX = e.deltaX; - - if (e.deltaMode === 1) { - // Line mode - multiply by line height approximation - deltaY *= SCROLL_SPEED; - deltaX *= SCROLL_SPEED; - } else if (e.deltaMode === 2) { - // Page mode - multiply by viewport height - deltaY *= window.innerHeight; - deltaX *= window.innerWidth; - } else { - // Pixel mode - normalize to consistent speed - // Clamp the delta to prevent extremely fast scrolling from some sites - const sign = deltaY > 0 ? 1 : -1; - deltaY = sign * Math.min(Math.abs(deltaY), SCROLL_SPEED * 3); - - const signX = deltaX > 0 ? 1 : -1; - deltaX = signX * Math.min(Math.abs(deltaX), SCROLL_SPEED * 3); - } - - // Apply scroll - e.preventDefault(); - scrollable.scrollBy({ - top: deltaY, - left: e.shiftKey ? deltaX : 0, - behavior: 'auto' - }); - }, { passive: false, capture: true }); -})(); -`; - -// Function to apply scroll normalization to a webview -function applyScrollNormalization(webview) { - try { - webview.insertCSS(SCROLL_NORMALIZATION_CSS); - webview.executeJavaScript(SCROLL_NORMALIZATION_JS); - console.log('[BigPicture] Applied scroll normalization to webview'); - } catch (err) { - console.warn('[BigPicture] Failed to apply scroll normalization:', err); - } -} - -// ============================================================================= -// CONFIGURATION -// ============================================================================= - -const CONFIG = { - // Navigation - NAV_SOUND_ENABLED: true, - HAPTIC_FEEDBACK: true, - - // Controller deadzone - STICK_DEADZONE: 0.3, - TRIGGER_DEADZONE: 0.1, - - // Timing - REPEAT_DELAY: 500, // Initial delay before key repeat - REPEAT_RATE: 100, // Rate of key repeat - - // Quick access sites - DEFAULT_QUICK_ACCESS: [ - { title: 'Google', url: 'https://www.google.com', icon: 'search' }, - { title: 'YouTube', url: 'https://www.youtube.com', icon: 'play_circle' }, - { title: 'Reddit', url: 'https://www.reddit.com', icon: 'forum' }, - { title: 'Twitter', url: 'https://twitter.com', icon: 'tag' }, - { title: 'Wikipedia', url: 'https://www.wikipedia.org', icon: 'school' }, - { title: 'Netflix', url: 'https://www.netflix.com', icon: 'movie' }, - ] -}; - -// ============================================================================= -// STATE -// ============================================================================= - -const state = { - currentSection: 'home', - focusedElement: null, - focusableElements: [], - focusIndex: 0, - - // Gamepad - gamepadConnected: false, - gamepadIndex: null, - lastInput: { x: 0, y: 0 }, - inputRepeatTimer: null, - - // Virtual cursor for webview - cursorEnabled: false, - cursorX: 0, - cursorY: 0, - cursorSpeed: 15, - cursorElement: null, - - // Sidebar visibility (for fullscreen webview) - sidebarHidden: false, - - // OSK (On-Screen Keyboard) - oskVisible: false, - oskCallback: null, - oskFocusIndex: 0, - oskContext: null, - - // Data - bookmarks: [], - history: [], - - // Mouse tracking - mouseTimeout: null, - - // Webview for browsing - currentWebview: null, - webviewContentsId: null, // For native input event injection - webviewStack: [] // Stack of webview instances for navigation history -}; - -// ============================================================================= -// INITIALIZATION -// ============================================================================= - -function applyDisplayScale(scalePercent, reason = 'unknown') { - const numeric = Number(scalePercent); - if (!Number.isFinite(numeric)) return; - - const clampedPercent = Math.min(300, Math.max(50, Math.round(numeric))); - const zoomFactor = Math.max(0.5, Math.min(3, clampedPercent / 100)); - - // Prefer Electron zoom (consistent across Chromium) with CSS fallback. - try { - if (ipcRenderer && typeof ipcRenderer.invoke === 'function') { - ipcRenderer.invoke('set-zoom-factor', zoomFactor).catch(err => { - console.warn('[BigPicture] set-zoom-factor failed; falling back to CSS zoom:', err); - applyCssZoom(zoomFactor); - }); - } else { - applyCssZoom(zoomFactor); - } - console.log(`[BigPicture] Applied display scale ${clampedPercent}% (zoom=${zoomFactor}) via ${reason}`); - } catch (err) { - console.warn('[BigPicture] Failed applying display scale:', err); - } -} - -function applyCssZoom(factor) { - try { - document.documentElement.style.zoom = factor; - } catch {} - try { - document.body.style.zoom = factor; - } catch {} - try { - document.documentElement.style.setProperty('--bp-scale-factor', factor); - document.body.style.setProperty('--bp-scale-factor', factor); - } catch {} -} - -function applyDisplayScaleFromStorage(reason = 'startup') { - try { - const savedScale = localStorage.getItem(DISPLAY_SCALE_KEY); - if (!savedScale) return; - const parsed = parseInt(savedScale, 10); - if (Number.isFinite(parsed)) { - currentDisplayScale = Math.min(300, Math.max(50, parsed)); - applyDisplayScale(currentDisplayScale, `${reason}-storage`); - updateScaleDisplay(); - } - } catch (err) { - console.warn('[BigPicture] Failed to read display scale from storage:', err); - } -} - -document.addEventListener('DOMContentLoaded', () => { - console.log('[BigPicture] Initializing Big Picture Mode'); - - // Apply saved display scale as early as possible for this window. - applyDisplayScaleFromStorage('DOMContentLoaded'); - - initClock(); - initNavigation(); - initGamepadSupport(); - initMouseTracking(); - initKeyboardShortcuts(); - initOSK(); - loadData(); - - // Set initial focus - setTimeout(() => { - updateFocusableElements(); - focusFirstElement(); - }, 100); -}); - -// ============================================================================= -// CLOCK & DATE -// ============================================================================= - -function initClock() { - updateClock(); - setInterval(updateClock, 1000); -} - -function updateClock() { - const now = new Date(); - const timeEl = document.getElementById('bp-time'); - const dateEl = document.getElementById('bp-date'); - - if (timeEl) { - timeEl.textContent = now.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: true - }); - } - - if (dateEl) { - dateEl.textContent = now.toLocaleDateString([], { - weekday: 'short', - month: 'short', - day: 'numeric' - }); - } - - // Update greeting based on time - const greetingEl = document.getElementById('greeting-text'); - if (greetingEl) { - const hour = now.getHours(); - let greeting = 'Welcome back'; - if (hour < 12) greeting = 'Good morning'; - else if (hour < 17) greeting = 'Good afternoon'; - else greeting = 'Good evening'; - greetingEl.textContent = greeting; - } -} - -// ============================================================================= -// NAVIGATION -// ============================================================================= - -function initNavigation() { - // Sidebar navigation - document.querySelectorAll('.nav-item').forEach(item => { - item.addEventListener('click', () => { - const section = item.dataset.section; - if (section) { - switchSection(section); - } - }); - }); - - // Exit button - const exitBtn = document.getElementById('exitBigPicture'); - if (exitBtn) { - exitBtn.addEventListener('click', exitBigPictureMode); - } - - // Search card click - const searchCard = document.querySelector('.search-card'); - if (searchCard) { - searchCard.addEventListener('click', () => openOSK('search')); - } - - // Search input - const searchInput = document.getElementById('bp-search'); - if (searchInput) { - searchInput.addEventListener('focus', () => openOSK('search')); - searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - performSearch(searchInput.value); - } - }); - } - - // NeBot launch - const launchNebot = document.getElementById('launchNebot'); - if (launchNebot) { - launchNebot.addEventListener('click', () => navigateTo('nebula://nebot')); - } - - // History section buttons - const clearHistoryBtn = document.getElementById('clearHistoryBtn'); - if (clearHistoryBtn) { - clearHistoryBtn.addEventListener('click', clearHistory); - } - - const refreshHistoryBtn = document.getElementById('refreshHistoryBtn'); - if (refreshHistoryBtn) { - refreshHistoryBtn.addEventListener('click', async () => { - await loadHistory(); - showToast('History refreshed'); - }); - } - - // Bookmarks actions - const addBookmarkBtn = document.getElementById('addBookmarkBtn'); - if (addBookmarkBtn) { - addBookmarkBtn.addEventListener('click', () => startAddBookmark()); - } - - const addCurrentBookmarkBtn = document.getElementById('addCurrentBookmarkBtn'); - if (addCurrentBookmarkBtn) { - addCurrentBookmarkBtn.addEventListener('click', () => addBookmarkFromCurrentPage()); - } - - // Settings cards - document.querySelectorAll('.settings-card').forEach(card => { - card.addEventListener('click', () => { - const action = card.dataset.action; - handleSettingsAction(action); - }); - }); -} - -// ============================================================================= -// SIDEBAR TOGGLE (for fullscreen webview) -// ============================================================================= - -function toggleSidebar() { - state.sidebarHidden = !state.sidebarHidden; - - const sidebar = document.querySelector('.bp-sidebar'); - const content = document.querySelector('.bp-content'); - const header = document.querySelector('.bp-header'); - - if (state.sidebarHidden) { - sidebar?.classList.add('sidebar-hidden'); - content?.classList.add('fullscreen'); - header?.classList.add('sidebar-hidden'); - showToast('๐Ÿ“บ Fullscreen mode | Press โ˜ฐ to show sidebar'); - } else { - sidebar?.classList.remove('sidebar-hidden'); - content?.classList.remove('fullscreen'); - header?.classList.remove('sidebar-hidden'); - showToast('Sidebar restored'); - } -} - -function showSidebar() { - if (state.sidebarHidden) { - toggleSidebar(); - } -} - -function switchSection(sectionId) { - console.log('[BigPicture] Switching to section:', sectionId); - - // Restore sidebar when leaving browse section - if (sectionId !== 'browse' && state.sidebarHidden) { - showSidebar(); - } - - // Handle webview container visibility (preserve state instead of destroying) - const webviewContainer = document.getElementById('webview-container'); - if (webviewContainer) { - if (sectionId === 'browse' && state.currentWebview) { - // Show the preserved webview when going back to browse - webviewContainer.classList.remove('hidden'); - // Re-enable cursor when returning to browse - enableCursor(); - } else if (sectionId !== 'browse') { - // Just hide the webview, don't destroy it - webviewContainer.classList.add('hidden'); - // Disable cursor when leaving browse - disableCursor(); - } - } - - // Update nav items - document.querySelectorAll('.nav-item').forEach(item => { - item.classList.toggle('active', item.dataset.section === sectionId); - }); - - // Update sections - document.querySelectorAll('.bp-section').forEach(section => { - section.classList.toggle('active', section.id === `section-${sectionId}`); - }); - - state.currentSection = sectionId; - - // Update focusable elements for new section - setTimeout(() => { - updateFocusableElements(); - focusFirstInContent(); - }, 50); - - playNavSound(); -} - -function updateFocusableElements() { - // If OSK is visible, only include OSK elements - if (state.oskVisible) { - const oskOverlay = document.getElementById('osk-overlay'); - if (oskOverlay) { - state.focusableElements = [...oskOverlay.querySelectorAll('[data-focusable]')]; - console.log('[BigPicture] OSK focusable elements:', state.focusableElements.length); - return; - } - } - - // When in webview mode, only sidebar navigation is available - if (state.cursorEnabled && state.currentWebview) { - state.focusableElements = [ - ...document.querySelectorAll('.bp-sidebar [data-focusable]'), - ...document.querySelectorAll('.bp-header [data-focusable]') - ]; - console.log('[BigPicture] Webview mode - sidebar focusable elements:', state.focusableElements.length); - return; - } - - const activeSection = document.querySelector('.bp-section.active'); - if (!activeSection) return; - - // Get all focusable elements in sidebar and active section - state.focusableElements = [ - ...document.querySelectorAll('.bp-sidebar [data-focusable]'), - ...activeSection.querySelectorAll('[data-focusable]'), - ...document.querySelectorAll('.bp-header [data-focusable]') - ]; - - console.log('[BigPicture] Focusable elements:', state.focusableElements.length); -} - -function focusFirstElement() { - if (state.focusableElements.length > 0) { - focusElement(state.focusableElements[0]); - state.focusIndex = 0; - } -} - -function focusFirstInContent() { - const activeSection = document.querySelector('.bp-section.active'); - if (!activeSection) return; - - const firstFocusable = activeSection.querySelector('[data-focusable]'); - if (firstFocusable) { - const index = state.focusableElements.indexOf(firstFocusable); - if (index !== -1) { - focusElement(firstFocusable); - state.focusIndex = index; - } - } -} - -function focusElement(element) { - if (!element) return; - - // Remove focus from previous - if (state.focusedElement) { - state.focusedElement.classList.remove('focused'); - } - - // Add focus to new element - element.classList.add('focused'); - element.focus(); - state.focusedElement = element; - - // Scroll into view if needed - element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); -} - -function navigateFocus(direction) { - if (state.focusableElements.length === 0) return; - - let newIndex = state.focusIndex; - - switch (direction) { - case 'up': - newIndex = findElementInDirection('up'); - break; - case 'down': - newIndex = findElementInDirection('down'); - break; - case 'left': - newIndex = findElementInDirection('left'); - break; - case 'right': - newIndex = findElementInDirection('right'); - break; - } - - if (newIndex !== state.focusIndex && newIndex >= 0 && newIndex < state.focusableElements.length) { - state.focusIndex = newIndex; - focusElement(state.focusableElements[newIndex]); - playNavSound(); - } -} - -function findElementInDirection(direction) { - const current = state.focusedElement; - if (!current) return 0; - - const currentRect = current.getBoundingClientRect(); - const currentCenter = { - x: currentRect.left + currentRect.width / 2, - y: currentRect.top + currentRect.height / 2 - }; - - // Detect if current element is in sidebar, header, or content area - const currentContainer = current.closest('.bp-sidebar, .bp-header, .bp-content, .osk-overlay, .sidebar, .content'); - - // Special case: if on a tab link in settings and going down/right, prioritize active panel content - const isTabLink = current.classList.contains('tab-link') || current.closest('.tabs, .tab-link'); - const isActiveTab = current.classList.contains('active'); - - let bestIndex = state.focusIndex; - let bestScore = Infinity; - - state.focusableElements.forEach((element, index) => { - if (element === current) return; - - const rect = element.getBoundingClientRect(); - const center = { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - }; - - // Detect element's container - const elementContainer = element.closest('.bp-sidebar, .bp-header, .bp-content, .osk-overlay, .sidebar, .content'); - const sameContainer = currentContainer === elementContainer; - - // Check if element is in active tab panel - const inActivePanel = element.closest('.tab-panel.active'); - - // Check if element is in the correct direction - let isValid = false; - let alignmentScore = 0; - let distanceInDirection = 0; - let distancePerpendicular = 0; - - switch (direction) { - case 'up': - isValid = center.y < currentCenter.y - 10; - distanceInDirection = currentCenter.y - center.y; - distancePerpendicular = Math.abs(center.x - currentCenter.x); - // Prioritize elements in the same vertical column - alignmentScore = distancePerpendicular < 50 ? 0 : distancePerpendicular; - break; - case 'down': - isValid = center.y > currentCenter.y + 10; - distanceInDirection = center.y - currentCenter.y; - distancePerpendicular = Math.abs(center.x - currentCenter.x); - // Prioritize elements in the same vertical column - alignmentScore = distancePerpendicular < 50 ? 0 : distancePerpendicular; - break; - case 'left': - isValid = center.x < currentCenter.x - 10; - distanceInDirection = currentCenter.x - center.x; - distancePerpendicular = Math.abs(center.y - currentCenter.y); - // Prioritize elements in the same horizontal row - alignmentScore = distancePerpendicular < 50 ? 0 : distancePerpendicular; - break; - case 'right': - isValid = center.x > currentCenter.x + 10; - distanceInDirection = center.x - currentCenter.x; - distancePerpendicular = Math.abs(center.y - currentCenter.y); - // Prioritize elements in the same horizontal row - alignmentScore = distancePerpendicular < 50 ? 0 : distancePerpendicular; - break; - } - - if (isValid) { - // Calculate score - lower is better - // Heavily favor same container, then alignment, then distance - let score = distanceInDirection + alignmentScore * 3; - - // Special handling: if on active tab and going down/right, strongly prefer active panel content - if (isTabLink && isActiveTab && (direction === 'down' || direction === 'right')) { - if (inActivePanel) { - score = distanceInDirection * 0.1; // Extremely high priority for panel content - } else { - score += 5000; // Very large penalty for non-panel elements - } - } - // Otherwise, strong bonus for staying in same container (sidebar, content, etc.) - else if (!sameContainer) { - score += 2000; // Large penalty for leaving container - } - - if (score < bestScore) { - bestScore = score; - bestIndex = index; - } - } - }); - - return bestIndex; -} - -function activateFocused() { - if (state.focusedElement) { - state.focusedElement.click(); - playSelectSound(); - } -} - -function goBack() { - // If OSK is open, close it - if (state.oskVisible) { - closeOSK(); - return; - } - - // If viewing a website, go back in browsing history - if (state.currentSection === 'browse' && state.currentWebview) { - if (state.currentWebview.canGoBack()) { - state.currentWebview.goBack(); - return; - } - } - - // If not on home, go to home - if (state.currentSection !== 'home') { - switchSection('home'); - // Cleanup webview - const container = document.getElementById('webview-container'); - if (container) { - const webview = container.querySelector('webview'); - if (webview) webview.remove(); - container.classList.add('hidden'); - } - state.currentWebview = null; - // Focus the home nav item - const homeNav = document.querySelector('.nav-item[data-section="home"]'); - if (homeNav) { - const index = state.focusableElements.indexOf(homeNav); - if (index !== -1) { - state.focusIndex = index; - focusElement(homeNav); - } - } - } -} - -function goForward() { - // If viewing a website, go forward in browsing history - if (state.currentSection === 'browse' && state.currentWebview) { - if (state.currentWebview.canGoForward()) { - state.currentWebview.goForward(); - } - } -} - -// ============================================================================= -// GAMEPAD SUPPORT -// ============================================================================= - -function initGamepadSupport() { - if (!navigator.getGamepads) { - console.warn('[BigPicture] Gamepad API not available in this environment'); - return; - } - - // The global gamepad handler (from gamepad-handler.js injected via preload) - // already polls navigator.getGamepads() continuously. This is what tells Steam - // that we're consuming gamepad input and it should stop mouse emulation. - // Big Picture Mode handles the actual UI navigation and button actions. - - console.log('[BigPicture] Global gamepad handler available:', !!window.__nebulaGamepadHandler); - - // 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 || '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:', 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 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); -} - -function readDpadFromButtons(gamepad) { - const up = !!gamepad.buttons[12]?.pressed; - const down = !!gamepad.buttons[13]?.pressed; - const left = !!gamepad.buttons[14]?.pressed; - const right = !!gamepad.buttons[15]?.pressed; - return { up, down, left, right, active: up || down || left || right, source: 'buttons' }; -} - -function readDpadFromAxes(gamepad) { - const axes = gamepad.axes || []; - const candidates = [ - { x: 6, y: 7 }, - { x: 9, y: 10 }, - { x: 4, y: 5 } - ]; - - for (const { x, y } of candidates) { - if (axes.length <= Math.max(x, y)) continue; - const ax = axes[x] || 0; - const ay = axes[y] || 0; - if (Math.abs(ax) > 0.5 || Math.abs(ay) > 0.5) { - return { - up: ay < -0.5, - down: ay > 0.5, - left: ax < -0.5, - right: ax > 0.5, - active: true, - source: 'axes' - }; - } - } - - return { up: false, down: false, left: false, right: false, active: false, source: 'axes' }; -} - -function handleGamepadInput(gamepad) { - // D-pad and left stick for navigation - const leftX = gamepad.axes[0] || 0; - const leftY = gamepad.axes[1] || 0; - - // D-pad buttons/axes (indices may vary by controller) - const buttonDpad = readDpadFromButtons(gamepad); - const axisDpad = readDpadFromAxes(gamepad); - const dpad = axisDpad.active && (!buttonDpad.active || gamepad.mapping !== 'standard') - ? axisDpad - : buttonDpad; - const dpadUp = dpad.up; - const dpadDown = dpad.down; - const dpadLeft = dpad.left; - const dpadRight = dpad.right; - - // Analog stick with deadzone - const stickUp = leftY < -CONFIG.STICK_DEADZONE; - const stickDown = leftY > CONFIG.STICK_DEADZONE; - const stickLeft = leftX < -CONFIG.STICK_DEADZONE; - const stickRight = leftX > CONFIG.STICK_DEADZONE; - - // When cursor is enabled (viewing a webpage), only D-Pad navigates sidebar - // Left stick is ignored for UI navigation in webview mode - const inWebviewMode = state.cursorEnabled && state.currentWebview; - - // Combine inputs - but only use D-Pad when in webview mode - const up = inWebviewMode ? dpadUp : (dpadUp || stickUp); - const down = inWebviewMode ? dpadDown : (dpadDown || stickDown); - const left = inWebviewMode ? dpadLeft : (dpadLeft || stickLeft); - const right = inWebviewMode ? dpadRight : (dpadRight || stickRight); - - // Navigation with repeat prevention - const now = Date.now(); - - if (up && !state.lastInput.up) { - navigateFocus('up'); - state.lastInput.up = now; - } else if (!up) { - state.lastInput.up = 0; - } - - if (down && !state.lastInput.down) { - navigateFocus('down'); - state.lastInput.down = now; - } else if (!down) { - state.lastInput.down = 0; - } - - if (left && !state.lastInput.left) { - navigateFocus('left'); - state.lastInput.left = now; - } else if (!left) { - state.lastInput.left = 0; - } - - if (right && !state.lastInput.right) { - navigateFocus('right'); - state.lastInput.right = now; - } else if (!right) { - state.lastInput.right = 0; - } - - // A button (usually index 0) - Always select/activate focused menu item - if (gamepad.buttons[0]?.pressed && !state.lastInput.a) { - activateFocused(); - state.lastInput.a = true; - } else if (!gamepad.buttons[0]?.pressed) { - state.lastInput.a = false; - } - - // B button (usually index 1) - Back/Close OSK - if (gamepad.buttons[1]?.pressed && !state.lastInput.b) { - goBack(); - state.lastInput.b = true; - } else if (!gamepad.buttons[1]?.pressed) { - state.lastInput.b = false; - } - - // X button (usually index 2) - Backspace when OSK is open - if (gamepad.buttons[2]?.pressed && !state.lastInput.x) { - if (state.oskVisible) { - backspaceOSK(); - } - state.lastInput.x = true; - } else if (!gamepad.buttons[2]?.pressed) { - state.lastInput.x = false; - } - - // Y button (usually index 3) - Space when OSK open, otherwise open search - if (gamepad.buttons[3]?.pressed && !state.lastInput.y) { - if (state.oskVisible) { - appendToOSK(' '); - } else { - openOSK('search'); - } - state.lastInput.y = true; - } else if (!gamepad.buttons[3]?.pressed) { - state.lastInput.y = false; - } - - // LB button (usually index 4) - Go back in webview / clear OSK - if (gamepad.buttons[4]?.pressed && !state.lastInput.lb) { - if (state.oskVisible) { - clearOSK(); - } else if (state.currentSection === 'browse' && state.currentWebview) { - goBack(); - } - state.lastInput.lb = true; - } else if (!gamepad.buttons[4]?.pressed) { - state.lastInput.lb = false; - } - - // RB button (usually index 5) - Go forward in webview / submit OSK - if (gamepad.buttons[5]?.pressed && !state.lastInput.rb) { - if (state.oskVisible) { - submitOSK(); - } else if (state.currentSection === 'browse' && state.currentWebview) { - goForward(); - } - state.lastInput.rb = true; - } else if (!gamepad.buttons[5]?.pressed) { - state.lastInput.rb = false; - } - - // Back/Select button (usually index 8) - Toggle sidebar when in webview - if (gamepad.buttons[8]?.pressed && !state.lastInput.select) { - if (state.currentSection === 'browse' && state.currentWebview) { - toggleSidebar(); - } - state.lastInput.select = true; - } else if (!gamepad.buttons[8]?.pressed) { - state.lastInput.select = false; - } - - // Start button (usually index 9) - Menu / Toggle sidebar when viewing webpage - if (gamepad.buttons[9]?.pressed && !state.lastInput.start) { - // If viewing a webpage, toggle sidebar instead of going to settings - if (state.currentSection === 'browse' && state.currentWebview) { - toggleSidebar(); - } else if (state.currentSection !== 'settings') { - switchSection('settings'); - } else { - switchSection('home'); - } - state.lastInput.start = true; - } else if (!gamepad.buttons[9]?.pressed) { - state.lastInput.start = false; - } - - // Virtual cursor handling when webview is active - if (state.cursorEnabled && state.currentWebview) { - // Right stick for cursor movement - const rightX = gamepad.axes[2] || 0; - const rightY = gamepad.axes[3] || 0; - - // Apply deadzone - const deadzone = 0.15; - const moveX = Math.abs(rightX) > deadzone ? rightX : 0; - const moveY = Math.abs(rightY) > deadzone ? rightY : 0; - - if (moveX !== 0 || moveY !== 0) { - moveCursor(moveX * state.cursorSpeed, moveY * state.cursorSpeed); - } - - // Left stick for scrolling in webview mode - const scrollDeadzone = 0.25; - const scrollX = Math.abs(leftX) > scrollDeadzone ? leftX : 0; - const scrollY = Math.abs(leftY) > scrollDeadzone ? leftY : 0; - - if (scrollX !== 0 || scrollY !== 0) { - scrollWebview(scrollY * 20, scrollX * 20); - } - - // Right trigger (index 7) - Left click - if (gamepad.buttons[7]?.pressed && !state.lastInput.rt) { - virtualClick(); - state.lastInput.rt = true; - } else if (!gamepad.buttons[7]?.pressed) { - state.lastInput.rt = false; - } - - // Left trigger (index 6) - Right click - if (gamepad.buttons[6]?.pressed && !state.lastInput.lt) { - virtualClick(true); - state.lastInput.lt = true; - } else if (!gamepad.buttons[6]?.pressed) { - state.lastInput.lt = false; - } - - // Right stick click (index 11) - Toggle cursor speed - if (gamepad.buttons[11]?.pressed && !state.lastInput.rs) { - state.cursorSpeed = state.cursorSpeed === 15 ? 8 : (state.cursorSpeed === 8 ? 25 : 15); - showToast(`Cursor speed: ${state.cursorSpeed === 8 ? 'Slow' : state.cursorSpeed === 15 ? 'Normal' : 'Fast'}`); - state.lastInput.rs = true; - } else if (!gamepad.buttons[11]?.pressed) { - state.lastInput.rs = false; - } - } -} - -// ============================================================================= -// KEYBOARD SHORTCUTS -// ============================================================================= - -function initKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { - // Don't handle if OSK is visible and we're typing - if (state.oskVisible) { - handleOSKKeyboard(e); - return; - } - - switch (e.key) { - case 'ArrowUp': - e.preventDefault(); - navigateFocus('up'); - break; - case 'ArrowDown': - e.preventDefault(); - navigateFocus('down'); - break; - case 'ArrowLeft': - e.preventDefault(); - navigateFocus('left'); - break; - case 'ArrowRight': - e.preventDefault(); - navigateFocus('right'); - break; - case 'Enter': - case ' ': - e.preventDefault(); - activateFocused(); - break; - case 'Escape': - case 'Backspace': - e.preventDefault(); - goBack(); - break; - case 'Tab': - // Allow tab navigation - break; - } - }); -} - -// ============================================================================= -// MOUSE TRACKING -// ============================================================================= - -function initMouseTracking() { - document.addEventListener('mousemove', () => { - document.body.classList.add('mouse-active'); - - clearTimeout(state.mouseTimeout); - state.mouseTimeout = setTimeout(() => { - document.body.classList.remove('mouse-active'); - }, 3000); - }); - - // Add hover focus for mouse - document.addEventListener('mouseover', (e) => { - const focusable = e.target.closest('[data-focusable]'); - if (focusable && state.focusableElements.includes(focusable)) { - const index = state.focusableElements.indexOf(focusable); - state.focusIndex = index; - focusElement(focusable); - } - }); -} - -// ============================================================================= -// ON-SCREEN KEYBOARD -// ============================================================================= - -function initOSK() { - const keyboard = document.getElementById('osk-keyboard'); - if (!keyboard) return; - - const rows = [ - '1234567890', - 'qwertyuiop', - 'asdfghjkl', - 'zxcvbnm', - ]; - - rows.forEach(row => { - const rowEl = document.createElement('div'); - rowEl.className = 'osk-row'; - - [...row].forEach(char => { - const key = document.createElement('button'); - key.className = 'osk-key'; - key.textContent = char; - key.dataset.focusable = ''; - key.tabIndex = 0; - key.addEventListener('click', () => appendToOSK(char)); - rowEl.appendChild(key); - }); - - keyboard.appendChild(rowEl); - }); - - // Special keys - const specialRow = document.createElement('div'); - specialRow.className = 'osk-row'; - - ['.', '-', '_', '@', '/', ':', '.com'].forEach(char => { - const key = document.createElement('button'); - key.className = 'osk-key' + (char === '.com' ? ' wide' : ''); - key.textContent = char; - key.dataset.focusable = ''; - key.tabIndex = 0; - key.addEventListener('click', () => appendToOSK(char)); - specialRow.appendChild(key); - }); - - keyboard.appendChild(specialRow); - - // Action buttons - document.getElementById('osk-space')?.addEventListener('click', () => appendToOSK(' ')); - document.getElementById('osk-backspace')?.addEventListener('click', () => backspaceOSK()); - document.getElementById('osk-clear')?.addEventListener('click', () => clearOSK()); - document.getElementById('osk-submit')?.addEventListener('click', () => submitOSK()); - - // Close button - document.querySelector('.osk-close')?.addEventListener('click', () => closeOSK()); -} - -function openOSK(mode = 'search', options = {}) { - const overlay = document.getElementById('osk-overlay'); - const input = document.getElementById('osk-input'); - const label = document.getElementById('osk-label'); - - if (!overlay || !input) return; - - state.oskVisible = true; - state.oskMode = mode; - overlay.classList.remove('hidden'); - - // Set input - input.value = typeof options.initialValue === 'string' ? options.initialValue : ''; - - // Reset cursor position - updateOSKCursorPosition(); - - // Update label based on mode - if (label) { - if (options.labelText) { - label.textContent = options.labelText; - } else if (mode === 'search') { - label.textContent = 'Search or enter URL'; - } else if (mode === 'bookmark-url') { - label.textContent = 'Bookmark URL'; - } else if (mode === 'bookmark-title') { - label.textContent = 'Bookmark title'; - } else { - label.textContent = 'Enter text'; - } - } - - // Update focusable elements to only include OSK keys - updateFocusableElements(); - - // Focus first key - setTimeout(() => { - const firstKey = overlay.querySelector('.osk-key'); - if (firstKey) { - const index = state.focusableElements.indexOf(firstKey); - if (index !== -1) { - state.focusIndex = index; - focusElement(firstKey); - } else { - firstKey.focus(); - } - } - }, 100); -} - -/** - * Open OSK for typing into a focused input field in the webview - */ -function openOSKForWebview() { - const overlay = document.getElementById('osk-overlay'); - const input = document.getElementById('osk-input'); - const label = document.getElementById('osk-label'); - - if (!overlay || !input) return; - - state.oskVisible = true; - state.oskMode = 'webview'; // Special mode for webview input - overlay.classList.remove('hidden'); - - // Clear input (could optionally preserve current input value) - input.value = ''; - - // Reset cursor position - updateOSKCursorPosition(); - - // Update the label to indicate webview mode - if (label) { - label.textContent = 'Type your text'; - } - - // Update focusable elements to only include OSK keys - updateFocusableElements(); - - // Focus first key - setTimeout(() => { - const firstKey = overlay.querySelector('.osk-key'); - if (firstKey) { - const index = state.focusableElements.indexOf(firstKey); - if (index !== -1) { - state.focusIndex = index; - focusElement(firstKey); - } else { - firstKey.focus(); - } - } - }, 100); - - showToast('๐Ÿ“ Type and press Submit to enter text'); -} - -function closeOSK() { - const overlay = document.getElementById('osk-overlay'); - if (!overlay) return; - - state.oskVisible = false; - overlay.classList.add('hidden'); - - // Return focus to main content - setTimeout(() => { - updateFocusableElements(); - focusFirstInContent(); - }, 100); -} - -function appendToOSK(char) { - const input = document.getElementById('osk-input'); - if (input) { - input.value += char; - updateOSKCursorPosition(); - } -} - -function backspaceOSK() { - const input = document.getElementById('osk-input'); - if (input && input.value.length > 0) { - input.value = input.value.slice(0, -1); - updateOSKCursorPosition(); - playNavSound(); - } -} - -function clearOSK() { - const input = document.getElementById('osk-input'); - if (input) { - input.value = ''; - updateOSKCursorPosition(); - playNavSound(); - } -} - -/** - * Update the blinking cursor position to follow the text - */ -function updateOSKCursorPosition() { - const input = document.getElementById('osk-input'); - const cursor = document.getElementById('osk-cursor'); - const measure = document.getElementById('osk-text-measure'); - - if (!input || !cursor || !measure) return; - - // Copy the input text to the measure element - measure.textContent = input.value || ''; - - // Get the text width + padding offset - const textWidth = measure.offsetWidth; - const paddingLeft = 32; // var(--bp-spacing-lg) = 32px - - // Position cursor right after the text - cursor.style.left = `${paddingLeft + textWidth}px`; -} - -async function submitOSK() { - const input = document.getElementById('osk-input'); - if (!input) return; - - const value = input.value; - - if (state.oskMode === 'search') { - if (!value.trim()) return; - performSearch(value.trim()); - } else if (state.oskMode === 'webview' && state.currentWebview) { - // Send the typed text to the webview's focused input - sendTextToWebview(value, true); // true = submit after setting - } else if (state.oskMode === 'bookmark-url') { - const normalized = normalizeBookmarkUrl(value); - if (!normalized) { - showToast('Enter a valid URL'); - return; - } - state.oskContext = { url: normalized }; - openOSK('bookmark-title', { - labelText: 'Bookmark title', - initialValue: getDomainFromUrl(normalized) - }); - return; - } else if (state.oskMode === 'bookmark-title') { - const url = state.oskContext?.url; - if (!url) { - closeOSK(); - return; - } - const title = value.trim() || getDomainFromUrl(url); - await addOrUpdateBookmark({ title, url, icon: getFaviconUrl(url) || 'bookmark' }); - state.oskContext = null; - } - - closeOSK(); -} - -/** - * Send typed text from OSK to the focused input field in webview - */ -function sendTextToWebview(text, submit = false) { - if (!state.currentWebview) return; - - try { - // Send the text value to the webview - const script = submit ? ` - (function() { - const activeEl = document.activeElement; - if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) { - activeEl.value = ${JSON.stringify(text)}; - activeEl.dispatchEvent(new Event('input', { bubbles: true })); - activeEl.dispatchEvent(new Event('change', { bubbles: true })); - - // Trigger Enter key to submit - setTimeout(() => { - activeEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', keyCode: 13, bubbles: true })); - activeEl.dispatchEvent(new KeyboardEvent('keypress', { key: 'Enter', keyCode: 13, bubbles: true })); - activeEl.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true })); - - // Also try form submission - const form = activeEl.closest('form'); - if (form) { - const submitBtn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])'); - if (submitBtn) submitBtn.click(); - } - }, 50); - } - })(); - ` : ` - (function() { - const activeEl = document.activeElement; - if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) { - activeEl.value = ${JSON.stringify(text)}; - activeEl.dispatchEvent(new Event('input', { bubbles: true })); - } - })(); - `; - - state.currentWebview.executeJavaScript(script).catch(err => { - console.log('[BigPicture] Send text error:', err); - }); - } catch (err) { - console.log('[BigPicture] sendTextToWebview error:', err); - } -} - -function handleOSKKeyboard(e) { - if (e.key === 'Escape') { - e.preventDefault(); - closeOSK(); - } else if (e.key === 'Enter') { - e.preventDefault(); - submitOSK(); - } else if (e.key === 'Backspace') { - backspaceOSK(); - } else if (e.key.length === 1) { - appendToOSK(e.key); - } -} - -// ============================================================================= -// DATA LOADING -// ============================================================================= - -async function loadData() { - await loadBookmarks(); - await loadHistory(); - renderQuickAccess(); - initSettings(); -} - -async function loadBookmarks() { - try { - if (window.bookmarksAPI && typeof window.bookmarksAPI.load === 'function') { - state.bookmarks = await window.bookmarksAPI.load() || []; - } else if (ipcRenderer && ipcRenderer.invoke) { - state.bookmarks = await ipcRenderer.invoke('load-bookmarks') || []; - } else { - // Fallback to localStorage - const stored = localStorage.getItem('bookmarks'); - state.bookmarks = stored ? JSON.parse(stored) : []; - } - renderBookmarks(); - } catch (err) { - console.error('[BigPicture] Failed to load bookmarks:', err); - state.bookmarks = []; - } -} - -async function saveBookmarks(bookmarks) { - try { - if (window.bookmarksAPI && typeof window.bookmarksAPI.save === 'function') { - await window.bookmarksAPI.save(bookmarks); - return true; - } - if (ipcRenderer && ipcRenderer.invoke) { - await ipcRenderer.invoke('save-bookmarks', bookmarks); - return true; - } - localStorage.setItem('bookmarks', JSON.stringify(bookmarks)); - return true; - } catch (err) { - console.error('[BigPicture] Failed to save bookmarks:', err); - return false; - } -} - -async function loadHistory() { - try { - if (ipcRenderer && ipcRenderer.invoke) { - state.history = await ipcRenderer.invoke('load-site-history') || []; - } else { - // Fallback to localStorage - const stored = localStorage.getItem('siteHistory'); - state.history = stored ? JSON.parse(stored) : []; - } - renderHistory(); - renderRecentSites(); - } catch (err) { - console.error('[BigPicture] Failed to load history:', err); - state.history = []; - } -} - -// Save a site to history -async function saveToHistory(url) { - if (!url || url.startsWith('nebula://')) return; - try { - if (ipcRenderer && ipcRenderer.invoke) { - await ipcRenderer.invoke('save-site-history-entry', url); - // Refresh history after saving - await loadHistory(); - } else { - // Fallback to localStorage - let history = state.history; - history = history.filter(item => item !== url); - history.unshift(url); - if (history.length > 100) history = history.slice(0, 100); - localStorage.setItem('siteHistory', JSON.stringify(history)); - state.history = history; - renderHistory(); - renderRecentSites(); - } - } catch (err) { - console.error('[BigPicture] Failed to save history:', err); - } -} - -// Clear all browsing history -async function clearHistory() { - try { - if (ipcRenderer && ipcRenderer.invoke) { - await ipcRenderer.invoke('clear-site-history'); - } else { - localStorage.removeItem('siteHistory'); - } - state.history = []; - renderHistory(); - renderRecentSites(); - showToast('History cleared'); - } catch (err) { - console.error('[BigPicture] Failed to clear history:', err); - showToast('Failed to clear history'); - } -} - -// ============================================================================= -// RENDERING -// ============================================================================= - -function renderQuickAccess() { - const grid = document.getElementById('quickAccessGrid'); - if (!grid) return; - - grid.innerHTML = ''; - - CONFIG.DEFAULT_QUICK_ACCESS.forEach(site => { - const tile = createTile(site.title, site.url, site.icon); - grid.appendChild(tile); - }); - - // Add "Add" tile - const addTile = document.createElement('div'); - addTile.className = 'tile add-tile'; - addTile.dataset.focusable = ''; - addTile.tabIndex = 0; - addTile.innerHTML = `add`; - addTile.addEventListener('click', () => startAddBookmark()); - grid.appendChild(addTile); - - updateFocusableElements(); -} - -function renderBookmarks() { - const grid = document.getElementById('bookmarksGrid'); - if (!grid) return; - - grid.innerHTML = ''; - - if (state.bookmarks.length === 0) { - grid.innerHTML = ` -
- bookmark_border -

No bookmarks yet

-

Add a bookmark here or in desktop mode

-
- `; - const addTile = createAddBookmarkTile(); - grid.appendChild(addTile); - updateFocusableElements(); - return; - } - - state.bookmarks.forEach(bookmark => { - const tile = createBookmarkTile(bookmark); - grid.appendChild(tile); - }); - - const addTile = createAddBookmarkTile(); - grid.appendChild(addTile); - - updateFocusableElements(); -} - -function createAddBookmarkTile() { - const addTile = document.createElement('div'); - addTile.className = 'tile add-tile'; - addTile.dataset.focusable = ''; - addTile.tabIndex = 0; - addTile.innerHTML = `bookmark_add`; - addTile.addEventListener('click', () => startAddBookmark()); - return addTile; -} - -function createBookmarkTile(bookmark) { - const tile = document.createElement('div'); - tile.className = 'tile bookmark-tile'; - tile.dataset.focusable = ''; - tile.tabIndex = 0; - tile.dataset.url = bookmark.url; - - const title = bookmark.title || bookmark.name || getDomainFromUrl(bookmark.url); - const icon = bookmark.icon || 'bookmark'; - - // Check if icon is a URL (favicon) or a material icon name - const isIconUrl = typeof icon === 'string' && /^(https?:|data:)/.test(icon); - - let iconHtml; - if (isIconUrl) { - iconHtml = ``; - } else { - iconHtml = `${escapeHtml(icon)}`; - } - - tile.innerHTML = ` -
- ${iconHtml} -
-
${escapeHtml(title)}
-
${getDomainFromUrl(bookmark.url)}
- `; - - tile.addEventListener('click', () => navigateTo(bookmark.url)); - - return tile; -} - -function startAddBookmark() { - state.oskContext = null; - openOSK('bookmark-url', { labelText: 'Bookmark URL' }); -} - -function addBookmarkFromCurrentPage() { - const webview = state.currentWebview; - if (!webview) { - showToast('No active page to bookmark'); - return; - } - - const url = typeof webview.getURL === 'function' ? webview.getURL() : webview.src; - if (!url) { - showToast('No active page to bookmark'); - return; - } - - const title = typeof webview.getTitle === 'function' ? webview.getTitle() : getDomainFromUrl(url); - addOrUpdateBookmark({ title, url, icon: getFaviconUrl(url) || 'bookmark' }); -} - -async function addOrUpdateBookmark(entry) { - const normalized = normalizeBookmarkUrl(entry.url); - if (!normalized) { - showToast('Enter a valid URL'); - return false; - } - - const title = (entry.title || '').trim() || getDomainFromUrl(normalized); - const icon = entry.icon || getFaviconUrl(normalized) || 'bookmark'; - - const existingIndex = state.bookmarks.findIndex(b => - (b.url || '').toLowerCase() === normalized.toLowerCase() - ); - - if (existingIndex >= 0) { - state.bookmarks[existingIndex] = { - ...state.bookmarks[existingIndex], - title, - url: normalized, - icon - }; - } else { - state.bookmarks.unshift({ title, url: normalized, icon }); - } - - const saved = await saveBookmarks(state.bookmarks); - if (saved) { - renderBookmarks(); - showToast(existingIndex >= 0 ? 'Bookmark updated' : 'Bookmark added'); - } else { - showToast('Failed to save bookmark'); - } - - return saved; -} - -function renderHistory() { - const list = document.getElementById('historyList'); - if (!list) return; - - list.innerHTML = ''; - - if (state.history.length === 0) { - list.innerHTML = ` -
- history -

No browsing history

-

Sites you visit will appear here

-
- `; - return; - } - - // Show last 30 items - state.history.slice(0, 30).forEach(url => { - const item = createHistoryItem(url); - list.appendChild(item); - }); - - updateFocusableElements(); -} - -function createHistoryItem(url) { - const item = document.createElement('div'); - item.className = 'list-item history-item'; - item.dataset.focusable = ''; - item.tabIndex = 0; - item.dataset.url = url; - - const domain = getDomainFromUrl(url); - const faviconUrl = getFaviconUrl(url); - - item.innerHTML = ` -
- - -
-
-
${escapeHtml(domain)}
-
${escapeHtml(url)}
-
-
- A -
- `; - - item.addEventListener('click', () => navigateTo(url)); - - return item; -} - -function renderRecentSites() { - const container = document.getElementById('recentSitesScroll'); - if (!container) return; - - container.innerHTML = ''; - - if (state.history.length === 0) { - container.innerHTML = ` -
- web -

Start browsing to see recent sites

-
- `; - return; - } - - // Show last 10 unique domains - const seenDomains = new Set(); - const uniqueSites = []; - - for (const url of state.history) { - const domain = getDomainFromUrl(url); - if (!seenDomains.has(domain)) { - seenDomains.add(domain); - uniqueSites.push({ url, domain }); - if (uniqueSites.length >= 10) break; - } - } - - uniqueSites.forEach(site => { - const card = createScrollCard(site.domain, site.url); - container.appendChild(card); - }); - - updateFocusableElements(); -} - -function createTile(title, url, icon, useFavicon = false) { - const tile = document.createElement('div'); - tile.className = 'tile'; - tile.dataset.focusable = ''; - tile.tabIndex = 0; - tile.dataset.url = url; - - let iconHtml; - const isIconUrl = typeof icon === 'string' && /^(https?:|data:)/.test(icon); - - if (isIconUrl || useFavicon) { - const faviconUrl = isIconUrl ? icon : getFaviconUrl(url); - iconHtml = ``; - } else { - iconHtml = `${escapeHtml(icon)}`; - } - - tile.innerHTML = ` -
- ${iconHtml} -
-
${escapeHtml(title)}
-
${getDomainFromUrl(url)}
- `; - - tile.addEventListener('click', () => navigateTo(url)); - - return tile; -} - -function getFaviconUrl(url) { - try { - const urlObj = new URL(url); - return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; - } catch { - return ''; - } -} - -function createListItem(title, url) { - const item = document.createElement('div'); - item.className = 'list-item'; - item.dataset.focusable = ''; - item.tabIndex = 0; - item.dataset.url = url; - - item.innerHTML = ` -
- public -
-
-
${escapeHtml(title)}
-
${escapeHtml(url)}
-
-
- A -
- `; - - item.addEventListener('click', () => navigateTo(url)); - - return item; -} - -function createScrollCard(title, url) { - const card = document.createElement('div'); - card.className = 'scroll-card'; - card.dataset.focusable = ''; - card.tabIndex = 0; - card.dataset.url = url; - - const faviconUrl = getFaviconUrl(url); - - card.innerHTML = ` -
- - -
-
${escapeHtml(title)}
-
Recently visited
- `; - - card.addEventListener('click', () => navigateTo(url)); - - return card; -} - -// ============================================================================= -// ACTIONS -// ============================================================================= - -function performSearch(query) { - if (!query.trim()) return; - - // Check if it's a URL - let url = query.trim(); - if (isUrl(url)) { - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url; - } - navigateTo(url); - } else { - // Search with default engine (Google) - navigateTo(`https://www.google.com/search?q=${encodeURIComponent(query)}`); - } -} - -function navigateTo(url) { - console.log('[BigPicture] Navigating to:', url); - - // Create or reuse webview for browsing - const container = document.getElementById('webview-container'); - if (!container) return; - - // Hide content and show webview - document.querySelectorAll('.bp-section').forEach(s => s.classList.remove('active')); - container.classList.remove('hidden'); - - // Remove existing webview if any - const existingWebview = container.querySelector('webview'); - if (existingWebview) { - existingWebview.remove(); - } - - // Create new webview - const webview = document.createElement('webview'); - webview.src = url; - webview.style.width = '100%'; - webview.style.height = '100%'; - webview.style.border = 'none'; - const preloadPath = window.electronAPI?.getWebviewPreloadPath?.(); - if (preloadPath) { - webview.setAttribute('preload', preloadPath); - } else { - webview.setAttribute('preload', '../preload.js'); - } - webview.partition = 'persist:main'; - webview.allowpopups = true; - webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true'); - - container.appendChild(webview); - state.currentWebview = webview; - state.webviewContentsId = null; // Will be set when webview is ready - - // Save initial URL to history - saveToHistory(url); - - // Get webContentsId when webview is ready for native input events - webview.addEventListener('dom-ready', () => { - try { - // getWebContentsId is available on webview element - state.webviewContentsId = webview.getWebContentsId(); - console.log('[BigPicture] WebContents ID:', state.webviewContentsId); - - // Apply scroll normalization for consistent scroll speed - applyScrollNormalization(webview); - - // Inject script to detect input field focus and notify the host - injectInputFocusDetection(webview); - } catch (err) { - console.log('[BigPicture] Could not get webContentsId:', err); - } - }); - - // Save navigation to history - webview.addEventListener('did-navigate', (event) => { - const newUrl = event.url; - if (newUrl && !newUrl.startsWith('about:')) { - saveToHistory(newUrl); - } - }); - - // Also save history on in-page navigations (e.g., SPA navigations) - webview.addEventListener('did-navigate-in-page', (event) => { - if (event.isMainFrame) { - const newUrl = event.url; - if (newUrl && !newUrl.startsWith('about:')) { - saveToHistory(newUrl); - } - } - }); - - // Listen for IPC messages from webview (for OSK requests) - webview.addEventListener('ipc-message', (event) => { - if (event.channel === 'bigpicture-input-focused') { - // Input field was clicked/focused in webview - show OSK for webview input - console.log('[BigPicture] Input focused in webview'); - openOSKForWebview(); - } - }); - - // Enable virtual cursor for webview interaction - enableCursor(); - - // Switch section to browse - switchSection('browse'); - - // Update focusable elements to include webview controls - setTimeout(() => { - updateFocusableElements(); - }, 100); -} - -/** - * Inject script to detect input focus in webview and send message to host - */ -function injectInputFocusDetection(webview) { - const script = ` - (function() { - if (window.__bigPictureInputDetection) return; - window.__bigPictureInputDetection = true; - - // Track the last focused input - let lastFocusedInput = null; - - document.addEventListener('focusin', (e) => { - const el = e.target; - const isInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || - el.contentEditable === 'true' || el.isContentEditable || - el.getAttribute('role') === 'textbox' || el.getAttribute('role') === 'searchbox'; - - // Check input type - exclude non-text inputs - if (el.tagName === 'INPUT') { - const type = el.type.toLowerCase(); - if (['checkbox', 'radio', 'submit', 'button', 'image', 'file', 'hidden', 'reset', 'range', 'color'].includes(type)) { - return; - } - } - - if (isInput) { - lastFocusedInput = el; - // Send message to host (Big Picture Mode) to show OSK - try { - if (window.electronAPI && window.electronAPI.sendToHost) { - window.electronAPI.sendToHost('bigpicture-input-focused', { - type: el.tagName, - inputType: el.type || 'text', - value: el.value || '' - }); - } - } catch(e) { - console.log('BigPicture: Could not notify input focus', e); - } - } - }, true); - - // Listen for text input from OSK - window.addEventListener('message', (e) => { - if (e.data && e.data.type === 'bigpicture-osk-input' && lastFocusedInput) { - lastFocusedInput.value = e.data.value; - lastFocusedInput.dispatchEvent(new Event('input', { bubbles: true })); - lastFocusedInput.dispatchEvent(new Event('change', { bubbles: true })); - } else if (e.data && e.data.type === 'bigpicture-osk-submit' && lastFocusedInput) { - // Submit the form or trigger search - const form = lastFocusedInput.closest('form'); - if (form) { - form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); - // Also try clicking any submit button - const submitBtn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])'); - if (submitBtn) submitBtn.click(); - } - // Trigger Enter key event - lastFocusedInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', keyCode: 13, bubbles: true })); - lastFocusedInput.dispatchEvent(new KeyboardEvent('keypress', { key: 'Enter', keyCode: 13, bubbles: true })); - lastFocusedInput.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true })); - } - }); - - console.log('[BigPicture] Input focus detection injected'); - })(); - `; - - webview.executeJavaScript(script).catch(err => { - console.log('[BigPicture] Could not inject input detection:', err); - }); -} - -function exitBigPictureMode() { - console.log('[BigPicture] Exiting Big Picture Mode'); - - if (ipcRenderer) { - ipcRenderer.send('exit-bigpicture'); - } else if (window.opener) { - window.opener.postMessage({ type: 'exit-bigpicture' }, '*'); - window.close(); - } -} - -function handleSettingsAction(action) { - switch (action) { - case 'theme': - switchSettingsTab('themes'); - break; - case 'privacy': - switchSettingsTab('privacy'); - break; - case 'display': - switchSettingsTab('display'); - break; - case 'exit-bigpicture': - exitBigPictureMode(); - break; - default: - console.log('[BigPicture] Unknown settings action:', action); - } -} - -// ============================================================================= -// SETTINGS FUNCTIONALITY -// ============================================================================= - -const DISPLAY_SCALE_KEY = 'nebula-display-scale'; -let currentDisplayScale = 100; -let currentThemeName = 'default'; - -// Theme definitions (matching customization.js) -const THEMES = { - default: { - name: 'Default', - colors: { - bg: '#121418', - darkPurple: '#1B1035', - primary: '#7B2EFF', - accent: '#00C6FF', - text: '#E0E0E0' - } - }, - ocean: { - name: 'Ocean', - colors: { - bg: '#1a365d', - darkPurple: '#2c5282', - primary: '#3182ce', - accent: '#00d9ff', - text: '#e2e8f0' - } - }, - forest: { - name: 'Forest', - colors: { - bg: '#1a202c', - darkPurple: '#2d3748', - primary: '#68d391', - accent: '#9ae6b4', - text: '#f7fafc' - } - }, - sunset: { - name: 'Sunset', - colors: { - bg: '#744210', - darkPurple: '#c05621', - primary: '#ed8936', - accent: '#fbb040', - text: '#fffaf0' - } - }, - cyberpunk: { - name: 'Cyberpunk', - colors: { - bg: '#0a0a0a', - darkPurple: '#2a0a3a', - primary: '#ff0080', - accent: '#00ffff', - text: '#ffffff' - } - }, - 'midnight-rose': { - name: 'Midnight Rose', - colors: { - bg: '#1c1820', - darkPurple: '#3d3046', - primary: '#d4af37', - accent: '#ffd700', - text: '#f5f5dc' - } - }, - 'arctic-ice': { - name: 'Arctic Ice', - colors: { - bg: '#f0f8ff', - darkPurple: '#d1e7ff', - primary: '#4169e1', - accent: '#87ceeb', - text: '#2f4f4f' - } - }, - 'cherry-blossom': { - name: 'Cherry Blossom', - colors: { - bg: '#fff5f8', - darkPurple: '#ffd4db', - primary: '#ff69b4', - accent: '#ffb6c1', - text: '#8b4513' - } - }, - 'cosmic-purple': { - name: 'Cosmic Purple', - colors: { - bg: '#0f0524', - darkPurple: '#2d1b69', - primary: '#9400d3', - accent: '#da70d6', - text: '#e6e6fa' - } - }, - 'emerald-dream': { - name: 'Emerald Dream', - colors: { - bg: '#0d2818', - darkPurple: '#2d5a44', - primary: '#50c878', - accent: '#00fa9a', - text: '#f0fff0' - } - }, - 'mocha-coffee': { - name: 'Mocha Coffee', - colors: { - bg: '#3c2414', - darkPurple: '#5d3a26', - primary: '#d2691e', - accent: '#deb887', - text: '#faf0e6' - } - }, - 'lavender-fields': { - name: 'Lavender Fields', - colors: { - bg: '#f8f4ff', - darkPurple: '#e6d8ff', - primary: '#9370db', - accent: '#dda0dd', - text: '#4b0082' - } - } -}; - -function initSettings() { - console.log('[BigPicture] Initializing settings...'); - - // Load saved settings - loadSavedSettings(); - - // Initialize settings tabs - initSettingsTabs(); - - // Initialize theme selection - initThemeSelection(); - - // Initialize display scale controls - initDisplayScaleControls(); - - // Initialize privacy controls - initPrivacyControls(); - - // Initialize about panel - initAboutPanel(); -} - -function loadSavedSettings() { - // Load display scale - try { - const savedScale = localStorage.getItem(DISPLAY_SCALE_KEY); - if (savedScale) { - const parsed = parseInt(savedScale, 10); - if (Number.isFinite(parsed)) { - currentDisplayScale = Math.min(300, Math.max(50, parsed)); - updateScaleDisplay(); - applyDisplayScale(currentDisplayScale, 'loadSavedSettings'); - } - } - } catch (err) { - console.warn('[BigPicture] Failed to load display scale:', err); - } - - // Load theme - try { - const savedTheme = localStorage.getItem('nebula-theme-name'); - if (savedTheme && THEMES[savedTheme]) { - currentThemeName = savedTheme; - applyTheme(THEMES[savedTheme]); - highlightActiveTheme(); - } - } catch (err) { - console.warn('[BigPicture] Failed to load theme:', err); - } -} - -function initSettingsTabs() { - document.querySelectorAll('.settings-tab').forEach(tab => { - tab.addEventListener('click', () => { - const tabName = tab.dataset.settingsTab; - if (tabName) { - switchSettingsTab(tabName); - } - }); - }); -} - -function switchSettingsTab(tabName) { - // Update tab buttons - document.querySelectorAll('.settings-tab').forEach(tab => { - tab.classList.toggle('active', tab.dataset.settingsTab === tabName); - }); - - // Update panels - document.querySelectorAll('.settings-panel').forEach(panel => { - panel.classList.toggle('active', panel.id === `settings-panel-${tabName}`); - }); - - // Update focusable elements - setTimeout(() => { - updateFocusableElements(); - }, 50); - - playNavSound(); -} - -function initThemeSelection() { - document.querySelectorAll('.theme-card').forEach(card => { - card.addEventListener('click', () => { - const themeName = card.dataset.theme; - if (themeName && THEMES[themeName]) { - selectTheme(themeName); - } - }); - }); - - // Highlight current theme - highlightActiveTheme(); -} - -function selectTheme(themeName) { - if (!THEMES[themeName]) return; - - currentThemeName = themeName; - const theme = THEMES[themeName]; - - // Apply theme locally - applyTheme(theme); - - // Save to localStorage - try { - localStorage.setItem('nebula-theme-name', themeName); - - // Also save the full theme data for other pages - const fullThemeData = { - name: theme.name, - colors: { - bg: theme.colors.bg, - darkBlue: theme.colors.darkPurple, - darkPurple: theme.colors.darkPurple, - primary: theme.colors.primary, - accent: theme.colors.accent, - text: theme.colors.text, - urlBarBg: theme.colors.darkPurple, - urlBarText: theme.colors.text, - urlBarBorder: theme.colors.primary, - tabBg: theme.colors.darkPurple, - tabText: theme.colors.text, - tabActive: theme.colors.bg, - tabActiveText: theme.colors.text, - tabBorder: theme.colors.bg - }, - gradient: `linear-gradient(145deg, ${theme.colors.bg} 0%, ${theme.colors.darkPurple} 100%)` - }; - localStorage.setItem('browserTheme', JSON.stringify(fullThemeData)); - } catch (err) { - console.warn('[BigPicture] Failed to save theme:', err); - } - - // Notify main process - if (ipcRenderer && ipcRenderer.send) { - ipcRenderer.send('theme-changed', { - name: themeName, - colors: theme.colors - }); - } - - highlightActiveTheme(); - showToast(`Theme changed to ${theme.name}`); - playSelectSound(); -} - -function highlightActiveTheme() { - document.querySelectorAll('.theme-card').forEach(card => { - card.classList.toggle('active', card.dataset.theme === currentThemeName); - }); -} - -function initDisplayScaleControls() { - const scaleDown = document.getElementById('bp-scale-down'); - const scaleUp = document.getElementById('bp-scale-up'); - const exitDesktop = document.getElementById('bp-exit-desktop'); - - if (scaleDown) { - scaleDown.addEventListener('click', () => { - adjustDisplayScale(-10); - }); - } - - if (scaleUp) { - scaleUp.addEventListener('click', () => { - adjustDisplayScale(10); - }); - } - - if (exitDesktop) { - exitDesktop.addEventListener('click', () => { - exitBigPictureMode(); - }); - } - - updateScaleDisplay(); - applyDisplayScale(currentDisplayScale, 'initDisplayScaleControls'); -} - -function adjustDisplayScale(delta) { - const newScale = Math.min(300, Math.max(50, currentDisplayScale + delta)); - if (newScale !== currentDisplayScale) { - currentDisplayScale = newScale; - updateScaleDisplay(); - saveDisplayScale(); - showToast(`Display scale: ${currentDisplayScale}%`); - playNavSound(); - } -} - -function updateScaleDisplay() { - const scaleValue = document.getElementById('bp-scale-value'); - if (scaleValue) { - scaleValue.textContent = `${currentDisplayScale}%`; - } -} - -function saveDisplayScale() { - try { - localStorage.setItem(DISPLAY_SCALE_KEY, currentDisplayScale.toString()); - - // Apply zoom immediately to Big Picture UI. - applyDisplayScale(currentDisplayScale, 'saveDisplayScale'); - - // Notify main process (legacy channel) for compatibility. - if (ipcRenderer && typeof ipcRenderer.send === 'function') { - ipcRenderer.send('set-display-scale', currentDisplayScale); - } - } catch (err) { - console.warn('[BigPicture] Failed to save display scale:', err); - } -} - -function initPrivacyControls() { - const clearDataBtn = document.getElementById('bp-clear-data'); - const clearHistoryBtn = document.getElementById('bp-clear-history'); - const clearSearchBtn = document.getElementById('bp-clear-search'); - - if (clearDataBtn) { - clearDataBtn.addEventListener('click', async () => { - if (await confirmAction('Clear all browsing data? This cannot be undone.')) { - await clearAllBrowsingData(); - } - }); - } - - if (clearHistoryBtn) { - clearHistoryBtn.addEventListener('click', async () => { - if (await confirmAction('Clear browsing history?')) { - await clearBrowsingHistory(); - } - }); - } - - if (clearSearchBtn) { - clearSearchBtn.addEventListener('click', async () => { - if (await confirmAction('Clear search history?')) { - await clearSearchHistory(); - } - }); - } -} - -async function confirmAction(message) { - // Simple confirmation using toast - could be enhanced with a modal - showToast(message + ' Press A to confirm.'); - return true; // For now, auto-confirm. Could implement modal confirmation. -} - -async function clearAllBrowsingData() { - try { - showToast('Clearing all browsing data...'); - - if (ipcRenderer && ipcRenderer.invoke) { - await ipcRenderer.invoke('clear-browser-data'); - } - - // Also clear localStorage - localStorage.removeItem('siteHistory'); - state.history = []; - renderHistory(); - renderRecentSites(); - - showToast('All browsing data cleared'); - playSelectSound(); - } catch (err) { - console.error('[BigPicture] Failed to clear browsing data:', err); - showToast('Failed to clear data'); - } -} - -async function clearBrowsingHistory() { - try { - if (ipcRenderer && ipcRenderer.invoke) { - await ipcRenderer.invoke('clear-site-history'); - } - - localStorage.removeItem('siteHistory'); - state.history = []; - renderHistory(); - renderRecentSites(); - - showToast('Browsing history cleared'); - playSelectSound(); - } catch (err) { - console.error('[BigPicture] Failed to clear history:', err); - showToast('Failed to clear history'); - } -} - -async function clearSearchHistory() { - try { - if (ipcRenderer && ipcRenderer.invoke) { - await ipcRenderer.invoke('clear-search-history'); - } - - showToast('Search history cleared'); - playSelectSound(); - } catch (err) { - console.error('[BigPicture] Failed to clear search history:', err); - showToast('Failed to clear search history'); - } -} - -async function initAboutPanel() { - // Load version info - try { - if (ipcRenderer && ipcRenderer.invoke) { - const appInfo = await ipcRenderer.invoke('get-app-info'); - - if (appInfo) { - const versionEl = document.getElementById('bp-version'); - const electronEl = document.getElementById('bp-electron-version'); - const chromiumEl = document.getElementById('bp-chromium-version'); - const nodeEl = document.getElementById('bp-node-version'); - const platformEl = document.getElementById('bp-platform'); - - if (versionEl) versionEl.textContent = `Version ${appInfo.version || 'Unknown'}`; - if (electronEl) electronEl.textContent = appInfo.electron || '--'; - if (chromiumEl) chromiumEl.textContent = appInfo.chrome || '--'; - if (nodeEl) nodeEl.textContent = appInfo.node || '--'; - if (platformEl) platformEl.textContent = `${appInfo.platform || ''} ${appInfo.arch || ''}`.trim() || '--'; - } - } - } catch (err) { - console.warn('[BigPicture] Failed to load app info:', err); - } - - // GitHub link - const githubBtn = document.getElementById('bp-github-link'); - if (githubBtn) { - githubBtn.addEventListener('click', () => { - navigateTo('https://github.com/Bobbybear007/NebulaBrowser'); - }); - } - - // Copy diagnostics - const copyBtn = document.getElementById('bp-copy-diagnostics'); - if (copyBtn) { - copyBtn.addEventListener('click', async () => { - await copyDiagnostics(); - }); - } -} - -async function copyDiagnostics() { - try { - const versionEl = document.getElementById('bp-version'); - const electronEl = document.getElementById('bp-electron-version'); - const chromiumEl = document.getElementById('bp-chromium-version'); - const nodeEl = document.getElementById('bp-node-version'); - const platformEl = document.getElementById('bp-platform'); - - const diagnostics = [ - 'Nebula Browser Diagnostics', - '========================', - versionEl ? versionEl.textContent : '', - `Electron: ${electronEl ? electronEl.textContent : '--'}`, - `Chromium: ${chromiumEl ? chromiumEl.textContent : '--'}`, - `Node.js: ${nodeEl ? nodeEl.textContent : '--'}`, - `Platform: ${platformEl ? platformEl.textContent : '--'}`, - `Date: ${new Date().toISOString()}` - ].join('\n'); - - await navigator.clipboard.writeText(diagnostics); - showToast('Diagnostics copied to clipboard'); - playSelectSound(); - } catch (err) { - console.error('[BigPicture] Failed to copy diagnostics:', err); - showToast('Failed to copy diagnostics'); - } -} - -// ============================================================================= -// UTILITIES -// ============================================================================= - -function normalizeBookmarkUrl(raw) { - if (!raw || !raw.trim()) return null; - let url = raw.trim(); - - if (url.startsWith('nebula://')) return url; - - // Add protocol if missing - if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) { - url = `https://${url}`; - } - - if (!isUrl(url)) return null; - return url; -} - -function isUrl(str) { - // Simple URL detection - return /^(https?:\/\/)?[\w-]+(\.[\w-]+)+/.test(str) || - str.includes('.com') || - str.includes('.org') || - str.includes('.net') || - str.includes('.io') || - str.startsWith('nebula://'); -} - -// ============================================================================= -// VIRTUAL CURSOR (for webview interaction) -// ============================================================================= - -function createCursorElement() { - if (state.cursorElement) return; - - const cursor = document.createElement('div'); - cursor.id = 'virtual-cursor'; - cursor.className = 'virtual-cursor'; - cursor.innerHTML = ` - - - -
- `; - document.body.appendChild(cursor); - state.cursorElement = cursor; -} - -function enableCursor() { - if (!state.cursorElement) { - createCursorElement(); - } - - const container = document.getElementById('webview-container'); - if (container) { - const rect = container.getBoundingClientRect(); - state.cursorX = rect.left + rect.width / 2; - state.cursorY = rect.top + rect.height / 2; - } else { - state.cursorX = window.innerWidth / 2; - state.cursorY = window.innerHeight / 2; - } - - state.cursorEnabled = true; - updateCursorPosition(); - state.cursorElement.classList.add('active'); - - // Update focusable elements to only include sidebar when in webview mode - updateFocusableElements(); - - // Show cursor hint - showToast('๐ŸŽฎ Right stick: Move cursor | RT: Click | Left stick: Scroll | B: Back'); -} - -function disableCursor() { - state.cursorEnabled = false; - if (state.cursorElement) { - state.cursorElement.classList.remove('active'); - } - - // Restore full focusable elements - updateFocusableElements(); -} - -function moveCursor(dx, dy) { - if (!state.cursorEnabled) return; - - const container = document.getElementById('webview-container'); - if (!container) return; - - const rect = container.getBoundingClientRect(); - - // Update cursor position with bounds checking - state.cursorX = Math.max(rect.left, Math.min(rect.right - 10, state.cursorX + dx)); - state.cursorY = Math.max(rect.top, Math.min(rect.bottom - 10, state.cursorY + dy)); - - updateCursorPosition(); -} - -function updateCursorPosition() { - if (!state.cursorElement) return; - - state.cursorElement.style.left = `${state.cursorX}px`; - state.cursorElement.style.top = `${state.cursorY}px`; -} - -function virtualClick(rightClick = false) { - if (!state.currentWebview || !state.cursorEnabled) return; - - const container = document.getElementById('webview-container'); - if (!container) return; - - const containerRect = container.getBoundingClientRect(); - - // Calculate position relative to webview - const x = Math.round(state.cursorX - containerRect.left); - const y = Math.round(state.cursorY - containerRect.top); - - // Show click animation - if (state.cursorElement) { - state.cursorElement.classList.add('clicking'); - setTimeout(() => state.cursorElement.classList.remove('clicking'), 150); - } - - const webview = state.currentWebview; - - // Try to use native input event injection via IPC (most reliable for complex sites) - if (state.webviewContentsId && window.bigPictureAPI && window.bigPictureAPI.sendInputEvent) { - const sendNativeClick = async () => { - try { - // Send mouseMove first to position the cursor - await window.bigPictureAPI.sendInputEvent(state.webviewContentsId, { - type: 'mouseMove', - x: x, - y: y - }); - - // Small delay then send mouseDown - await new Promise(r => setTimeout(r, 10)); - - await window.bigPictureAPI.sendInputEvent(state.webviewContentsId, { - type: 'mouseDown', - x: x, - y: y, - button: rightClick ? 'right' : 'left', - clickCount: 1 - }); - - // Small delay then send mouseUp - await new Promise(r => setTimeout(r, 50)); - - await window.bigPictureAPI.sendInputEvent(state.webviewContentsId, { - type: 'mouseUp', - x: x, - y: y, - button: rightClick ? 'right' : 'left', - clickCount: 1 - }); - - console.log('[BigPicture] Native click sent at', x, y); - } catch (err) { - console.log('[BigPicture] Native input error, falling back to JS:', err); - fallbackJavaScriptClick(webview, x, y, rightClick); - } - }; - - sendNativeClick(); - return; - } - - // Fallback to JavaScript injection - fallbackJavaScriptClick(webview, x, y, rightClick); -} - -function fallbackJavaScriptClick(webview, x, y, rightClick) { - try { - if (rightClick) { - // For right-click, use JavaScript injection - const rightClickScript = ` - (function() { - const el = document.elementFromPoint(${x}, ${y}); - if (el) { - const event = new MouseEvent('contextmenu', { - bubbles: true, - cancelable: true, - clientX: ${x}, - clientY: ${y}, - button: 2 - }); - el.dispatchEvent(event); - } - })(); - `; - webview.executeJavaScript(rightClickScript).catch(err => { - console.log('[BigPicture] Right-click injection error:', err); - }); - } else { - // Comprehensive JavaScript injection with pointer events - const clickScript = ` - (function() { - const x = ${x}; - const y = ${y}; - const el = document.elementFromPoint(x, y); - if (!el) return; - - // Check if we're clicking on YouTube player area - const isYouTubePlayer = el.closest('.html5-video-player') || - el.closest('.ytp-player') || - el.closest('#movie_player') || - el.closest('.html5-main-video') || - el.closest('.video-stream') || - (window.location.hostname.includes('youtube.com') && - (el.tagName === 'VIDEO' || el.closest('#player'))); - - if (isYouTubePlayer) { - // For YouTube player, directly toggle playback - const video = document.querySelector('video.html5-main-video') || - document.querySelector('video.video-stream') || - document.querySelector('#movie_player video') || - document.querySelector('video'); - if (video) { - if (video.paused) { - video.play().catch(() => {}); - } else { - video.pause(); - } - return; - } - } - - // Find the actual clickable element (may be parent) - let clickTarget = el; - let current = el; - for (let i = 0; i < 10 && current; i++) { - if (current.tagName === 'A' || current.tagName === 'BUTTON' || - current.onclick || current.getAttribute('role') === 'button' || - window.getComputedStyle(current).cursor === 'pointer') { - clickTarget = current; - break; - } - current = current.parentElement; - } - - // Common event options - const eventOptions = { - bubbles: true, - cancelable: true, - view: window, - clientX: x, - clientY: y, - screenX: x, - screenY: y, - button: 0, - buttons: 1, - pointerId: 1, - pointerType: 'mouse', - isPrimary: true, - pressure: 0.5, - width: 1, - height: 1 - }; - - // Handle input elements specially - focus first - const isInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || - el.contentEditable === 'true' || el.isContentEditable || - el.getAttribute('role') === 'textbox' || el.getAttribute('role') === 'searchbox' || - el.closest('[contenteditable="true"]'); - - if (isInput) { - // Focus the input element - el.focus(); - // Dispatch proper focus sequence - el.dispatchEvent(new FocusEvent('focus', { bubbles: true })); - el.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); - // Dispatch click to activate any click handlers - el.dispatchEvent(new MouseEvent('click', eventOptions)); - return; - } - - // For general video elements (not YouTube specific) - if (el.tagName === 'VIDEO') { - if (el.paused) { - el.play().catch(() => {}); - } else { - el.pause(); - } - return; - } - - // Dispatch pointer events (used by modern sites) - try { - clickTarget.dispatchEvent(new PointerEvent('pointerdown', eventOptions)); - clickTarget.dispatchEvent(new PointerEvent('pointerup', eventOptions)); - } catch(e) {} - - // Dispatch mouse events - clickTarget.dispatchEvent(new MouseEvent('mousedown', eventOptions)); - clickTarget.dispatchEvent(new MouseEvent('mouseup', eventOptions)); - clickTarget.dispatchEvent(new MouseEvent('click', eventOptions)); - - // Direct click as final fallback - if (clickTarget.click) clickTarget.click(); - })(); - `; - - webview.executeJavaScript(clickScript).catch(err => { - console.log('[BigPicture] Click injection error:', err); - }); - } - } catch (err) { - console.log('[BigPicture] Virtual click error:', err); - } -} - -function scrollWebview(amountY, amountX = 0) { - if (!state.currentWebview) return; - - try { - state.currentWebview.executeJavaScript(`window.scrollBy(${amountX}, ${amountY})`); - } catch (err) { - console.log('[BigPicture] Scroll error:', err); - } -} - -// ============================================================================= -// UTILITIES -// ============================================================================= - -function getDomainFromUrl(url) { - try { - if (url.startsWith('nebula://')) { - return url.replace('nebula://', '').split('/')[0]; - } - const hostname = new URL(url).hostname; - return hostname.replace(/^www\./, ''); - } catch { - return url; - } -} - -function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; -} - -function showToast(message) { - // Remove existing toast - const existing = document.querySelector('.toast'); - if (existing) existing.remove(); - - const toast = document.createElement('div'); - toast.className = 'toast'; - toast.textContent = message; - document.body.appendChild(toast); - - setTimeout(() => toast.remove(), 3000); -} - -function playNavSound() { - if (!CONFIG.NAV_SOUND_ENABLED) return; - - // Simple beep using Web Audio API - try { - const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - const oscillator = audioCtx.createOscillator(); - const gainNode = audioCtx.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(audioCtx.destination); - - oscillator.frequency.value = 800; - oscillator.type = 'sine'; - gainNode.gain.value = 0.05; - - oscillator.start(); - oscillator.stop(audioCtx.currentTime + 0.03); - } catch (e) { - // Audio not available - } -} - -function playSelectSound() { - if (!CONFIG.NAV_SOUND_ENABLED) return; - - try { - const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - const oscillator = audioCtx.createOscillator(); - const gainNode = audioCtx.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(audioCtx.destination); - - oscillator.frequency.value = 1200; - oscillator.type = 'sine'; - gainNode.gain.value = 0.08; - - oscillator.start(); - oscillator.stop(audioCtx.currentTime + 0.05); - } catch (e) { - // Audio not available - } -} - -// ============================================================================= -// IPC HANDLERS -// ============================================================================= - -if (ipcRenderer) { - // Listen for theme changes - ipcRenderer.on('theme-changed', (theme) => { - if (theme && theme.colors) { - applyTheme(theme); - } - }); -} - -function applyTheme(theme) { - if (!theme || !theme.colors) return; - - const root = document.documentElement; - - if (theme.colors.bg) root.style.setProperty('--bp-bg', theme.colors.bg); - if (theme.colors.darkPurple) root.style.setProperty('--bp-surface', theme.colors.darkPurple); - if (theme.colors.primary) { - root.style.setProperty('--bp-primary', theme.colors.primary); - root.style.setProperty('--bp-primary-glow', `${theme.colors.primary}66`); - } - if (theme.colors.accent) { - root.style.setProperty('--bp-accent', theme.colors.accent); - root.style.setProperty('--bp-accent-glow', `${theme.colors.accent}4d`); - } - if (theme.colors.text) root.style.setProperty('--bp-text', theme.colors.text); -} - -console.log('[BigPicture] Module loaded'); diff --git a/renderer/customization.js b/renderer/customization.js deleted file mode 100644 index 1576f28..0000000 --- a/renderer/customization.js +++ /dev/null @@ -1,849 +0,0 @@ -/** - * Browser Customization System - * Allows users to customize themes, colors, and layouts non-destructively - */ - -class BrowserCustomizer { - constructor() { - this.defaultTheme = { - name: 'Default', - colors: { - bg: '#121418', - darkBlue: '#0B1C2B', - darkPurple: '#1B1035', - primary: '#7B2EFF', - accent: '#00C6FF', - text: '#E0E0E0', - urlBarBg: '#1C2030', - urlBarText: '#E0E0E0', - urlBarBorder: '#3E4652', - tabBg: '#161925', - tabText: '#A4A7B3', - tabActive: '#1C2030', - tabActiveText: '#E0E0E0', - tabBorder: '#2B3040' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #121418 0%, #1B1035 100%)' - }; - - this.predefinedThemes = { - default: this.defaultTheme, - ocean: { - name: 'Ocean', - colors: { - bg: '#1a365d', - darkBlue: '#2a4365', - darkPurple: '#2c5282', - primary: '#3182ce', - accent: '#00d9ff', - text: '#e2e8f0', - urlBarBg: '#2d5282', - urlBarText: '#e2e8f0', - urlBarBorder: '#1e3a5f', - tabBg: '#2a4365', - tabText: '#cbd5e0', - tabActive: '#2d5282', - tabActiveText: '#e2e8f0', - tabBorder: '#1a365d' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #1a365d 0%, #2c5282 100%)' - }, - forest: { - name: 'Forest', - colors: { - bg: '#1a202c', - darkBlue: '#2d3748', - darkPurple: '#4a5568', - primary: '#68d391', - accent: '#9ae6b4', - text: '#f7fafc', - urlBarBg: '#2d3748', - urlBarText: '#f7fafc', - urlBarBorder: '#4a5568', - tabBg: '#2d3748', - tabText: '#cbd5e0', - tabActive: '#4a5568', - tabActiveText: '#f7fafc', - tabBorder: '#1a202c' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #1a202c 0%, #2d3748 100%)' - }, - sunset: { - name: 'Sunset', - colors: { - bg: '#744210', - darkBlue: '#975a16', - darkPurple: '#c05621', - primary: '#ed8936', - accent: '#fbb040', - text: '#fffaf0', - urlBarBg: '#975a16', - urlBarText: '#fffaf0', - urlBarBorder: '#c05621', - tabBg: '#975a16', - tabText: '#fde4b6', - tabActive: '#c05621', - tabActiveText: '#fffaf0', - tabBorder: '#744210' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #744210 0%, #c05621 100%)' - }, - cyberpunk: { - name: 'Cyberpunk Neon', - colors: { - bg: '#0a0a0a', - darkBlue: '#1a0520', - darkPurple: '#2a0a3a', - primary: '#ff0080', - accent: '#00ffff', - text: '#ffffff', - urlBarBg: '#1a0520', - urlBarText: '#ffffff', - urlBarBorder: '#ff0080', - tabBg: '#1a0520', - tabText: '#00ffff', - tabActive: '#2a0a3a', - tabActiveText: '#ff0080', - tabBorder: '#ff0080' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #0a0a0a 0%, #2a0a3a 50%, #1a0520 100%)' - }, - 'midnight-rose': { - name: 'Midnight Rose', - colors: { - bg: '#1c1820', - darkBlue: '#2d2433', - darkPurple: '#3d3046', - primary: '#d4af37', - accent: '#ffd700', - text: '#f5f5dc', - urlBarBg: '#3d3046', - urlBarText: '#f5f5dc', - urlBarBorder: '#d4af37', - tabBg: '#2d2433', - tabText: '#d4af37', - tabActive: '#3d3046', - tabActiveText: '#ffd700', - tabBorder: '#1c1820' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #1c1820 0%, #3d3046 100%)' - }, - 'arctic-ice': { - name: 'Arctic Ice', - colors: { - bg: '#f0f8ff', - darkBlue: '#e6f3ff', - darkPurple: '#d1e7ff', - primary: '#4169e1', - accent: '#87ceeb', - text: '#2f4f4f', - urlBarBg: '#e6f3ff', - urlBarText: '#2f4f4f', - urlBarBorder: '#4169e1', - tabBg: '#e6f3ff', - tabText: '#4169e1', - tabActive: '#d1e7ff', - tabActiveText: '#2f4f4f', - tabBorder: '#f0f8ff' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #f0f8ff 0%, #d1e7ff 100%)' - }, - 'cherry-blossom': { - name: 'Cherry Blossom', - colors: { - bg: '#fff5f8', - darkBlue: '#ffe4e8', - darkPurple: '#ffd4db', - primary: '#ff69b4', - accent: '#ffb6c1', - text: '#8b4513', - urlBarBg: '#ffe4e8', - urlBarText: '#8b4513', - urlBarBorder: '#ff69b4', - tabBg: '#ffe4e8', - tabText: '#ff69b4', - tabActive: '#ffd4db', - tabActiveText: '#8b4513', - tabBorder: '#fff5f8' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #fff5f8 0%, #ffd4db 100%)' - }, - 'cosmic-purple': { - name: 'Cosmic Purple', - colors: { - bg: '#0f0524', - darkBlue: '#1a0b3d', - darkPurple: '#2d1b69', - primary: '#8a2be2', - accent: '#da70d6', - text: '#e6e6fa', - urlBarBg: '#1a0b3d', - urlBarText: '#e6e6fa', - urlBarBorder: '#8a2be2', - tabBg: '#1a0b3d', - tabText: '#da70d6', - tabActive: '#2d1b69', - tabActiveText: '#e6e6fa', - tabBorder: '#0f0524' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #0f0524 0%, #2d1b69 50%, #4b0082 100%)' - }, - 'emerald-dream': { - name: 'Emerald Dream', - colors: { - bg: '#0d2818', - darkBlue: '#1a3a2e', - darkPurple: '#2d5a44', - primary: '#50c878', - accent: '#98fb98', - text: '#f0fff0', - urlBarBg: '#1a3a2e', - urlBarText: '#f0fff0', - urlBarBorder: '#50c878', - tabBg: '#1a3a2e', - tabText: '#98fb98', - tabActive: '#2d5a44', - tabActiveText: '#f0fff0', - tabBorder: '#0d2818' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #0d2818 0%, #2d5a44 100%)' - }, - 'mocha-coffee': { - name: 'Mocha Coffee', - colors: { - bg: '#3c2414', - darkBlue: '#4a2c1a', - darkPurple: '#5d3a26', - primary: '#d2691e', - accent: '#daa520', - text: '#faf0e6', - urlBarBg: '#4a2c1a', - urlBarText: '#faf0e6', - urlBarBorder: '#d2691e', - tabBg: '#4a2c1a', - tabText: '#daa520', - tabActive: '#5d3a26', - tabActiveText: '#faf0e6', - tabBorder: '#3c2414' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #3c2414 0%, #5d3a26 100%)' - }, - 'lavender-fields': { - name: 'Lavender Fields', - colors: { - bg: '#f8f4ff', - darkBlue: '#ede4ff', - darkPurple: '#e6d8ff', - primary: '#9370db', - accent: '#dda0dd', - text: '#4b0082', - urlBarBg: '#ede4ff', - urlBarText: '#4b0082', - urlBarBorder: '#9370db', - tabBg: '#ede4ff', - tabText: '#9370db', - tabActive: '#e6d8ff', - tabActiveText: '#4b0082', - tabBorder: '#f8f4ff' - }, - layout: 'centered', - showLogo: true, - customTitle: 'Nebula Browser', - gradient: 'linear-gradient(145deg, #f8f4ff 0%, #e6d8ff 100%)' - } - }; - - this.currentTheme = this.loadTheme(); - this.activeThemeName = this.loadActiveThemeName(); - this.init(); - } - - init() { - this.setupEventListeners(); - this.loadCurrentTheme(); - this.restoreActiveThemeButton(); - this.updatePreview(); - this.updateCustomThemeButton(); - } - - setupEventListeners() { - // Theme preset buttons - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const themeName = e.currentTarget.dataset.theme; - this.applyPredefinedTheme(themeName); - }); - }); - - // Color inputs - const colorInputs = ['bg-color', 'gradient-color', 'accent-color', 'secondary-color', 'text-color']; - colorInputs.forEach(inputId => { - const input = document.getElementById(inputId); - if (input) { - input.addEventListener('input', (e) => { - this.updateColorFromInput(inputId, e.target.value); - }); - } - }); - - // Layout options - document.querySelectorAll('input[name="layout"]').forEach(input => { - input.addEventListener('change', (e) => { - this.currentTheme.layout = e.target.value; - - // Clear active theme name since this is now a custom theme - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - - // Remove active class from all theme buttons - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - this.saveTheme(); - this.updatePreview(); - this.applyThemeToPages(); - this.updateCustomThemeButton(); - }); - }); - - // Logo options - const showLogoInput = document.getElementById('show-logo'); - if (showLogoInput) { - showLogoInput.addEventListener('change', (e) => { - this.currentTheme.showLogo = e.target.checked; - - // Clear active theme name since this is now a custom theme - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - - // Remove active class from all theme buttons - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - this.saveTheme(); - this.updatePreview(); - this.applyThemeToPages(); - this.updateCustomThemeButton(); - }); - } - - const customTitleInput = document.getElementById('custom-title'); - if (customTitleInput) { - customTitleInput.addEventListener('input', (e) => { - this.currentTheme.customTitle = e.target.value || 'Nebula Browser'; - - // Clear active theme name since this is now a custom theme - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - - // Remove active class from all theme buttons - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - this.saveTheme(); - this.updatePreview(); - this.applyThemeToPages(); - this.updateCustomThemeButton(); - }); - } - - // Theme management buttons - this.setupThemeManagementButtons(); - } - - setupThemeManagementButtons() { - const saveBtn = document.getElementById('save-custom-theme'); - const exportBtn = document.getElementById('export-theme'); - const importBtn = document.getElementById('import-theme'); - const resetBtn = document.getElementById('reset-to-default'); - const fileInput = document.getElementById('theme-file-input'); - - if (saveBtn) { - saveBtn.addEventListener('click', () => this.saveCustomTheme()); - } - - if (exportBtn) { - exportBtn.addEventListener('click', () => this.exportTheme()); - } - - if (importBtn) { - importBtn.addEventListener('click', () => fileInput.click()); - } - - if (fileInput) { - fileInput.addEventListener('change', (e) => this.importTheme(e)); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => this.resetToDefault()); - } - } - - updateColorFromInput(inputId, color) { - const colorMap = { - 'bg-color': 'bg', - 'gradient-color': 'darkPurple', - 'accent-color': 'primary', - 'secondary-color': 'accent', - 'text-color': 'text' - }; - - const colorKey = colorMap[inputId]; - if (colorKey) { - this.currentTheme.colors[colorKey] = color; - - // Update gradient for background or gradient changes - if (colorKey === 'bg' || colorKey === 'darkPurple') { - this.currentTheme.gradient = `linear-gradient(145deg, ${this.currentTheme.colors.bg} 0%, ${this.currentTheme.colors.darkPurple} 100%)`; - } - - // Clear active theme name since this is now a custom theme - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - - // Remove active class from all theme buttons - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - this.saveTheme(); - this.updatePreview(); - this.applyThemeToPages(); - this.updateCustomThemeButton(); - } - } - - applyPredefinedTheme(themeName) { - if (themeName === 'custom') { - // For custom theme, just activate the button but don't change the current theme - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - this.updateThemeButtons('custom'); - this.updateCustomThemeButton(); - } else if (this.predefinedThemes[themeName]) { - this.currentTheme = { ...this.predefinedThemes[themeName] }; - this.activeThemeName = themeName; - this.saveTheme(); - this.saveActiveThemeName(themeName); - this.loadCurrentTheme(); - this.updatePreview(); - this.applyThemeToCurrentPage(); - this.applyThemeToPages(); - this.updateThemeButtons(themeName); - this.updateCustomThemeButton(); - } - } - - updateThemeButtons(activeTheme) { - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.classList.remove('active'); - if (btn.dataset.theme === activeTheme) { - btn.classList.add('active'); - } - }); - } - - updateCustomThemeButton() { - const customBtn = document.getElementById('theme-custom'); - if (!customBtn) return; - - // Check if current theme matches any predefined theme - const matchingTheme = this.detectMatchingPredefinedTheme(); - const isCustomTheme = !matchingTheme; - - if (isCustomTheme) { - customBtn.style.display = 'flex'; - // Update the preview to show current colors - const preview = customBtn.querySelector('.theme-preview'); - if (preview && this.currentTheme) { - preview.style.background = this.currentTheme.gradient || - `linear-gradient(145deg, ${this.currentTheme.colors.bg}, ${this.currentTheme.colors.darkPurple})`; - } - // Set active theme name to custom if it's not already set to a predefined theme - if (this.activeThemeName !== 'custom') { - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - } - } else { - customBtn.style.display = 'none'; - // If we found a matching predefined theme, update activeThemeName if it was set to custom - if (this.activeThemeName === 'custom') { - this.activeThemeName = matchingTheme; - this.saveActiveThemeName(matchingTheme); - } - } - } - - loadCurrentTheme() { - // Update color inputs - document.getElementById('bg-color').value = this.currentTheme.colors.bg; - document.getElementById('gradient-color').value = this.currentTheme.colors.darkPurple; - document.getElementById('accent-color').value = this.currentTheme.colors.primary; - document.getElementById('secondary-color').value = this.currentTheme.colors.accent; - document.getElementById('text-color').value = this.currentTheme.colors.text; - - // Update layout radio - const layoutInput = document.querySelector(`input[name="layout"][value="${this.currentTheme.layout}"]`); - if (layoutInput) layoutInput.checked = true; - - // Update logo options - document.getElementById('show-logo').checked = this.currentTheme.showLogo; - document.getElementById('custom-title').value = this.currentTheme.customTitle; - } - - updatePreview() { - const preview = document.getElementById('preview-container'); - const previewHome = preview.querySelector('.preview-home'); - const previewLogo = preview.querySelector('.preview-logo'); - const previewText = preview.querySelector('.preview-text'); - - // Apply colors to preview - previewHome.style.background = this.currentTheme.gradient; - - // Handle logo visibility - if (this.currentTheme.showLogo) { - previewLogo.style.display = 'block'; - previewLogo.style.color = this.currentTheme.colors.primary; - previewLogo.textContent = '๐ŸŒŒ'; - } else { - previewLogo.style.display = 'none'; - } - - // Always show preview text with custom title - if (previewText) { - previewText.style.color = this.currentTheme.colors.primary; - previewText.textContent = this.currentTheme.customTitle; - } - - // Update CSS custom properties for live preview - this.applyThemeToCurrentPage(); - } - - applyThemeToCurrentPage() { - const root = document.documentElement; - root.style.setProperty('--bg', this.currentTheme.colors.bg); - root.style.setProperty('--dark-blue', this.currentTheme.colors.darkBlue); - root.style.setProperty('--dark-purple', this.currentTheme.colors.darkPurple); - root.style.setProperty('--primary', this.currentTheme.colors.primary); - root.style.setProperty('--accent', this.currentTheme.colors.accent); - root.style.setProperty('--text', this.currentTheme.colors.text); - root.style.setProperty('--url-bar-bg', this.currentTheme.colors.urlBarBg); - root.style.setProperty('--url-bar-text', this.currentTheme.colors.urlBarText); - root.style.setProperty('--url-bar-border', this.currentTheme.colors.urlBarBorder); - root.style.setProperty('--tab-bg', this.currentTheme.colors.tabBg); - root.style.setProperty('--tab-text', this.currentTheme.colors.tabText); - root.style.setProperty('--tab-active', this.currentTheme.colors.tabActive); - root.style.setProperty('--tab-active-text', this.currentTheme.colors.tabActiveText); - root.style.setProperty('--tab-border', this.currentTheme.colors.tabBorder); - - // Apply gradient to body if it exists - const body = document.body; - if (body && this.currentTheme.gradient) { - body.style.background = this.currentTheme.gradient; - console.log('[THEME] Applied gradient:', this.currentTheme.gradient); - } - } - - applyThemeToPages() { - // This will be called to apply theme to home.html and other pages - this.saveTheme(); - - // Send theme update to host (for settings webview) - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('theme-update', this.currentTheme); - } - // Fallback: send via postMessage (for iframe embedding) - try { - if (window.parent && window.parent !== window) { - window.parent.postMessage({ - type: 'theme-update', - theme: this.currentTheme - }, '*'); - } - } catch (e) { - console.log('Could not send theme update to parent window'); - } - } - - saveCustomTheme() { - const themeName = prompt('Enter a name for your custom theme:', 'My Custom Theme'); - if (themeName) { - const customThemes = this.getCustomThemes(); - customThemes[themeName.toLowerCase().replace(/\s+/g, '-')] = { - ...this.currentTheme, - name: themeName - }; - localStorage.setItem('customThemes', JSON.stringify(customThemes)); - - // Show success message - this.showMessage('Custom theme saved successfully!', 'success'); - } - } - - exportTheme() { - const themeData = { - ...this.currentTheme, - exportedAt: new Date().toISOString(), - version: '1.0' - }; - - const blob = new Blob([JSON.stringify(themeData, null, 2)], { - type: 'application/json' - }); - - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `nebula-theme-${themeData.name.toLowerCase().replace(/\s+/g, '-')}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - this.showMessage('Theme exported successfully!', 'success'); - } - - importTheme(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - try { - const themeData = JSON.parse(e.target.result); - - // Validate theme structure - if (this.validateTheme(themeData)) { - this.currentTheme = themeData; - this.saveTheme(); - this.loadCurrentTheme(); - this.updatePreview(); - this.applyThemeToCurrentPage(); - this.applyThemeToPages(); - this.showMessage('Theme imported successfully!', 'success'); - } else { - this.showMessage('Invalid theme file format.', 'error'); - } - } catch (error) { - this.showMessage('Error reading theme file.', 'error'); - } - }; - reader.readAsText(file); - } - - validateTheme(theme) { - return theme && - theme.colors && - theme.colors.bg && - theme.colors.primary && - theme.colors.accent && - theme.colors.text; - } - - resetToDefault() { - if (confirm('Are you sure you want to reset to the default theme? This will lose your current customizations.')) { - this.currentTheme = { ...this.defaultTheme }; - this.activeThemeName = 'default'; - this.saveTheme(); - this.saveActiveThemeName('default'); - this.loadCurrentTheme(); - this.updatePreview(); - this.applyThemeToCurrentPage(); - this.applyThemeToPages(); - this.updateThemeButtons('default'); - this.showMessage('Theme reset to default.', 'success'); - } - } - - saveTheme() { - localStorage.setItem('currentTheme', JSON.stringify(this.currentTheme)); - } - - loadTheme() { - const savedTheme = localStorage.getItem('currentTheme'); - return savedTheme ? JSON.parse(savedTheme) : { ...this.defaultTheme }; - } - - saveActiveThemeName(themeName) { - localStorage.setItem('activeThemeName', themeName); - } - - loadActiveThemeName() { - return localStorage.getItem('activeThemeName') || 'default'; - } - - restoreActiveThemeButton() { - // First, remove active class from all buttons - document.querySelectorAll('.theme-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - // If no active theme name is saved, try to detect which predefined theme matches current theme - if (!this.activeThemeName) { - this.activeThemeName = this.detectMatchingPredefinedTheme(); - if (this.activeThemeName) { - this.saveActiveThemeName(this.activeThemeName); - } else { - // If no predefined theme matches, this is a custom theme - this.activeThemeName = 'custom'; - this.saveActiveThemeName('custom'); - } - } - - // Update the custom theme button visibility - this.updateCustomThemeButton(); - - // Then, add active class to the currently active theme button - const activeBtn = document.querySelector(`[data-theme="${this.activeThemeName}"]`); - if (activeBtn) { - activeBtn.classList.add('active'); - } - } - - detectMatchingPredefinedTheme() { - // Check if current theme matches any predefined theme - for (const [themeName, themeData] of Object.entries(this.predefinedThemes)) { - if (this.themesMatch(this.currentTheme, themeData)) { - return themeName; - } - } - return null; - } - - themesMatch(theme1, theme2) { - // Compare essential properties to determine if themes match - return theme1.colors.bg === theme2.colors.bg && - theme1.colors.darkPurple === theme2.colors.darkPurple && - theme1.colors.primary === theme2.colors.primary && - theme1.colors.accent === theme2.colors.accent && - theme1.colors.text === theme2.colors.text && - theme1.layout === theme2.layout && - theme1.showLogo === theme2.showLogo && - theme1.customTitle === theme2.customTitle; - } - - getCustomThemes() { - const customThemes = localStorage.getItem('customThemes'); - return customThemes ? JSON.parse(customThemes) : {}; - } - - showMessage(message, type = 'info') { - const messageDiv = document.createElement('div'); - messageDiv.className = `message message-${type}`; - messageDiv.textContent = message; - messageDiv.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 12px 20px; - border-radius: 4px; - color: white; - font-weight: 500; - z-index: 10000; - animation: slideIn 0.3s ease; - background: ${type === 'success' ? '#48bb78' : type === 'error' ? '#e53e3e' : '#4299e1'}; - `; - - document.body.appendChild(messageDiv); - - setTimeout(() => { - messageDiv.style.animation = 'slideOut 0.3s ease'; - setTimeout(() => { - if (messageDiv.parentNode) { - messageDiv.parentNode.removeChild(messageDiv); - } - }, 300); - }, 3000); - } - - // Static method to apply theme to any page - static applyThemeToPage() { - const savedTheme = localStorage.getItem('currentTheme'); - if (savedTheme) { - const theme = JSON.parse(savedTheme); - const root = document.documentElement; - - root.style.setProperty('--bg', theme.colors.bg); - root.style.setProperty('--dark-blue', theme.colors.darkBlue); - root.style.setProperty('--dark-purple', theme.colors.darkPurple); - root.style.setProperty('--primary', theme.colors.primary); - root.style.setProperty('--accent', theme.colors.accent); - root.style.setProperty('--text', theme.colors.text); - root.style.setProperty('--url-bar-bg', theme.colors.urlBarBg); - root.style.setProperty('--url-bar-text', theme.colors.urlBarText); - root.style.setProperty('--url-bar-border', theme.colors.urlBarBorder); - root.style.setProperty('--tab-bg', theme.colors.tabBg); - root.style.setProperty('--tab-text', theme.colors.tabText); - root.style.setProperty('--tab-active', theme.colors.tabActive); - root.style.setProperty('--tab-active-text', theme.colors.tabActiveText); - root.style.setProperty('--tab-border', theme.colors.tabBorder); - - // Apply gradient to body if it exists - const body = document.body; - if (body && theme.gradient) { - body.style.background = theme.gradient; - console.log('[THEME] Applied gradient from storage:', theme.gradient); - } - - return theme; - } - return null; - } -} - -// Auto-initialize on settings page -if (window.location.pathname.includes('settings.html')) { - document.addEventListener('DOMContentLoaded', () => { - window.browserCustomizer = new BrowserCustomizer(); - }); -} - -// Add keyframe animations for messages -const style = document.createElement('style'); -style.textContent = ` - @keyframes slideIn { - from { transform: translateX(100%); opacity: 0; } - to { transform: translateX(0); opacity: 1; } - } - @keyframes slideOut { - from { transform: translateX(0); opacity: 1; } - to { transform: translateX(100%); opacity: 0; } - } -`; -document.head.appendChild(style); diff --git a/renderer/downloads.html b/renderer/downloads.html deleted file mode 100644 index 020130f..0000000 --- a/renderer/downloads.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - Downloads - - - - - -
-

Downloads

-
- -
-
-
- - - - - diff --git a/renderer/gpu-diagnostics.html b/renderer/gpu-diagnostics.html deleted file mode 100644 index 89d5f6b..0000000 --- a/renderer/gpu-diagnostics.html +++ /dev/null @@ -1,215 +0,0 @@ - - - - GPU Diagnostics - Nebula Browser - - - -
-

GPU Diagnostics

- -
-

GPU Status

-

Loading GPU information...

-
- -
-

WebGL Test

- -

Testing WebGL...

-
- -
-

Canvas 2D Acceleration Test

- -

Testing Canvas 2D...

-
- -
-

Actions

- - - - -
- -
-

Detailed GPU Information

-
Loading...
-
-
- - - - diff --git a/renderer/home.css b/renderer/home.css deleted file mode 100644 index a407812..0000000 --- a/renderer/home.css +++ /dev/null @@ -1,606 +0,0 @@ -/* Load InterVariable */ -@font-face { - font-family: 'InterVariable'; - src: url('../assets/images/fonts/InterVariable.ttf') format('truetype'); - font-weight: 100 900; - font-display: swap; -} - -/* CSS Custom Properties for Theming */ -:root { - --bg: #121418; - --dark-blue: #0B1C2B; - --dark-purple: #1B1035; - --primary: #7B2EFF; - --accent: #00C6FF; - --text: #E0E0E0; - --home-greeting-y: 12vh; /* fixed vertical baseline */ - --home-search-y: 22vh; /* user adjustable */ - --home-bookmarks-y: 40vh; /* user adjustable */ -} - -/* Base reset */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body, html { - /* Use CSS custom properties for theming */ - margin: 0; - padding: 0; - height: 100%; - background: linear-gradient(145deg, var(--bg) 0%, var(--dark-purple) 100%); - color: var(--text); - overflow: hidden; - font-family: 'InterVariable', sans-serif; -} - -/* Center everything */ -.home-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - min-height: 100vh; - overflow-y: auto; - text-align: center; - padding: 4rem 2rem 2rem; -} - -.edit-btn { position: fixed; top: 16px; right: 16px; z-index: 5; background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--text); border:1px solid color-mix(in srgb, var(--text) 20%, transparent); border-radius:8px; padding:6px 10px; cursor:pointer; backdrop-filter: blur(6px); } -.edit-btn[aria-pressed="true"] { background: color-mix(in srgb, var(--text) 22%, transparent); } -.edit-mode .edit-btn { display:none; } -.edit-mode .greeting-title, .edit-mode .search-container, .edit-mode .top-sites-card, .edit-mode .glance { outline: 2px dashed color-mix(in srgb, var(--text) 35%, transparent); outline-offset: 4px; cursor: grab; } -.edit-mode .glance.dragging { cursor: grabbing; } - -/* Edit toolbar */ -.edit-toolbar { position: fixed; top: 16px; right: 16px; display:none; gap:10px; z-index:6; backdrop-filter: blur(8px); background: color-mix(in srgb, var(--bg) 50%, transparent); border:1px solid color-mix(in srgb, var(--text) 15%, transparent); padding:8px 10px; border-radius:12px; box-shadow: 0 12px 30px -14px color-mix(in srgb, var(--bg) 70%, transparent); } -.edit-mode .edit-toolbar { display:flex; } -.edit-toolbar[hidden] { display: none !important; } - -/* Corner helpers for edit controls */ -.edit-btn.pos-br, .edit-toolbar.pos-br { right:16px; bottom:16px; left:auto; top:auto; } -.edit-btn.pos-bl, .edit-toolbar.pos-bl { left:16px; bottom:16px; right:auto; top:auto; } -.edit-btn.pos-tr, .edit-toolbar.pos-tr { right:16px; top:16px; left:auto; bottom:auto; } -.edit-btn.pos-tl, .edit-toolbar.pos-tl { left:16px; top:16px; right:auto; bottom:auto; } -.edit-toolbar .btn { min-width:90px; padding:8px 12px; border-radius:8px; border:1px solid transparent; color: var(--text); cursor:pointer; } -.edit-toolbar .btn.primary { background: linear-gradient(135deg, var(--accent), var(--primary)); } -.edit-toolbar .btn.secondary { background: color-mix(in srgb, var(--text) 14%, transparent); border-color: color-mix(in srgb, var(--text) 20%, transparent); } - -/* Greeting hero title */ -.greeting-title { - font-size: clamp(2rem, 5vw, 3.5rem); - font-weight: 700; - letter-spacing: 0.3px; - color: var(--text); - text-shadow: 0 4px 22px color-mix(in srgb, var(--bg) 60%, transparent); - margin-bottom: 1.25rem; - position: relative; - top: var(--home-greeting-y); -} - - -/* Logo block */ -.logo { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 2rem; -} - -.logo-img { - /* bump up logo size and add subtle shadow */ - width: 150px; - height: 150px; - margin-bottom: 1rem; - filter: drop-shadow(0 4px 8px rgba(0,0,0,0.5)); -} - -.logo-text { - font-size: 2rem; - font-weight: bold; - color: var(--primary); -} - -/* Utility: fully hide elements when user toggles them off */ -.is-hidden { display: none !important; } - -/* Search bar container */ -.search-container { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 1.5rem; - width: 680px; - max-width: min(92vw, 900px); - position: relative; - z-index: 300; /* ensure dropdown overlays bookmarks/top-sites stacking contexts */ - top: var(--home-search-y); - /* Unified glassy pill */ - background: color-mix(in srgb, var(--text) 12%, transparent); - border: 1px solid color-mix(in srgb, var(--text) 20%, transparent); - border-radius: 9999px; - box-shadow: 0 18px 50px -22px color-mix(in srgb, var(--bg) 80%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 12%, transparent); - backdrop-filter: blur(10px) saturate(140%); - -webkit-backdrop-filter: blur(10px) saturate(140%); - padding: 6px 8px; - transition: box-shadow 180ms ease, border-color 180ms ease, background 180ms ease, transform 120ms ease; -} - -.search-container:hover { - background: color-mix(in srgb, var(--text) 16%, transparent); - border-color: color-mix(in srgb, var(--text) 28%, transparent); -} - -.search-container:focus-within { - box-shadow: 0 22px 60px -24px color-mix(in srgb, var(--bg) 90%, transparent), 0 0 0 2px color-mix(in srgb, var(--primary) 45%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 16%, transparent); - border-color: color-mix(in srgb, var(--primary) 55%, transparent); -} - -/* Search bar */ -.search-bar { - display: flex; - flex: 1; /* Take remaining space inside the pill */ - align-items: center; - background: transparent; - border-radius: 9999px; - padding: 0 6px 0 2px; - height: 44px; -} - -.search-bar input.search-input { - flex: 1; - border: none; - background: transparent; - padding: 0 10px 0 8px; - font-size: 1.05rem; - line-height: 1; - color: var(--text); - caret-color: var(--accent); -} - -.search-bar input.search-input::placeholder { - color: color-mix(in srgb, var(--text) 55%, transparent); -} - -.search-bar button.search-btn { - border: 1px solid color-mix(in srgb, var(--text) 14%, transparent); - background: color-mix(in srgb, var(--bg) 45%, transparent); - color: var(--text); - width: 40px; - height: 40px; - border-radius: 9999px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - box-shadow: none; - transition: transform 120ms ease, background 160ms ease, border-color 160ms ease; -} - -.search-bar button.search-btn:hover { transform: scale(1.02); background: color-mix(in srgb, var(--bg) 55%, transparent); border-color: color-mix(in srgb, var(--text) 24%, transparent); } -.search-bar button.search-btn:active { transform: scale(0.98); } - -.search-bar button.search-btn .material-symbols-outlined { - font-size: 1.25rem; -} - -/* Search engine trigger unified look */ -.search-engine-selector { position: relative; display: flex; align-items: center; } -.search-engine-btn { - background: color-mix(in srgb, var(--bg) 45%, transparent); - border: 1px solid color-mix(in srgb, var(--text) 14%, transparent); - border-radius: 9999px; - padding: 8px 10px 8px 12px; - cursor: pointer; - height: 44px; - width: 48px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: none; - transition: background 160ms ease, border-color 160ms ease, transform 120ms ease; -} -.search-engine-btn:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); border-color: color-mix(in srgb, var(--text) 24%, transparent); } -.search-engine-btn:active { transform: scale(0.98); } - -.search-engine-btn img { width: 22px; height: 22px; filter: none; } - -/* Subtle divider after the engine button */ -.search-engine-selector::after { - content: ''; - position: absolute; - right: -4px; - top: 8px; - bottom: 8px; - width: 1px; - background: linear-gradient(to bottom, color-mix(in srgb, var(--text) 6%, transparent), color-mix(in srgb, var(--text) 24%, transparent), color-mix(in srgb, var(--text) 6%, transparent)); - pointer-events: none; -} - -@media (max-width: 520px) { - .search-container { width: 94vw; padding: 6px; } - .search-bar { height: 42px; } - .search-engine-btn { height: 42px; width: 46px; } - .search-bar button.search-btn { width: 38px; height: 38px; } -} - -/* Remove default focus outline */ -.search-bar input.search-input:focus, -.search-bar button.search-btn:focus { - outline: none; - box-shadow: none; -} - -/* (legacy Search Engine Selector block removed; unified styles are defined above) */ - -.search-engine-dropdown { - position: absolute; - top: 110%; - left: 0; - background: color-mix(in srgb, var(--bg) 94%, #000 6%); - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); - box-shadow: 0 18px 50px -22px color-mix(in srgb, var(--bg) 80%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent); - z-index: 100; - padding: 0.5rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - /* Animated open/close */ - overflow: hidden; - opacity: 1; - transform: translateY(0) scale(1); - transform-origin: top left; - visibility: visible; - pointer-events: auto; - max-height: 320px; /* enough for options; adjust if you add more */ - transition: opacity 160ms ease, transform 160ms ease, max-height 200ms ease; -} - -.search-engine-dropdown.hidden { - opacity: 0; - transform: translateY(-6px) scale(0.98); - visibility: hidden; - pointer-events: none; - max-height: 0; -} - -.search-engine-option { - cursor: pointer; - padding: 0.5rem; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; -} - -.search-engine-option:hover { background-color: rgba(255,255,255,0.08); } - -.search-engine-option img { - width: 24px; - height: 24px; -} - -/* Respect reduced motion preferences */ -@media (prefers-reduced-motion: reduce) { - .search-engine-dropdown { - transition: none; - } -} - -/* Bookmark grid */ -.bookmarks { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 1rem; - max-width: 800px; -} - -/* Top Sites card wrapper */ -.top-sites-card { - width: min(900px, 96vw); - margin-top: 1.25rem; - padding: 1rem 1rem 1.25rem; - border-radius: 16px; - background: color-mix(in srgb, var(--text) 6%, transparent); - border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); - box-shadow: 0 18px 50px -20px color-mix(in srgb, var(--bg) 60%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent); - backdrop-filter: blur(6px); - position: relative; - top: var(--home-bookmarks-y); -} -.top-sites-header { - display:flex; align-items:center; justify-content:space-between; - margin-bottom: 0.75rem; padding: 0 0.25rem; -} -.top-sites-header h2 { font-size: 1rem; font-weight: 700; color: var(--text); opacity: .9; } -.link-btn { - background: none; border: none; color: var(--accent); cursor: pointer; font-size: .9rem; -} -.link-btn:hover { color: var(--primary); text-decoration: underline; } - -/* Individual bookmark tile */ -.bookmark { - background: color-mix(in srgb, var(--text) 5%, transparent); - border: 1px solid color-mix(in srgb, var(--text) 10%, transparent); - backdrop-filter: blur(6px); - box-shadow: 0 4px 16px color-mix(in srgb, var(--bg) 30%, transparent); - transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; - color: var(--text); - width: 100px; - height: 100px; - border-radius: 20px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: relative; - cursor: pointer; - transform: translateY(0) scale(1); -} - -.bookmark:hover { - transform: translateY(-4px) scale(1.1); - box-shadow: 0 8px 24px color-mix(in srgb, var(--bg) 50%, transparent); -} - -.bookmark-icon { - font-size: 1.75rem; - margin-bottom: 0.25rem; - /* accentuate icons & add-button */ - color: var(--accent); -} - -/* Favicon image in bookmark tile */ -.bookmark-favicon { - width: 28px; - height: 28px; - object-fit: contain; - margin-bottom: 0.25rem; - image-rendering: -webkit-optimize-contrast; - filter: drop-shadow(0 0 2px rgba(0,0,0,0.4)); -} - -/* SVG icons in picker grid */ -.icon-item .grid-svg { - width: 24px; - height: 24px; - display: block; - pointer-events: none; -} - -/* Icon category navigation */ -.icon-categories-bar { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin: 0.25rem 0 0.75rem; -} -.icon-picker-layout { display:flex; gap:1rem; align-items:stretch; background:#f6f7f9; border:1px solid #e2e5ea; border-radius:14px; padding:0.85rem 0.85rem 0.85rem 0.75rem; box-shadow:inset 0 0 0 1px #ffffff, 0 2px 4px rgba(0,0,0,0.05); min-height:280px; max-height:320px; overflow:hidden; } -.icon-side-nav { width:200px; display:flex; flex-direction:column; gap:0.3rem; overflow-y:auto; padding:0.3rem; background:linear-gradient(180deg,#fff,#f4f5f7); border:1px solid #d9dde2; border-radius:10px; max-height:100%; } -.icon-main { flex:1; min-width:0; display:flex; flex-direction:column; } -.icon-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem; } -.icon-filter-label { font-size:0.875rem; color:#555; font-weight:600; } -.favicon-toggle { display:flex; align-items:center; gap:0.4rem; } -.favicon-checkbox { width:16px; height:16px; accent-color:var(--accent); } -.favicon-label { font-size:0.75rem; color:#666; cursor:pointer; user-select:none; } -.icon-filter { margin-bottom:0.5rem; background:#fff; border:1px solid #d4d9df; border-radius:8px; padding:0.5rem 0.7rem; font-size:0.8rem; } -.icon-filter:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 2px rgba(123,46,255,0.25); } -.icon-cat-btn { width:100%; text-align:left; padding:0.55rem 0.65rem 0.55rem 0.55rem; font-size:0.7rem; font-weight:600; letter-spacing:.5px; background:transparent; border:1px solid transparent; border-radius:8px; cursor:pointer; color:#4a4f55; display:flex; align-items:center; gap:0.55rem; transition: background .18s, color .18s, border-color .2s; position:relative; } -.icon-cat-btn .material-symbols-outlined { font-size:18px; width:30px; height:30px; background:#e9edf1; border:1px solid #d4d9df; border-radius:8px; flex-shrink:0; display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 1px #fff inset; } -.icon-cat-btn::before { display:none; } -.icon-cat-btn:hover { background:#eef2f6; } -.icon-cat-btn.active { background:linear-gradient(135deg,var(--accent),var(--primary)); color:#fff; border-color:rgba(255,255,255,0.35); box-shadow:0 2px 6px -2px rgba(0,0,0,0.35); } -.icon-cat-btn.active .material-symbols-outlined { background:rgba(255,255,255,0.22); border-color:rgba(255,255,255,0.4); color:#fff; } - -.icon-section-label { display:none; } -.icon-section-anchor { height:1px; width:100%; margin-top:4px; } - -.bookmark-title { - font-size: 0.8rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.delete-btn { - position: absolute; - top: 5px; - right: 7px; - background: none; - border: none; - color: red; - font-size: 1rem; - cursor: pointer; -} - -/* Dynamic bookmark icon colors based on theme */ -.bookmark .material-symbols-outlined { - color: var(--text, #E0E0E0) !important; - transition: color 0.2s ease; -} - -/* Ensure dark theme compatibility - fallback rules */ -body[data-theme="dark"] .bookmark .material-symbols-outlined, -.bookmark .material-symbols-outlined[style*="color: white"] { - color: white !important; -} - -/* Add button style */ -.add-bookmark { - display: flex; - align-items: center; - justify-content: center; - width: 100px; - height: 100px; - border-radius: 20px; - font-size: 2rem; - background: rgba(255,255,255,0.05); - border: 1px dashed rgba(255,255,255,0.3); - backdrop-filter: blur(6px); - transition: transform 0.2s ease-in-out, background 0.3s, border-color 0.3s; - color: white; - transform: scale(1); -} - -.add-bookmark:hover { - transform: scale(1.1); -} - -/* Popup styling */ -.popup { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: color-mix(in srgb, var(--bg) 80%, transparent); - display: flex; - align-items: center; - justify-content: center; - /* Ensure popup overlays search bar and other UI (search-container has z-index:300) */ - z-index: 1005; - backdrop-filter: blur(4px); /* add subtle blur behind the overlay */ -} - -.popup.hidden { - display: none; -} - -/* Popup inner as white Material card */ -.popup-inner { display:flex; flex-direction:column; gap:1.1rem; color:#222; min-width:400px; background:#ffffff; border-radius:16px; box-shadow:0 12px 40px -8px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.06); padding:1.5rem 1.5rem 1.25rem; transition:transform .35s cubic-bezier(.16,.84,.44,1), opacity .35s; transform:translateY(14px) scale(.94); opacity:0; width:760px; max-width:90vw; max-height:85vh; overflow:hidden; } - -/* animate in when not hidden */ -.popup:not(.hidden) .popup-inner { transform:translateY(0) scale(1); opacity:1; } - -/* dialog title */ -.popup-inner h2 { - margin: 0 0 1rem; - font-size: 1.5rem; - text-align: center; - color: #333333; -} - -/* field labels */ -.popup-inner label { - display: block; - margin-bottom: 0.25rem; - font-size: 0.875rem; - color: #555555; -} - -/* text/url/icon inputs */ -.popup-inner input[type="text"], -.popup-inner input[type="url"] { - width: 100%; - background: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; - padding: 0.75rem 1rem; - font-size: 1rem; - color: #222222; - margin-bottom: 1rem; - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -.popup-inner input[type="text"]:focus, -.popup-inner input[type="url"]:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(123,46,255,0.2); -} - -/* Removed earlier duplicate icon-grid + icon-item block (consolidated below) */ - -/* action buttons container */ -.popup-buttons { - display: flex; - justify-content: flex-end; - gap: 0.75rem; -} - -.popup-buttons button { - min-width: 80px; - padding: 0.5rem 1rem; - font-size: 0.875rem; - border-radius: 4px; - border: none; - cursor: pointer; -} - -/* Cancel button */ -#cancelBtn { - background: #e0e0e0; - color: #222222; -} - -#cancelBtn:hover { - background: #d5d5d5; -} - -/* Add button */ -#saveBookmarkBtn { - background: var(--primary); - color: #ffffff; -} - -#saveBookmarkBtn:hover { - background: #6a24e5; -} - -/* At a glance widget */ -.glance { position: fixed; right: 22px; bottom: 22px; } -.glance.pos-br { right:22px; bottom:22px; left:auto; top:auto; } -.glance.pos-bl { left:22px; bottom:22px; right:auto; top:auto; } -.glance.pos-tr { right:22px; top:22px; left:auto; bottom:auto; } -.glance.pos-tl { left:22px; top:22px; right:auto; bottom:auto; } -.glance-card { - min-width: 280px; background: color-mix(in srgb, var(--bg) 55%, transparent); border: 1px solid color-mix(in srgb, var(--text) 10%, transparent); - border-radius: 16px; padding: 1rem; box-shadow: 0 14px 40px -18px color-mix(in srgb, var(--bg) 80%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 5%, transparent); - backdrop-filter: blur(8px); -} -.glance { transition: transform 0.06s linear; will-change: transform; } -.glance.dragging { transform: translate3d(var(--drag-x, 0px), var(--drag-y, 0px), 0) scale(1.02); } -.glance.dragging .glance-card { box-shadow: 0 24px 60px -24px color-mix(in srgb, var(--bg) 90%, transparent), 0 0 0 2px color-mix(in srgb, var(--text) 12%, transparent) inset; } -.glance-title { font-size: .95rem; color: var(--text); opacity: .9; margin-bottom: .65rem; } -.glance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; } -.glance-tile { background: color-mix(in srgb, var(--text) 5%, transparent); border: 1px solid color-mix(in srgb, var(--text) 10%, transparent); border-radius: 12px; padding: .6rem .75rem; text-align:left; } -.glance-label { font-size: .7rem; color: var(--accent); opacity: .85; margin-bottom: .25rem; } -.glance-value { font-size: 1.05rem; letter-spacing: .3px; color: var(--text); } - -@media (max-width: 700px) { - .glance { position: static; margin-top: 1rem; } -} - -/* Color Palette */ -:root { - --bg: #121418; - --dark-blue: #0B1C2B; - --dark-purple: #1B1035; - --primary: #7B2EFF; - --accent: #00C6FF; - --text: #E0E0E0; -} - -/* Unified icon grid styling */ -.icon-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(42px,1fr)); gap:5px; flex:1 1 auto; min-height:160px; max-height:260px; overflow-y:auto; overflow-x:hidden; padding:0.6rem 0.6rem 0.7rem; background:rgba(255,255,255,0.45); backdrop-filter:blur(6px); border:1px solid rgba(0,0,0,0.08); border-radius:10px; position:relative; scroll-behavior:smooth; } -.icon-main { flex:1; min-height:300px; } -.icon-item { cursor:pointer; padding:6px; border:1px solid rgba(255,255,255,0.06); border-radius:10px; text-align:center; display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.05); transition: background .15s, transform .15s, box-shadow .2s; font-size:0.65rem; line-height:1; font-weight:500; position:relative; } -.icon-item::after { content:""; position:absolute; inset:0; border-radius:inherit; box-shadow:0 0 0 0 rgba(0,0,0,0); transition:box-shadow .25s; } -.icon-item:hover { background:rgba(255,255,255,0.12); } -.icon-item:active { transform:scale(.92); } -.icon-item.selected { background:linear-gradient(135deg,var(--accent),var(--primary)); color:#fff; border-color:transparent; box-shadow:0 4px 10px -3px rgba(0,0,0,.6); } -.icon-item.selected::after { box-shadow:0 0 0 2px rgba(255,255,255,0.65); } - -/* Icon set selector row */ -.icon-set-row { - display: flex; - align-items: center; - gap: 0.5rem; - margin-top: 0.75rem; -} -.icon-section-label { display:none; } diff --git a/renderer/home.html b/renderer/home.html deleted file mode 100644 index 5e7e56f..0000000 --- a/renderer/home.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - New Tab - - - - - - - -
- - -

Welcome

- - - - -
-
- - -
- -
- - -
-
-

Bookmarks

- -
-
- -
-
-
- - - - - - - - - - - - - - - - - - diff --git a/renderer/home.js b/renderer/home.js deleted file mode 100644 index 010257f..0000000 --- a/renderer/home.js +++ /dev/null @@ -1,1007 +0,0 @@ -import { icons as initialIcons, fetchAllIcons } from './icons.js'; -import { iconSets } from './iconSets.js'; - -const bookmarkList = document.getElementById('bookmarkList'); -const titleInput = document.getElementById('titleInput'); -const urlInput = document.getElementById('urlInput'); -const saveBookmarkBtn = document.getElementById('saveBookmarkBtn'); -const cancelBtn = document.getElementById('cancelBtn'); -const addPopup = document.getElementById('addPopup'); -const searchBtn = document.getElementById('searchBtn'); -const searchInput = document.getElementById('searchInput'); -const searchEngineBtn = document.getElementById('searchEngineBtn'); -const searchEngineDropdown = document.getElementById('searchEngineDropdown'); -const searchEngineLogo = document.getElementById('searchEngineLogo'); -const iconFilter = document.getElementById('iconFilter'); -const iconGrid = document.getElementById('iconGrid'); -const selectedIconInput= document.getElementById('selectedIcon'); -const iconCategoryNav = document.getElementById('iconCategoryNav'); -const useFaviconCheckbox = document.getElementById('useFavicon'); -const greetingEl = document.getElementById('greeting'); -const resetTopSitesBtn = document.getElementById('resetTopSites'); -const clockEl = document.getElementById('clock'); -const weatherEl = document.getElementById('weather'); -const glanceEl = document.querySelector('.glance'); -const searchContainerEl = document.querySelector('.search-container'); -const topSitesEl = document.querySelector('.top-sites-card'); -const editBtn = document.getElementById('editLayoutBtn'); -const greetingTitleEl = document.getElementById('greeting'); -const editToolbar = document.getElementById('editToolbar'); -const saveEditBtn = document.getElementById('saveEditBtn'); -const cancelEditBtn = document.getElementById('cancelEditBtn'); -const toggleShowGreeting = document.getElementById('toggleShowGreeting'); -const toggleShowBookmarks= document.getElementById('toggleShowBookmarks'); -const toggleShowGlance = document.getElementById('toggleShowGlance'); -let selectedIcon = initialIcons[0]; -let availableIcons = initialIcons; -let currentIconSetKey = 'material'; -const loadedSetsCache = new Map(); // key -> array -let unifiedCatalog = []; // aggregated icons with categories -// Semantic icon categories (ordered) with predicate tests -const iconCategories = [ - { id: 'services', label: 'Services', test: (n, set) => set === 'simple' || /(github|gitlab|google|twitter|facebook|discord|slack|whatsapp|youtube|spotify|apple|microsoft|aws|azure|gcp|cloudflare|figma|notion|paypal|stripe|reddit|steam|xbox|playstation|nintendo|openai|vercel|netlify|docker|kubernetes)/.test(n), icon: 'cloud' }, - { id: 'settings', label: 'Settings', test: n => /(setting|settings|cog|gear|tools?|wrench|sliders?|command|preferences?)/.test(n), icon: 'settings' }, - { id: 'files', label: 'Files & Data', test: n => /(file|folder|archive|book|bookmark|save|upload|download|cloud|database|server)/.test(n), icon: 'folder' }, - { id: 'media', label: 'Media', test: n => /(camera|video|film|image|photo|music|play|pause|mic|microphone|volume|speaker)/.test(n), icon: 'video_camera_front' }, - { id: 'social', label: 'Social & Communication', test: n => /(chat|message|mail|envelope|phone|comment|share|rss)/.test(n), icon: 'chat' }, - { id: 'nav', label: 'Navigation', test: n => /(map|compass|globe|route|pin|location|world|earth)/.test(n), icon: 'explore' }, - { id: 'security', label: 'Security', test: n => /(lock|shield|key|alert|warning|info|question|bug)/.test(n), icon: 'security' }, - { id: 'commerce', label: 'Commerce', test: n => /(cart|shopping|wallet|credit|bank|price|tag|sale|bag|store|shop)/.test(n), icon: 'shopping_cart' }, - { id: 'status', label: 'Status', test: n => /(star|heart|award|trophy|badge|bell|notification)/.test(n), icon: 'star' }, - { id: 'food', label: 'Food', test: n => /(apple|cake|coffee|cookie|beer|wine|food|restaurant|cup|tea)/.test(n), icon: 'restaurant' }, - { id: 'devices', label: 'Devices', test: n => /(cpu|laptop|desktop|tablet|phone|smartphone|device|monitor|tv)/.test(n), icon: 'devices' }, - { id: 'other', label: 'Other', test: () => true, icon: 'more_horiz' } -]; - -const searchEngines = { - google: 'https://www.google.com/search?q=', - bing: 'https://www.bing.com/search?q=', - duckduckgo: 'https://duckduckgo.com/?q=' -}; -let selectedSearchEngine = 'google'; - -let bookmarks = []; - -// Load bookmarks from main via Electron IPC -// Load bookmarks via contextBridge API -async function loadBookmarks() { - try { - let data = []; - // Use bookmarksAPI if available - if (window.bookmarksAPI && typeof window.bookmarksAPI.load === 'function') { - data = await window.bookmarksAPI.load(); - } else if (window.electronAPI && typeof window.electronAPI.invoke === 'function') { - data = await window.electronAPI.invoke('load-bookmarks'); - } else { - console.error('No API available to load bookmarks'); - } - return Array.isArray(data) ? data : []; - } catch (error) { - console.error('Error loading bookmarks:', error); - return []; - } -} - -// Save bookmarks to main process -// Save bookmarks via contextBridge API -async function saveBookmarks() { - try { - await window.bookmarksAPI.save(bookmarks); - } catch (error) { - console.error('Error saving bookmarks:', error); - } -} - -// Render bookmarks -function renderBookmarks() { - bookmarkList.innerHTML = ''; - - // Render each bookmark - bookmarks.forEach((b, index) => { - const box = document.createElement('div'); - box.className = 'bookmark'; - - // prepend icon - const iconVal = b.icon || 'bookmark'; - let iconEl; - if (typeof iconVal === 'string' && /^(https?:|data:)/.test(iconVal)) { - // Treat as favicon/image URL - iconEl = document.createElement('img'); - iconEl.src = iconVal; - iconEl.alt = 'favicon'; - iconEl.className = 'bookmark-favicon'; - iconEl.referrerPolicy = 'no-referrer'; - // Apply filter for dark backgrounds to ensure visibility - if (isDarkBackground()) { - iconEl.style.filter = 'brightness(0) saturate(100%) invert(100%)'; - } - box.appendChild(iconEl); - } else { - iconEl = document.createElement('span'); - iconEl.className = 'material-symbols-outlined'; - iconEl.textContent = iconVal; - box.appendChild(iconEl); - } - - const label = document.createElement('span'); - label.className = 'bookmark-title'; - label.textContent = b.title; - - const close = document.createElement('button'); - close.textContent = 'ร—'; - close.className = 'delete-btn'; - close.onclick = async (e) => { - e.stopPropagation(); - bookmarks.splice(index, 1); - await saveBookmarks(); - renderBookmarks(); - }; - - // Navigate via IPC to host page - box.onclick = () => { - const url = b.url; - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('navigate', url); - } else { - console.error('Unable to send navigation IPC to host'); - } - // Fallback: post message to embedding page - if (window.parent && typeof window.parent.postMessage === 'function') { - window.parent.postMessage({ type: 'navigate', url }, '*'); - } - }; - - box.appendChild(label); - box.appendChild(close); - bookmarkList.appendChild(box); - }); - - // Add "+" box - const addBox = document.createElement('div'); - addBox.className = 'bookmark add-bookmark'; - addBox.textContent = '+'; - addBox.onclick = () => addPopup.classList.remove('hidden'); - - bookmarkList.appendChild(addBox); -} - -// Reset Top Sites (bookmarks) to empty state -if (resetTopSitesBtn) { - resetTopSitesBtn.addEventListener('click', async (e) => { - e.preventDefault(); - if (!bookmarks.length) return; - const yes = confirm('Clear all Top Sites?'); - if (!yes) return; - bookmarks = []; - await saveBookmarks(); - renderBookmarks(); - }); -} - -// draw the iconโ€grid, filtering by the search term -function renderIconGrid(filter = '') { - const f = filter.toLowerCase(); - iconGrid.innerHTML = ''; - const frag = document.createDocumentFragment(); - let lastCat = null; - const filtered = unifiedCatalog.filter(e => !f || e.name.includes(f)); - filtered.forEach(entry => { - if (entry.category !== lastCat) { - lastCat = entry.category; - const anchor = document.createElement('div'); - anchor.className = 'icon-section-anchor'; - anchor.id = `section-${entry.category}`; - frag.appendChild(anchor); - } - const span = document.createElement('span'); - span.className = 'icon-item'; - const def = iconSets[entry.set]; - if (entry.set === 'material') { - span.classList.add('material-symbols-outlined'); - span.textContent = entry.name; - } else if (def && def.fontClass) { - const i = document.createElement('i'); - i.className = def.fontClass(entry.name); - span.appendChild(i); - } else if (entry.dataUrl) { - const img = document.createElement('img'); - img.src = entry.dataUrl; img.alt = entry.name; img.className = 'grid-svg'; - span.appendChild(img); - } else { - span.textContent = 'โ€ฆ'; - (async () => { - if (def && def.fetchIcon) { - const dataUrl = await def.fetchIcon(entry.name); - if (dataUrl) { - entry.dataUrl = dataUrl; - if (span.isConnected) { - span.textContent=''; - const img=document.createElement('img'); - img.src=dataUrl; img.alt=entry.name; img.className='grid-svg'; - span.appendChild(img); - } - } else { - // If SVG fetch fails, try font class or show truncated name - if (def.fontClass && span.isConnected) { - span.textContent=''; - const i = document.createElement('i'); - i.className = def.fontClass(entry.name); - span.appendChild(i); - } else { - span.textContent = entry.name.slice(0,3); - } - } - } else { - // No fetchIcon available, show name - span.textContent = entry.name.slice(0,3); - } - })(); - } - span.onclick = () => { - const currentSelected = iconGrid.querySelector('.icon-item.selected'); - if (currentSelected) currentSelected.classList.remove('selected'); - span.classList.add('selected'); - selectedIcon = entry.name; - selectedIconInput.value = entry.name; - selectedIconInput.dataset.iconSet = entry.set; - if (entry.dataUrl) selectedIconInput.dataset.dataUrl = entry.dataUrl; else delete selectedIconInput.dataset.dataUrl; - }; - frag.appendChild(span); - }); - iconGrid.appendChild(frag); - // Don't auto-select first icon to allow favicon usage -} - -// filter as the user types -iconFilter.addEventListener('input', () => renderIconGrid(iconFilter.value.trim())); - -// initial render -renderIconGrid(); - -// Asynchronously fetch all icons and update the grid -async function buildUnifiedCatalog() { - const keys = Object.keys(iconSets); - for (const k of keys) { - if (!loadedSetsCache.has(k)) { - try { loadedSetsCache.set(k, await iconSets[k].loader()); } - catch(e) { console.warn('Icon set load failed', k, e); loadedSetsCache.set(k, []); } - } - } - const temp = []; - for (const k of keys) { - const arr = loadedSetsCache.get(k) || []; - for (const name of arr) { - const lower = name.toLowerCase(); - const category = iconCategories.find(c => c.test(lower, k)).id; - temp.push({ set: k, name, category }); - } - } - // order by category then by name - unifiedCatalog = temp.sort((a,b)=> { - if (a.category === b.category) return a.name.localeCompare(b.name); - return iconCategories.findIndex(c=>c.id===a.category) - iconCategories.findIndex(c=>c.id===b.category); - }); - buildCategoryNav(); - renderIconGrid(iconFilter.value.trim()); -} -buildUnifiedCatalog(); - -// --- Favicon resolution helpers --- -async function resolveFavicon(rawUrl) { - if (!rawUrl) return null; - let url = rawUrl.trim(); - if (!/^https?:\/\//i.test(url)) { - url = 'https://' + url; // assume https if protocol missing - } - try { - const u = new URL(url); - // Prefer Google favicon service for simplicity & size; fall back to /favicon.ico - const googleService = `https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(u.origin)}`; - // We'll optimisticly use google service; optionally we could verify it loads, but browsers will handle 404 gracefully. - return googleService; - } catch (_) { - return null; - } -} - -// Helper function to detect if background is dark -function isDarkBackground() { - // For SVG color modification, check if we have a dark theme - const rootStyles = window.getComputedStyle(document.documentElement); - const bgVar = rootStyles.getPropertyValue('--bg').trim(); - - if (bgVar && bgVar.startsWith('#')) { - const hex = bgVar.slice(1); - const r = parseInt(hex.substr(0, 2), 16); - const g = parseInt(hex.substr(2, 2), 16); - const b = parseInt(hex.substr(4, 2), 16); - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - return luminance < 0.5; - } - - // Fallback: assume dark theme for this app - return true; -} - -saveBookmarkBtn.onclick = async () => { - const title = titleInput.value.trim(); - const url = urlInput.value.trim(); - let icon = selectedIcon; - if (!title || !url) return; - - // Check if user wants to use favicon via checkbox - const wantFavicon = useFaviconCheckbox.checked; - - if (wantFavicon) { - try { - const faviconUrl = await resolveFavicon(url); - if (faviconUrl) icon = faviconUrl; - } catch (e) { - console.warn('Favicon fetch failed, falling back to icon symbol:', e); - } - } else { - // Use selected icon if available - const hasSelectedIcon = document.querySelector('.icon-item.selected'); - if (hasSelectedIcon) { - if (selectedIconInput.dataset.iconSet && selectedIconInput.dataset.iconSet !== 'material') { - if (selectedIconInput.dataset.dataUrl) { - icon = selectedIconInput.dataset.dataUrl; - - // For SVG icons, modify color based on background - if (icon.startsWith('data:image/svg+xml') && isDarkBackground()) { - try { - // Decode the SVG and modify its color - const svgData = decodeURIComponent(icon.split(',')[1]); - const modifiedSvg = svgData.replace(/fill="[^"]*"/g, 'fill="white"') - .replace(/stroke="[^"]*"/g, 'stroke="white"') - .replace(/]*)>/, ''); - icon = 'data:image/svg+xml;utf8,' + encodeURIComponent(modifiedSvg); - } catch (e) { - console.warn('Failed to modify SVG color:', e); - } - } - } else { - const def = iconSets[selectedIconInput.dataset.iconSet]; - if (def && def.fetchIcon) { - const dataUrl = await def.fetchIcon(selectedIcon); - if (dataUrl) { - icon = dataUrl; - - // Apply same color modification for fetched SVGs - if (icon.startsWith('data:image/svg+xml') && isDarkBackground()) { - try { - const svgData = decodeURIComponent(icon.split(',')[1]); - const modifiedSvg = svgData.replace(/fill="[^"]*"/g, 'fill="white"') - .replace(/stroke="[^"]*"/g, 'stroke="white"') - .replace(/]*)>/, ''); - icon = 'data:image/svg+xml;utf8,' + encodeURIComponent(modifiedSvg); - } catch (e) { - console.warn('Failed to modify fetched SVG color:', e); - } - } - } - } - } - } else { - // For Material icons, just use the icon name - CSS will handle color - icon = selectedIcon; - } - } else { - // No icon selected and no favicon requested, use default bookmark icon - icon = 'bookmark'; - } - } - - bookmarks.push({ title, url, icon, iconSet: selectedIconInput.dataset.iconSet || 'material' }); - await saveBookmarks(); - renderBookmarks(); - - titleInput.value = ''; - urlInput.value = ''; - iconFilter.value = ''; - useFaviconCheckbox.checked = false; - // Clear any selected icon - const selected = document.querySelector('.icon-item.selected'); - if (selected) selected.classList.remove('selected'); - addPopup.classList.add('hidden'); -}; - -// Disable icon selection when favicon toggle is checked -useFaviconCheckbox.addEventListener('change', () => { - const iconItems = document.querySelectorAll('.icon-item'); - if (useFaviconCheckbox.checked) { - iconItems.forEach(item => { - item.style.opacity = '0.5'; - item.style.pointerEvents = 'none'; - }); - // Clear any selection - const selected = document.querySelector('.icon-item.selected'); - if (selected) selected.classList.remove('selected'); - } else { - iconItems.forEach(item => { - item.style.opacity = ''; - item.style.pointerEvents = ''; - }); - } -}); - -cancelBtn.onclick = () => { - addPopup.classList.add('hidden'); -}; - -// --- Search Engine Dropdown Logic --- -searchEngineBtn.addEventListener('click', (e) => { - e.stopPropagation(); - searchEngineDropdown.classList.toggle('hidden'); -}); - -document.addEventListener('click', () => { - if (!searchEngineDropdown.classList.contains('hidden')) { - searchEngineDropdown.classList.add('hidden'); - } -}); - -searchEngineDropdown.addEventListener('click', (e) => { - const option = e.target.closest('.search-engine-option'); - if (option) { - selectedSearchEngine = option.dataset.engine; - const newLogoSrc = option.querySelector('img').src; - searchEngineLogo.src = newLogoSrc; - searchEngineDropdown.classList.add('hidden'); - } -}); -// --- End Search Engine Dropdown Logic --- - -searchBtn.addEventListener('click', () => { - const input = searchInput.value.trim(); - const hasProtocol = /^https?:\/\//i.test(input); - const looksLikeUrl = hasProtocol || /\./.test(input); - let target; - if (looksLikeUrl) { - target = hasProtocol ? input : `https://${input}`; - } else { - const searchEngineUrl = searchEngines[selectedSearchEngine]; - target = `${searchEngineUrl}${encodeURIComponent(input)}`; - } - // Always send navigation request to host - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('navigate', target); - return; - } - // Fallback: post message to embedding page - if (window.parent && typeof window.parent.postMessage === 'function') { - window.parent.postMessage({ type: 'navigate', url: target }, '*'); - return; - } -}); - -searchInput.addEventListener('keydown', e => { - if (e.key === 'Enter') searchBtn.click(); -}); - -function buildCategoryNav() { - iconCategoryNav.innerHTML = ''; - const usedCategories = [...new Set(unifiedCatalog.map(e=>e.category))]; - iconCategories.filter(c=>usedCategories.includes(c.id)).forEach(cat => { - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'icon-cat-btn'; - - // Create icon element - const iconSpan = document.createElement('span'); - iconSpan.className = 'material-symbols-outlined'; - iconSpan.textContent = cat.icon; - - // Create text element - const textSpan = document.createElement('span'); - textSpan.textContent = cat.label; - - btn.appendChild(iconSpan); - btn.appendChild(textSpan); - - btn.onclick = () => { - const target = document.getElementById(`section-${cat.id}`); - if (target) { - const top = target.offsetTop; - iconGrid.scrollTo({ top: top - 4, behavior: 'smooth' }); - iconCategoryNav.querySelectorAll('.icon-cat-btn').forEach(b => b.classList.toggle('active', b === btn)); - } - }; - iconCategoryNav.appendChild(btn); - }); - setupSectionObserver(); -} - -function setupSectionObserver() { - const observer = new IntersectionObserver(entries => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const id = entry.target.id.replace('section-',''); - const cat = iconCategories.find(c=>c.id===id); - if (!cat) return; - iconCategoryNav.querySelectorAll('.icon-cat-btn').forEach(b => { - const isActive = b.querySelector('span:last-child').textContent === cat.label; - b.classList.toggle('active', isActive); - }); - } - }); - }, { root: iconGrid, threshold: 0, rootMargin: '0px 0px -85% 0px' }); - // Observe after grid populated - const watch = () => { - iconGrid.querySelectorAll('.icon-section-anchor').forEach(l => observer.observe(l)); - }; - // Re-run after each render - const origRender = renderIconGrid; - renderIconGrid = function(filter='') { origRender(filter); watch(); }; - watch(); -} - -// Load and render bookmarks immediately -(async () => { - bookmarks = await loadBookmarks(); - - setTimeout(() => { - renderBookmarks(); - }, 100); -})(); - -// ---- Greeting / Clock / Weather widgets ---- -function computeGreeting(d = new Date()) { - const h = d.getHours(); - if (h < 5) return 'Good Night'; - if (h < 12) return 'Good Morning'; - if (h < 18) return 'Good Afternoon'; - return 'Good Evening'; -} - -function startClock() { - const format = { hour: 'numeric', minute: '2-digit', hour12: true }; - const update = () => { - const now = new Date(); - if (greetingEl) greetingEl.textContent = computeGreeting(now); - if (clockEl) clockEl.textContent = now.toLocaleTimeString([], format); - }; - // Initial draw - update(); - // Align updates to the start of each minute - const now = new Date(); - const delay = 60000 - (now.getSeconds() * 1000 + now.getMilliseconds()); - setTimeout(() => { - update(); - setInterval(update, 60000); - }, delay); -} - -// Unit helpers -const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f' -const COUNTRIES_FAHRENHEIT = new Set(['US','BS','KY','LR','PW','FM','MH']); -function useFahrenheit() { - try { - const pref = localStorage.getItem(WEATHER_UNIT_KEY); - if (pref === 'c') return false; if (pref === 'f') return true; - } catch {} - try { - const loc = Intl.DateTimeFormat().resolvedOptions().locale || navigator.language || ''; - const region = loc.split('-')[1]; - return region ? COUNTRIES_FAHRENHEIT.has(region.toUpperCase()) : false; - } catch { return false; } -} - -function getPosition(timeoutMs = 6000) { - return new Promise((resolve, reject) => { - if (!('geolocation' in navigator)) return reject(new Error('geolocation unavailable')); - const opts = { enableHighAccuracy: false, timeout: timeoutMs, maximumAge: 60_000 }; - navigator.geolocation.getCurrentPosition( - pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }), - err => reject(err), - opts - ); - }); -} - -async function geoByIP() { - // Try a couple of CORS-friendly IP services - try { - const r = await fetch('https://ipapi.co/json/'); - if (r.ok) { - const j = await r.json(); - if (j && typeof j.latitude === 'number' && typeof j.longitude === 'number') { - return { lat: j.latitude, lon: j.longitude, city: j.city, country: j.country_code }; - } - } - } catch {} - try { - const r = await fetch('https://ipwho.is/'); - if (r.ok) { - const j = await r.json(); - if (j && j.success && j.latitude && j.longitude) { - return { lat: j.latitude, lon: j.longitude, city: j.city, country: j.country_code }; - } - } - } catch {} - return null; -} - -async function fetchOpenMeteo(lat, lon, fahrenheit) { - const tUnit = fahrenheit ? 'fahrenheit' : 'celsius'; - const wUnit = fahrenheit ? 'mph' : 'kmh'; - const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code,wind_speed_10m&temperature_unit=${tUnit}&windspeed_unit=${wUnit}&timezone=auto`; - const r = await fetch(url); - if (!r.ok) throw new Error('weather fetch failed'); - const j = await r.json(); - return { - temp: j?.current?.temperature_2m, - wind: j?.current?.wind_speed_10m, - code: j?.current?.weather_code, - tUnit: fahrenheit ? 'ยฐF' : 'ยฐC', - wUnit: fahrenheit ? 'mph' : 'km/h', - }; -} - -function codeToSummary(code) { - // Minimal Openโ€‘Meteo WMO code mapping - const m = new Map([ - [0,'Clear'], [1,'Mainly clear'], [2,'Partly cloudy'], [3,'Cloudy'], - [45,'Fog'], [48,'Rime fog'], [51,'Drizzle'], [53,'Drizzle'], [55,'Drizzle'], - [56,'Freezing drizzle'], [57,'Freezing drizzle'], - [61,'Rain'], [63,'Rain'], [65,'Rain'], - [66,'Freezing rain'], [67,'Freezing rain'], - [71,'Snow'], [73,'Snow'], [75,'Snow'], [77,'Snow grains'], - [80,'Showers'], [81,'Showers'], [82,'Heavy showers'], - [85,'Snow showers'], [86,'Snow showers'], - [95,'Thunderstorm'], [96,'Storm'], [99,'Severe storm'] - ]); - return m.get(Number(code)) || 'Weather'; -} - -async function loadWeather() { - if (!weatherEl) return; - // Prefer an app-provided IPC source if available - try { - if (window.electronAPI && typeof window.electronAPI.invoke === 'function') { - const res = await window.electronAPI.invoke('get-weather'); - if (res && (res.temp || res.summary)) { - const summaryText = res.summary || ''; - const tempText = typeof res.temp === 'number' ? `${Math.round(res.temp)}ยฐ` : ''; - const windText = res.wind ? ` ยท Wind ${Math.round(res.wind)} ${res.wUnit || 'km/h'}` : ''; - weatherEl.textContent = `${tempText}${summaryText ? ' ยท ' + summaryText : ''}${windText}`.trim() || 'โ€”'; - return; - } - } - } catch (e) { console.warn('IPC weather failed', e); } - - try { - // 1) Try browser geolocation - let loc = null; - try { loc = await getPosition(); } catch {} - if (!loc) loc = await geoByIP(); - if (!loc) throw new Error('no location'); - const f = useFahrenheit(); - const data = await fetchOpenMeteo(loc.lat, loc.lon, f); - const summary = codeToSummary(data.code); - const temp = typeof data.temp === 'number' ? Math.round(data.temp) : data.temp; - const wind = typeof data.wind === 'number' ? Math.round(data.wind) : data.wind; - weatherEl.textContent = `${temp}${data.tUnit} ยท Wind ${wind} ${data.wUnit}`; - } catch (err) { - console.warn('Weather fetch failed', err); - weatherEl.textContent = 'โ€”'; - } -} - -startClock(); -loadWeather(); - -// Refresh weather when unit preference changes -window.addEventListener('storage', (e) => { - if (e && e.key === WEATHER_UNIT_KEY) { - loadWeather(); - } -}); - -// ---- Home layout preferences ---- -const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; -const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; -const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; -const HOME_GREETING_Y_KEY = 'nebula-home-greeting-y'; - -function applyHomeLayoutPrefs() { - try { - const root = document.documentElement; - const greetY = Number(localStorage.getItem(HOME_GREETING_Y_KEY) || 12); - const searchY = Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22); - const bmY = Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40); - root.style.setProperty('--home-greeting-y', `${greetY}vh`); - root.style.setProperty('--home-search-y', `${searchY}vh`); - root.style.setProperty('--home-bookmarks-y', `${bmY}vh`); - const corner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br'; - if (glanceEl) { - glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - glanceEl.classList.add(`pos-${corner}`); - } - // Position edit controls at the opposite horizontal side of glance (X-only move) - const oppositeHorizontal = (c) => ({ br:'bl', bl:'br', tr:'tl', tl:'tr' }[c] || 'tr'); - const editCorner = oppositeHorizontal(corner); - [editBtn, editToolbar].forEach(ctrl => { - if (!ctrl) return; - ctrl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - ctrl.classList.add(`pos-${editCorner}`); - }); - } catch (e) { console.warn('applyHomeLayoutPrefs failed', e); } -} - -applyHomeLayoutPrefs(); - -// React to settings updates via storage or host messages -window.addEventListener('storage', (e) => { - if (!e) return; - if ([HOME_SEARCH_Y_KEY, HOME_BOOKMARKS_Y_KEY, HOME_GLANCE_CORNER_KEY].includes(e.key)) { - applyHomeLayoutPrefs(); - } -}); - -if (window.electronAPI && typeof window.electronAPI.on === 'function') { - window.electronAPI.on('settings-update', (payload) => { - if (!payload) return; - if (payload.searchY != null) document.documentElement.style.setProperty('--home-search-y', `${payload.searchY}vh`); - if (payload.bookmarksY != null) document.documentElement.style.setProperty('--home-bookmarks-y', `${payload.bookmarksY}vh`); - if (payload.glanceCorner && glanceEl) { - glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - glanceEl.classList.add(`pos-${payload.glanceCorner}`); - // Update edit controls to opposite horizontal side (X-only) - const oppositeHorizontal = (c) => ({ br:'bl', bl:'br', tr:'tl', tl:'tr' }[c] || 'tr'); - const editCorner = oppositeHorizontal(payload.glanceCorner); - [editBtn, editToolbar].forEach(ctrl => { - if (!ctrl) return; - ctrl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - ctrl.classList.add(`pos-${editCorner}`); - }); - } - }); -} - -// ---- Edit mode drag support ---- -let editMode = false; -let snapshot = null; // stores values before edits -function setEditMode(on) { - editMode = !!on; - document.body.classList.toggle('edit-mode', editMode); - if (editBtn) editBtn.setAttribute('aria-pressed', String(editMode)); - if (editToolbar) editToolbar.hidden = !editMode; - if (editMode) { - // Take a snapshot of current persisted values - snapshot = { - greetY: Number(localStorage.getItem('nebula-home-greeting-y') || 12), - searchY: Number(localStorage.getItem('nebula-home-search-y') || 22), - bmY: Number(localStorage.getItem('nebula-home-bookmarks-y') || 40), - corner: localStorage.getItem('nebula-home-glance-corner') || 'br', - showGreeting: localStorage.getItem('nebula-show-greeting') !== 'false', - showBookmarks: localStorage.getItem('nebula-show-bookmarks') !== 'false', - showGlance: localStorage.getItem('nebula-show-glance') !== 'false' - }; - // Initialize toggles to snapshot values - if (toggleShowGreeting) toggleShowGreeting.checked = snapshot.showGreeting; - if (toggleShowBookmarks) toggleShowBookmarks.checked = snapshot.showBookmarks; - if (toggleShowGlance) toggleShowGlance.checked = snapshot.showGlance; - } else { - snapshot = null; - } -} - -if (editBtn) { - editBtn.addEventListener('click', () => setEditMode(!editMode)); -} - -function vhFromPx(px) { return (px / window.innerHeight) * 100; } -function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); } - -// Visibility helpers -function applyVisibilityFromStorage() { - const showGreeting = localStorage.getItem('nebula-show-greeting') !== 'false'; - const showBookmarks = localStorage.getItem('nebula-show-bookmarks') !== 'false'; - const showGlance = localStorage.getItem('nebula-show-glance') !== 'false'; - if (greetingEl) greetingEl.classList.toggle('is-hidden', !showGreeting); - if (topSitesEl) topSitesEl.classList.toggle('is-hidden', !showBookmarks); - if (glanceEl) glanceEl.classList.toggle('is-hidden', !showGlance); -} - -applyVisibilityFromStorage(); - -function makeDragY(el, storageKey, cssVar) { - if (!el) return; - let startY = 0; let startTopVh = 0; let dragging = false; - // Cache geometry at drag start for consistent clamping in px - let startRectTopPx = 0; let elHeightPx = 0; const MARGIN_PX = 12; - const onDown = (ev) => { - if (!editMode) return; dragging = true; - const p = ev.touches ? ev.touches[0] : ev; - startY = p.clientY; - // current computed var (in vh) - const current = Number((getComputedStyle(document.documentElement).getPropertyValue(cssVar) || '0vh').replace('vh','')); - startTopVh = isNaN(current) ? 0 : current; - // snapshot element geometry - const rect = el.getBoundingClientRect(); - startRectTopPx = rect.top; - elHeightPx = rect.height; - ev.preventDefault(); - }; - const onMove = (ev) => { - if (!dragging) return; - const p = ev.touches ? ev.touches[0] : ev; - const deltaPx = p.clientY - startY; - // Clamp so the element stays within the viewport with a small margin - const minTopPx = MARGIN_PX; - const maxTopPx = Math.max(minTopPx, window.innerHeight - MARGIN_PX - elHeightPx); - const desiredTopPx = startRectTopPx + deltaPx; - const clampedTopPx = clamp(desiredTopPx, minTopPx, maxTopPx); - const clampedDeltaPx = clampedTopPx - startRectTopPx; - const deltaVh = vhFromPx(clampedDeltaPx); - const nextVh = startTopVh + deltaVh; - document.documentElement.style.setProperty(cssVar, `${nextVh}vh`); - }; - const onUp = () => { - if (!dragging) return; dragging = false; - // Don't persist here; only on Save. Values still applied via CSS var. - }; - el.addEventListener('mousedown', onDown); - el.addEventListener('touchstart', onDown, { passive:false }); - window.addEventListener('mousemove', onMove); - window.addEventListener('touchmove', onMove, { passive:false }); - window.addEventListener('mouseup', onUp); - window.addEventListener('touchend', onUp); -} - -function makeDragGlance(el) { - if (!el) return; - let dragging = false; let start; - const onDown = (ev) => { - if (!editMode) return; dragging = true; el.classList.add('dragging'); - const p = ev.touches?ev.touches[0]:ev; start = { x:p.clientX, y:p.clientY }; - // reset any prior drag offsets - el.style.setProperty('--drag-x','0px'); el.style.setProperty('--drag-y','0px'); - ev.preventDefault(); - }; - const onMove = (ev) => { - if (!dragging) return; const p = ev.touches?ev.touches[0]:ev; - const dx = p.clientX - start.x; const dy = p.clientY - start.y; - el.style.setProperty('--drag-x', `${dx}px`); - el.style.setProperty('--drag-y', `${dy}px`); - }; - const onUp = (ev) => { - if (!dragging) return; dragging = false; el.classList.remove('dragging'); - el.style.removeProperty('--drag-x'); el.style.removeProperty('--drag-y'); - const p = ev.changedTouches?ev.changedTouches[0]:ev; - const x = p.clientX; const y = p.clientY; - // snap to nearest corner - const left = x < window.innerWidth/2; - const top = y < window.innerHeight/2; - const corner = top ? (left ? 'tl' : 'tr') : (left ? 'bl' : 'br'); - // Only store corner on Save; temporarily apply class for preview - if (glanceEl) { - glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - glanceEl.classList.add(`pos-${corner}`); - // Stash pending corner choice on the element during edit mode - glanceEl.dataset.pendingCorner = corner; - } - // Also move edit controls to opposite corner during preview - const opposite = (c) => ({ br:'tl', bl:'tr', tr:'bl', tl:'br' }[c] || 'tl'); - const editCorner = opposite(corner); - [editBtn, editToolbar].forEach(ctrl => { - if (!ctrl) return; - ctrl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - ctrl.classList.add(`pos-${editCorner}`); - }); - }; - el.addEventListener('mousedown', onDown); - el.addEventListener('touchstart', onDown, { passive:false }); - window.addEventListener('mousemove', onMove); - window.addEventListener('touchmove', onMove, { passive:false }); - window.addEventListener('mouseup', onUp); - window.addEventListener('touchend', onUp); -} - -makeDragY(searchContainerEl, 'nebula-home-search-y', '--home-search-y'); -makeDragY(topSitesEl, 'nebula-home-bookmarks-y', '--home-bookmarks-y'); -makeDragGlance(glanceEl); -// Restore greeting to Y-only drag -makeDragY(greetingTitleEl, 'nebula-home-greeting-y', '--home-greeting-y'); - -// Keep draggable blocks within viewport on resize -function keepVisibleWithinViewport() { - const root = document.documentElement; - const adjust = (el, cssVar) => { - if (!el) return; - const rect = el.getBoundingClientRect(); - const MARGIN_PX = 12; - const minTopPx = MARGIN_PX; - const maxTopPx = Math.max(minTopPx, window.innerHeight - MARGIN_PX - rect.height); - let topPx = rect.top; - if (topPx < minTopPx || topPx > maxTopPx) { - const currentVh = Number((getComputedStyle(root).getPropertyValue(cssVar) || '0vh').replace('vh','')) || 0; - // Compute how far to move (px) to bring within range, then convert to vh and adjust var - const targetTopPx = clamp(topPx, minTopPx, maxTopPx); - const deltaPx = targetTopPx - topPx; - const nextVh = currentVh + vhFromPx(deltaPx); - root.style.setProperty(cssVar, `${nextVh}vh`); - } - }; - adjust(greetingTitleEl, '--home-greeting-y'); - adjust(searchContainerEl, '--home-search-y'); - adjust(topSitesEl, '--home-bookmarks-y'); -} - -let resizeTimer; -window.addEventListener('resize', () => { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(keepVisibleWithinViewport, 80); -}); - -// Toggle handlers (search cannot be hidden) -function bindVisibilityToggles() { - if (toggleShowGreeting) toggleShowGreeting.addEventListener('change', () => { - const val = toggleShowGreeting.checked; - if (greetingEl) greetingEl.classList.toggle('is-hidden', !val); - }); - if (toggleShowBookmarks) toggleShowBookmarks.addEventListener('change', () => { - const val = toggleShowBookmarks.checked; - if (topSitesEl) topSitesEl.classList.toggle('is-hidden', !val); - }); - if (toggleShowGlance) toggleShowGlance.addEventListener('change', () => { - const val = toggleShowGlance.checked; - if (glanceEl) glanceEl.classList.toggle('is-hidden', !val); - }); -} - -bindVisibilityToggles(); - -// Save/Cancel handlers -if (saveEditBtn) saveEditBtn.addEventListener('click', () => { - // Persist current CSS variable values and pending corner - const rootStyle = getComputedStyle(document.documentElement); - const getVh = (v) => Math.round(Number((v || '0vh').replace('vh',''))); - const gy = getVh(rootStyle.getPropertyValue('--home-greeting-y')); - const sy = getVh(rootStyle.getPropertyValue('--home-search-y')); - const by = getVh(rootStyle.getPropertyValue('--home-bookmarks-y')); - try { - localStorage.setItem('nebula-home-greeting-y', String(gy)); - localStorage.setItem('nebula-home-search-y', String(sy)); - localStorage.setItem('nebula-home-bookmarks-y', String(by)); - // Persist visibility - if (toggleShowGreeting) localStorage.setItem('nebula-show-greeting', String(!!toggleShowGreeting.checked)); - if (toggleShowBookmarks) localStorage.setItem('nebula-show-bookmarks', String(!!toggleShowBookmarks.checked)); - if (toggleShowGlance) localStorage.setItem('nebula-show-glance', String(!!toggleShowGlance.checked)); - } catch {} - const corner = glanceEl?.dataset?.pendingCorner || localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br'; - try { localStorage.setItem(HOME_GLANCE_CORNER_KEY, corner); } catch {} - if (glanceEl) delete glanceEl.dataset.pendingCorner; - setEditMode(false); - // Re-apply from saved storage to ensure consistent state after exiting edit mode - applyVisibilityFromStorage(); -}); - -if (cancelEditBtn) cancelEditBtn.addEventListener('click', () => { - // Revert CSS vars and glance corner to snapshot - if (snapshot) { - document.documentElement.style.setProperty('--home-greeting-y', `${snapshot.greetY}vh`); - document.documentElement.style.setProperty('--home-search-y', `${snapshot.searchY}vh`); - document.documentElement.style.setProperty('--home-bookmarks-y', `${snapshot.bmY}vh`); - if (glanceEl) { - glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl'); - glanceEl.classList.add(`pos-${snapshot.corner}`); - delete glanceEl.dataset.pendingCorner; - } - } else { - applyHomeLayoutPrefs(); - } - setEditMode(false); - // Revert visibility to snapshot - if (snapshot) { - if (greetingEl) greetingEl.classList.toggle('is-hidden', !snapshot.showGreeting); - if (topSitesEl) topSitesEl.classList.toggle('is-hidden', !snapshot.showBookmarks); - if (glanceEl) glanceEl.classList.toggle('is-hidden', !snapshot.showGlance); - if (toggleShowGreeting) toggleShowGreeting.checked = snapshot.showGreeting; - if (toggleShowBookmarks) toggleShowBookmarks.checked = snapshot.showBookmarks; - if (toggleShowGlance) toggleShowGlance.checked = snapshot.showGlance; - } else { - applyVisibilityFromStorage(); - } -}); diff --git a/renderer/iconSets.js b/renderer/iconSets.js deleted file mode 100644 index 302c032..0000000 --- a/renderer/iconSets.js +++ /dev/null @@ -1,148 +0,0 @@ -// Unified icon set loaders with graceful fallbacks. -// Each loader returns an array of string icon names (NOT SVG markup) suitable for name-based selection. -// Some libraries don't have an easy metadata endpoint; we attempt a fetch and fall back to a small curated subset. - -import { fetchAllIcons as fetchMaterialIcons, icons as materialFallback } from './icons.js'; - -async function attemptJSON(url, transform) { - try { - const res = await fetch(url, { cache: 'no-store' }); - if (!res.ok) throw new Error(res.status + ' ' + res.statusText); - const data = await res.json(); - return transform ? transform(data) : data; - } catch (e) { - console.warn('[IconSets] Failed to fetch', url, e); - return null; - } -} - -// --- SVG helpers --- -async function attemptText(url) { - try { - const res = await fetch(url, { cache: 'force-cache' }); - if (!res.ok) throw new Error(res.status + ' ' + res.statusText); - const txt = await res.text(); - if (!/^$/i.test(txt.trim())) throw new Error('Not SVG'); - return txt; - } catch { - return null; - } -} -function svgToDataUrl(svg) { - return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg.replace(//gi, '')); -} - -const staticFallbacks = { - lucide: ['activity','airplay','alarm-clock','align-center','anchor','apple','archive','arrow-big-up','at-sign','award','battery','bell','bluetooth','book','bookmark','briefcase','calendar','camera','cast','check','chevron-down','chrome','cloud','code','command','compass','cpu','database','download','edit','external-link','eye','file','folder','gamepad','globe','heart','help-circle','home','image','info','keyboard','layers','link','list','lock','mail','map','menu','mic','moon','music','package','pie-chart','play','plus','pocket','power','refresh-ccw','rss','save','scissors','search','settings','share','shield','smartphone','speaker','star','sun','tablet','tag','terminal','thumbs-up','trash','tv','twitter','upload','user','video','wifi','x','zap'], - tabler: ['activity','alarm','affiliate','anchor','api','app-window','apple','archive','armchair','arrow-down','at','award','backspace','ballon','battery','bell','bluetooth','bolt','book','bookmark','briefcase','browser','bug','building','calendar','camera','car','chart-area','chart-bar','chart-pie','chart-scatter','check','chevron-down','cloud','code','coffee','color-swatch','command','compass','cpu','credit-card','dashboard','database','device-desktop','device-mobile','dice','dna','download','drop','edit','file','filter','flag','flame','folder','gift','globe','grid','hash','headphones','heart','help','home','id','inbox','info-circle','key','keyboard','language','layers','layout','layout-grid','letter-a','link','lock','login','logout','mail','map','menu','message','microphone','mood-happy','moon','music','news','note','package','password','phone','photo','player-play','plug','plus','power','printer','puzzle','refresh','rocket','route','rss','school','search','server','settings','share','shield','smart-home','snowflake','sparkles','star','sun','switch','tag','thumb-up','tool','trash','trophy','typography','upload','user','video','wifi','world','x'], - phosphor: ['activity','airplane','anchor','apple-logo','archive','arrow-down','arrow-up','at','bag','bell','book','bookmark','bounding-box','briefcase','browser','bug','calendar','camera','car','check','clipboard','cloud','code','command','compass','cpu','credit-card','database','device-mobile','device-tablet','door','download','drop','envelope','eye','eyedropper','file','film-strip','flag','flame','folder','funnel','game-controller','gear','globe','hand','hash','headphones','heart','house','image','info','key','keyboard','leaf','link','lock','magnet','magnifying-glass','map-pin','microphone','moon','music-note','note','nut','package','paper-plane','paperclip','path','pen','phone','plug','plus','power','printer','question','rocket','rss','scissors','share','shield','shopping-cart','sketch-logo','smiley','sparkle','speaker-high','star','sun','swatches','tag','terminal','thumbs-up','toolbox','trash','trophy','tv','user','users','video-camera','wifi-high','x','yarn','youtube-logo','zap'], - remix: ['add','alarm','alert','anchor','apps','archive','arrow-down','arrow-right','arrow-up','at','award','bank','bar-chart','battery','bell','bluetooth','book','bookmark','briefcase','bug','building','calendar','camera','car','chat','chrome','clipboard','cloud','code','command','compass','copyleft','copyright','cpu','dashboard','database','delete-bin','device','dice','download','dribbble','drive','earth','edge','edit','facebook','file','filter','fire','flag','folder','gamepad','gift','github','gitlab','global','google','group','hard-drive','heart','home','image','inbox','instagram','keyboard','keynote','layout','links','list','lock','login','logout','mac','mail','map','menu','message','mic','moon','music','notification','paragraph','pause','phone','picture-in-picture','play','plug','price-tag','print','qr-code','question','reddit','refresh','restart','rocket','rss','scales','search','secure-payment','send','settings','share','shield','shopping-bag','slack','smartphone','sound-module','star','sun','t-box','tablet','tag','telegram','thumb-up','timer','tool','trophy','twitter','tv','upload','usb','user','video','visa','voicemail','volume-up','wallet','wifi','windows','xbox','youtube','zoom-in'], - bootstrap: ['alarm','android','apple','archive','arrow-down','arrow-up','arrow-left','arrow-right','at','award','backspace','badge-4k','bag','bank','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','brush','bug','calendar','camera','card-image','card-list','cart','chat','check','chevron-down','circle','cloud','code','command','compass','cpu','credit-card','database','device-hdd','device-ssd','display','download','droplet','earbuds','emoji-smile','envelope','exclamation','eye','facebook','file','filter','flag','folder','funnel','gear','gift','globe','google','graph-up','grid','hammer','hand-thumbs-up','hash','headphones','heart','house','image','info','instagram','joystick','keyboard','laptop','layers','layout-split','lightning','link','lock','mailbox','map','megaphone','menu-button','mic','moon','music-note','nut','palette','paperclip','patch-check','pen','pencil','people','phone','pin','play','plug','plus','power','printer','qr-code','question','rocket','rss','save','scissors','search','server','share','shield','shop','skip-forward','slack','speaker','speedometer','star','sun','tablet','tag','terminal','tools','trash','trophy','truck','twitch','twitter','type','ui-checks','upload','usb','vector-pen','wallet','whatsapp','wifi','windows','wrench','x','youtube'], - heroicons: ['academic-cap','adjustments-horizontal','adjustments-vertical','archive-box','arrow-down','arrow-up','arrow-right','arrow-left','at-symbol','backspace','banknotes','bars-2','bars-3','battery-100','beaker','bell','bookmark','briefcase','cake','calendar','camera','chart-bar','chat-bubble-bottom-center','chat-bubble-left','check','chevron-down','chip','circle-stack','cloud','code-bracket','cog','command-line','computer-desktop','cpu-chip','cube','currency-dollar','device-phone-mobile','device-tablet','document','document-text','ellipsis-horizontal','envelope','exclamation-circle','eye','film','finger-print','fire','flag','folder','gift','globe-alt','hand-thumb-up','heart','home','identification','inbox','information-circle','key','language','lifebuoy','light-bulb','link','lock-closed','magnifying-glass','map','megaphone','microphone','moon','musical-note','newspaper','paint-brush','paper-airplane','paper-clip','phone','photo','play','plus','power','printer','puzzle-piece','qr-code','question-mark-circle','rocket-launch','rss','scale','scissors','server','share','shield-check','sparkles','square-3-stack-3d','star','sun','swatch','tag','trophy','tv','user','users','video-camera','wallet','wifi','wrench','x-mark'], - feather: ['activity','airplay','alert-circle','alert-triangle','anchor','aperture','archive','at-sign','award','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','calendar','camera','cast','check','chevron-down','chrome','circle','clipboard','cloud','code','command','compass','cpu','database','download','droplet','edit','eye','facebook','file','film','filter','flag','folder','gift','git-branch','git-commit','git-merge','github','gitlab','globe','grid','hash','headphones','heart','help-circle','home','image','info','instagram','key','layers','layout','link','lock','mail','map','menu','mic','monitor','moon','music','package','paperclip','pause','pen-tool','phone','play','plus','pocket','power','printer','radio','refresh-ccw','refresh-cw','repeat','rewind','rss','save','scissors','search','send','server','settings','share','shield','shopping-bag','shopping-cart','shuffle','slack','smartphone','speaker','square','star','sun','tablet','tag','target','terminal','thumbs-up','tool','trash','trello','trending-up','triangle','truck','tv','twitter','type','umbrella','unlock','upload','user','users','video','voicemail','volume','watch','wifi','wind','x','zap'], - simple: ['github','gitlab','google','youtube','twitter','facebook','twitch','discord','spotify','apple','microsoft','android','linux','ubuntu','x','linkedin','npm','pypi','docker','kubernetes','aws','azure','gcp','cloudflare','figma','notion','slack','whatsapp','meta','paypal','stripe','reddit','snapchat','steam','xbox','playstation','nintendo','instagram','pinterest','soundcloud','openai','vercel','netlify','digitalocean'], - radix: ['activity-log','airplane','backpack','bell','bookmark','calendar','camera','card-stack','caret-down','caret-up','chat-bubble','chat-dots','check','chevron-down','chevron-left','chevron-right','chevron-up','clock','code','component-1','component-2','cookie','copy','cube','discord-logo','double-arrow-down','double-arrow-left','double-arrow-right','double-arrow-up','drag-handle-dots-2','envelope-closed','envelope-open','exclamation-triangle','external-link','eye-open','file','file-text','file-plus','gear','globe','heart','home','image','info-circled','keyboard','laptop','layers','link-1','link-2','lock-closed','magic-wand','magnifying-glass','moon','notebook','open-in-new-window','paper-plane','pencil-1','person','pie-chart','pin-left','pin-right','plus','question-mark-circled','reload','rocket','rows','scissors','share-1','share-2','shield','speaker-loud','star','sun','target','trash','upload','video','zoom-in','zoom-out'] -}; - -export const iconSets = { - material: { - label: 'Material', - loader: async () => { try { return await fetchMaterialIcons(); } catch { return materialFallback; } }, - fetchIcon: async () => null - }, - lucide: { - label: 'Lucide', - loader: async () => { - const data = await attemptJSON('https://cdn.jsdelivr.net/npm/lucide@latest/dist/metadata.json', d => Object.keys(d)); - return data && data.length ? data : staticFallbacks.lucide; - }, - fetchIcon: async (name) => { - const svg = await attemptText(`https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${name}.svg`); - return svg ? svgToDataUrl(svg) : null; - } - }, - tabler: { - label: 'Tabler', - loader: async () => { - const data = await attemptJSON('https://cdn.jsdelivr.net/gh/tabler/tabler-icons@latest/icons.json', d => d.map(o => o.name)); - return data && data.length ? data : staticFallbacks.tabler; - }, - fetchIcon: async (name) => { - const urls = [ - `https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/outline/${name}.svg`, - `https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/filled/${name}.svg` - ]; - for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); } - return null; - } - }, - phosphor: { - label: 'Phosphor', - loader: async () => staticFallbacks.phosphor, - fetchIcon: async (name) => { - const styles = ['regular','bold','duotone','fill','light','thin']; - for (const style of styles) { - const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/${style}/${name}.svg`); - if (svg) return svgToDataUrl(svg); - } - return null; - } - }, - remix: { - label: 'Remix', - loader: async () => staticFallbacks.remix, - fetchIcon: async () => null, - fontClass: (name) => `ri-${name}-line` // use line style font sprite - }, - bootstrap: { - label: 'Bootstrap', - loader: async () => staticFallbacks.bootstrap, - fetchIcon: async (name) => { - const svg = await attemptText(`https://cdn.jsdelivr.net/npm/bootstrap-icons@latest/icons/${name}.svg`); - return svg ? svgToDataUrl(svg) : null; - }, - fontClass: (name) => `bi-${name}` - }, - heroicons: { - label: 'Heroicons', - loader: async () => staticFallbacks.heroicons, - fetchIcon: async (name) => { - const urls = [ - `https://cdn.jsdelivr.net/npm/heroicons@2/24/outline/${name}.svg`, - `https://cdn.jsdelivr.net/npm/heroicons@2/24/solid/${name}.svg` - ]; - for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); } - return null; - } - }, - feather: { - label: 'Feather', - loader: async () => staticFallbacks.feather, - fetchIcon: async (name) => { - const svg = await attemptText(`https://cdn.jsdelivr.net/npm/feather-icons@4/dist/icons/${name}.svg`); - return svg ? svgToDataUrl(svg) : null; - }, - fontClass: (name) => `icon-${name}` // fallback for display - }, - simple: { - label: 'Simple Icons', - loader: async () => staticFallbacks.simple, - fetchIcon: async (name) => { - const svg = await attemptText(`https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${name}.svg`); - return svg ? svgToDataUrl(svg) : null; - } - }, - radix: { - label: 'Radix', - loader: async () => staticFallbacks.radix, - fetchIcon: async (name) => { - const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@radix-ui/icons@latest/icons/${name}.svg`); - return svg ? svgToDataUrl(svg) : null; - } - } -}; - -// Utility: get list of set keys + label for UI -export function listIconSets() { - return Object.entries(iconSets).map(([key, val]) => ({ key, label: val.label })); -} diff --git a/renderer/icons.js b/renderer/icons.js deleted file mode 100644 index 988991a..0000000 --- a/renderer/icons.js +++ /dev/null @@ -1,21 +0,0 @@ -// This file is automatically generated from Google's Material Icons. -/** - * Fetches the full list of Material Icon names from Google Fonts. - * Returns an array of strings like ["3d_rotation","access_alarm",โ€ฆ] - */ -export async function fetchAllIcons() { - const res = await fetch("https://fonts.google.com/metadata/icons"); - let txt = await res.text(); - // strip the weird prefix )]}'\n - txt = txt.replace(/^\)\]\}'\s*/, ""); - const json = JSON.parse(txt); - return json.icons.map(icon => icon.name); -} - -// Fallback static array for immediate use (e.g. the "+" button and bookmark icons) -export const icons = [ - 'add', - 'bookmark', - 'star', - // โ€ฆadd any other icons your components expect synchronouslyโ€ฆ -]; \ No newline at end of file diff --git a/renderer/icons.json b/renderer/icons.json deleted file mode 100644 index 99093e0..0000000 --- a/renderer/icons.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - "home", - "star", - "bookmark", - "favorite", - "public", - "search", - "settings" - // โ€ฆ add as many icon names as you like โ€ฆ -] diff --git a/renderer/index.html b/renderer/index.html deleted file mode 100644 index f8c78e5..0000000 --- a/renderer/index.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - Nebula Browser - - - - - -
-
-
- - - -
-
- - - -
- - - - \ No newline at end of file diff --git a/renderer/insecure.html b/renderer/insecure.html deleted file mode 100644 index f215953..0000000 --- a/renderer/insecure.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - -Connection Not Secure - - - - -
-

- - Connection Not Secure http -

-

Youโ€™re about to visit a page using HTTP (unencrypted). Information you send or view can potentially be intercepted or modified. If this is a site you trust and you understand the risks, you can continue anyway.

-
-
    -
  • No TLS encryption โ€“ data (including passwords or forms) travels in plain text.
  • -
  • Attackers on the same network (cafรฉ Wiโ€‘Fi, school, workplace) could tamper with or read content.
  • -
  • The site might support HTTPS. Try manually changing to https:// first.
  • -
  • Proceed only if necessary and you have a reason to trust this destination.
  • -
-
- - - -
-
Nebula Secure Navigation Interstitial
-
- - - diff --git a/renderer/menu-popup.css b/renderer/menu-popup.css deleted file mode 100644 index ffc9cef..0000000 --- a/renderer/menu-popup.css +++ /dev/null @@ -1,66 +0,0 @@ -:root { - --bg: #0b0d10; - --primary: #7b2eff; - --accent: #00c6ff; - --text: #e0e0e0; - --url-bar-bg: #1c2030; - --url-bar-border: #3e4652; - --shadow-1: 0 12px 30px rgba(0, 0, 0, 0.35); - --blur: 12px; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - background: transparent; - color: var(--text); - font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; -} - -#menu-popup { - background: color-mix(in srgb, var(--url-bar-bg) 92%, var(--text) 8%); - border: 1px solid color-mix(in srgb, var(--primary) 25%, color-mix(in srgb, var(--accent) 18%, transparent)); - border-radius: 14px; - padding: 8px; - display: flex; - flex-direction: column; - min-width: 220px; - box-shadow: var(--shadow-1); - -webkit-backdrop-filter: blur(var(--blur)); - backdrop-filter: blur(var(--blur)); -} - -#menu-popup button { - background: transparent; - border: none; - color: var(--text); - text-align: left; - padding: 8px 10px; - border-radius: 10px; - cursor: pointer; - transition: background 120ms ease, filter 120ms ease; -} - -#menu-popup button:hover { - background: color-mix(in srgb, var(--text) 8%, transparent); -} - -.zoom-controls { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 6px 8px; -} - -.zoom-controls button { - width: 28px; - height: 28px; - text-align: center; -} - -#zoom-percent { - min-width: 54px; - text-align: center; -} diff --git a/renderer/menu-popup.html b/renderer/menu-popup.html deleted file mode 100644 index 3e8678c..0000000 --- a/renderer/menu-popup.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - Menu - - - - - - - diff --git a/renderer/menu-popup.js b/renderer/menu-popup.js deleted file mode 100644 index 9d24789..0000000 --- a/renderer/menu-popup.js +++ /dev/null @@ -1,49 +0,0 @@ -const zoomPercentEl = document.getElementById('zoom-percent'); - -function setCssVar(name, value, fallback) { - const val = value || fallback; - if (val) document.documentElement.style.setProperty(name, val); -} - -function applyTheme(theme) { - const colors = theme?.colors || theme || {}; - setCssVar('--bg', colors.bg, '#0b0d10'); - setCssVar('--dark-blue', colors.darkBlue, '#0b1c2b'); - setCssVar('--dark-purple', colors.darkPurple, '#1b1035'); - setCssVar('--primary', colors.primary, '#7b2eff'); - setCssVar('--accent', colors.accent, '#00c6ff'); - setCssVar('--text', colors.text, '#e0e0e0'); - setCssVar('--url-bar-bg', colors.urlBarBg, '#1c2030'); - setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652'); -} - -async function refreshZoom() { - if (!window.electronAPI?.invoke || !zoomPercentEl) return; - try { - const z = await window.electronAPI.invoke('get-zoom-factor'); - zoomPercentEl.textContent = `${Math.round(z * 100)}%`; - } catch {} -} - -window.electronAPI?.on?.('menu-popup-init', (payload) => { - applyTheme(payload?.theme); - refreshZoom(); -}); - -window.addEventListener('click', (e) => { - const btn = e.target.closest('button[data-cmd]'); - if (!btn) return; - const cmd = btn.getAttribute('data-cmd'); - window.electronAPI?.send?.('menu-popup-command', { cmd }); - if (cmd === 'zoom-in' || cmd === 'zoom-out') { - setTimeout(refreshZoom, 50); - } -}); - -window.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - window.electronAPI?.send?.('menu-popup-command', { cmd: 'close' }); - } -}); - -refreshZoom(); diff --git a/renderer/nebot.html b/renderer/nebot.html deleted file mode 100644 index 58af12b..0000000 --- a/renderer/nebot.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - Nebot - - - - -
- - - diff --git a/renderer/performance.css b/renderer/performance.css deleted file mode 100644 index fa1a115..0000000 --- a/renderer/performance.css +++ /dev/null @@ -1,72 +0,0 @@ -/* Performance optimizations for renderer CSS - GPU Error 18 compatible */ - -/* Conservative hardware acceleration for animations */ -.tab, .bookmark, .icon-item { - /* Only enable will-change when actually needed */ - transform: translateZ(0); -} - -.tab:hover, .bookmark:hover, .icon-item:hover { - will-change: transform; -} - -.tab:not(:hover), .bookmark:not(:hover), .icon-item:not(:hover) { - will-change: auto; -} - -/* Optimize scrolling - more conservative approach */ -#webviews, #bookmarkList, #iconGrid { - -webkit-overflow-scrolling: touch; - /* Use layout containment only, avoid paint containment which can cause GPU issues */ - contain: layout style; -} - -/* Use CSS containment for better performance - conservative approach */ -.tab-content { - contain: layout style; -} - -/* Optimize transitions - reduced complexity */ -.tab { - transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Reduce paint areas - more conservative transforms */ -.tab:hover, .bookmark:hover { - transform: scale(1.01); /* Reduced scale to minimize GPU load */ -} - -/* Use efficient selectors */ -.material-symbols-outlined { - font-display: swap; -} - -/* Optimize text rendering - conservative settings */ -body { - text-rendering: optimizeSpeed; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/* Conditional subpixel rendering for retina displays */ -@media (-webkit-min-device-pixel-ratio: 2) { - body { - -webkit-font-smoothing: subpixel-antialiased; - } -} - -/* Additional GPU-safe optimizations */ -* { - /* Prevent unnecessary repaints */ - backface-visibility: hidden; -} - -/* Safe animation performance */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.fade-in { - animation: fadeIn 0.3s ease-in-out; -} diff --git a/renderer/script.js b/renderer/script.js deleted file mode 100644 index 8a10270..0000000 --- a/renderer/script.js +++ /dev/null @@ -1,1657 +0,0 @@ -const ipcRenderer = window.electronAPI; -// Lightweight debug logger (toggleable) -const DEBUG = false; -const debug = (...args) => { if (DEBUG) console.log(...args); }; - -// Scroll normalization CSS and JS to ensure consistent scroll speed across all sites -const SCROLL_NORMALIZATION_CSS = ` - /* Disable smooth scrolling behavior that some sites force */ - *, *::before, *::after { - scroll-behavior: auto !important; - } - html, body { - scroll-behavior: auto !important; - } -`; - -const SCROLL_NORMALIZATION_JS = ` -(function() { - if (window.__nebulaScrollNormalized) return; - window.__nebulaScrollNormalized = true; - - // Consistent scroll amount in pixels per wheel delta unit - const SCROLL_SPEED = 100; - - // Intercept wheel events to normalize scroll speed - document.addEventListener('wheel', function(e) { - // Don't interfere if modifier keys are pressed (zoom, horizontal scroll, etc.) - if (e.ctrlKey || e.metaKey || e.altKey) return; - - // Get the scroll target - let target = e.target; - let scrollable = null; - - // Find the nearest scrollable element - while (target && target !== document.body && target !== document.documentElement) { - const style = window.getComputedStyle(target); - const overflowY = style.overflowY; - const overflowX = style.overflowX; - - if ((overflowY === 'auto' || overflowY === 'scroll') && target.scrollHeight > target.clientHeight) { - scrollable = target; - break; - } - if ((overflowX === 'auto' || overflowX === 'scroll') && target.scrollWidth > target.clientWidth && e.shiftKey) { - scrollable = target; - break; - } - target = target.parentElement; - } - - // If no scrollable container found, use the document - if (!scrollable) { - scrollable = document.scrollingElement || document.documentElement || document.body; - } - - // Calculate normalized scroll delta - // deltaMode: 0 = pixels, 1 = lines, 2 = pages - let deltaY = e.deltaY; - let deltaX = e.deltaX; - - if (e.deltaMode === 1) { - // Line mode - multiply by line height approximation - deltaY *= SCROLL_SPEED; - deltaX *= SCROLL_SPEED; - } else if (e.deltaMode === 2) { - // Page mode - multiply by viewport height - deltaY *= window.innerHeight; - deltaX *= window.innerWidth; - } else { - // Pixel mode - normalize to consistent speed - // Clamp the delta to prevent extremely fast scrolling from some sites - const sign = deltaY > 0 ? 1 : -1; - deltaY = sign * Math.min(Math.abs(deltaY), SCROLL_SPEED * 3); - - const signX = deltaX > 0 ? 1 : -1; - deltaX = signX * Math.min(Math.abs(deltaX), SCROLL_SPEED * 3); - } - - // Apply scroll - e.preventDefault(); - scrollable.scrollBy({ - top: deltaY, - left: e.shiftKey ? deltaX : 0, - behavior: 'auto' - }); - }, { passive: false, capture: true }); -})(); -`; - -// Function to apply scroll normalization to a webview -function applyScrollNormalization(webview) { - try { - // Inject CSS to disable smooth scrolling - webview.insertCSS(SCROLL_NORMALIZATION_CSS); - // Inject JS to normalize wheel scroll speed - webview.executeJavaScript(SCROLL_NORMALIZATION_JS); - debug('[Scroll] Applied scroll normalization to webview'); - } catch (err) { - console.warn('[Scroll] Failed to apply scroll normalization:', err); - } -} - -// Site history management using localStorage -function getSiteHistory() { - try { - const history = localStorage.getItem('siteHistory'); - return history ? JSON.parse(history) : []; - } catch (err) { - console.error('Error reading site history from localStorage:', err); - return []; - } -} - -function addToSiteHistory(url) { - try { - let history = getSiteHistory(); - // Remove if already exists to avoid duplicates - history = history.filter(item => item !== url); - // Add to beginning - history.unshift(url); - // Keep only last 100 entries - if (history.length > 100) { - history = history.slice(0, 100); - } - localStorage.setItem('siteHistory', JSON.stringify(history)); - } catch (err) { - console.error('Error saving site history to localStorage:', err); - } -} - -// Search history management using localStorage -function getSearchHistory() { - try { - const history = localStorage.getItem('searchHistory'); - return history ? JSON.parse(history) : []; - } catch (err) { - console.error('Error reading search history from localStorage:', err); - return []; - } -} - -function addToSearchHistory(searchQuery) { - try { - let history = getSearchHistory(); - // Remove if already exists to avoid duplicates - history = history.filter(item => item !== searchQuery); - // Add to beginning - history.unshift(searchQuery); - // Keep only last 100 entries - if (history.length > 100) { - history = history.slice(0, 100); - } - localStorage.setItem('searchHistory', JSON.stringify(history)); - // Also save to file via IPC for persistence - if (window.electronAPI && window.electronAPI.invoke) { - window.electronAPI.invoke('save-search-history', history); - } - } catch (err) { - console.error('Error saving search history to localStorage:', err); - } -} - -// Store current theme colors globally for use by renderTabs -let currentThemeColors = null; - -// Apply theme colors to the main UI (URL bar and tabs) -function applyThemeToMainUI(theme) { - if (!theme || !theme.colors) return; - const root = document.documentElement; - const colors = theme.colors; - - // Store colors globally for renderTabs to use - currentThemeColors = colors; - - // Set CSS variables on root for elements using var() - const setVar = (cssVar, value, fallback) => { - const val = value || fallback; - if (val) root.style.setProperty(cssVar, val); - }; - - // Core palette so popups/menus and the address bar stay in sync - setVar('--bg', colors.bg, '#0b0d10'); - setVar('--dark-blue', colors.darkBlue, '#0b1c2b'); - setVar('--dark-purple', colors.darkPurple, '#1b1035'); - setVar('--primary', colors.primary, '#7b2eff'); - setVar('--accent', colors.accent, '#00c6ff'); - setVar('--text', colors.text, '#e0e0e0'); - - // URL bar + tab strip styling - setVar('--url-bar-bg', colors.urlBarBg, '#1c2030'); - setVar('--url-bar-text', colors.urlBarText, '#e0e0e0'); - setVar('--url-bar-border', colors.urlBarBorder, '#3e4652'); - setVar('--tab-bg', colors.tabBg, '#161925'); - setVar('--tab-text', colors.tabText, '#a4a7b3'); - setVar('--tab-active', colors.tabActive, '#1c2030'); - setVar('--tab-active-text', colors.tabActiveText, '#e0e0e0'); - setVar('--tab-border', colors.tabBorder, '#2b3040'); - - // Also directly apply to key elements to ensure styles take effect - const nav = document.getElementById('nav'); - const titlebarContainer = document.getElementById('titlebar-container'); - const tabBar = document.getElementById('tab-bar'); - const urlBox = document.getElementById('url'); - const navCenter = document.querySelector('.nav-center'); - - if (nav) { - nav.style.setProperty('background', colors.urlBarBg || '#1c2030', 'important'); - nav.style.setProperty('border-bottom-color', colors.urlBarBorder || '#3e4652', 'important'); - } - if (navCenter) { - navCenter.style.setProperty('background', colors.urlBarBg || '#1c2030', 'important'); - navCenter.style.setProperty('border-color', colors.urlBarBorder || '#3e4652', 'important'); - } - if (titlebarContainer) { - titlebarContainer.style.setProperty('background', colors.tabBg || '#161925', 'important'); - } - if (tabBar) { - tabBar.style.setProperty('background', colors.tabBg || '#161925', 'important'); - tabBar.style.setProperty('border-bottom-color', colors.tabBorder || '#2b3040', 'important'); - } - if (urlBox) { - urlBox.style.setProperty('color', colors.urlBarText || '#e0e0e0', 'important'); - } - - // Update existing tab elements to reflect new theme colors - document.querySelectorAll('.tab').forEach(tab => { - const isActive = tab.classList.contains('active'); - tab.style.setProperty('background', isActive - ? (colors.tabActive || '#1c2030') - : (colors.tabBg || '#161925'), 'important'); - tab.style.setProperty('color', isActive - ? (colors.tabActiveText || '#e0e0e0') - : (colors.tabText || '#a4a7b3'), 'important'); - tab.style.setProperty('border-color', colors.tabBorder || '#2b3040', 'important'); - }); - - // Align the chrome background with the theme gradient or fallback - if (theme.gradient) { - document.body.style.background = theme.gradient; - } else if (colors.bg) { - document.body.style.background = colors.bg; - } - - // Persist so other pages (home/settings) can pull the latest palette - try { localStorage.setItem('currentTheme', JSON.stringify(theme)); } catch {} - - console.log('[THEME] Applied theme to main UI:', { - urlBarBg: colors.urlBarBg, - tabBg: colors.tabBg, - navFound: !!nav, - titlebarFound: !!titlebarContainer, - tabBarFound: !!tabBar - }); -} - -// Detect platform and add class to body for CSS platform-specific styling -(function detectPlatform() { - const platform = navigator.platform.toLowerCase(); - if (platform.includes('mac')) { - document.body.classList.add('platform-darwin'); - } else if (platform.includes('win')) { - document.body.classList.add('platform-win32'); - } else { - document.body.classList.add('platform-linux'); - } -})(); - -// 1) cache hot DOM references -const urlBox = document.getElementById('url'); -const tabBarEl = document.getElementById('tab-bar'); -const viewHostEl = document.getElementById('view-host'); -const menuPopup = document.getElementById('menu-popup'); -// (Removed old custom HTML context menu in favor of native Electron menu) - -function updateBrowserViewBounds() { - if (!viewHostEl) return; - const rect = viewHostEl.getBoundingClientRect(); - ipcRenderer.invoke('browserview-set-bounds', { - x: rect.left, - y: rect.top, - width: rect.width, - height: rect.height - }).catch(() => {}); -} - -window.addEventListener('resize', () => { - updateBrowserViewBounds(); -}); - -// Select all text on focus and prevent mouseup from deselecting -urlBox.addEventListener('focus', () => { - urlBox.select(); -}); -urlBox.addEventListener('mouseup', e => e.preventDefault()); -// Add Enter key navigation -urlBox.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - navigate(); - } -}); - -let tabs = []; -let activeTabId = null; -let isHistoryNavigation = false; // Flag to prevent duplicate history entries during back/forward -const allowedInternalPages = ['settings', 'home', 'downloads', 'nebot', 'insecure']; -// Session-scoped allowlist of HTTP hosts the user explicitly chose to proceed with. -const insecureBypassedHosts = new Set(); -let pluginPages = []; // { id, file, fileUrl, pluginId } -let pluginPagesReady = false; -const pendingInternalNavigations = []; - -// Allow isolated worlds / plugin preloads (contextIsolation) to request opening an internal page -window.addEventListener('message', (e) => { - try { - const data = e.data; - if (!data || typeof data !== 'object') return; - if (data.type === 'open-internal-page' && typeof data.url === 'string') { - console.log('[DEBUG] Message request to open internal page:', data.url); - createTab(data.url); - } else if (data.type === 'navigate' && typeof data.url === 'string') { - // Fallback navigation from pages (like insecure.html) when electronAPI.sendToHost is unavailable - try { - if (data.opts && data.opts.insecureBypass && /^http:\/\//i.test(data.url)) { - const h = new URL(data.url).hostname; - insecureBypassedHosts.add(h); - } - } catch {} - urlBox.value = data.url; - navigate(); - } - } catch (err) { - console.warn('[DEBUG] open-internal-page handler error', err); - } -}); - -// Fetch plugin-provided pages (nebula://) once on startup -(async () => { - try { - console.log('[DEBUG] About to request plugin pages from main process...'); - pluginPages = await ipcRenderer.invoke('plugins-get-pages'); - console.log('[DEBUG] Loaded pluginPages:', pluginPages); - console.log('[DEBUG] allowedInternalPages before:', allowedInternalPages); - for (const p of pluginPages) { - if (p && p.id && !allowedInternalPages.includes(p.id)) { - console.log('[DEBUG] Adding plugin page to allowed list:', p.id); - allowedInternalPages.push(p.id); - } - } - console.log('[DEBUG] allowedInternalPages after:', allowedInternalPages); - } catch (e) { - console.warn('Failed to load plugin pages', e); - } - finally { - pluginPagesReady = true; - console.log('[DEBUG] Plugin pages ready, flushing', pendingInternalNavigations.length, 'pending navigations'); - // Flush any queued internal navigations that occurred before readiness - while (pendingInternalNavigations.length) { - const fn = pendingInternalNavigations.shift(); - try { fn(); } catch {} - } - } -})(); -let bookmarks = []; - -// Efficient render scheduling to avoid redundant DOM work -let tabsRenderPending = false; -// Track previous order and positions for FLIP animations -let lastTabOrder = []; -let closingTabs = new Set(); -function scheduleRenderTabs() { - if (tabsRenderPending) return; - tabsRenderPending = true; - requestAnimationFrame(() => { - tabsRenderPending = false; - renderTabs(); - }); -} - -// Debounce nav button updates to reduce layout work -let navButtonsPending = false; -let backBtnCached = null; -let fwdBtnCached = null; -function scheduleUpdateNavButtons() { - if (navButtonsPending) return; - navButtonsPending = true; - requestAnimationFrame(() => { - navButtonsPending = false; - try { updateNavButtons(); } catch {} - }); -} - -// Derive a stable, safe label for a tab without throwing on non-URLs -function getTabLabel(tab) { - if (tab.title && tab.title !== 'New Tab') return tab.title; - const u = tab.url || ''; - try { - if (u.startsWith('data:image')) return 'Image'; - if (u.startsWith('data:')) return 'Data'; - if (u.startsWith('blob:')) return 'Resource'; - if (u.startsWith('http')) return new URL(u).hostname; - if (u.startsWith('nebula://')) return u.replace('nebula://', ''); - return u || 'New Tab'; - } catch { - return u || 'New Tab'; - } -} - -// Load bookmarks on startup -async function loadBookmarks() { - try { - bookmarks = await ipcRenderer.invoke('load-bookmarks'); - } catch (error) { - console.error('Error loading bookmarks in main context:', error); - bookmarks = []; - } -} - -// Function to save bookmarks -async function saveBookmarks(newBookmarks) { - try { - bookmarks = newBookmarks; - await ipcRenderer.invoke('save-bookmarks', bookmarks); - } catch (error) { - console.error('Error saving bookmarks in main context:', error); - } -} - -// Load bookmarks when the script starts -loadBookmarks(); -// Initial home tab will be created on DOMContentLoaded - -// Remove iframe-based navigation listener (using webview IPC now) - -// Listen for site history updates from main process -// NOTE: electronAPI.on wrapper strips the original event object and only forwards args. -// Handlers therefore must NOT expect the event parameter. -ipcRenderer.on('record-site-history', (url) => { - debug('[DEBUG] Received site history update:', url); - if (typeof url === 'string' && url) addToSiteHistory(url); -}); - -// Main process requests opening a URL in a new tab (window.open interception) -ipcRenderer.on('open-url-new-tab', (url) => { - console.log('[DEBUG] IPC open-url-new-tab received:', url); - if (typeof url === 'string' && url) createTab(url); -}); - -// Messages from BrowserView pages (sendToHost fallback) -ipcRenderer.on('browserview-host-message', (payload) => { - console.log('[Renderer] browserview-host-message received:', payload); - const data = payload || {}; - const channel = data.channel; - const args = data.args || []; - if (!channel) return; - - if (channel === 'navigate' && args[0]) { - console.log('[Renderer] Navigating to:', args[0]); - const targetUrl = args[0]; - const opts = args[1] || {}; - try { - if (opts.insecureBypass && /^http:\/\//i.test(targetUrl)) { - const h = new URL(targetUrl).hostname; - insecureBypassedHosts.add(h); - } - } catch {} - if (opts.newTab) { - createTab(targetUrl); - } else { - urlBox.value = targetUrl; - navigate(); - } - } else if (channel === 'theme-update' && args[0]) { - const theme = args[0]; - applyThemeToMainUI(theme); - ipcRenderer.send('browserview-broadcast', { channel: 'theme-update', args: [theme] }); - } -}); - -// Commands from the overlay menu window -function runMenuCommand(cmd) { - if (!cmd) return; - switch (cmd) { - case 'open-settings': - openSettings(); - break; - case 'open-downloads': - openDownloads(); - break; - case 'toggle-devtools': - window.electronAPI?.toggleDevTools?.(); - break; - case 'big-picture': - window.bigPictureAPI?.launch?.(); - break; - case 'zoom-in': - zoomIn(); - break; - case 'zoom-out': - zoomOut(); - break; - case 'hard-reload': - hardReload(); - break; - case 'fresh-reload': - freshReload(); - break; - default: - break; - } -} - -const isLinux = document.body.classList.contains('platform-linux'); - -function hideMainMenuPopup() { - if (!isLinux) { - // Windows/macOS: hide the popup window - ipcRenderer.send('menu-popup-hide'); - } -} - -function showNativeAppMenu() { - if (!menuBtn) return; - const rect = menuBtn.getBoundingClientRect(); - - if (isLinux) { - // Linux: use native OS menu (renders above BrowserView reliably) - ipcRenderer.invoke('show-app-menu', { - x: Math.round(rect.right - 200), - y: Math.round(rect.bottom + 4) - }).catch(() => {}); - } else { - // Windows/macOS: use the custom popup window - const theme = currentThemeColors ? { colors: currentThemeColors } : null; - ipcRenderer.send('menu-popup-toggle', { - anchorRect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height }, - anchorScreenPoint: { - x: Math.round(window.screenX + rect.right), - y: Math.round(window.screenY + rect.bottom) - }, - theme - }); - } -} - -ipcRenderer.on('menu-command', (payload) => { - const cmd = payload?.cmd; - runMenuCommand(cmd); - hideMainMenuPopup(); -}); - -// Auto-open on download start is disabled by design now. - -function createTab(inputUrl) { - inputUrl = inputUrl || 'nebula://home'; - console.log('[DEBUG] createTab() inputUrl =', inputUrl); - const id = crypto.randomUUID(); - if (inputUrl.startsWith('nebula://') && !pluginPagesReady) { - // Defer creation until plugin pages known to avoid 404 race - console.log('[DEBUG] Deferring createTab until pluginPagesReady'); - pendingInternalNavigations.push(() => createTab(inputUrl)); - return id; - } - let resolvedUrl = resolveInternalUrl(inputUrl); - console.log('[DEBUG] createTab resolvedUrl:', resolvedUrl, 'from inputUrl:', inputUrl); - // Keep data: URLs intact; BrowserView cannot consume blob URLs created in the UI process. - - tabs.push({ - id, - url: inputUrl, - title: 'New Tab', - favicon: null, - history: [inputUrl], - historyIndex: 0 - }); - - ipcRenderer.invoke('browserview-create', { tabId: id, url: resolvedUrl }) - .then(() => { - setActiveTab(id); - updateBrowserViewBounds(); - }) - .catch(() => {}); - scheduleRenderTabs(); - return id; -} - -// Expose for plugin usage (e.g., Nebot panel "Open Page") -try { window.createTab = createTab; } catch {} - - - -function resolveInternalUrl(url) { - console.log('[DEBUG] resolveInternalUrl called with:', url); - if (url.startsWith('nebula://')) { - // Support query / hash on internal pages (e.g., nebula://insecure?target=...) - const tail = url.replace('nebula://', ''); - const page = tail.split(/[?#]/)[0]; - const suffix = tail.slice(page.length); // includes ? and/or # if present - console.log('[DEBUG] Extracted page:', page); - // Fast path: if user typed nebula://nebot and plugin page exists, return immediately - if (page === 'nebot') { - const nebotPage = pluginPages.find(p => p.id === 'nebot'); - console.log('[DEBUG] Fast path for nebot, pluginPages:', pluginPages, 'nebotPage:', nebotPage); - if (nebotPage && (nebotPage.fileUrl || nebotPage.file)) { - const resolvedFast = nebotPage.fileUrl || (nebotPage.file.startsWith('file://') ? nebotPage.file : 'file://' + nebotPage.file.replace(/\\/g,'/')); - console.log('[DEBUG] Fast path nebot resolve ->', resolvedFast); - return resolvedFast; - } - console.log('[DEBUG] No plugin page found for nebot, falling back to nebot.html'); - } - console.log('[DEBUG] Checking if page in allowedInternalPages:', page, 'list:', allowedInternalPages); - if (allowedInternalPages.includes(page)) { - // Check if this page is provided by a plugin (absolute file path) - const plug = pluginPages.find(p => p.id === page); - console.log('[DEBUG] Resolving nebula://' + page, 'plug:', plug); - if (plug && (plug.fileUrl || plug.file)) { - // Prefer pre-built fileUrl for correctness across platforms - const resolved = plug.fileUrl ? plug.fileUrl : (plug.file.startsWith('file://') ? plug.file : 'file://' + plug.file.replace(/\\/g,'/')); - console.log('[DEBUG] Resolved plugin page', page, '->', resolved); - return resolved + suffix; - } - // Fallback: built-in renderer copy (resolve to absolute file URL) - console.log('[DEBUG] Using fallback for page:', page); - const rel = `${page}.html${suffix}`; - try { - return new URL(rel, window.location.href).toString(); - } catch { - return rel; - } - } - console.log('[DEBUG] Page not in allowedInternalPages, returning 404'); - try { - return new URL('404.html', window.location.href).toString(); - } catch { - return '404.html'; - } - } - // Allow direct loading of common schemes without forcing https:// - if (/^(https?:|file:|data:|blob:)/i.test(url)) return url; - return `https://${url}`; -} - - -function handleLoadFail(tabId) { - return (event) => { - if (!event.validatedURL.includes('nebula://') && event.errorCode !== -3) { - const badUrl = tabs.find(t => t.id === tabId)?.url || ''; - ipcRenderer.invoke('browserview-load-url', { - tabId, - url: `404.html?url=${encodeURIComponent(badUrl)}` - }).catch(() => {}); - } - }; -} - -function updateTabMetadata(id, key, value) { - const tab = tabs.find(t => t.id === id); - if (tab) { - tab[key] = value; - scheduleRenderTabs(); - } -} - -function performNavigation(input, originalInputForHistory) { - const tab = tabs.find(t => t.id === activeTabId); - if (!tab) return; - const hasProtocol = /^https?:\/\//i.test(input); - const isFileProtocol = /^file:\/\//i.test(input); - const looksLikeLocalPath = /^(?:[A-Za-z]:\\|\\\\|\/?)[^?]*\.(?:x?html?)$/i.test(input); - const isInternal = input.startsWith('nebula://'); - const isLikelyUrl = hasProtocol || input.includes('.'); - let resolved; - let isSearch = false; - if (isFileProtocol) { - resolved = input; - } else if (looksLikeLocalPath) { - let p = input.replace(/\\/g,'/'); - if (/^[A-Za-z]:\//.test(p)) resolved = 'file:///' + encodeURI(p); else if (p.startsWith('/')) resolved = 'file://' + encodeURI(p); else resolved = 'file://' + encodeURI(p); - } else if (!isInternal && !isLikelyUrl) { - resolved = `https://www.google.com/search?q=${encodeURIComponent(input)}`; - isSearch = true; - // Save to search history - addToSearchHistory(input); - } else { - resolved = resolveInternalUrl(input); - } - - console.log('[DEBUG] performNavigation input:', input, 'resolved:', resolved, 'isInternal:', isInternal); - - // Intercept plain HTTP (not HTTPS) navigations (excluding localhost / 127.* / internal pages) - try { - if (!isInternal && /^http:\/\//i.test(resolved)) { - const u = new URL(resolved); - const host = u.hostname; - const isLoopback = /^(localhost|127\.0\.0\.1|::1)$/.test(host); - if (!isLoopback && !insecureBypassedHosts.has(host)) { - const encoded = encodeURIComponent(resolved); - // Directly load insecure.html (avoid custom scheme so OS doesn't try to resolve an external handler) - resolved = `insecure.html?target=${encoded}`; - } - } - } catch (e) { debug('[DEBUG] HTTP interception error', e); } - - if (!activeTabId) { - createTab(input); - return; - } - tab.history = tab.history.slice(0, tab.historyIndex + 1); - tab.history.push(originalInputForHistory); - tab.historyIndex++; - tab.url = originalInputForHistory; - ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolved }).catch(() => {}); - scheduleRenderTabs(); - scheduleUpdateNavButtons(); -} - -function navigate() { - const rawInput = urlBox.value.trim(); - let input = rawInput; - if ((input.startsWith('"') && input.endsWith('"')) || (input.startsWith("'") && input.endsWith("'"))) input = input.slice(1, -1); - if (input !== rawInput) urlBox.value = input; - const isInternal = input.startsWith('nebula://'); - if (isInternal && !pluginPagesReady) { - const captured = input; // preserve original - pendingInternalNavigations.push(() => performNavigation(captured, captured)); - return; - } - performNavigation(input, input); -} - -// Keyboard shortcut: Ctrl+O (Cmd+O on mac) to open a local file -document.addEventListener('keydown', async (e) => { - const isAccel = (navigator.platform.includes('Mac') ? e.metaKey : e.ctrlKey); - if (isAccel && e.key.toLowerCase() === 'o') { - e.preventDefault(); - if (window.electronAPI && window.electronAPI.openLocalFile) { - const fileUrl = await window.electronAPI.openLocalFile(); - if (fileUrl) { - urlBox.value = fileUrl; - navigate(); - } - } - } -}); - - -function handleNavigation(tabId, newUrl) { - const tab = tabs.find(t => t.id === tabId); - if (!tab) return; - - debug('[DEBUG] handleNavigation called with:', newUrl); - - // --- record every real navigation into history --- - // Skip adding to history if this is a programmatic back/forward navigation - if (!isHistoryNavigation) { - // Check both current position AND last recorded URL to prevent duplicates from - // multiple event firings (did-navigate + did-navigate-in-page) - const lastRecordedUrl = tab.history[tab.history.length - 1]; - if (tab.history[tab.historyIndex] !== newUrl && lastRecordedUrl !== newUrl) { - tab.history = tab.history.slice(0, tab.historyIndex + 1); - tab.history.push(newUrl); - tab.historyIndex++; - } - } else { - // Reset flag after handling the navigation - isHistoryNavigation = false; - } - - // Record site history in localStorage (skip internal pages and file:// URLs) - if (!newUrl.endsWith('home.html') && - !newUrl.endsWith('settings.html') && - !newUrl.startsWith('file://') && - !newUrl.includes('nebula://') && - newUrl.startsWith('http')) { - debug('[DEBUG] Adding to site history:', newUrl); - addToSiteHistory(newUrl); - // Also send to main process for file storage - ipcRenderer.invoke('save-site-history-entry', newUrl); - } - - // translate local files back to our nebula:// scheme - const isHome = newUrl.endsWith('home.html'); - const isSettings = newUrl.endsWith('settings.html'); - const isDownloads = newUrl.endsWith('downloads.html'); - const isNebot = newUrl.endsWith('nebot.html'); - const isInsecure = newUrl.includes('insecure.html'); - const is404 = newUrl.includes('404.html'); - const displayUrl = isHome - ? 'nebula://home' - : isSettings - ? 'nebula://settings' - : isDownloads - ? 'nebula://downloads' - : isNebot - ? 'nebula://nebot' - : isInsecure - ? 'nebula://insecure' - : is404 - ? 'nebula://404' - : newUrl; - - tab.url = displayUrl; - - // Clear favicon and reset title for internal nebula:// pages - if (displayUrl.startsWith('nebula://')) { - tab.favicon = null; - // Set appropriate title for each internal page - if (isHome) { - tab.title = 'New Tab'; - } else if (isSettings) { - tab.title = 'Settings'; - } else if (isDownloads) { - tab.title = 'Downloads'; - } else if (isNebot) { - tab.title = 'Nebot'; - } else if (isInsecure) { - tab.title = 'Insecure Connection'; - } else if (is404) { - tab.title = 'Page Not Found'; - } - } - - if (tabId === activeTabId) { - urlBox.value = displayUrl === 'nebula://home' ? '' : displayUrl; - } - - scheduleRenderTabs(); - scheduleUpdateNavButtons(); -} - - -function setActiveTab(id) { - activeTabId = id; - ipcRenderer.invoke('browserview-set-active', { tabId: id }).catch(() => {}); - updateBrowserViewBounds(); - - const tab = tabs.find(t => t.id === id); - if (tab) { - urlBox.value = tab.url === 'nebula://home' ? '' : tab.url; - scheduleRenderTabs(); - updateNavButtons(); - updateZoomUI(); - } -} - -function closeTab(id) { - // Play closing animation on tab button, then remove - const btn = tabBarEl.querySelector(`[data-tab-id="${id}"]`); - if (btn && !closingTabs.has(id)) { - closingTabs.add(id); - btn.classList.add('tab--closing'); - // Pre-calc which tab should become active if we're closing the active tab - const idx = tabs.findIndex(t => t.id === id); - const nextActiveId = (id === activeTabId) - ? (tabs[idx - 1]?.id ?? tabs[idx + 1]?.id ?? tabs[0]?.id) - : activeTabId; - btn.addEventListener('animationend', () => { - ipcRenderer.invoke('browserview-destroy', { tabId: id }).catch(() => {}); - // Remove from model - tabs = tabs.filter(t => t.id !== id); - // Choose a new active tab if needed - if (tabs.length > 0 && nextActiveId) setActiveTab(nextActiveId); - closingTabs.delete(id); - scheduleRenderTabs(); - updateNavButtons(); - }, { once: true }); - return; - } - // Fallback (no button rendered yet) - ipcRenderer.invoke('browserview-destroy', { tabId: id }).catch(() => {}); - tabs = tabs.filter(t => t.id !== id); - if (id === activeTabId && tabs.length > 0) setActiveTab(tabs[0].id); - scheduleRenderTabs(); - updateNavButtons(); -} - -// 2) streamline renderTabs with a fragment -function renderTabs() { - // Measure initial positions (First) for existing elements - const firstRects = new Map(); - const existing = Array.from(tabBarEl.querySelectorAll('.tab')); - existing.forEach(el => { - firstRects.set(el.dataset.tabId, el.getBoundingClientRect()); - }); - - const frag = document.createDocumentFragment(); - if (tabBarEl && tabBarEl.getAttribute('role') !== 'tablist') { - tabBarEl.setAttribute('role', 'tablist'); - } - - // Create tab elements - const currentOrder = []; - tabs.forEach(tab => { - const el = document.createElement('div'); - el.className = 'tab' + (tab.id === activeTabId ? ' active' : ''); - el.classList.add('tab--flip'); - el.setAttribute('role', 'tab'); - el.setAttribute('aria-selected', String(tab.id === activeTabId)); - el.setAttribute('tabindex', tab.id === activeTabId ? '0' : '-1'); - el.dataset.tabId = tab.id; - currentOrder.push(tab.id); - - // Apply theme colors to new tab element - if (currentThemeColors) { - const isActive = tab.id === activeTabId; - el.style.setProperty('background', isActive - ? (currentThemeColors.tabActive || '#1c2030') - : (currentThemeColors.tabBg || '#161925'), 'important'); - el.style.setProperty('color', isActive - ? (currentThemeColors.tabActiveText || '#e0e0e0') - : (currentThemeColors.tabText || '#a4a7b3'), 'important'); - el.style.setProperty('border-color', currentThemeColors.tabBorder || '#2b3040', 'important'); - } - - if (!lastTabOrder.includes(tab.id)) { - // New tab enters with animation - el.classList.add('tab--enter'); - } - - if (tab.favicon) { - const icon = document.createElement('img'); - icon.src = tab.favicon; - icon.className = 'tab-favicon'; - icon.onerror = function() { - this.style.display = 'none'; - }; - el.appendChild(icon); - } - - const title = document.createElement('span'); - title.className = 'tab-title'; - title.textContent = getTabLabel(tab); - el.appendChild(title); - - const closeBtn = document.createElement('button'); - closeBtn.className = 'tab-close'; - closeBtn.title = 'Close tab'; - closeBtn.textContent = 'ร—'; - closeBtn.addEventListener('click', (e) => { - e.stopPropagation(); - closeTab(tab.id); - }); - el.appendChild(closeBtn); - - el.addEventListener('mousedown', (e) => { - if (e.button === 1) { - e.preventDefault(); - closeTab(tab.id); - } - }); - - el.draggable = true; - el.addEventListener('dragstart', e => { - e.dataTransfer.setData('tabId', tab.id); - e.dataTransfer.setData('text/plain', tab.id); - // Hide default ghost image; use an empty drag image - const ghost = document.createElement('canvas'); - ghost.width = 1; ghost.height = 1; // 1x1 transparent pixel - const ctx = ghost.getContext('2d'); - if (ctx) { ctx.clearRect(0, 0, 1, 1); } - e.dataTransfer.setDragImage(ghost, 0, 0); - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.dropEffect = 'move'; - } - // visual lift on drag start - el.classList.add('tab--dragging'); - // Store initial pointer offset to keep tab under cursor - const rect = el.getBoundingClientRect(); - el._dragOffsetX = e.clientX - rect.left; - el._dragStartLeft = rect.left; - el._dragStartTop = rect.top; - }); - el.addEventListener('dragenter', e => { - // If another tab is being dragged over this one, hint before/after - const draggedId = (e.dataTransfer && (e.dataTransfer.getData('tabId') || e.dataTransfer.getData('text/plain'))) || null; - if (!draggedId || draggedId === tab.id) return; - const rect = el.getBoundingClientRect(); - const x = e.clientX; - const before = (x - rect.left) < rect.width / 2; - el.classList.toggle('tab--drop-before', before); - el.classList.toggle('tab--drop-after', !before); - }); - el.addEventListener('dragover', e => { - e.preventDefault(); - // Continuously update hint side while hovering - const rect = el.getBoundingClientRect(); - const before = (e.clientX - rect.left) < rect.width / 2; - el.classList.toggle('tab--drop-before', before); - el.classList.toggle('tab--drop-after', !before); - }); - // While dragging, move the actual element to follow cursor horizontally (attach once). - if (!tabBarEl._dragoverAttached) { - tabBarEl.addEventListener('dragover', (evt) => { - const draggingEl = tabBarEl.querySelector('.tab.tab--dragging'); - if (!draggingEl) return; - evt.preventDefault(); - if (evt.dataTransfer) evt.dataTransfer.dropEffect = 'move'; - const barRect = tabBarEl.getBoundingClientRect(); - const targetX = evt.clientX - barRect.left - (draggingEl._dragOffsetX || 0); - // Translate relative to its current position - const elRect = draggingEl.getBoundingClientRect(); - const dx = targetX - (elRect.left - barRect.left); - draggingEl.style.transform = `translateX(${dx}px)`; - }); - tabBarEl._dragoverAttached = true; - } - el.addEventListener('dragleave', () => { - el.classList.remove('tab--drop-before', 'tab--drop-after'); - }); - el.addEventListener('drop', e => { - e.preventDefault(); - const draggedId = e.dataTransfer.getData('tabId') || e.dataTransfer.getData('text/plain'); - if (!draggedId || draggedId === tab.id) return; - const fromIndex = tabs.findIndex(t => t.id === draggedId); - const toIndex = tabs.findIndex(t => t.id === tab.id); - if (fromIndex === -1 || toIndex === -1) return; - const rect = el.getBoundingClientRect(); - const after = (e.clientX - rect.left) > rect.width / 2; - const newIndex = toIndex + (after ? 1 : 0); - const [moved] = tabs.splice(fromIndex, 1); - const adjIndex = fromIndex < newIndex ? newIndex - 1 : newIndex; - tabs.splice(adjIndex, 0, moved); - el.classList.remove('tab--drop-before', 'tab--drop-after'); - // Reset dragging transform before re-render FLIP - const draggingEl = tabBarEl.querySelector('.tab.tab--dragging'); - if (draggingEl) draggingEl.style.transform = ''; - scheduleRenderTabs(); - }); - el.addEventListener('dragend', e => { - // Clear dragging visual state - el.classList.remove('tab--dragging'); - el.style.transform = ''; - // Clean any lingering hints - el.classList.remove('tab--drop-before', 'tab--drop-after'); - if ( - e.clientX < 0 || e.clientX > window.innerWidth || - e.clientY < 0 || e.clientY > window.innerHeight - ) { - ipcRenderer.invoke('open-tab-in-new-window', tab.url); - closeTab(tab.id); - } - }); - - el.addEventListener('click', () => setActiveTab(tab.id)); - frag.appendChild(el); - }); - - // New tab button - const plus = document.createElement('button'); - plus.className = 'new-tab-button'; - plus.title = 'New tab'; - plus.setAttribute('aria-label', 'New tab'); - plus.textContent = '+'; - plus.addEventListener('click', () => createTab()); - frag.appendChild(plus); - - // Swap DOM: to support FLIP, we need to keep the old nodes around until we can measure Last - tabBarEl.innerHTML = ''; - tabBarEl.appendChild(frag); - - // Measure final positions (Last) - const lastRects = new Map(); - Array.from(tabBarEl.querySelectorAll('.tab')).forEach(el => { - lastRects.set(el.dataset.tabId, el.getBoundingClientRect()); - }); - - // Apply FLIP: invert then play - Array.from(tabBarEl.querySelectorAll('.tab')).forEach(el => { - const id = el.dataset.tabId; - const first = firstRects.get(id); - const last = lastRects.get(id); - if (!first || !last) return; - const dx = first.left - last.left; - const dy = first.top - last.top; - if (dx || dy) { - el.style.transform = `translate(${dx}px, ${dy}px)`; - el.getBoundingClientRect(); // force reflow - el.style.transform = ''; - } - }); - - // Update order for next render - lastTabOrder = currentOrder.slice(); -} - -// 1) handle URL sent by main for a detached window -ipcRenderer.on('open-url', (url) => { - for (const t of tabs) { - ipcRenderer.invoke('browserview-destroy', { tabId: t.id }).catch(() => {}); - } - tabs = []; - activeTabId = null; - tabBarEl.innerHTML = ''; - if (typeof url === 'string' && url) createTab(url); else createTab(); -}); - -function goBack() { - const tab = tabs.find(t => t.id === activeTabId); - if (!tab) return; - - // Use custom history tracking to properly handle internal pages like home - if (tab.historyIndex > 0) { - tab.historyIndex--; - const targetUrl = tab.history[tab.historyIndex]; - isHistoryNavigation = true; - const resolvedUrl = resolveInternalUrl(targetUrl); - ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolvedUrl }).catch(() => {}); - } -} - -function goForward() { - const tab = tabs.find(t => t.id === activeTabId); - if (!tab) return; - - // Use custom history tracking to properly handle internal pages like home - if (tab.historyIndex < tab.history.length - 1) { - tab.historyIndex++; - const targetUrl = tab.history[tab.historyIndex]; - isHistoryNavigation = true; - const resolvedUrl = resolveInternalUrl(targetUrl); - ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolvedUrl }).catch(() => {}); - } -} - -function updateNavButtons() { - const tab = tabs.find(t => t.id === activeTabId); - if (!backBtnCached || !fwdBtnCached) { - backBtnCached = document.querySelector('.nav-left button:nth-child(1)'); - fwdBtnCached = document.querySelector('.nav-left button:nth-child(2)'); - } - // Use custom history tracking for button state - if (backBtnCached) backBtnCached.disabled = !tab || tab.historyIndex <= 0; - if (fwdBtnCached) fwdBtnCached.disabled = !tab || tab.historyIndex >= tab.history.length - 1; -} - -function reload() { - if (!activeTabId) return; - ipcRenderer.invoke('browserview-reload', { tabId: activeTabId, ignoreCache: false }).catch(() => {}); - scheduleUpdateNavButtons(); -} - -function hardReload() { - if (!activeTabId) return; - ipcRenderer.invoke('browserview-reload', { tabId: activeTabId, ignoreCache: true }).catch(() => {}); - scheduleUpdateNavButtons(); -} - -function freshReload() { - if (!activeTabId) return; - ipcRenderer.invoke('browserview-get-url', { tabId: activeTabId }).then((currentUrl) => { - if (!currentUrl) return hardReload(); - try { - const u = new URL(currentUrl); - u.searchParams.set('_bust', Date.now().toString()); - ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: u.toString() }).catch(() => {}); - } catch { - hardReload(); - } - }); -} - -// Function to open the Settings page -function openSettings() { - createTab('nebula://settings'); -} - -// Open Downloads manager page -function openDownloads() { - createTab('nebula://downloads'); -} - -// Toggle menu dropdown -const menuBtn = document.getElementById('menu-btn'); -const menuWrapper = document.querySelector('.menu-wrapper'); -// Downloads mini popup elements -let downloadsBtnEl = null; -let downloadsPopupEl = null; -let downloadsListEl = null; -let downloadsEmptyEl = null; -let downloadsShowAllBtn = null; -let ringSvgEl = null; - -// Open/close on button click; stop propagation so outside-click handler doesn't immediately close it -menuBtn.addEventListener('click', (e) => { - e.stopPropagation(); - showNativeAppMenu(); -}); - -// Close on Escape key -document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') hideMainMenuPopup(); - if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) { - hideDownloadsPopup(); - } -}); - -// Close menus when BrowserView receives focus -ipcRenderer.on('browserview-event', (payload) => { - if (!payload || !payload.type) return; - const { tabId, type } = payload; - if (type === 'focus') { - hideMainMenuPopup(); - if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) hideDownloadsPopup(); - return; - } - if (type === 'page-title-updated') { - updateTabMetadata(tabId, 'title', payload.title); - return; - } - if (type === 'page-favicon-updated') { - const fav = payload.favicons?.[0]; - if (fav) updateTabMetadata(tabId, 'favicon', fav); - return; - } - if (type === 'did-navigate' || type === 'did-navigate-in-page') { - if (payload.url) { - handleNavigation(tabId, payload.url); - if (/\/cdn-cgi\//.test(payload.url) || /challenge/i.test(payload.url)) { - console.log('[Nebula] Cloudflare challenge detected at', payload.url); - } - } - return; - } - if (type === 'did-finish-load') { - scheduleUpdateNavButtons(); - return; - } - if (type === 'did-fail-load') { - handleLoadFail(tabId)({ - validatedURL: payload.validatedURL || '', - errorCode: payload.errorCode, - errorDescription: payload.errorDescription, - isMainFrame: payload.isMainFrame - }); - } -}); - -window.addEventListener('DOMContentLoaded', () => { - // Initialize theme from localStorage - const savedTheme = localStorage.getItem('currentTheme'); - if (savedTheme) { - try { - const theme = JSON.parse(savedTheme); - applyThemeToMainUI(theme); - ipcRenderer.send('browserview-broadcast', { channel: 'theme-update', args: [theme] }); - } catch (err) { - console.error('Error applying saved theme:', err); - } - } - - // Initialize display scale (zoom) from localStorage - const savedDisplayScale = localStorage.getItem('nebula-display-scale'); - if (savedDisplayScale) { - try { - const scale = Number(savedDisplayScale); - if (scale > 0 && scale <= 300) { - const zoomFactor = scale / 100; - if (ipcRenderer && typeof ipcRenderer.invoke === 'function') { - ipcRenderer.invoke('set-zoom-factor', zoomFactor).catch(err => { - console.error('Error setting zoom factor:', err); - }); - } - } - } catch (err) { - console.error('Error applying saved display scale:', err); - } - } - - // Initial boot - createTab(); - updateBrowserViewBounds(); - // Fallback: listen for postMessage navigations from embedded pages (home/settings) - window.addEventListener('message', (event) => { - if (event.data && event.data.type === 'navigate' && event.data.url) { - if (event.data.newTab) { - createTab(event.data.url); - } else { - urlBox.value = event.data.url; - navigate(); - } - } - }); - // only now bind the reload button (guaranteed to exist) - const reloadBtn = document.getElementById('reload-btn'); - reloadBtn.addEventListener('click', reload); - const hardReloadBtn = document.getElementById('hard-reload-btn'); - if (hardReloadBtn) hardReloadBtn.addEventListener('click', hardReload); - const freshReloadBtn = document.getElementById('fresh-reload-btn'); - if (freshReloadBtn) freshReloadBtn.addEventListener('click', freshReload); - - // bind zoom buttons (single binding) - const zoomInBtn = document.getElementById('zoom-in-btn'); - const zoomOutBtn = document.getElementById('zoom-out-btn'); - zoomInBtn.addEventListener('click', zoomIn); - zoomOutBtn.addEventListener('click', zoomOut); - - // DevTools toggle button - const devtoolsBtn = document.getElementById('devtools-btn'); - if (devtoolsBtn && window.electronAPI && window.electronAPI.toggleDevTools) { - devtoolsBtn.addEventListener('click', () => { - window.electronAPI.toggleDevTools(); - }); - } - - // Big Picture Mode button - const bigPictureBtn = document.getElementById('bigpicture-btn'); - if (bigPictureBtn && window.bigPictureAPI && window.bigPictureAPI.launch) { - bigPictureBtn.addEventListener('click', async () => { - try { - await window.bigPictureAPI.launch(); - hideMainMenuPopup(); - } catch (e) { - console.error('Failed to launch Big Picture Mode:', e); - } - }); - } - - if (menuPopup) { - menuPopup.addEventListener('click', (e) => { - const button = e.target instanceof HTMLElement ? e.target.closest('button') : null; - if (!button) return; - const id = button.id; - if (!id) return; - if (id !== 'zoom-in-btn' && id !== 'zoom-out-btn') { - hideMainMenuPopup(); - } - e.stopPropagation(); - }); - } - - document.addEventListener('click', (e) => { - if (!menuPopup || menuPopup.classList.contains('hidden')) return; - if (menuWrapper && !menuWrapper.contains(e.target)) { - hideMainMenuPopup(); - } - }); - - // Cache back/forward buttons for faster updates (no need to add listeners - already in HTML) - backBtnCached = document.querySelector('.nav-left button:nth-child(1)'); - fwdBtnCached = document.querySelector('.nav-left button:nth-child(2)'); - - // settings button - const settingsBtn = document.getElementById('open-settings-btn'); - if (settingsBtn) settingsBtn.addEventListener('click', openSettings); - - // downloads button - downloadsBtnEl = document.getElementById('downloads-btn'); - downloadsPopupEl = document.getElementById('downloads-popup'); - downloadsListEl = document.getElementById('downloads-list'); - downloadsEmptyEl = document.getElementById('downloads-empty'); - downloadsShowAllBtn = document.getElementById('downloads-show-all'); - if (downloadsBtnEl) { - // Insert progress ring SVG - const ring = document.createElement('div'); - ring.className = 'ring'; - ring.innerHTML = ''; - downloadsBtnEl.appendChild(ring); - ringSvgEl = ring.querySelector('circle.fg'); - downloadsBtnEl.addEventListener('click', (e)=>{ - e.stopPropagation(); - toggleDownloadsPopup(); - }); - } - if (downloadsShowAllBtn) downloadsShowAllBtn.addEventListener('click', ()=> { hideDownloadsPopup(); openDownloads(); }); - // Close popup if clicking elsewhere - document.addEventListener('click', (e)=>{ - if (!downloadsPopupEl || downloadsPopupEl.classList.contains('hidden')) return; - const wrapper = downloadsPopupEl.parentElement; - if (wrapper && !wrapper.contains(e.target)) hideDownloadsPopup(); - }); - - // Initialize list with any existing downloads - refreshDownloadsMini(); - // Subscribe to updates - window.downloadsAPI?.onStarted(()=> { refreshDownloadsMini(); }); - window.downloadsAPI?.onUpdated(()=> { refreshDownloadsMini(); }); - window.downloadsAPI?.onDone(()=> { refreshDownloadsMini(); }); - window.downloadsAPI?.onCleared(()=> { refreshDownloadsMini(); }); - - // window control bindings (Windows frameless window) - const minBtn = document.getElementById('min-btn'); - const maxBtn = document.getElementById('max-btn'); - const closeBtn = document.getElementById('close-btn'); - const windowControls = document.getElementById('window-controls'); - - console.log('[WindowControls] Elements found:', { minBtn: !!minBtn, maxBtn: !!maxBtn, closeBtn: !!closeBtn, windowControls: !!windowControls }); - - // Detect platform - hide controls on macOS (uses native traffic lights) - const isMacOS = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - console.log('[WindowControls] Platform:', navigator.platform, 'isMacOS:', isMacOS); - - if (windowControls) { - if (isMacOS) { - // Hide window controls on macOS - windowControls.style.display = 'none'; - // Remove right padding for window controls - document.getElementById('tab-bar').style.paddingRight = '10px'; - } else if (minBtn && maxBtn && closeBtn) { - // Windows/Linux: Set up custom title bar controls - console.log('[WindowControls] Setting up event listeners for Windows/Linux'); - - minBtn.addEventListener('click', (e) => { - console.log('[WindowControls] Minimize clicked'); - e.stopPropagation(); - ipcRenderer.invoke('window-minimize'); - }); - maxBtn.addEventListener('click', async (e) => { - console.log('[WindowControls] Maximize clicked'); - e.stopPropagation(); - await ipcRenderer.invoke('window-maximize'); - updateMaximizeIcon(); - }); - closeBtn.addEventListener('click', (e) => { - console.log('[WindowControls] Close clicked'); - e.stopPropagation(); - ipcRenderer.invoke('window-close'); - }); - - // Update maximize icon based on window state - async function updateMaximizeIcon() { - try { - const isMaximized = await ipcRenderer.invoke('window-is-maximized'); - const maximizeIcon = maxBtn.querySelector('.maximize-icon'); - const restoreIcon = maxBtn.querySelector('.restore-icon'); - if (maximizeIcon && restoreIcon) { - maximizeIcon.style.display = isMaximized ? 'none' : 'block'; - restoreIcon.style.display = isMaximized ? 'block' : 'none'; - maxBtn.title = isMaximized ? 'Restore' : 'Maximize'; - maxBtn.setAttribute('aria-label', isMaximized ? 'Restore' : 'Maximize'); - } - } catch (e) { - // Ignore errors during state check - } - } - - // Initial state check - updateMaximizeIcon(); - - // Listen for window resize to update maximize icon - window.addEventListener('resize', () => { - // Debounce resize events - clearTimeout(window._maximizeIconTimeout); - window._maximizeIconTimeout = setTimeout(updateMaximizeIcon, 100); - }); - } - } - - // update initial zoom display - ipcRenderer.invoke('get-zoom-factor').then(z => { - document.getElementById('zoom-percent').textContent = `${Math.round(z * 100)}%`; - }); - - // (Removed broken duplicate context menu wiring) - - // Migrate existing site history from JSON file to localStorage (one-time migration) - const migrateSiteHistory = async () => { - try { - // Check if we already have data in localStorage - const existingHistory = getSiteHistory(); - if (existingHistory.length === 0) { - // Try to load from the old JSON file system - console.log('Attempting to migrate site history from JSON file...'); - // Since we can't access the file directly, we'll just start fresh - // The site-history.json file was the old method, localStorage is the new method - } - } catch (err) { - console.log('Site history migration skipped:', err.message); - } - }; - migrateSiteHistory(); - - // ipcRenderer.invoke('load-bookmarks').then(bs => { - // bookmarks = bs; - // console.log('[DEBUG] Loaded bookmarks:', bookmarks); - // }); -}); - -// Global keyboard shortcut for DevTools (Ctrl+Shift+I or F12) -document.addEventListener('keydown', (e) => { - const isMod = (e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'I' || e.key === 'i'); - if (isMod || e.key === 'F12') { - if (window.electronAPI && window.electronAPI.toggleDevTools) { - window.electronAPI.toggleDevTools(); - e.preventDefault(); - } - } -}); - -// zoom helpers -function updateZoomUI() { - const zp = document.getElementById('zoom-percent'); - if (zp) { - ipcRenderer.invoke('get-zoom-factor').then(zf => { - // just show "NN%", not "Zoom: NN%" - zp.textContent = `${Math.round(zf * 100)}%`; - }); - } -} - -function zoomIn() { ipcRenderer.invoke('zoom-in').then(updateZoomUI); } -function zoomOut() { ipcRenderer.invoke('zoom-out').then(updateZoomUI); } - -// Optional: sample plugin demo hook (safe if plugin missing) -try { - if (window.sampleHello && typeof window.sampleHello.onHello === 'function') { - window.sampleHello.onHello((payload) => { - console.log('[Sample Plugin] Hello message:', payload); - }); - } -} catch {} - -// Utility: close the menu when interacting with a given element (e.g., webview) -function attachCloseMenuOnInteract(el) { - if (!el) return; - const closeIfOpen = () => { - hideMainMenuPopup(); - if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) { - hideDownloadsPopup(); - } - }; - el.addEventListener('mousedown', closeIfOpen); - el.addEventListener('pointerdown', closeIfOpen); - el.addEventListener('focus', closeIfOpen, true); -} - -// Use electronAPI from preload - already defined at top of file - -// Native context menu: delegate to main via preload API -document.addEventListener('contextmenu', (e) => { - // Determine if inside a webview or general renderer area - const inWebviewArea = e.target.tagName === 'WEBVIEW' || e.composedPath().some(el => el.id === 'webviews'); - if (!inWebviewArea) return; // Let default OS menu appear in text inputs etc. if desired - e.preventDefault(); - - // Try to extract link/image/selection info (limited for , better done inside page but sandboxed) - const selection = window.getSelection()?.toString() || ''; - window.electronAPI?.showContextMenu({ - clientX: e.clientX, - clientY: e.clientY, - selectionText: selection, - isEditable: false - }); -}); - -// Handle commands from main process triggered by context menu -window.addEventListener('nebula-context-command', (e) => { - const { cmd, url } = e.detail || {}; - if (!cmd) return; - switch (cmd) { - case 'open-link-new-tab': - if (url) createTab(url); - break; - case 'open-image-new-tab': - if (url) createTab(url); - break; - case 'save-image': - if (!url) return; - // Try direct network save first (http/file/data) - if (/^(https?:|file:|data:)/i.test(url)) { - window.electronAPI.saveImageFromNet(url); - return; - } - // For blob: URLs we need to resolve inside the active webview by converting to dataURL - if (url.startsWith('blob:')) { - if (activeTabId) { - const code = `(async()=>{try{const r=await fetch('${url}');const b=await r.blob();return new Promise(res=>{const fr=new FileReader();fr.onload=()=>res(fr.result);fr.readAsDataURL(b);});}catch(e){return null;}})();`; - ipcRenderer.invoke('browserview-execute-js', { tabId: activeTabId, code }).then(dataUrl => { - if (dataUrl) { - window.electronAPI.saveImageToDisk('image', dataUrl); - } - }); - } - } - break; - } -}); - -// ------------------------------ -// Downloads mini UI helpers -// ------------------------------ -function toggleDownloadsPopup() { - if (!downloadsPopupEl) return; - if (downloadsPopupEl.classList.contains('hidden')) showDownloadsPopup(); else hideDownloadsPopup(); -} -function showDownloadsPopup() { - if (!downloadsPopupEl) return; - downloadsPopupEl.classList.remove('hidden'); -} -function hideDownloadsPopup() { - if (!downloadsPopupEl) return; - downloadsPopupEl.classList.add('hidden'); -} - -function fmtBytesMini(n) { - if (!n || n <= 0) return '0 B'; - const u = ['B','KB','MB','GB','TB']; - const i = Math.floor(Math.log(n)/Math.log(1024)); - return (n/Math.pow(1024,i)).toFixed(i===0?0:1) + ' ' + u[i]; -} - -async function refreshDownloadsMini() { - if (!window.downloadsAPI) return; - const items = await window.downloadsAPI.list(); - const has = items && items.length > 0; - if (downloadsEmptyEl) downloadsEmptyEl.style.display = has ? 'none' : 'block'; - if (downloadsListEl) downloadsListEl.innerHTML = (items||[]).slice(0,5).map(d => { - const pct = d.totalBytes > 0 ? Math.min(100, Math.round((d.receivedBytes||0)*100/d.totalBytes)) : (d.state==='completed'?100:0); - return ` -
-
${d.filename}
-
- ${d.state==='in-progress' ? ` - - - ` : ` - - - `} -
-
${d.state} ยท ${fmtBytesMini(d.receivedBytes||0)} / ${fmtBytesMini(d.totalBytes||0)}
-
-
`; - }).join(''); - - if (downloadsListEl) { - downloadsListEl.onclick = async (e) => { - const btn = e.target.closest('button'); - if (!btn) return; - const itemEl = btn.closest('.dl-item'); - const id = itemEl?.getAttribute('data-id'); - const act = btn.getAttribute('data-act'); - if (!id || !act) return; - await window.downloadsAPI.action(id, act); - if (act==='cancel') refreshDownloadsMini(); - }; - } - - updateDownloadsRing(items||[]); -} - -function updateDownloadsRing(items) { - if (!ringSvgEl) return; - // Compute aggregate progress for in-progress downloads - const inprog = items.filter(d => d.state === 'in-progress'); - const total = inprog.reduce((a,d)=> a + (d.totalBytes||0), 0); - const done = inprog.reduce((a,d)=> a + (d.receivedBytes||0), 0); - let pct = 0; - if (total > 0) pct = Math.max(0, Math.min(1, done/total)); - // If none in progress but some completed recently, show full ring briefly; else hide - const circumference = 103.67; // 2 * PI * r (r=16.5) - const offset = circumference * (1 - pct); - ringSvgEl.style.strokeDasharray = `${circumference}`; - ringSvgEl.style.strokeDashoffset = `${offset}`; - // Hide ring when no active downloads - const show = inprog.length > 0; - ringSvgEl.style.opacity = show ? '1' : '0'; -} diff --git a/renderer/settings.css b/renderer/settings.css deleted file mode 100644 index 345d5c6..0000000 --- a/renderer/settings.css +++ /dev/null @@ -1,798 +0,0 @@ -/* existing styles */ - -/* Plugins panel */ -.plugins-list { display: grid; gap: 10px; } -.plugin-item { display:flex; justify-content:space-between; align-items:center; border:1px solid rgba(255,255,255,0.12); padding:10px; border-radius:8px; background: rgba(255,255,255,0.03); } -.plugin-meta { display:flex; flex-direction:column; gap:2px; min-width:0; } -.plugin-title { font-weight:600; } -.plugin-desc { opacity:.8; font-size:.9em; } -.plugin-actions { display:flex; gap:8px; align-items:center; } -.plugin-actions .spacer { width:8px; } -.plugin-tags { display:flex; flex-wrap: wrap; gap:6px; margin-top: 4px; } -.plugin-tag { display:inline-flex; align-items:center; padding:2px 8px; border-radius:999px; font-size:.75em; opacity:.9; border:1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.06); } -.plugin-authors { margin-top: 4px; font-size:.85em; opacity:.85; } -.plugin-authors .muted { opacity:.7; margin-right: 6px; } -:root { - --bg: #121418; - --gradient-end: #1B1035; - --surface: rgba(255, 255, 255, 0.06); - --surface-hover: rgba(255, 255, 255, 0.10); - --primary: #7B2EFF; - --primary-hover: #9654FF; - --accent: #00C6FF; - --text: #E0E0E0; - --text-secondary: #B8B8C0; - --text-muted: #8f8f9d; - --border: rgba(255, 255, 255, 0.12); - --border-subtle: rgba(255, 255, 255, 0.06); - --ring: 0 0 0 2px rgba(123, 46, 255, 0.4); - --glow-subtle: 0 4px 20px rgba(123, 46, 255, 0.15); -} - -/* Load InterVariable */ -@font-face { - font-family: 'InterVariable'; - src: url('../assets/images/fonts/InterVariable.ttf') format('truetype'); - font-weight: 100 900; - font-display: swap; -} - -body { - background: - radial-gradient(800px 400px at 10% 0%, rgba(123, 46, 255, 0.08), transparent 60%), - radial-gradient(800px 400px at 100% 20%, rgba(0, 198, 255, 0.06), transparent 60%), - linear-gradient(180deg, var(--bg), var(--gradient-end)); - color: var(--text); - font-family: 'InterVariable', system-ui, -apple-system, 'Segoe UI', 'Roboto', sans-serif; - margin: 0; - padding: 0; - display: flex; - justify-content: center; - align-items: flex-start; - min-height: 100vh; - overflow: auto; -} - -.container { - position: relative; - background: var(--bg); - padding: 0; - border-radius: 0; - border: none; - box-shadow: none; - max-width: 100vw; - width: 100%; - display: flex; - overflow: hidden; - height: 100vh; -} - -/* Subtle animated sheen around the container */ - - -/* Sidebar + content layout */ -.sidebar { - width: 260px; - background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); - border-right: 1px solid var(--border); - padding: 1.5rem 0; - position: relative; - z-index: 1; - overflow-y: auto; -} - -.sidebar h1 { - font-size: 1.35rem; - font-weight: 300; - margin: 0 0 1.5rem 0; - padding: 0 1rem; - color: var(--primary); - letter-spacing: -0.01em; - display: flex; - align-items: center; - gap: 8px; -} - -.tabs { - display: flex; - flex-direction: column; - gap: 2px; -} - -.tab-link { - text-align: left; - background: transparent; - color: var(--text-secondary); - border: none; - border-radius: 4px; - padding: 10px 1rem; - margin: 0; - cursor: pointer; - transition: background 0.15s ease, color 0.15s ease; - font-size: 15px; - font-weight: 400; - font-family: inherit; - width: 100%; - position: relative; - z-index: 1; - border-left: 3px solid transparent; -} - -.tab-link:hover { - background: var(--surface); - color: var(--text); -} - -.tab-link.active { - background: linear-gradient(90deg, rgba(123, 46, 255, 0.12), rgba(0, 198, 255, 0.08)); - color: var(--text); - border-left-color: var(--primary); - font-weight: 500; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05); -} - - - -.content { - flex: 1; - padding: 2rem 3rem; - overflow: auto; - position: relative; - z-index: 1; - background: var(--bg); -} - -.tab-panel { - display: none; -} - -.tab-panel.active { - display: block; -} - - - -.setting-group { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1.5rem; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1rem 1.25rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -label { - font-weight: 400; - margin-bottom: 0.35rem; - color: var(--text); - font-size: 15px; -} - -input[type="text"], -input[type="file"] { - padding: 0.5rem 0.65rem; - font-size: 14px; - border: 1px solid var(--border); - border-radius: 6px; - margin-bottom: 0.5rem; - background-color: var(--surface); - color: var(--text); - outline: none; - transition: all 0.15s ease; -} -input[type="text"]:focus, -input[type="file"]:focus { - box-shadow: var(--ring); - border-color: var(--primary); - background-color: var(--surface-hover); -} - -select { - padding: 0.5rem 0.65rem; - font-size: 14px; - border: 1px solid var(--border); - border-radius: 6px; - background-color: var(--surface); - color: var(--text); - outline: none; - transition: all 0.15s ease; -} - -select:focus { - box-shadow: var(--ring); - border-color: var(--primary); - background-color: var(--surface-hover); -} - -button { - padding: 0.5rem 1rem; - font-size: 14px; - background: var(--surface); - color: var(--text); - border: 1px solid var(--border); - border-radius: 6px; - cursor: pointer; - font-weight: 400; - transition: all 0.15s ease; -} - -button:hover { - background: var(--surface-hover); - box-shadow: var(--glow-subtle); -} - - - -/* Primary button style (e.g., Big Picture Mode) */ -.primary-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0.5rem 1rem; - font-size: 14px; - font-weight: 500; - background: var(--primary); - color: var(--text); - border: 1px solid var(--primary); - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - box-shadow: var(--glow-subtle); -} - -.primary-btn:hover { - background: var(--primary-hover); - box-shadow: 0 4px 24px rgba(123, 46, 255, 0.25); - transform: translateY(-1px); -} - - - -.note { - font-size: 13px; - color: var(--text-muted); - margin-top: 0.35rem; - line-height: 1.5; -} - -.status { - position: fixed; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - display: flex; - align-items: center; - gap: 0.75rem; - background-color: var(--surface); - color: var(--text); - padding: 0.75rem 1.25rem; - border-radius: 6px; - border: 1px solid var(--border); - box-shadow: 0 4px 12px rgba(0,0,0,0.4); - font-size: 14px; - z-index: 1000; -} - -.status.hidden { - display: none; -} - -.spinner { - width: 16px; - height: 16px; - border: 2px solid transparent; - border-top: 2px solid white; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -.setting-group input, -.setting-group button { - width: 100%; - box-sizing: border-box; -} - -.setting-group .setting-row button, -.setting-group .setting-row input, -.setting-group .setting-row select { - width: auto; -} - -/* Inline layout helpers (Firefox-like settings rows) */ -.setting-row { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; -} - -.setting-row label { - margin-bottom: 0; -} - -.setting-row .note { - margin: 0; -} - -.label-min { - min-width: 100px; -} - -.setting-row button, -.setting-row input, -.setting-row select { - width: auto; - min-width: 160px; -} - -.setting-row select { - flex: 1 1 220px; -} - -.button-row { - display: flex; - gap: 10px; - align-items: center; - flex-wrap: wrap; -} - -.stack { - display: flex; - flex-direction: column; - gap: 10px; -} - -.range-row { - display: flex; - align-items: center; - gap: 12px; -} - -.range-row input[type="range"] { - flex: 1 1 auto; - min-width: 160px; -} - -.range-row .range-value { - min-width: 56px; - text-align: right; - font-weight: 600; - color: color-mix(in srgb, var(--text) 85%, transparent); -} - -/* Zoom controls */ -.zoom-controls { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} - -.zoom-btn { - width: 40px; - height: 40px; - border: 1px solid var(--border); - background: var(--surface); - color: var(--text); - border-radius: 6px; - font-size: 20px; - font-weight: 400; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - padding: 0; -} - -.zoom-btn:hover { - background: var(--surface-hover); - border-color: var(--primary); -} - -.zoom-btn:active { - transform: scale(0.95); -} - -.zoom-value { - flex: 1; - font-size: 15px; - font-weight: 600; - color: var(--text); - text-align: center; - padding: 0 8px; -} - -.zoom-presets { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); - gap: 8px; -} - -.zoom-preset-btn { - padding: 10px 16px; - border: 1px solid var(--border); - background: var(--surface); - color: var(--text-secondary); - border-radius: 6px; - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.zoom-preset-btn:hover { - background: var(--surface-hover); - border-color: var(--primary); - color: var(--text); -} - -.zoom-preset-btn.active { - background: var(--primary); - border-color: var(--primary); - color: white; - box-shadow: var(--glow-subtle); -} - -.zoom-preset-btn:active { - transform: scale(0.95); -} - -.settings-fieldset { - border: 1px solid var(--border); - border-radius: 6px; - padding: 1rem; - background: rgba(123, 46, 255, 0.03); -} - -.settings-fieldset legend { - padding: 0 0.5rem; - font-size: 14px; - font-weight: 500; - color: var(--primary); -} - -/* Cards (customization groups) */ -.customization-group { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1rem 1.25rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - margin-bottom: 1.5rem; -} - -.customization-group > h3 { - margin: 0 0 0.75rem 0; - position: relative; - padding-left: 0; - font-size: 1.1rem; - font-weight: 600; - color: var(--primary); -} - - - -.setting-group > h3 { - margin: 0 0 0.75rem 0; - font-size: 1.1rem; - font-weight: 600; - position: relative; - padding-left: 0; - color: var(--primary); -} - - - -/* Section titles */ -h2 { - display: block; - font-size: 1.5rem; - font-weight: 300; - margin: 0 0 1.5rem 0; - color: var(--text); - letter-spacing: -0.01em; - position: relative; - padding-bottom: 0.75rem; -} - -h2::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 60px; - height: 3px; - background: linear-gradient(90deg, var(--primary), var(--accent)); - border-radius: 2px; -} - - - -/* Theming: theme selector buttons override */ -.theme-selector { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - gap: 12px; - padding: 0; - background: transparent; - border: none; - border-radius: 0; -} -.theme-btn { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - padding: 10px 8px; - border: 2px solid var(--border) !important; - border-radius: 8px !important; - background: var(--surface) !important; - color: var(--text); - text-align: center; - font-size: 13px; - min-height: 90px; - font-weight: 400; -} -.theme-btn:hover { - background: var(--surface-hover) !important; - border-color: var(--text-muted) !important; -} -.theme-btn.active { - border-color: var(--primary) !important; - box-shadow: 0 0 0 2px var(--primary), var(--glow-subtle); - background: linear-gradient(180deg, rgba(123, 46, 255, 0.08), rgba(0, 198, 255, 0.05)) !important; -} -.theme-preview { - width: 64px; - height: 42px; - border-radius: 4px !important; - border: 1px solid var(--border); - position: relative; - overflow: hidden; -} -.custom-theme-btn { border-style: dashed !important; opacity: 0.95; } -.custom-theme-btn:hover { opacity: 1; } - -/* Range sliders */ -input[type="range"] { - -webkit-appearance: none; - appearance: none; - height: 4px; - background: linear-gradient(90deg, var(--primary), var(--accent)); - border-radius: 2px; - outline: none; - border: none; - cursor: pointer; - opacity: 0.8; -} - -input[type="range"]:hover { - opacity: 1; -} - -input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--primary); - border: 2px solid #fff; - box-shadow: 0 1px 3px rgba(0,0,0,0.3); - cursor: pointer; -} - -input[type="range"]::-moz-range-thumb { - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--primary); - border: 2px solid #fff; - box-shadow: 0 1px 3px rgba(0,0,0,0.3); - cursor: pointer; -} - -/* Checkboxes/radios */ -input[type="checkbox"], input[type="radio"] { - accent-color: var(--primary); - width: 16px; - height: 16px; - margin-right: 8px; -} - -/* Layout & logo options */ -.layout-options { display: flex; flex-direction: column; gap: 10px; } -.layout-options label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - padding: 8px; - border-radius: 4px; - transition: background 0.15s ease; -} -.layout-options label:hover { background: var(--surface); } -.logo-options { display: flex; flex-direction: column; gap: 12px; } -.logo-options label { display: flex; align-items: center; gap: 8px; } -.logo-options input[type="text"] { flex: 1; } - -/* Color customization controls */ -.color-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; } -.color-group { display: flex; flex-direction: column; gap: 8px; } -.color-group label { font-size: 14px; color: var(--text-secondary); font-weight: 500; } -input[type="color"] { - width: 100%; - height: 40px; - padding: 0; - border: 1px solid var(--border); - border-radius: 4px; - background: transparent; - cursor: pointer; -} - -/* Preview area */ -.preview-container { - background: var(--surface) !important; - border-radius: 8px !important; - border: 1px solid var(--border) !important; - box-shadow: none; - overflow: hidden; -} -.preview-home { - display: flex; - flex-direction: column; - align-items: center; - gap: 15px; - padding: 20px; - background: var(--bg); - border-radius: 4px; - min-height: 200px; - border: 1px dashed var(--border); -} -.preview-text { letter-spacing: 0.3px; } -.preview-logo { font-size: 1.5rem; font-weight: 700; color: var(--primary); } -.preview-search { width: 60%; height: 40px; background: rgba(255,255,255,0.1); border-radius: 20px; border: 1px solid rgba(255,255,255,0.2); } -.preview-bookmarks { display: flex; gap: 10px; } -.preview-bookmark { width: 50px; height: 50px; background: var(--accent); border-radius: 8px; } - -/* History lists */ -#search-history-list, #site-history-list { - padding: 0; - margin: 6px 0 0 0; - display: grid; - gap: 6px; -} -#search-history-list li, #site-history-list li { - list-style: none; - padding: 10px 12px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 4px; - font-size: 14px; -} -#site-history-list a { - color: var(--primary); - text-decoration: none; -} -#site-history-list a:hover { - text-decoration: underline; - color: var(--primary-hover); -} - -/* About buttons */ -.github-btn, .help-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0.5rem 1rem; - background: var(--surface); - color: var(--text); - border: 1px solid var(--border) !important; - border-radius: 4px !important; - text-decoration: none; - cursor: pointer; - transition: background 0.15s ease; - font-size: 14px; -} -.github-btn:hover, .help-btn:hover { - background: var(--surface-hover); -} -.github-btn svg, .help-btn svg { - width: 16px; - height: 16px; - fill: currentColor; -} -.about-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } - -/* Debug info */ -.debug-info { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 4px; - padding: 12px; - font-size: 13px; - line-height: 1.6; - color: var(--text-secondary); - font-family: 'Consolas', 'Monaco', monospace; -} - -/* General lists inside cards */ -.customization-group ul { list-style: none; padding: 0; margin: 0; } -.customization-group ul li { - padding: 10px 0; - border-bottom: 1px solid var(--border-subtle); - font-size: 14px; - line-height: 1.5; -} -.customization-group ul li:last-child { border-bottom: none; } - -/* Theme management buttons */ -.theme-management { display: flex; flex-wrap: wrap; gap: 10px; } -#reset-to-default { - background: #d41b2c; - border-color: #d41b2c; - color: white; -} -#reset-to-default:hover { - background: #a4161a; - border-color: #a4161a; -} - -/* Scrollbar styling (Chromium) */ -*::-webkit-scrollbar { height: 12px; width: 12px; } -*::-webkit-scrollbar-track { background: var(--bg); } -*::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, var(--primary), var(--accent)); - border-radius: 6px; - border: 2px solid var(--bg); -} -*::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, var(--primary-hover), var(--accent)); -} - -/* small-screen adjustments */ -@media (max-width: 768px) { - body { - padding: 0; - } - .container { - flex-direction: column; - max-width: 100%; - height: 100vh; - border-radius: 0; - } - .sidebar { - width: 100%; - border-right: none; - border-bottom: 1px solid var(--border); - padding: 1rem; - } - .tabs { - flex-direction: row; - flex-wrap: wrap; - gap: 6px; - } - .tab-link { - flex: 1 1 auto; - min-width: 120px; - } - .content { - padding: 1.5rem 1rem; - } - h2 { - font-size: 1.25rem; - } -} diff --git a/renderer/settings.html b/renderer/settings.html deleted file mode 100644 index d0944cd..0000000 --- a/renderer/settings.html +++ /dev/null @@ -1,633 +0,0 @@ - - - - - Settings - - - - - -
- - -
- -
-

General

- -
-

Data Management

-

Clear all cookies, cache, and browsing data stored locally on this device.

-
- -
-
- - -
-

Big Picture Mode

-

A controller-friendly UI designed for handheld devices (e.g., Steam Deck).

-
- - -
-
- -
-

Weather Display

-

Choose how temperature is displayed on the Home page weather card.

-
- Temperature units - - - -
-
- -
-

System Information

-
Loading debug info...
-
-
- - -
-

Appearance

- -
-

Theme Presets

-
- - - - - - - - - - - - - -
-
- - -
-

Display Scale

-

Adjust the zoom level for this window. Changes apply immediately.

-
- - 100% - -
-
- - - - - - - - -
-
- - -
-

Custom Colors

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - - - -
-

Logo & Branding

-
- - -
-
- - -
-

Theme Management

-
- - - - - -
-
- - -
-

Preview

-
-
- -
Nebula
- -
-
-
-
-
-
-
-
-
- - -
-

History

-
-

Search History

-
    - -
    -
    -

    Site History

    -
      -
      - - -
      -
      -
      - - -
      -

      Plugins

      -
      -
      - - Changes to renderer preloads may require app restart. -
      -
      -
      -

      Installed

      -
      -
      -
      - - -
      -

      About

      -
      -

      Application

      -
        -
      • Name: Loading...
      • -
      • Version: Loading...
      • -
      • Packaged: Loading...
      • -
      • User data: Loading...
      • -
      -
      - -
      -

      Runtime

      -
        -
      • Electron: Loading...
      • -
      • Chromium: Loading...
      • -
      • Node.js: Loading...
      • -
      • V8: Loading...
      • -
      -
      - -
      -

      System

      -
        -
      • OS: Loading...
      • -
      • CPU: Loading...
      • -
      • Architecture: Loading...
      • -
      • Memory: Loading...
      • -
      -
      - -
      -

      Security Updates

      -

      Nebula Browser is distributed via itch.io and other nonโ€‘Steam channels; the Electron runtime can be updated separately for security patches and performance improvements.

      - -
      - - - - -
      -
      - - -
      -
      -

      Current Electron: Loading...

      -

      Nebula Browser: Loading...

      -
      -
      -
      -
      - - -
      -
      -
      - - - - - - - - - diff --git a/renderer/settings.js b/renderer/settings.js deleted file mode 100644 index 52de55e..0000000 --- a/renderer/settings.js +++ /dev/null @@ -1,889 +0,0 @@ -// Prefer contextBridge-exposed API -const ipc = (window.electronAPI && typeof window.electronAPI.invoke === 'function') - ? window.electronAPI - : null; - -let clearBtn = document.getElementById('clear-data-btn'); -const statusDiv = document.getElementById('status'); -const statusText = document.getElementById('status-text'); -const TAB_STORAGE_KEY = 'nebula-settings-active-tab'; -const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f' -const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; // number (vh) -const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh) -const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl' -const DISPLAY_SCALE_KEY = 'nebula-display-scale'; // number (50-300) - -function showStatus(message) { - if (statusText && statusDiv) { - statusText.textContent = message; - statusDiv.classList.remove('hidden'); - setTimeout(() => { - statusDiv.classList.add('hidden'); - }, 2000); - } else { - console.log('[STATUS]', message); - } -} - -function showStatus(message) { - statusText.textContent = message; - statusDiv.classList.remove('hidden'); // Ensure the hidden class is removed - setTimeout(() => { - statusDiv.classList.add('hidden'); // Add the hidden class back after 2 seconds - }, 2000); -} - -function attachClearHandler(btn) { - if (!btn) return; - btn.onclick = async () => { - if (statusDiv && statusText) { - statusDiv.classList.remove('hidden'); - statusText.textContent = 'Clearing cookies, storage, cache, and history...'; - } - - try { - if (ipc) { - const ok = await ipc.invoke('clear-browser-data'); - // Also clear localStorage site history in this context - try { localStorage.removeItem('siteHistory'); } catch {} - // Try to refresh lists if present - try { if (typeof loadHistories === 'function') await loadHistories(); } catch {} - showStatus(ok - ? 'All browser data cleared.' - : 'Failed to clear browser data.'); - } else { - showStatus('Clear data feature not available in this context.'); - } - } catch (error) { - console.error('Error clearing browser data:', error); - showStatus('An error occurred while clearing data.'); - } finally { - const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null; - if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('theme-update', currentTheme); - } - } - }; -} - -// Try attaching immediately, and again on DOMContentLoaded -attachClearHandler(clearBtn); -window.addEventListener('DOMContentLoaded', () => { - if (!clearBtn) { - clearBtn = document.getElementById('clear-data-btn'); - attachClearHandler(clearBtn); - } - - // Wire per-section clear buttons to main when possible - const clearSiteBtn = document.getElementById('clear-site-history-btn'); - if (clearSiteBtn) { - clearSiteBtn.addEventListener('click', async () => { - try { - // Clear localStorage copy - try { localStorage.removeItem('siteHistory'); } catch {} - // Ask main to clear file-based history for consistency - if (ipc) { await ipc.invoke('clear-site-history'); } - showStatus('Site history cleared'); - try { if (typeof loadHistories === 'function') await loadHistories(); } catch {} - } catch (e) { - console.error('Clear site history error:', e); - showStatus('Failed clearing site history'); - } - }); - } - const clearSearchBtn = document.getElementById('clear-search-history-btn'); - if (clearSearchBtn) { - clearSearchBtn.addEventListener('click', async () => { - try { - // Clear from localStorage in this context - try { localStorage.removeItem('searchHistory'); } catch {} - - if (ipc) { await ipc.invoke('clear-search-history'); } - showStatus('Search history cleared'); - } catch (e) { - console.error('Clear search history error:', e); - showStatus('Failed clearing search history'); - } - }); - } - - // Weather unit controls - try { - const stored = localStorage.getItem(WEATHER_UNIT_KEY) || 'auto'; - const radios = document.querySelectorAll('input[name="weather-unit"]'); - radios.forEach(r => r.checked = (r.value === stored)); - radios.forEach(radio => radio.addEventListener('change', () => { - const val = document.querySelector('input[name="weather-unit"]:checked')?.value || 'auto'; - localStorage.setItem(WEATHER_UNIT_KEY, val); - showStatus(`Weather units set to ${val === 'c' ? 'Celsius' : val === 'f' ? 'Fahrenheit' : 'Auto'}`); - // Hint home page to refresh weather if it listens to storage events - try { window.dispatchEvent(new StorageEvent('storage', { key: WEATHER_UNIT_KEY, newValue: val })); } catch {} - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('settings-update', { weatherUnit: val }); - } - })); - } catch (e) { console.warn('Weather unit setup failed', e); } - - // Home layout controls - try { - const searchRange = document.getElementById('home-search-y'); - const searchVal = document.getElementById('home-search-y-val'); - const bmRange = document.getElementById('home-bookmarks-y'); - const bmVal = document.getElementById('home-bookmarks-y-val'); - const cornerRadios = document.querySelectorAll('input[name="home-glance-corner"]'); - - const initNum = (key, def, input, label) => { - const v = Number(localStorage.getItem(key) || def); - if (input) input.value = String(v); - if (label) label.textContent = v + 'vh'; - return v; - }; - initNum(HOME_SEARCH_Y_KEY, 22, searchRange, searchVal); - initNum(HOME_BOOKMARKS_Y_KEY, 40, bmRange, bmVal); - const storedCorner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br'; - cornerRadios.forEach(r => r.checked = (r.value === storedCorner)); - - const notify = () => { - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('settings-update', { - searchY: Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22), - bookmarksY: Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40), - glanceCorner: localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br' - }); - } - }; - - if (searchRange) searchRange.addEventListener('input', () => { - const val = Number(searchRange.value); - searchVal.textContent = val + 'vh'; - localStorage.setItem(HOME_SEARCH_Y_KEY, String(val)); - notify(); - }); - if (bmRange) bmRange.addEventListener('input', () => { - const val = Number(bmRange.value); - bmVal.textContent = val + 'vh'; - localStorage.setItem(HOME_BOOKMARKS_Y_KEY, String(val)); - notify(); - }); - cornerRadios.forEach(r => r.addEventListener('change', () => { - const val = document.querySelector('input[name="home-glance-corner"]:checked')?.value || 'br'; - localStorage.setItem(HOME_GLANCE_CORNER_KEY, val); - notify(); - })); - } catch (e) { console.warn('Home layout control setup failed', e); } - - // Display scale controls - try { - const scaleValue = document.getElementById('display-scale-value'); - const zoomDecrease = document.getElementById('zoom-decrease'); - const zoomIncrease = document.getElementById('zoom-increase'); - const zoomPresets = document.querySelectorAll('.zoom-preset-btn'); - - let currentScale = Number(localStorage.getItem(DISPLAY_SCALE_KEY) || 100); - - // Function to apply zoom - async function applyZoom(scale) { - currentScale = Math.max(50, Math.min(300, scale)); - if (scaleValue) scaleValue.textContent = currentScale + '%'; - localStorage.setItem(DISPLAY_SCALE_KEY, String(currentScale)); - - // Highlight active preset - zoomPresets.forEach(btn => { - btn.classList.toggle('active', Number(btn.dataset.zoom) === currentScale); - }); - - if (ipc && typeof ipc.invoke === 'function') { - try { - const zoomFactor = currentScale / 100; - await ipc.invoke('set-zoom-factor', zoomFactor); - showStatus(`Zoom set to ${currentScale}%`); - } catch (err) { - console.warn('Failed to apply zoom:', err); - showStatus(`Zoom saved to ${currentScale}%`); - } - } - } - - // Initialize display - if (scaleValue) scaleValue.textContent = currentScale + '%'; - zoomPresets.forEach(btn => { - btn.classList.toggle('active', Number(btn.dataset.zoom) === currentScale); - }); - - // Apply saved zoom on load - if (ipc && typeof ipc.invoke === 'function' && currentScale !== 100) { - try { - const zoomFactor = currentScale / 100; - ipc.invoke('set-zoom-factor', zoomFactor).catch(err => { - console.warn('Failed to apply initial zoom:', err); - }); - } catch (err) { - console.warn('Failed to apply initial zoom:', err); - } - } - - // Decrease button - if (zoomDecrease) { - zoomDecrease.addEventListener('click', () => { - applyZoom(currentScale - 10); - }); - } - - // Increase button - if (zoomIncrease) { - zoomIncrease.addEventListener('click', () => { - applyZoom(currentScale + 10); - }); - } - - // Preset buttons - zoomPresets.forEach(btn => { - btn.addEventListener('click', () => { - const zoom = Number(btn.dataset.zoom); - applyZoom(zoom); - }); - }); - } catch (e) { console.warn('Display scale setup failed', e); } - - // Big Picture Mode controls - try { - const bigPictureBtn = document.getElementById('launch-bigpicture-btn'); - const bigPictureStatus = document.getElementById('bigpicture-status'); - - // Check if Big Picture Mode is recommended for this display - if (window.bigPictureAPI && typeof window.bigPictureAPI.isSuggested === 'function') { - window.bigPictureAPI.isSuggested().then(suggested => { - if (suggested && bigPictureStatus) { - bigPictureStatus.textContent = 'โœ“ Recommended for your display'; - bigPictureStatus.style.color = '#4ade80'; - } - }).catch(() => {}); - - // Get screen info for display - window.bigPictureAPI.getScreenInfo().then(info => { - if (info && bigPictureStatus) { - const hint = info.isSteamDeck ? 'Steam Deck detected' : - info.isSmallScreen ? 'Small screen detected' : ''; - if (hint && !bigPictureStatus.textContent) { - bigPictureStatus.textContent = hint; - } - } - }).catch(() => {}); - } - - if (bigPictureBtn) { - bigPictureBtn.addEventListener('click', async () => { - try { - if (window.bigPictureAPI && typeof window.bigPictureAPI.launch === 'function') { - showStatus('Launching Big Picture Mode...'); - await window.bigPictureAPI.launch(); - } else { - showStatus('Big Picture Mode not available'); - } - } catch (e) { - console.error('Big Picture Mode launch error:', e); - showStatus('Failed to launch Big Picture Mode'); - } - }); - } - } catch (e) { console.warn('Big Picture Mode setup failed', e); } -}); - -// Tabs: simple controller -function activateTab(tabName) { - const links = document.querySelectorAll('.tab-link'); - const panels = document.querySelectorAll('.tab-panel'); - - links.forEach(l => { - const isActive = l.dataset.tab === tabName; - l.classList.toggle('active', isActive); - l.setAttribute('aria-selected', isActive ? 'true' : 'false'); - if (isActive) l.focus({ preventScroll: true }); - }); - panels.forEach(p => { - const isActive = p.id === `panel-${tabName}`; - p.classList.toggle('active', isActive); - p.hidden = !isActive; - // noop - }); - try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {} -} - -function initTabs() { - const links = document.querySelectorAll('.tab-link'); - - const getFocusableElements = (container) => { - if (!container) return []; - const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; - return Array.from(container.querySelectorAll(selector)) - .filter(el => !el.disabled && el.getAttribute('aria-hidden') !== 'true' && el.offsetParent !== null); - }; - - const focusFirstInActivePanel = () => { - const activePanel = document.querySelector('.tab-panel.active') || null; - const focusables = getFocusableElements(activePanel); - if (focusables.length > 0) { - focusables[0].focus({ preventScroll: true }); - return true; - } - if (activePanel) { - if (!activePanel.hasAttribute('tabindex')) { - activePanel.setAttribute('tabindex', '-1'); - } - activePanel.focus({ preventScroll: true }); - return true; - } - return false; - }; - - // Direct listeners (for accessibility focus handling) - links.forEach((link, index) => { - link.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const name = link.dataset.tab; - if (!name) return; - if (location.hash !== `#${name}`) { - history.replaceState(null, '', `#${name}`); - } - activateTab(name); - }); - - // Controller/keyboard: move from tab to panel content - link.addEventListener('keydown', (e) => { - if (e.defaultPrevented) return; - if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { - const moved = focusFirstInActivePanel(); - if (moved) { - e.preventDefault(); - e.stopPropagation(); - } - } - }); - }); - - // Delegation as a fallback if elements are re-rendered - const tabContainer = document.querySelector('.tabs'); - if (tabContainer) { - tabContainer.addEventListener('click', (e) => { - const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null; - if (!btn || !tabContainer.contains(btn)) return; - const name = btn.dataset.tab; - if (!name) return; - if (location.hash !== `#${name}`) { - history.replaceState(null, '', `#${name}`); - } - activateTab(name); - }); - } - - // Global fallback: if focus is on sidebar tabs, move into active panel on down/right - document.addEventListener('keydown', (e) => { - if (e.defaultPrevented) return; - if (e.key !== 'ArrowDown' && e.key !== 'ArrowRight') return; - - const activeEl = document.activeElement; - const inTabs = activeEl && (activeEl.classList?.contains('tab-link') || activeEl.closest?.('.tabs')); - const inSidebar = activeEl && activeEl.closest?.('.sidebar'); - - if (inTabs || inSidebar) { - const moved = focusFirstInActivePanel(); - if (moved) { - e.preventDefault(); - e.stopPropagation(); - } - } - }, true); - - // Resolve initial tab: hash > storage > default 'general' - let initial = (location.hash || '').replace('#', '') || null; - if (!initial) { - try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {} - } - if (!initial) initial = 'general'; - activateTab(initial); -} - -// Initialize tabs after DOM is ready but before customization init uses the DOM -window.addEventListener('DOMContentLoaded', () => { - initTabs(); -}); - -// Apply current theme to settings page -function applyCurrentThemeToSettings() { - if (!window.BrowserCustomizer) return; - - const savedTheme = localStorage.getItem('nebula-theme'); - let theme = null; - - if (savedTheme) { - try { - theme = JSON.parse(savedTheme); - } catch (e) { - console.warn('Failed to parse saved theme', e); - } - } - - if (!theme || !theme.colors) return; - - // Apply theme colors to CSS variables - const root = document.documentElement; - root.style.setProperty('--bg', theme.colors.bg || '#121418'); - root.style.setProperty('--gradient-end', theme.colors.darkPurple || '#1B1035'); - root.style.setProperty('--primary', theme.colors.primary || '#7B2EFF'); - root.style.setProperty('--accent', theme.colors.accent || '#00C6FF'); - root.style.setProperty('--text', theme.colors.text || '#E0E0E0'); - - // Update glow colors based on theme - const primaryRgb = hexToRgb(theme.colors.primary || '#7B2EFF'); - if (primaryRgb) { - root.style.setProperty('--ring', `0 0 0 2px rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.4)`); - root.style.setProperty('--glow-subtle', `0 4px 20px rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.15)`); - } -} - -// Helper to convert hex to RGB -function hexToRgb(hex) { - const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null; -} - -// Listen for theme changes -window.addEventListener('storage', (e) => { - if (e.key === 'nebula-theme') { - applyCurrentThemeToSettings(); - } -}); - -// About tab population -async function populateAbout() { - try { - const info = (window.aboutAPI && typeof window.aboutAPI.getInfo === 'function') - ? await window.aboutAPI.getInfo() - : null; - if (!info || info.error) { - console.warn('[ABOUT] Unable to load about info', info && info.error); - return; - } - const byId = (id) => document.getElementById(id); - byId('about-app-name').textContent = info.appName; - byId('about-app-version').textContent = info.appVersion; - byId('about-packaged').textContent = info.isPackaged ? 'Yes' : 'No'; - byId('about-userdata').textContent = info.userDataPath; - - byId('about-electron').textContent = info.electronVersion; - byId('about-chrome').textContent = info.chromeVersion; - byId('about-node').textContent = info.nodeVersion; - byId('about-v8').textContent = info.v8Version; - - byId('about-os').textContent = `${info.osType} ${info.osRelease}`; - byId('about-cpu').textContent = info.cpu; - byId('about-arch').textContent = info.arch; - byId('about-mem').textContent = `${info.totalMemGB} GB`; - - const copyBtn = document.getElementById('copy-about-btn'); - if (copyBtn && !copyBtn.dataset.listenerAttached) { - copyBtn.dataset.listenerAttached = 'true'; - copyBtn.addEventListener('click', async () => { - const payload = [ - `Nebula ${info.appVersion} (${info.isPackaged ? 'packaged' : 'dev'})`, - `Electron ${info.electronVersion} | Chromium ${info.chromeVersion} | Node ${info.nodeVersion} | V8 ${info.v8Version}`, - `${info.osType} ${info.osRelease} ${info.arch}`, - `CPU: ${info.cpu}`, - `RAM: ${info.totalMemGB} GB`, - `UserData: ${info.userDataPath}` - ].join('\n'); - try { - await navigator.clipboard.writeText(payload); - showStatus('Diagnostics copied'); - } catch (err) { - console.error('Clipboard error:', err); - showStatus('Failed to copy diagnostics'); - } - }); - } - } catch (err) { - console.error('[ABOUT] Error populating about info:', err); - } -} - -// Populate about info after DOM is ready -window.addEventListener('DOMContentLoaded', () => { - populateAbout(); - setupElectronUpdater(); - applyCurrentThemeToSettings(); - - // Refresh about info when About tab is clicked - const aboutTabBtn = document.getElementById('tab-about'); - if (aboutTabBtn) { - aboutTabBtn.addEventListener('click', () => { - // Refresh after a short delay to allow tab transition - setTimeout(() => { - populateAbout(); - // Auto-check for updates when About tab is opened - const checkBtn = document.getElementById('check-electron-versions'); - if (checkBtn && !checkBtn.disabled) { - checkBtn.click(); - } - }, 100); - }); - } -}); - -// Electron updater feature setup (for security updates) -async function setupElectronUpdater() { - const securityUpdatesSection = document.querySelector('.customization-group:has(#electron-update-banner)'); - const banner = document.getElementById('electron-update-banner'); - const statusSpan = document.getElementById('electron-update-status'); - const detailsDiv = document.getElementById('electron-update-details'); - const progressDiv = document.getElementById('electron-update-progress'); - const checkBtn = document.getElementById('check-electron-versions'); - const upgradeBtn = document.getElementById('electron-upgrade-btn'); - const versionSelect = document.getElementById('electron-version-select'); - const currentVersionSpan = document.getElementById('electron-current-version'); - const appVersionSpan = document.getElementById('about-app-version-copy'); - - if (!ipc) { - console.warn('[ELECTRON-UPDATER] IPC not available'); - return; - } - - // Check if app is packaged - if so, hide the entire Security Updates section - try { - const appInfo = await ipc.invoke('get-app-info'); - console.log('[ELECTRON-UPDATER] App info:', appInfo); - - if (appInfo && appInfo.isPackaged) { - console.log('[ELECTRON-UPDATER] Packaged build detected - hiding Security Updates section'); - if (securityUpdatesSection) { - securityUpdatesSection.style.display = 'none'; - } - return; - } - - console.log('[ELECTRON-UPDATER] Development mode - showing Security Updates section'); - } catch (err) { - console.error('[ELECTRON-UPDATER] Failed to get app info:', err); - // On error, hide the section to be safe - if (securityUpdatesSection) { - securityUpdatesSection.style.display = 'none'; - } - return; - } - - let availableVersion = null; - let currentVersion = null; - let isUpgrading = false; - - // Get current app version - try { - const info = await window.aboutAPI?.getInfo(); - if (info && appVersionSpan) { - appVersionSpan.textContent = info.appVersion || 'Unknown'; - } - } catch (err) { - console.error('[ELECTRON-UPDATER] Failed to get app version:', err); - } - - // Check for Electron updates - const checkVersions = async () => { - if (isUpgrading) return; - - try { - checkBtn.disabled = true; - banner.style.display = 'block'; - statusSpan.textContent = 'Checking for updates...'; - detailsDiv.textContent = ''; - progressDiv.style.display = 'none'; - upgradeBtn.style.display = 'none'; - banner.style.borderColor = 'rgba(123, 46, 255, 0.3)'; - banner.style.background = 'rgba(123, 46, 255, 0.1)'; - - const buildType = versionSelect.value; - const result = await ipc.invoke('get-electron-versions', buildType); - - if (result.error) { - statusSpan.textContent = 'Update check failed'; - detailsDiv.textContent = result.error; - banner.style.borderColor = 'rgba(244, 67, 54, 0.5)'; - banner.style.background = 'rgba(244, 67, 54, 0.1)'; - showStatus(`Failed: ${result.error}`); - } else { - availableVersion = result.available; - currentVersion = result.current; - - if (currentVersionSpan) { - currentVersionSpan.textContent = currentVersion || 'Unknown'; - } - - const isNewer = compareVersions(availableVersion, currentVersion) > 0; - - if (isNewer) { - statusSpan.textContent = 'Security update available'; - detailsDiv.textContent = `Electron ${availableVersion} is available (you have ${currentVersion}). This update includes security patches and performance improvements.`; - upgradeBtn.style.display = 'inline-block'; - upgradeBtn.disabled = false; - banner.style.borderColor = 'rgba(76, 175, 80, 0.5)'; - banner.style.background = 'rgba(76, 175, 80, 0.1)'; - showStatus(`Update available: ${availableVersion}`); - } else { - statusSpan.textContent = 'Up to date'; - detailsDiv.textContent = `You are running the latest ${buildType} version of Electron (${currentVersion}).`; - upgradeBtn.style.display = 'none'; - banner.style.borderColor = 'rgba(100, 100, 100, 0.3)'; - banner.style.background = 'rgba(100, 100, 100, 0.1)'; - showStatus('Electron is up to date'); - } - } - } catch (err) { - console.error('[ELECTRON-UPDATER] Check failed:', err); - statusSpan.textContent = 'Update check failed'; - detailsDiv.textContent = err.message; - banner.style.borderColor = 'rgba(244, 67, 54, 0.5)'; - banner.style.background = 'rgba(244, 67, 54, 0.1)'; - showStatus('Check failed'); - } finally { - checkBtn.disabled = false; - } - }; - - // Install Electron update - const handleUpgrade = async () => { - if (isUpgrading) return; - - const buildType = versionSelect.value; - if (!availableVersion) { - showStatus('No update available'); - return; - } - - const confirmed = confirm( - `Update Electron from ${currentVersion} to ${availableVersion}?\n\nThis will download and install the ${buildType} version, then restart the application.\n\nThis process may take a few minutes.` - ); - - if (!confirmed) return; - - try { - isUpgrading = true; - upgradeBtn.disabled = true; - checkBtn.disabled = true; - versionSelect.disabled = true; - - statusSpan.textContent = 'Installing update...'; - detailsDiv.textContent = `Downloading and installing Electron ${availableVersion}. Please wait...`; - progressDiv.style.display = 'block'; - banner.style.borderColor = 'rgba(255, 193, 7, 0.5)'; - banner.style.background = 'rgba(255, 193, 7, 0.1)'; - showStatus('Installing Electron update...'); - - const result = await ipc.invoke('upgrade-electron', buildType); - - if (result.success) { - statusSpan.textContent = 'Update installed'; - detailsDiv.textContent = 'Electron has been updated successfully. The application will restart now.'; - progressDiv.style.display = 'none'; - banner.style.borderColor = 'rgba(76, 175, 80, 0.5)'; - banner.style.background = 'rgba(76, 175, 80, 0.1)'; - showStatus('Update complete - restarting...'); - - // Restart the app - setTimeout(() => { - if (ipc) { - ipc.invoke('restart-app').catch(err => { - console.error('Restart failed:', err); - showStatus('Please restart the app manually'); - }); - } - }, 2000); - } else { - throw new Error(result.error || 'Upgrade failed'); - } - } catch (err) { - console.error('[ELECTRON-UPDATER] Upgrade failed:', err); - statusSpan.textContent = 'Update failed'; - detailsDiv.textContent = `Failed to install update: ${err.message}`; - progressDiv.style.display = 'none'; - banner.style.borderColor = 'rgba(244, 67, 54, 0.5)'; - banner.style.background = 'rgba(244, 67, 54, 0.1)'; - showStatus(`Update failed: ${err.message}`); - - isUpgrading = false; - upgradeBtn.disabled = false; - checkBtn.disabled = false; - versionSelect.disabled = false; - } - }; - - // Wire up event handlers - if (checkBtn) { - checkBtn.addEventListener('click', checkVersions); - } - - if (upgradeBtn) { - upgradeBtn.addEventListener('click', handleUpgrade); - } - - if (versionSelect) { - versionSelect.addEventListener('change', () => { - // Reset UI when build type changes - banner.style.display = 'none'; - upgradeBtn.style.display = 'none'; - upgradeBtn.disabled = true; - availableVersion = null; - }); - } -} - - -// Helper function to compare semantic versions -function compareVersions(v1, v2) { - const parts1 = v1.split('-')[0].split('.').map(x => parseInt(x, 10)); - const parts2 = v2.split('-')[0].split('.').map(x => parseInt(x, 10)); - - for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { - const p1 = parts1[i] || 0; - const p2 = parts2[i] || 0; - if (p1 > p2) return 1; - if (p1 < p2) return -1; - } - return 0; -} - -// Keep settings open when clicking GitHub by asking host to open externally/new tab -window.addEventListener('DOMContentLoaded', () => { - const gh = document.getElementById('github-link'); - if (gh) { - gh.addEventListener('click', (e) => { - try { - e.preventDefault(); - const url = gh.getAttribute('href'); - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('navigate', url, { newTab: true }); - } else if (window.parent) { - window.parent.postMessage({ type: 'navigate', url, newTab: true }, '*'); - } else { - window.open(url, '_blank', 'noopener'); - } - } catch (err) { - console.error('Failed to open GitHub link:', err); - window.open(gh.getAttribute('href'), '_blank'); - } - }); - } - const help = document.getElementById('help-link'); - if (help) { - help.addEventListener('click', (e) => { - try { - e.preventDefault(); - const url = help.getAttribute('href'); - if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('navigate', url, { newTab: true }); - } else if (window.parent) { - window.parent.postMessage({ type: 'navigate', url, newTab: true }, '*'); - } else { - window.open(url, '_blank', 'noopener'); - } - } catch (err) { - console.error('Failed to open Help link:', err); - window.open(help.getAttribute('href'), '_blank'); - } - }); - } -}); - -// ----------------------------- -// Plugins management (Settings) -// ----------------------------- -async function loadPluginsUI() { - const listEl = document.getElementById('plugins-list'); - const reloadAllBtn = document.getElementById('plugins-reload-all'); - if (!listEl) return; - // Load list - let items = []; - try { - items = (ipc ? await ipc.invoke('plugins-list') : []) || []; - } catch (e) { - console.warn('plugins-list failed', e); - } - listEl.innerHTML = ''; - if (!items.length) { - const empty = document.createElement('div'); - empty.className = 'plugin-item'; - empty.textContent = 'No plugins found'; - listEl.appendChild(empty); - } else { - for (const p of items) { - const categories = Array.isArray(p.categories) ? p.categories.filter(x => x && typeof x === 'string') : []; - const authors = Array.isArray(p.authors) ? p.authors.filter(x => x && typeof x === 'string') : []; - const tagsHtml = categories.length ? `
      ${categories.map(c => `${escapeHtml(c)}`).join('')}
      ` : ''; - const authorsHtml = authors.length ? `
      Authors: ${authors.map(a => `${escapeHtml(a)}`).join(', ')}
      ` : ''; - const row = document.createElement('div'); - row.className = 'plugin-item'; - row.setAttribute('role', 'listitem'); - row.innerHTML = ` -
      -
      ${escapeHtml(p.name)} v${escapeHtml(p.version)}
      -
      ${escapeHtml(p.description || '')}
      - ${tagsHtml} - ${authorsHtml} -
      ${escapeHtml(p.dir)}
      -
      -
      - - - -
      `; - // Wire actions - const enableInput = row.querySelector('input.plugin-enable'); - const labelSpan = row.querySelector('label span'); - enableInput.addEventListener('change', async () => { - const enabled = enableInput.checked; - try { - if (ipc) await ipc.invoke('plugins-set-enabled', { id: p.id, enabled }); - labelSpan.textContent = enabled ? 'Enabled' : 'Disabled'; - showStatus(`${p.name}: ${enabled ? 'Enabled' : 'Disabled'}.`); - } catch (e) { - console.error('Failed to toggle plugin', p.id, e); - enableInput.checked = !enabled; - labelSpan.textContent = enableInput.checked ? 'Enabled' : 'Disabled'; - showStatus('Failed updating plugin'); - } - }); - const reloadBtn = row.querySelector('button.plugin-reload'); - reloadBtn.addEventListener('click', async () => { - try { - if (ipc) await ipc.invoke('plugins-reload', { id: p.id }); - showStatus(`${p.name} reloaded.`); - } catch (e) { - console.error('Plugin reload failed', e); - showStatus('Reload failed'); - } - }); - listEl.appendChild(row); - } - } - if (reloadAllBtn) reloadAllBtn.onclick = async () => { - try { if (ipc) await ipc.invoke('plugins-reload', {}); showStatus('Plugins reloaded.'); } catch { showStatus('Reload failed'); } - }; -} - -function escapeHtml(s) { - return String(s).replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c])); -} - -// Load when settings page shows Plugins tab for the first time -window.addEventListener('DOMContentLoaded', () => { - const tabBtn = document.getElementById('tab-plugins'); - if (!tabBtn) return; - let loaded = false; - const ensureLoad = () => { if (!loaded) { loaded = true; loadPluginsUI(); } }; - tabBtn.addEventListener('click', ensureLoad); - if (location.hash === '#plugins') ensureLoad(); -}); diff --git a/renderer/setup.css b/renderer/setup.css deleted file mode 100644 index 938b4cb..0000000 --- a/renderer/setup.css +++ /dev/null @@ -1,669 +0,0 @@ -/* Load InterVariable Font */ -@font-face { - font-family: 'InterVariable'; - src: url('../assets/images/fonts/InterVariable.ttf') format('truetype'); - font-weight: 100 900; - font-display: swap; -} - -/* CSS Custom Properties */ -:root { - --bg: #121418; - --dark-blue: #0B1C2B; - --dark-purple: #1B1035; - --primary: #7B2EFF; - --primary-rgb: 123, 46, 255; - --accent: #00C6FF; - --accent-rgb: 0, 198, 255; - --text: #E0E0E0; - --text-secondary: #A0A0A0; - --card-bg: rgba(255, 255, 255, 0.05); - --card-hover: rgba(255, 255, 255, 0.08); - --border: rgba(255, 255, 255, 0.1); - --success: #4CAF50; - --success-rgb: 76, 175, 80; - --warning: #FF9800; - --warning-rgb: 255, 152, 0; - --gradient-primary: linear-gradient(135deg, var(--accent), var(--primary)); - --gradient-bg: linear-gradient(145deg, var(--bg) 0%, var(--dark-purple) 100%); - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4); -} - -/* Base Styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body, html { - margin: 0; - padding: 0; - height: 100%; - background: var(--gradient-bg); - color: var(--text); - font-family: 'InterVariable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - overflow: hidden; -} - -/* Setup Container */ -.setup-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - height: 100vh; - min-height: 100vh; - padding: 2rem; - overflow: hidden; -} - -/* Progress Bar */ -.progress-bar { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - margin-bottom: 3rem; - width: 100%; - max-width: 800px; -} - -.progress-step { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - opacity: 0.4; - transition: opacity 0.3s ease; -} - -.progress-step.active, -.progress-step.completed { - opacity: 1; -} - -.step-circle { - width: 48px; - height: 48px; - border-radius: 50%; - background: var(--card-bg); - border: 2px solid var(--border); - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 1.1rem; - transition: all 0.3s ease; -} - -.progress-step.active .step-circle { - background: var(--primary); - border-color: transparent; - box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.4); - transform: scale(1.1); -} - -.progress-step.completed .step-circle { - background: var(--success); - border-color: transparent; -} - -.step-label { - font-size: 0.85rem; - font-weight: 500; - color: var(--text-secondary); -} - -.progress-step.active .step-label { - color: var(--text); - font-weight: 600; -} - -.progress-line { - flex: 1; - height: 2px; - background: var(--border); - margin: 0 0.5rem; - max-width: 100px; -} - -/* Setup Steps */ -.setup-step { - display: none; - flex-direction: column; - align-items: center; - justify-content: flex-start; - width: 100%; - max-width: 900px; - flex: 1; - animation: fadeIn 0.4s ease; - min-height: 0; -} - -.setup-step.active { - display: flex; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.step-content { - width: 100%; - text-align: center; - margin-bottom: 2rem; - flex: 1; - min-height: 0; - overflow-y: auto; - padding-right: 0.5rem; -} - -.setup-title { - font-size: clamp(2rem, 5vw, 3rem); - font-weight: 700; - letter-spacing: -0.5px; - margin-bottom: 0.75rem; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.setup-subtitle { - font-size: 1.1rem; - color: var(--text-secondary); - margin-bottom: 2rem; - font-weight: 400; -} - -/* Logo */ -.logo-container { - margin-bottom: 2rem; -} - -.setup-logo { - width: 120px; - height: 120px; - filter: drop-shadow(0 8px 24px rgba(var(--primary-rgb), 0.3)); - animation: float 3s ease-in-out infinite; -} - -@keyframes float { - 0%, 100% { - transform: translateY(0px); - } - 50% { - transform: translateY(-10px); - } -} - -/* Feature Grid */ -.feature-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; - margin: 2rem 0; -} - -.feature-item { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 16px; - padding: 1.5rem; - transition: all 0.3s ease; -} - -.feature-item:hover { - background: var(--card-hover); - border-color: var(--primary); - transform: translateY(-4px); - box-shadow: var(--shadow-md); -} - -.feature-icon { - font-size: 2.5rem; - margin-bottom: 0.75rem; -} - -.feature-item h3 { - font-size: 1.1rem; - font-weight: 600; - margin-bottom: 0.5rem; - color: var(--text); -} - -.feature-item p { - font-size: 0.9rem; - color: var(--text-secondary); - line-height: 1.5; -} - -/* Theme Grid */ -.theme-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 1rem; - margin: 1.5rem 0 2rem; - align-content: start; -} - -.theme-card { - background: var(--card-bg); - border: 2px solid var(--border); - border-radius: 16px; - padding: 1rem; - cursor: pointer; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.theme-card:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-md); - border-color: var(--accent); -} - -.theme-card.selected { - border-color: var(--primary); - background: var(--card-hover); - box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.2); -} - -.theme-card.selected::after { - content: 'โœ“'; - position: absolute; - top: 12px; - right: 12px; - width: 32px; - height: 32px; - background: var(--gradient-primary); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - font-size: 1.2rem; - box-shadow: var(--shadow-sm); -} - -.theme-preview { - width: 100%; - height: 72px; - border-radius: 8px; - margin-bottom: 0.75rem; - display: flex; - gap: 4px; - padding: 8px; -} - -.theme-color { - flex: 1; - border-radius: 4px; - transition: transform 0.2s ease; -} - -.theme-card:hover .theme-color { - transform: scale(1.05); -} - -.theme-name { - font-size: 1rem; - font-weight: 600; - color: var(--text); - margin-bottom: 0.2rem; -} - -.theme-description { - font-size: 0.8rem; - color: var(--text-secondary); -} - -/* Default Browser Section */ -.default-browser-section { - max-width: 500px; - margin: 2rem auto; -} - -.default-browser-card { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 16px; - padding: 2rem; - margin-bottom: 2rem; -} - -.browser-icon { - font-size: 4rem; - margin-bottom: 1rem; -} - -.default-browser-card h3 { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 0.5rem; - color: var(--text); -} - -.default-browser-card p { - color: var(--text-secondary); - font-size: 1rem; - line-height: 1.6; -} - -.default-browser-status { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 12px; - padding: 1.5rem; - margin-bottom: 1.5rem; - display: flex; - align-items: center; - gap: 1rem; -} - -.status-icon { - font-size: 2rem; -} - -.status-text { - font-size: 0.95rem; - color: var(--text-secondary); -} - -.default-browser-status.checking .status-icon { - animation: spin 1s linear infinite; -} - -.default-browser-status.is-default { - border-color: var(--success); - background: rgba(var(--success-rgb), 0.1); -} - -.default-browser-status.is-default .status-icon { - color: var(--success); -} - -.default-browser-status.is-default .status-text { - color: var(--success); -} - -.default-browser-status.not-default { - border-color: var(--warning); - background: rgba(var(--warning-rgb), 0.1); -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -.default-browser-actions { - text-align: center; -} - -.help-text { - margin-top: 1rem; - font-size: 0.85rem; - color: var(--text-secondary); -} - -/* Success Icon */ -.success-icon { - width: 120px; - height: 120px; - margin: 0 auto 2rem; - background: var(--gradient-primary); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 4rem; - font-weight: bold; - box-shadow: 0 8px 32px rgba(var(--primary-rgb), 0.4); - animation: scaleIn 0.5s ease; -} - -@keyframes scaleIn { - from { - transform: scale(0); - opacity: 0; - } - to { - transform: scale(1); - opacity: 1; - } -} - -/* Completion Summary */ -.completion-summary { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 16px; - padding: 2rem; - margin: 2rem auto; - max-width: 500px; - text-align: left; -} - -.summary-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 1rem 0; - border-bottom: 1px solid var(--border); -} - -.summary-item:last-child { - border-bottom: none; -} - -.summary-icon { - font-size: 1.5rem; - width: 40px; - text-align: center; -} - -.summary-content { - flex: 1; -} - -.summary-label { - font-size: 0.85rem; - color: var(--text-secondary); - margin-bottom: 0.25rem; -} - -.summary-value { - font-size: 1rem; - color: var(--text); - font-weight: 500; -} - -/* Future Feature Teaser */ -.future-feature-teaser { - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.1), rgba(var(--accent-rgb), 0.1)); - border: 1px solid var(--border); - border-radius: 16px; - padding: 2rem; - margin: 2rem auto; - max-width: 600px; -} - -.future-feature-teaser h3 { - font-size: 1.3rem; - margin-bottom: 1rem; - color: var(--text); -} - -.teaser-text { - display: flex; - align-items: center; - gap: 1rem; - font-size: 1rem; - color: var(--text-secondary); - line-height: 1.6; -} - -.teaser-icon { - font-size: 2rem; -} - -/* Buttons */ -.step-actions { - display: flex; - gap: 1rem; - justify-content: center; - width: 100%; - max-width: 500px; - margin-top: auto; - padding-top: 1.5rem; - padding-bottom: 1rem; - position: sticky; - bottom: 0; - background: transparent; - backdrop-filter: none; -} - -.btn { - padding: 0.875rem 2rem; - border: none; - border-radius: 12px; - font-size: 1rem; - font-weight: 600; - font-family: 'InterVariable', sans-serif; - cursor: pointer; - transition: all 0.3s ease; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - min-width: 140px; -} - -.btn-primary { - background: var(--primary); - color: white; - box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.4); -} - -.btn-primary:active { - transform: translateY(0); -} - -.btn-secondary { - background: var(--card-bg); - color: var(--text); - border: 1px solid var(--border); -} - -.btn-secondary:hover { - background: var(--card-hover); - border-color: var(--primary); -} - -.btn-large { - padding: 1.125rem 2.5rem; - font-size: 1.125rem; - min-width: 200px; -} - -.btn-icon { - font-size: 1.2rem; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none !important; -} - -/* Responsive Design */ -@media (max-width: 768px) { - .setup-container { - padding: 1rem; - } - - .progress-bar { - gap: 0.5rem; - margin-bottom: 2rem; - } - - .step-circle { - width: 40px; - height: 40px; - font-size: 0.95rem; - } - - .step-label { - font-size: 0.75rem; - } - - .progress-line { - max-width: 50px; - } - - .setup-title { - font-size: 2rem; - } - - .setup-subtitle { - font-size: 1rem; - } - - .feature-grid { - grid-template-columns: 1fr; - gap: 1rem; - } - - .theme-grid { - grid-template-columns: 1fr; - margin-bottom: 1.5rem; - } - - .step-actions { - flex-direction: column-reverse; - width: 100%; - } - - .btn { - width: 100%; - } -} - -@media (max-height: 700px) { - .progress-bar { - margin-bottom: 1.25rem; - } - - .step-content { - margin-bottom: 1rem; - } - - .setup-subtitle { - margin-bottom: 1.25rem; - } - - .feature-grid { - margin: 1rem 0; - } -} diff --git a/renderer/setup.html b/renderer/setup.html deleted file mode 100644 index d620825..0000000 --- a/renderer/setup.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - Welcome to Nebula - - - -
      - -
      -
      -
      1
      -
      Welcome
      -
      -
      -
      -
      2
      -
      Theme
      -
      -
      -
      -
      3
      -
      Default Browser
      -
      -
      -
      -
      4
      -
      Complete
      -
      -
      - - -
      -
      - -

      Welcome to Nebula

      -

      Let's personalize your browsing experience

      -
      -
      -
      ๐ŸŽจ
      -

      Beautiful Themes

      -

      Choose from stunning themes or create your own

      -
      -
      -
      ๐Ÿš€
      -

      Lightning Fast

      -

      Built for speed and performance

      -
      -
      -
      ๐ŸŽฎ
      -

      Steam Deck Ready

      -

      Optimized for gaming handhelds

      -
      -
      -
      ๐Ÿ”’
      -

      Privacy First

      -

      Your data stays yours

      -
      -
      -
      -
      - - -
      -
      - - -
      -
      -

      Choose Your Theme

      -

      Pick a color scheme that suits your style

      -
      - -
      -
      -
      - - -
      -
      - - -
      -
      -

      Set as Default Browser

      -

      Make Nebula your go-to browser for all links

      -
      -
      -
      ๐ŸŒ
      -

      Quick Access

      -

      Open all web links automatically with Nebula

      -
      -
      -
      โณ
      -

      Checking default browser status...

      -
      -
      - -

      You can always change this later in settings

      -
      -
      -
      -
      - - -
      -
      - - -
      -
      -
      โœ“
      -

      All Set!

      -

      You're ready to explore the web with Nebula

      -
      - -
      - - -
      -
      - -
      -
      -
      - - - - diff --git a/renderer/setup.js b/renderer/setup.js deleted file mode 100644 index 522bbe8..0000000 --- a/renderer/setup.js +++ /dev/null @@ -1,587 +0,0 @@ -/** - * First-Time Setup Script for Nebula Browser - * Handles theme selection, default browser setup, and first-run completion - */ - -// State management -const setupState = { - currentStep: 1, - selectedTheme: 'default', - defaultBrowserSet: false, - skipped: false, - themes: [] -}; - -// Initialize setup when DOM is ready -document.addEventListener('DOMContentLoaded', async () => { - console.log('[Setup] Initializing first-time setup...'); - - // Load available themes - await loadThemes(); - - // Initialize button handlers - initializeButtons(); - - // Check default browser status - checkDefaultBrowserStatus(); -}); - -/** - * Load available themes from main process - */ -async function loadThemes() { - try { - const themes = await window.api.getAllThemes(); - console.log('[Setup] Loaded themes:', themes); - setupState.themes = themes; - - // Render theme grid - renderThemeGrid(themes); - } catch (error) { - console.error('[Setup] Error loading themes:', error); - // Fallback to a default theme - setupState.themes = { - default: { - default: { name: 'Default', description: 'Classic Nebula theme', colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF' } } - } - }; - renderThemeGrid(setupState.themes); - } -} - -/** - * Render theme selection grid - */ -function renderThemeGrid(themes) { - const themeGrid = document.getElementById('theme-grid'); - if (!themeGrid) return; - - themeGrid.innerHTML = ''; - - // Convert themes object to array - let themeArray = []; - - if (Array.isArray(themes)) { - // Already an array - themeArray = themes; - } else if (themes.default) { - // Has default property, extract themes from it - themeArray = Object.entries(themes.default).map(([id, data]) => ({ - id, - name: data.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '), - description: data.description || 'A beautiful color scheme', - colors: data.colors || {} - })); - } else { - // Direct object of themes - themeArray = Object.entries(themes).map(([id, data]) => ({ - id, - name: data.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '), - description: data.description || 'A beautiful color scheme', - colors: data.colors || {} - })); - } - - console.log('[Setup] Rendering', themeArray.length, 'themes'); - - // If no themes found, add a default one - if (themeArray.length === 0) { - themeArray = [{ - id: 'default', - name: 'Default', - description: 'Classic Nebula theme', - colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' } - }]; - } - - themeArray.forEach(theme => { - const themeCard = createThemeCard(theme); - themeGrid.appendChild(themeCard); - }); - - // Select default theme - const defaultCard = themeGrid.querySelector('[data-theme-id="default"]'); - if (defaultCard) { - defaultCard.classList.add('selected'); - const defaultTheme = getThemeById('default'); - if (defaultTheme) { - applyThemeToSetupPage(defaultTheme, 'default'); - } - } -} - -/** - * Get a theme by id from loaded theme sets - */ -function getThemeById(themeId) { - const themes = setupState.themes || {}; - if (themes.default && themes.default[themeId]) return themes.default[themeId]; - if (themes.user && themes.user[themeId]) return themes.user[themeId]; - if (themes.downloaded && themes.downloaded[themeId]) return themes.downloaded[themeId]; - return null; -} - -function hexToRgb(hex) { - if (!hex || typeof hex !== 'string') return null; - let normalized = hex.trim().replace(/^#/, ''); - if (normalized.length === 3) { - normalized = normalized.split('').map(char => char + char).join(''); - } - if (!/^[a-fA-F\d]{6}$/.test(normalized)) return null; - - const intValue = parseInt(normalized, 16); - return { - r: (intValue >> 16) & 255, - g: (intValue >> 8) & 255, - b: intValue & 255 - }; -} - -/** - * Apply theme to the setup page UI and persist selection - */ -function applyThemeToSetupPage(theme, themeId = null) { - if (!theme || !theme.colors) return; - const colors = theme.colors; - const root = document.documentElement; - - const setVar = (cssVar, value, fallback) => { - const val = value || fallback; - if (val) root.style.setProperty(cssVar, val); - }; - - setVar('--bg', colors.bg, '#121418'); - setVar('--dark-blue', colors.darkBlue, '#0B1C2B'); - setVar('--dark-purple', colors.darkPurple, '#1B1035'); - setVar('--primary', colors.primary, '#7B2EFF'); - setVar('--accent', colors.accent, '#00C6FF'); - setVar('--text', colors.text, '#E0E0E0'); - setVar('--success', colors.accent, '#4CAF50'); - setVar('--warning', colors.primary, '#FF9800'); - - const primaryRgb = hexToRgb(colors.primary || '#7B2EFF'); - const accentRgb = hexToRgb(colors.accent || '#00C6FF'); - const successRgb = hexToRgb(colors.accent || '#4CAF50'); - const warningRgb = hexToRgb(colors.primary || '#FF9800'); - if (primaryRgb) { - setVar('--primary-rgb', `${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}`); - } - if (accentRgb) { - setVar('--accent-rgb', `${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}`); - } - if (successRgb) { - setVar('--success-rgb', `${successRgb.r}, ${successRgb.g}, ${successRgb.b}`); - } - if (warningRgb) { - setVar('--warning-rgb', `${warningRgb.r}, ${warningRgb.g}, ${warningRgb.b}`); - } - - if (theme.gradient) { - document.body.style.background = theme.gradient; - } else if (colors.bg) { - document.body.style.background = colors.bg; - } - - // Persist for main UI to pick up on first load - try { - localStorage.setItem('currentTheme', JSON.stringify(theme)); - if (themeId) localStorage.setItem('activeThemeName', themeId); - } catch (err) { - console.warn('[Setup] Failed to persist theme:', err); - } -} - -/** - * Create a theme card element - */ -function createThemeCard(theme) { - const card = document.createElement('div'); - card.className = 'theme-card'; - card.dataset.themeId = theme.id; - - // Create color preview - const preview = document.createElement('div'); - preview.className = 'theme-preview'; - - const colors = theme.colors || {}; - - // Get color values, trying multiple property naming conventions - const getColor = (keys, fallback) => { - for (const key of keys) { - if (colors[key]) return colors[key]; - } - return fallback; - }; - - const previewColors = [ - getColor(['bg', '--bg', 'background'], '#121418'), - getColor(['primary', '--primary'], '#7B2EFF'), - getColor(['accent', '--accent'], '#00C6FF'), - getColor(['text', '--text'], '#E0E0E0') - ]; - - previewColors.forEach(color => { - const colorDiv = document.createElement('div'); - colorDiv.className = 'theme-color'; - colorDiv.style.backgroundColor = color; - preview.appendChild(colorDiv); - }); - - // Create theme info - const name = document.createElement('div'); - name.className = 'theme-name'; - name.textContent = theme.name || theme.id; - - const description = document.createElement('div'); - description.className = 'theme-description'; - description.textContent = theme.description || 'A beautiful color scheme'; - - // Assemble card - card.appendChild(preview); - card.appendChild(name); - card.appendChild(description); - - // Add click handler - card.addEventListener('click', () => selectTheme(theme.id, card)); - - return card; -} - -/** - * Select a theme - */ -function selectTheme(themeId, cardElement) { - // Update state - setupState.selectedTheme = themeId; - - // Update UI - document.querySelectorAll('.theme-card').forEach(card => { - card.classList.remove('selected'); - }); - cardElement.classList.add('selected'); - - const theme = getThemeById(themeId); - if (theme) { - applyThemeToSetupPage(theme, themeId); - } - - console.log('[Setup] Selected theme:', themeId); -} - -/** - * Check if Nebula is the default browser - */ -async function checkDefaultBrowserStatus() { - const statusEl = document.getElementById('default-status'); - if (!statusEl) return; - - statusEl.classList.add('checking'); - - try { - const isDefault = await window.api.isDefaultBrowser(); - - statusEl.classList.remove('checking'); - - if (isDefault) { - statusEl.classList.add('is-default'); - statusEl.innerHTML = ` -
      โœ“
      -

      Nebula is already your default browser

      - `; - setupState.defaultBrowserSet = true; - - // Update button - const setDefaultBtn = document.getElementById('btn-set-default'); - if (setDefaultBtn) { - setDefaultBtn.textContent = 'โœ“ Already Default'; - setDefaultBtn.disabled = true; - } - } else { - statusEl.classList.add('not-default'); - statusEl.innerHTML = ` -
      โ„น๏ธ
      -

      Nebula is not your default browser

      - `; - } - } catch (error) { - console.error('[Setup] Error checking default browser status:', error); - statusEl.classList.remove('checking'); - statusEl.innerHTML = ` -
      โš ๏ธ
      -

      Unable to check default browser status

      - `; - } -} - -/** - * Set Nebula as default browser - */ -async function setDefaultBrowser() { - const btn = document.getElementById('btn-set-default'); - const statusEl = document.getElementById('default-status'); - - if (btn) { - btn.disabled = true; - btn.innerHTML = 'โณ Setting...'; - } - - try { - const result = await window.api.setAsDefaultBrowser(); - - if (result.success) { - const isDefault = await window.api.isDefaultBrowser(); - if (isDefault) { - setupState.defaultBrowserSet = true; - - if (statusEl) { - statusEl.classList.remove('not-default'); - statusEl.classList.add('is-default'); - statusEl.innerHTML = ` -
      โœ“
      -

      Nebula is now your default browser!

      - `; - } - - if (btn) { - btn.innerHTML = 'โœ“ Set Successfully'; - } - - // Auto-advance after a brief delay - setTimeout(() => goToStep(4), 1500); - return; - } - - if (statusEl) { - statusEl.classList.remove('not-default'); - statusEl.innerHTML = ` -
      โ„น๏ธ
      -

      System settings opened. Choose Nebula as your default browser to finish.

      - `; - } - - if (btn) { - btn.disabled = false; - btn.innerHTML = 'โ†ป Check Again'; - } - - if (result.needsUserAction && window.api.openDefaultBrowserSettings) { - try { await window.api.openDefaultBrowserSettings(); } catch {} - } - return; - } - throw new Error(result.error || 'Failed to set default browser'); - } catch (error) { - console.error('[Setup] Error setting default browser:', error); - - if (statusEl) { - statusEl.innerHTML = ` -
      โš ๏ธ
      -

      Failed to set default browser. You can try again from settings.

      - `; - } - - if (btn) { - btn.disabled = false; - btn.innerHTML = 'โ†ป Try Again'; - } - } -} - -/** - * Navigate to a specific step - */ -function goToStep(stepNumber) { - // Hide current step - document.querySelectorAll('.setup-step').forEach(step => { - step.classList.remove('active'); - }); - - // Show target step - const targetStep = document.querySelector(`.setup-step[data-step="${stepNumber}"]`); - if (targetStep) { - targetStep.classList.add('active'); - } - - // Update progress bar - document.querySelectorAll('.progress-step').forEach((step, index) => { - const stepNum = index + 1; - if (stepNum < stepNumber) { - step.classList.add('completed'); - step.classList.remove('active'); - } else if (stepNum === stepNumber) { - step.classList.add('active'); - step.classList.remove('completed'); - } else { - step.classList.remove('active', 'completed'); - } - }); - - setupState.currentStep = stepNumber; - - // Special handling for completion step - if (stepNumber === 4) { - renderCompletionSummary(); - } - - console.log('[Setup] Navigated to step:', stepNumber); -} - -/** - * Render completion summary - */ -function renderCompletionSummary() { - const summaryEl = document.getElementById('completion-summary'); - if (!summaryEl) return; - - const selectedThemeName = setupState.themes.default?.[setupState.selectedTheme]?.name || - setupState.selectedTheme.charAt(0).toUpperCase() + setupState.selectedTheme.slice(1); - - summaryEl.innerHTML = ` -
      -
      ๐ŸŽจ
      -
      -
      Selected Theme
      -
      ${selectedThemeName}
      -
      -
      -
      -
      ๐ŸŒ
      -
      -
      Default Browser
      -
      ${setupState.defaultBrowserSet ? 'Set as Default' : 'Not Set'}
      -
      -
      - `; -} - -/** - * Complete setup and save preferences - */ -async function completeSetup() { - console.log('[Setup] Completing first-time setup...', setupState); - - try { - // Apply selected theme - await window.api.applyTheme(setupState.selectedTheme); - - // Save first-run completion - await window.api.completeFirstRun({ - selectedTheme: setupState.selectedTheme, - defaultBrowserSet: setupState.defaultBrowserSet, - skipped: setupState.skipped - }); - - console.log('[Setup] First-time setup completed successfully'); - - // Navigate to main browser interface (index.html has tabs and URL bar) - window.location.href = 'index.html'; - } catch (error) { - console.error('[Setup] Error completing setup:', error); - alert('There was an error saving your preferences. Please try again.'); - } -} - -/** - * Skip setup and use defaults - */ -async function skipSetup() { - setupState.skipped = true; - - try { - // Save that first-run was completed (even if skipped) - await window.api.completeFirstRun({ - selectedTheme: 'default', - defaultBrowserSet: false, - skipped: true - }); - - console.log('[Setup] Setup skipped, using defaults'); - - // Navigate to main browser interface (index.html has tabs and URL bar) - window.location.href = 'index.html'; - } catch (error) { - console.error('[Setup] Error skipping setup:', error); - window.location.href = 'index.html'; - } -} - -/** - * Initialize button event handlers - */ -function initializeButtons() { - // Step 1: Welcome - const btnStart = document.getElementById('btn-start'); - const btnSkipAll = document.getElementById('btn-skip-all'); - - if (btnStart) { - btnStart.addEventListener('click', () => goToStep(2)); - } - - if (btnSkipAll) { - btnSkipAll.addEventListener('click', skipSetup); - } - - // Step 2: Theme Selection - const btnBack2 = document.getElementById('btn-back-2'); - const btnNext2 = document.getElementById('btn-next-2'); - - if (btnBack2) { - btnBack2.addEventListener('click', () => goToStep(1)); - } - - if (btnNext2) { - btnNext2.addEventListener('click', () => goToStep(3)); - } - - // Step 3: Default Browser - const btnBack3 = document.getElementById('btn-back-3'); - const btnSkip3 = document.getElementById('btn-skip-3'); - const btnSetDefault = document.getElementById('btn-set-default'); - - if (btnBack3) { - btnBack3.addEventListener('click', () => goToStep(2)); - } - - if (btnSkip3) { - btnSkip3.addEventListener('click', () => goToStep(4)); - } - - if (btnSetDefault) { - btnSetDefault.addEventListener('click', setDefaultBrowser); - } - - // Step 4: Complete - const btnFinish = document.getElementById('btn-finish'); - - if (btnFinish) { - btnFinish.addEventListener('click', completeSetup); - } -} - -// Keyboard navigation -document.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - const currentStep = setupState.currentStep; - - switch (currentStep) { - case 1: - goToStep(2); - break; - case 2: - goToStep(3); - break; - case 3: - if (!setupState.defaultBrowserSet) { - setDefaultBrowser(); - } else { - goToStep(4); - } - break; - case 4: - completeSetup(); - break; - } - } else if (e.key === 'Escape' && setupState.currentStep > 1) { - goToStep(setupState.currentStep - 1); - } -}); diff --git a/renderer/style.css b/renderer/style.css deleted file mode 100644 index 2e3ef1e..0000000 --- a/renderer/style.css +++ /dev/null @@ -1,762 +0,0 @@ -html, body { - height: 100%; - margin: 0; - padding: 0; - /* Background now driven by theme */ - background: var(--bg, #0b0d10); - color: var(--text, white); - font-family: 'Segoe UI', system-ui, -apple-system, Ubuntu, Roboto, sans-serif; -} - -/* Global variables */ -:root { - /* Space reserved on the left for system window controls (traffic lights on macOS). - Applied cross-platform per request to keep a consistent layout. */ - --window-controls-offset: 80px; /* adjust if needed */ - /* Space reserved on the right for Windows title bar controls */ - --window-controls-width: 138px; - - /* Design tokens */ - --bg: #0b0d10; - --surface-1: #11131a; - --surface-2: #161925; - --surface-3: #1c2030; - --text: #e8e8f0; - --muted: #a4a7b3; - --outline: #2b3040; - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --shadow-1: 0 6px 20px rgba(0,0,0,.35); - --shadow-2: 0 12px 36px rgba(0,0,0,.45); - --blur: 12px; - /* Accent palette */ - --accent-h: 265; - --accent-s: 86%; - --accent-l: 62%; - --accent: hsl(var(--accent-h) var(--accent-s) var(--accent-l)); - --accent-600: hsl(var(--accent-h) var(--accent-s) 52%); - --accent-700: hsl(var(--accent-h) var(--accent-s) 46%); - - /* URL bar and tab theme colors (defaults) */ - --url-bar-bg: #1C2030; - --url-bar-text: #E0E0E0; - --url-bar-border: #3E4652; - --tab-bg: #161925; - --tab-text: #A4A7B3; - --tab-active: #1C2030; - --tab-active-text: #E0E0E0; - --tab-border: #2B3040; -} - -/* TITLEBAR CONTAINER - holds tab bar and window controls */ -#titlebar-container { - display: flex; - position: relative; - background: var(--tab-bg); - -webkit-app-region: drag; -} - -/* TAB STRIP */ -#tab-bar { - display: flex; - align-items: flex-end; - gap: 2px; - flex: 1; - /* Default: small left padding for Windows/Linux (no traffic lights) */ - padding: 6px 10px 0 10px; - background: var(--tab-bg); - border-bottom: 1px solid var(--tab-border); - overflow-x: auto; /* scroll when many tabs */ - scrollbar-color: #444 #2a2a3c; /* thumb and track for Firefox */ - scrollbar-width: thin; /* slimmer track */ - /* Inherit drag from container */ - -webkit-app-region: drag; - min-height: 38px; -} - -/* NAVBAR LAYOUT */ -#nav { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 12px; - background: var(--url-bar-bg); - gap: 12px; - /* flatter header to reduce paint cost */ - box-shadow: none; - /* Ensure the nav sits above embedded surfaces */ - position: relative; - z-index: 10000; - -webkit-backdrop-filter: blur(var(--blur)); - backdrop-filter: blur(var(--blur)); - border-bottom: 1px solid var(--url-bar-border); -} - -/* Make the top nav a draggable region on macOS when we use a hidden titlebar. - Interactive controls inside must opt-out with -webkit-app-region: no-drag. */ -@supports (-webkit-app-region: drag) { - /* Make the top nav a draggable region, but only in its background/gaps. */ - #nav { -webkit-app-region: drag; user-select: none; } - - /* Interactive controls must explicitly opt-out of dragging. This keeps - the larger draggable area while preserving normal click behavior. */ - #nav button, - #nav input, - #menu-popup, - .tab, - .tab *, - .new-tab-button, - .tab .tab-close, - #window-controls { - -webkit-app-region: no-drag; - } - - /* Ensure the new-tab button (which sits on the tab strip) is not draggable */ - #tab-bar .new-tab-button { - -webkit-app-region: no-drag; - } -} - -.nav-left, -.nav-center, -.nav-right { - display: flex; - align-items: center; - gap: 8px; -} - -.nav-center { - flex: 1; - padding: 6px 10px; - border-radius: 999px; /* pill */ - /* glassy, accented border */ - background: var(--url-bar-bg); - border: 1px solid var(--url-bar-border); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); -} - -#favicon { - width: 16px; - height: 16px; - margin-right: 4px; -} - -#url { - flex: 1; - background: transparent; - border: none; - color: var(--url-bar-text); - font-size: 14px; - outline: none; -} - -#url::placeholder { - color: rgba(255, 255, 255, 0.45); -} - -/* Iconic circular chrome buttons */ -.nav-left button, -.nav-right > button, -#reload-btn, -#menu-btn { - background: color-mix(in srgb, var(--url-bar-bg) 90%, var(--text) 10%); - color: var(--text); - border: 1px solid color-mix(in srgb, var(--text) 15%, transparent); - width: 34px; - height: 34px; - display: inline-grid; - place-items: center; - border-radius: 50%; - cursor: pointer; - /* subtle inner highlight adds edge definition */ - box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent); - transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; - line-height: 0; /* avoid vertical misalignment for glyphs */ - padding: 0; -} - -#downloads-btn svg { display:block; width: 18px; height: 18px; } - -/* Downloads button chrome to match other nav buttons */ -#downloads-btn { - background: color-mix(in srgb, var(--url-bar-bg) 90%, var(--text) 10%); - color: var(--text); - border: 1px solid color-mix(in srgb, var(--text) 15%, transparent); - width: 34px; - height: 34px; - display: inline-grid; - place-items: center; - border-radius: 50%; - cursor: pointer; - box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent); - transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease, color 120ms ease; - line-height: 0; - padding: 0; - -webkit-appearance: none; - appearance: none; -} -#downloads-btn:hover { filter: brightness(1.05); box-shadow: 0 4px 14px rgba(0,0,0,0.35); color: var(--text); } -#downloads-btn:active { transform: translateY(1px) scale(0.98); } -#downloads-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.55); } -#downloads-btn:focus { outline: none; box-shadow: none; } - -/* Match home-active chrome variant */ -body:has(#home-container.active) #downloads-btn { - background: color-mix(in srgb, var(--url-bar-bg) 85%, var(--text) 15%); - box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent); -} - -.nav-left button:hover, -.nav-right > button:hover, -#reload-btn:hover, -#menu-btn:hover { - filter: brightness(1.05); - box-shadow: 0 4px 14px color-mix(in srgb, var(--bg) 50%, transparent); -} - -.nav-left button:active, -.nav-right > button:active, -#reload-btn:active, -#menu-btn:active { - transform: translateY(1px) scale(0.98); -} - -/* Primary action (Go button) keeps rectangular look but modernized */ -.nav-center + button, -.nav-center button { - background: var(--accent); - color: var(--text); - border: 1px solid transparent; - padding: 8px 14px; - border-radius: 10px; - cursor: pointer; - transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; -} -.nav-center + button:hover, -.nav-center button:hover { box-shadow: 0 8px 20px color-mix(in srgb, var(--primary) 35%, transparent); } -.nav-center + button:active, -.nav-center button:active { transform: translateY(1px) scale(0.98); } - -/* MENU DROPDOWN */ -.menu-wrapper { - position: relative; - /* keep wrapper on a higher layer so absolute popup can composit above webviews */ - z-index: 10001; -} - -#menu-popup { - position: absolute; - top: 30px; - right: 0; - background: color-mix(in srgb, var(--url-bar-bg) 92%, var(--text) 8%); - border: 1px solid color-mix(in srgb, var(--primary) 25%, color-mix(in srgb, var(--accent) 18%, transparent)); - border-radius: 14px; - padding: 8px; - display: flex; - flex-direction: column; - min-width: 200px; /* wider dropdown */ - box-shadow: var(--shadow-1); - /* Much higher z-index and force its own compositing layer so it renders above guests */ - z-index: 20000; - -webkit-transform: translateZ(0); - transform: translateZ(0); - will-change: transform, opacity; - /* animated open/close */ - opacity: 1; - transform-origin: top right; - transition: opacity 160ms ease, transform 160ms ease; - /* ensure interactions only when visible */ - visibility: visible; - pointer-events: auto; - -webkit-backdrop-filter: blur(var(--blur)); - backdrop-filter: blur(var(--blur)); -} - -#menu-popup button { - background: transparent; - border: none; - color: var(--text); - text-align: left; - padding: 8px 10px; - border-radius: 10px; - transition: background 120ms ease, filter 120ms ease; -} - -#menu-popup button:hover { - background: color-mix(in srgb, var(--text) 8%, transparent); -} - -/* Big Picture Mode button special style */ -#bigpicture-btn { - background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 15%, transparent) 0%, color-mix(in srgb, var(--accent) 10%, transparent) 100%) !important; - border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent) !important; - margin: 4px 0; -} - -#bigpicture-btn:hover { - background: linear-gradient(135deg, rgba(123, 46, 255, 0.25) 0%, rgba(0, 198, 255, 0.15) 100%) !important; - border-color: rgba(123, 46, 255, 0.5) !important; -} - -.hidden { - display: none; -} - -/* Animate menu dropdown instead of removing from flow */ -#menu-popup.hidden { - display: block; /* override global .hidden */ - opacity: 0; - transform: translateY(-8px) scale(0.98); - visibility: hidden; - pointer-events: none; -} - -/* Downloads mini popup anchored to the downloads button */ -.downloads-wrapper { position: relative; z-index: 10002; } -#downloads-popup { - position: absolute; - top: 34px; - right: 0; - background: color-mix(in srgb, var(--url-bar-bg) 95%, var(--text) 5%); - border: 1px solid color-mix(in srgb, var(--primary) 18%, color-mix(in srgb, var(--accent) 14%, transparent)); - border-radius: 12px; - min-width: 280px; - box-shadow: var(--shadow-1); - padding: 8px; - -webkit-backdrop-filter: blur(var(--blur)); - backdrop-filter: blur(var(--blur)); - transition: opacity 160ms ease, transform 160ms ease; -} -#downloads-popup.hidden { opacity: 0; transform: translateY(-6px); visibility: hidden; pointer-events: none; } -.downloads-pop-header { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:4px 2px 8px; } -.downloads-pop-header > span { font-weight: 600; color: var(--text); } -.downloads-pop-header > button { background: transparent; border: none; color: var(--accent); cursor: pointer; } -.downloads-pop-list { display:flex; flex-direction: column; gap: 8px; max-height: 280px; overflow: auto; } -.downloads-empty { color: var(--tab-text); font-size: 12px; text-align: center; padding: 16px 8px; } -.dl-item { display:grid; grid-template-columns: 1fr auto; gap: 6px 8px; background: color-mix(in srgb, var(--text) 3%, transparent); border: 1px solid color-mix(in srgb, var(--text) 6%, transparent); border-radius: 10px; padding: 8px; } -.dl-file { font-size: 12px; color: var(--text); white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } -.dl-meta { font-size: 11px; color: var(--tab-text); } -.dl-actions { display:flex; gap:6px; } -.dl-actions button { background: transparent; color: var(--text); border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); border-radius: 8px; padding: 4px 8px; cursor: pointer; } -.dl-progress { height: 4px; background: color-mix(in srgb, var(--text) 8%, transparent); border-radius: 3px; overflow: hidden; grid-column: 1 / -1; } -.dl-bar { height: 100%; background: var(--accent); width: 0%; transition: width .12s linear; } - -/* Circular progress ring around downloads button */ -#downloads-btn { position: relative; } -#downloads-btn .ring { - position: absolute; - top: 50%; - left: 50%; - width: 40px; /* slightly larger than 34px button for halo */ - height: 40px; - transform: translate(-50%, -50%); - border-radius: 50%; - pointer-events: none; -} -#downloads-btn .ring svg { width: 100%; height: 100%; transform: rotate(-90deg); } -#downloads-btn .ring circle.bg { stroke: color-mix(in srgb, var(--text) 15%, transparent); stroke-width: 3; fill: none; } -#downloads-btn .ring circle.fg { stroke: var(--accent); stroke-width: 3; fill: none; stroke-linecap: round; transition: stroke-dashoffset .12s linear, opacity .12s ease; } - -/* WEBVIEWS */ -#webviews { - flex: 1; - display: flex; - width: 100%; - position: relative; - /* make sure webviews render on a separate base layer behind nav */ - z-index: 0; -} -#webviews.hidden { - display: none; -} - -#webviews webview { - flex: 1; - width: 100%; - height: 100%; - border: none; - display: none; -} -#webviews webview.active { - display: flex; -} -/* When webviews is hidden, collapse its flex size */ -#webviews.hidden { - flex: 0; -} - -/* HOME CONTAINER */ -#home-container { - flex: 1; - display: none; - width: 100%; - position: relative; - z-index: 0; -} - -#home-container.active { - display: flex; -} - -#home-webview { - width: 100%; - height: 100%; - border: none; - display: none; - flex: 1; - position: relative; - z-index: 0; -} -/* Show home webview when container is active */ -#home-container.active > #home-webview { - display: flex; -} - -/* TABS */ -.tab { - position: relative; - display: flex; - align-items: center; - gap: 8px; - -webkit-app-region: no-drag; /* allow HTML5 DnD to work */ - padding: 4px 10px; /* slimmer padding */ - margin: 0; - height: 28px; /* reduce overall tab height */ - color: var(--tab-text); - /* sleek glass tile */ - background: var(--tab-bg); - border: 1px solid var(--tab-border); - border-bottom: none; /* let it visually merge with the strip line */ - border-radius: 10px 10px 0 0; /* slightly tighter radius */ - cursor: pointer; - user-select: none; - max-width: 260px; - min-width: 120px; - flex: 0 1 180px; /* like Chrome: shrink when crowded */ - overflow: hidden; - transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease; -} - -.tab:hover { - background: var(--tab-bg); - opacity: 0.85; -} - -.tab.active { - color: var(--tab-active-text); - background: var(--tab-active); - box-shadow: 0 8px 22px color-mix(in srgb, var(--bg) 35%, transparent); -} - -.tab.active::after { - content: ""; - position: absolute; - left: 0; - right: 0; - top: 0; - height: 2px; - background: var(--accent); -} - -.tab .tab-favicon { - width: 16px; - height: 16px; - border-radius: 2px; - flex: 0 0 auto; -} - -.tab .tab-title { - flex: 1 1 auto; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-size: 12px; -} - -.tab .tab-close { - flex: 0 0 auto; - width: 22px; - height: 22px; - display: grid; - place-items: center; - border: none; - border-radius: 11px; - background: transparent; - color: var(--tab-text); - opacity: 0; /* hidden by default */ - transition: background 120ms ease, color 120ms ease, opacity 120ms ease; -} - -.tab:hover .tab-close, -.tab.active .tab-close { opacity: 1; } -.tab .tab-close:hover { background: color-mix(in srgb, var(--text) 20%, transparent); color: var(--text); } -.tab .tab-close:active { background: color-mix(in srgb, var(--text) 15%, transparent); } - -/* New tab (+) button aligned to the right end of the strip */ -.new-tab-button { - margin-left: 6px; - flex: 0 0 auto; - width: 26px; /* tighter button */ - height: 26px; - display: grid; - place-items: center; - border-radius: 13px; - border: 1px solid color-mix(in srgb, var(--text) 18%, transparent); - background: color-mix(in srgb, var(--tab-bg) 90%, var(--text) 10%); - color: var(--text); - cursor: pointer; - transition: transform 120ms ease, background 120ms ease, color 120ms ease, border-color 120ms ease, box-shadow 120ms ease; -} -.new-tab-button:hover { filter: brightness(1.06); box-shadow: 0 6px 16px color-mix(in srgb, var(--bg) 35%, transparent); } -.new-tab-button:active { transform: translateY(1px) scale(0.98); } - -/* ZOOM CONTROLS */ -.zoom-controls { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 8px; - background: color-mix(in srgb, var(--text) 4%, transparent); - border: 1px solid color-mix(in srgb, var(--text) 7%, transparent); - border-radius: 10px; -} -.zoom-controls .zoom-label { - flex: 1; - font-size: 14px; -} -.zoom-controls button { - background: transparent; - border: none; - color: var(--text); - font-size: 16px; - cursor: pointer; - width: 28px; - height: 28px; - display: grid; - place-items: center; - border-radius: 8px; - transition: background 120ms ease; -} -.zoom-controls button:hover { - background: rgba(255,255,255,0.06); -} -#zoom-percent { - min-width: 38px; - text-align: center; - font-size: 13px; - color: var(--muted); -} - -/* window controls (Windows/Linux only) - Firefox-style next to tabs */ -#window-controls { - display: flex; - align-items: stretch; - align-self: stretch; - -webkit-app-region: no-drag; - background: transparent; - border-bottom: 1px solid var(--tab-border); -} - -#window-controls button { - width: 46px; - background: transparent; - border: none; - color: var(--text); - cursor: pointer; - transition: background 120ms ease, color 120ms ease; - display: flex; - align-items: center; - justify-content: center; - -webkit-app-region: no-drag; - padding: 0; -} - -#window-controls button svg { - width: 12px; - height: 12px; - pointer-events: none; -} - -#window-controls button:hover { - background: rgba(255,255,255,0.1); - color: var(--text); -} - -#window-controls #close-btn:hover { - background: #e81123; - color: white; -} - -#window-controls #close-btn:hover svg path { - stroke: white; -} - -/* Hide window controls on macOS via body class set by JS */ -body.platform-darwin #window-controls { - display: none !important; -} - -/* macOS: add left padding for traffic lights */ -body.platform-darwin #tab-bar { - padding-left: var(--window-controls-offset); -} - -#tab-bar::-webkit-scrollbar { - height: 8px; /* horizontal scrollbar height */ -} -#tab-bar::-webkit-scrollbar-track { - background: #2a2a3c; - border-radius: 4px; -} -#tab-bar::-webkit-scrollbar-thumb { - background: #3f4152; - border-radius: 4px; -} -#tab-bar::-webkit-scrollbar-thumb:hover { - background: #56586a; -} - -/* Tab animations */ -.tab--flip { - transition: transform 180ms cubic-bezier(0.2, 0, 0, 1); -} -.tab--enter { - animation: tab-enter 160ms ease-out both; -} -.tab--closing { - animation: tab-exit 140ms ease-in both; -} - -/* While dragging a tab, lift it slightly for feedback */ -.tab--dragging { - transform: translateY(-2px) scale(1.04); - box-shadow: 0 10px 26px rgba(0,0,0,0.45); - z-index: 5; - transition: none !important; /* follow cursor without lag */ - will-change: transform; - cursor: grabbing; -} - -/* Show an insertion hint on hovered tab and nudge it */ -.tab--drop-before, -.tab--drop-after { - position: relative; -} -.tab--drop-before { transform: translateX(-10px); } -.tab--drop-after { transform: translateX(10px); } - -.tab--drop-before::before, -.tab--drop-after::after { - content: ""; - position: absolute; - top: 4px; - bottom: 0; - width: 2px; - border-radius: 2px; - background: linear-gradient(180deg, var(--accent), var(--accent-600)); - opacity: 0.9; -} -.tab--drop-before::before { left: 0; } -.tab--drop-after::after { right: 0; } - -@keyframes tab-enter { - from { - opacity: 0; - transform: translateY(6px) scale(0.98); - } - to { - opacity: 1; - transform: none; - } -} - -@keyframes tab-exit { - to { - opacity: 0; - transform: translateY(-6px) scale(0.95); - } -} - -/* Respect reduced motion preferences */ -@media (prefers-reduced-motion: reduce) { - * { - animation: none !important; - transition: none !important; - scroll-behavior: auto !important; - } -} - -/* Focus rings for accessibility */ -.nav-left button:focus-visible, -.nav-right > button:focus-visible, -#reload-btn:focus-visible, -.menu-wrapper #menu-btn:focus-visible, -.new-tab-button:focus-visible, -#menu-popup button:focus-visible, -.zoom-controls button:focus-visible, -.tab .tab-close:focus-visible, -#window-controls button:focus-visible { - outline: none; - box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.55); -} - -/* Keyboard-only ring around the entire address bar (input + Go) */ -.nav-center:has(:focus-visible) { - box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.55), inset 0 1px 0 rgba(255,255,255,0.05); -} -/* Fallback for engines without :has support */ -@supports not selector(.nav-center:has(:focus-visible)) { - .nav-center:focus-within { - box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.45), inset 0 1px 0 rgba(255,255,255,0.05); - } -} - -/* Remove default click outline but keep keyboard focus via :focus-visible */ -.nav-left button:focus, -.nav-right > button:focus, -#reload-btn:focus, -#menu-btn:focus, -#url:focus, -.nav-center button:focus, -.new-tab-button:focus, -#menu-popup button:focus, -.zoom-controls button:focus, -.tab .tab-close:focus, -#window-controls button:focus { - outline: none; - box-shadow: none; -} - -/* Stronger chrome contrast when Home is visible - uses theme variables */ -body:has(#home-container.active) #tab-bar { - background: color-mix(in srgb, var(--tab-bg) 92%, black); - border-bottom-color: color-mix(in srgb, var(--tab-border) 80%, transparent); - box-shadow: 0 10px 24px -12px rgba(0,0,0,0.6); -} - -body:has(#home-container.active) #nav { - background: color-mix(in srgb, var(--url-bar-bg) 92%, black); - border-bottom: 1px solid color-mix(in srgb, var(--url-bar-border) 80%, transparent); - box-shadow: 0 14px 36px -16px rgba(0,0,0,0.7); -} - -body:has(#home-container.active) .nav-center { - background: color-mix(in srgb, var(--url-bar-bg) 96%, black); - border: 1px solid color-mix(in srgb, var(--primary, var(--accent)) 45%, transparent); -} - -body:has(#home-container.active) .nav-left button, -body:has(#home-container.active) .nav-right > button, -body:has(#home-container.active) #reload-btn, -body:has(#home-container.active) #menu-btn { - /* slightly lighter than nav to pop over Home - uses theme colors */ - background: color-mix(in srgb, var(--url-bar-bg) 85%, var(--text) 15%); - border: 1px solid color-mix(in srgb, var(--text) 20%, transparent); - box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent); -} - -/* Elevate active tab a touch more over home */ -body:has(#home-container.active) .tab.active { - box-shadow: 0 10px 26px -8px rgba(0,0,0,0.6); -} diff --git a/search-history.json b/search-history.json deleted file mode 100644 index 0637a08..0000000 --- a/search-history.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/setup.sh b/setup.sh deleted file mode 100755 index 5447f6d..0000000 --- a/setup.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -# Nebula Browser Setup Script -# This script installs dependencies and fixes Electron sandbox permissions -# Works on Steam Deck and other Linux systems without sudo - -echo "=========================================" -echo " Nebula Browser Setup Script" -echo "=========================================" -echo "" - -# Navigate to the project directory -cd "$(dirname "$0")" - -# Run npm install -echo "[1/2] Installing dependencies..." -npm install - -if [ $? -ne 0 ]; then - echo "โŒ npm install failed!" - exit 1 -fi - -echo "" -echo "[2/2] Fixing Electron sandbox permissions..." -echo "This requires root access. You may be prompted for your password." -echo "" - -# Fix chrome-sandbox permissions -SANDBOX_PATH="$(pwd)/node_modules/electron/dist/chrome-sandbox" - -if [ ! -f "$SANDBOX_PATH" ]; then - echo "โŒ chrome-sandbox not found at $SANDBOX_PATH" - echo " Make sure npm install completed successfully." - exit 1 -fi - -# Function to run command as root -run_as_root() { - if command -v sudo &> /dev/null; then - sudo "$@" - elif command -v pkexec &> /dev/null; then - pkexec "$@" - elif command -v doas &> /dev/null; then - doas "$@" - else - echo "No privilege escalation tool found (sudo/pkexec/doas)" - return 1 - fi -} - -# Try to fix permissions -echo "Attempting to set sandbox permissions..." -run_as_root chown root:root "$SANDBOX_PATH" -CHOWN_RESULT=$? - -run_as_root chmod 4755 "$SANDBOX_PATH" -CHMOD_RESULT=$? - -if [ $CHOWN_RESULT -eq 0 ] && [ $CHMOD_RESULT -eq 0 ]; then - echo "โœ… Sandbox permissions fixed successfully!" - echo "" - echo "=========================================" - echo " Setup complete! Run 'npm start' to launch Nebula" - echo "=========================================" - echo "" - echo "๐Ÿ’ก TIP: For GPU acceleration on Linux, run:" - echo " NEBULA_GPU_ALLOW_LINUX=1 npm start" - echo "=========================================" -else - echo "โŒ Failed to set sandbox permissions automatically." - echo "" - echo "On Steam Deck, open Konsole and run:" - echo " pkexec bash -c 'chown root:root $SANDBOX_PATH && chmod 4755 $SANDBOX_PATH'" - echo "" - echo "Or switch to desktop mode and run as root:" - echo " su -c 'chown root:root $SANDBOX_PATH && chmod 4755 $SANDBOX_PATH'" - exit 1 -fi diff --git a/start-gpu-safe.bat b/start-gpu-safe.bat deleted file mode 100644 index 51ed911..0000000 --- a/start-gpu-safe.bat +++ /dev/null @@ -1,48 +0,0 @@ -@echo off -echo Starting Nebula Browser with GPU diagnostics... -echo. - -REM Check if running with administrator privileges -net session >nul 2>&1 -if %errorLevel% neq 0 ( - echo Warning: Not running as administrator. Some GPU features may not work. - echo Consider running as administrator if you encounter GPU issues. - echo. -) - -REM Set environment variables for better GPU support -set ELECTRON_ENABLE_LOGGING=1 -set ELECTRON_LOG_FILE=gpu-debug.log - -REM Alternative GPU configurations -echo Choose GPU configuration: -echo 1. Default (recommended) -echo 2. Force GPU acceleration -echo 3. Disable GPU (software rendering) -echo 4. Debug mode with verbose logging -echo. -set /p choice="Enter choice (1-4): " - -if "%choice%"=="1" ( - echo Starting with default GPU configuration... - npm start -) else if "%choice%"=="2" ( - echo Forcing GPU acceleration... - set ELECTRON_FORCE_HARDWARE_ACCELERATION=1 - npm start -) else if "%choice%"=="3" ( - echo Using software rendering... - set ELECTRON_DISABLE_GPU=1 - npm start -- --disable-gpu -) else if "%choice%"=="4" ( - echo Starting in debug mode... - set ELECTRON_ENABLE_STACK_DUMPING=1 - npm start -- --enable-logging --log-level=0 --vmodule=gpu*=3 -) else ( - echo Invalid choice, using default... - npm start -) - -echo. -echo Browser closed. Check gpu-debug.log for GPU-related messages. -pause diff --git a/start-gpu.sh b/start-gpu.sh deleted file mode 100755 index 660c2b1..0000000 --- a/start-gpu.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# Start Nebula Browser with GPU acceleration enabled on Linux -cd "$(dirname "$0")" -NEBULA_GPU_ALLOW_LINUX=1 npm start diff --git a/start-steamdeck.sh b/start-steamdeck.sh deleted file mode 100644 index 3a9aa26..0000000 --- a/start-steamdeck.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -# ============================================================================= -# NEBULA BROWSER - Steam Deck / SteamOS Launch Script -# ============================================================================= -# This script is designed for launching Nebula on Steam Deck in Game Mode. -# It sets necessary environment variables to disable Steam's mouse/keyboard -# emulation so the app's native controller support works properly. -# -# Usage: -# 1. Add Nebula as a Non-Steam game (or via Steamworks) -# 2. Set launch options to: ./start-steamdeck.sh -# OR -# 3. Use this script's environment variables in Steam Launch Options: -# SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 %command% --big-picture -# ============================================================================= - -cd "$(dirname "$0")" - -# ============================================================================= -# STEAM INPUT CONFIGURATION -# ============================================================================= -# These variables tell Steam's input layer that this app handles controller -# input natively and should NOT have mouse/keyboard emulation applied. - -# Disable Steam's virtual gamepad layer -export SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 -export STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 - -# Allow raw gamepad access -export SDL_GAMECONTROLLER_IGNORE_DEVICES="" - -# Allow background gamepad events (useful when app doesn't have focus) -export SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1 - -# Hint to SDL that we're using gamepads natively -export SDL_GAMECONTROLLERCONFIG="${SDL_GAMECONTROLLERCONFIG:-}" - -# ============================================================================= -# NEBULA CONFIGURATION -# ============================================================================= - -# Enable Big Picture Mode for controller-friendly UI -export NEBULA_BIG_PICTURE=1 - -# Enable GPU acceleration on Linux -export NEBULA_GPU_ALLOW_LINUX=1 - -# ============================================================================= -# LAUNCH -# ============================================================================= - -# Check if we're in an AppImage/AppDir or dev environment -if [ -f "./nebula" ]; then - # Packaged AppDir - exec ./nebula --big-picture "$@" -elif [ -f "./Nebula" ]; then - # Alternate launcher name - exec ./Nebula --big-picture "$@" -elif command -v npm &> /dev/null && [ -f "package.json" ]; then - # Development environment - npm start -- --big-picture "$@" -else - echo "Error: Could not find Nebula executable or npm" - echo "Make sure you're running this script from the Nebula directory" - exit 1 -fi diff --git a/theme-manager.js b/theme-manager.js deleted file mode 100644 index 5a22c31..0000000 --- a/theme-manager.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Theme Manager for Nebula Browser - * Handles theme loading, saving, and management at the application level - */ - -const fs = require('fs'); -const path = require('path'); -const { app } = require('electron'); - -class ThemeManager { - constructor() { - this.themesDir = path.join(__dirname, 'themes'); - this.userDataThemesDir = path.join(app.getPath('userData'), 'themes'); - this.userThemesDir = path.join(this.userDataThemesDir, 'user'); - this.downloadedThemesDir = path.join(this.userDataThemesDir, 'downloaded'); - this.legacyUserThemesDir = path.join(this.themesDir, 'user'); - this.legacyDownloadedThemesDir = path.join(this.themesDir, 'downloaded'); - - this.ensureDirectories(); - this.migrateLegacyThemes(); - } - - ensureDirectories() { - [this.userDataThemesDir, this.userThemesDir, this.downloadedThemesDir].forEach(dir => { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - }); - } - - migrateLegacyThemes() { - this.migrateDirectoryIfNeeded(this.legacyUserThemesDir, this.userThemesDir); - this.migrateDirectoryIfNeeded(this.legacyDownloadedThemesDir, this.downloadedThemesDir); - } - - migrateDirectoryIfNeeded(fromDir, toDir) { - try { - if (!fs.existsSync(fromDir)) return; - if (!fs.existsSync(toDir)) fs.mkdirSync(toDir, { recursive: true }); - - const toFiles = fs.readdirSync(toDir).filter(file => file.endsWith('.json')); - if (toFiles.length > 0) return; - - const fromFiles = fs.readdirSync(fromDir).filter(file => file.endsWith('.json')); - fromFiles.forEach(file => { - const sourcePath = path.join(fromDir, file); - const destinationPath = path.join(toDir, file); - if (!fs.existsSync(destinationPath)) { - fs.copyFileSync(sourcePath, destinationPath); - } - }); - } catch (error) { - console.warn('[Themes] Failed to migrate legacy themes:', error); - } - } - - /** - * Get all available themes - * @returns {Object} Object containing default, user, and downloaded themes - */ - getAllThemes() { - const themes = { - default: this.loadDefaultThemes(), - user: this.loadUserThemes(), - downloaded: this.loadDownloadedThemes() - }; - - return themes; - } - - loadDefaultThemes() { - const defaultThemes = {}; - const defaultFiles = [ - 'default.json', - 'ocean.json', - 'forest.json', - 'sunset.json', - 'cyberpunk.json', - 'midnight-rose.json', - 'arctic-ice.json', - 'cherry-blossom.json', - 'cosmic-purple.json', - 'emerald-dream.json', - 'mocha-coffee.json', - 'lavender-fields.json' - ]; - - defaultFiles.forEach(file => { - try { - const themePath = path.join(this.themesDir, file); - if (fs.existsSync(themePath)) { - const themeData = JSON.parse(fs.readFileSync(themePath, 'utf8')); - const themeName = path.basename(file, '.json'); - defaultThemes[themeName] = themeData; - } - } catch (error) { - console.error(`Error loading default theme ${file}:`, error); - } - }); - - return defaultThemes; - } - - loadUserThemes() { - return this.loadThemesFromDirectory(this.userThemesDir); - } - - loadDownloadedThemes() { - return this.loadThemesFromDirectory(this.downloadedThemesDir); - } - - loadThemesFromDirectory(directory) { - const themes = {}; - - try { - if (!fs.existsSync(directory)) { - return themes; - } - - const files = fs.readdirSync(directory).filter(file => file.endsWith('.json')); - - files.forEach(file => { - try { - const themePath = path.join(directory, file); - const themeData = JSON.parse(fs.readFileSync(themePath, 'utf8')); - const themeName = path.basename(file, '.json'); - themes[themeName] = themeData; - } catch (error) { - console.error(`Error loading theme ${file}:`, error); - } - }); - } catch (error) { - console.error(`Error reading themes directory ${directory}:`, error); - } - - return themes; - } - - /** - * Save a user theme - * @param {string} name - Theme name - * @param {Object} themeData - Theme data - * @returns {boolean} Success status - */ - saveUserTheme(name, themeData) { - try { - const filename = name.toLowerCase().replace(/[^a-z0-9]/g, '-') + '.json'; - const filepath = path.join(this.userThemesDir, filename); - - const themeWithMetadata = { - ...themeData, - name: name, - createdAt: new Date().toISOString(), - type: 'user' - }; - - fs.writeFileSync(filepath, JSON.stringify(themeWithMetadata, null, 2)); - return true; - } catch (error) { - console.error('Error saving user theme:', error); - return false; - } - } - - /** - * Delete a user theme - * @param {string} filename - Theme filename - * @returns {boolean} Success status - */ - deleteUserTheme(filename) { - try { - const filepath = path.join(this.userThemesDir, filename); - if (fs.existsSync(filepath)) { - fs.unlinkSync(filepath); - return true; - } - return false; - } catch (error) { - console.error('Error deleting user theme:', error); - return false; - } - } - - /** - * Import a theme file to downloaded themes - * @param {string} sourceFile - Source file path - * @returns {boolean} Success status - */ - importTheme(sourceFile) { - try { - const themeData = JSON.parse(fs.readFileSync(sourceFile, 'utf8')); - - // Validate theme structure - if (!this.validateTheme(themeData)) { - throw new Error('Invalid theme structure'); - } - - const filename = (themeData.name || 'imported-theme') - .toLowerCase() - .replace(/[^a-z0-9]/g, '-') + '.json'; - - const destinationPath = path.join(this.downloadedThemesDir, filename); - - const themeWithMetadata = { - ...themeData, - importedAt: new Date().toISOString(), - type: 'downloaded' - }; - - fs.writeFileSync(destinationPath, JSON.stringify(themeWithMetadata, null, 2)); - return true; - } catch (error) { - console.error('Error importing theme:', error); - return false; - } - } - - /** - * Validate theme structure - * @param {Object} theme - Theme object to validate - * @returns {boolean} Is valid - */ - validateTheme(theme) { - return theme && - theme.colors && - theme.colors.bg && - theme.colors.primary && - theme.colors.accent && - theme.colors.text && - typeof theme.showLogo === 'boolean' && - theme.layout && - theme.customTitle; - } - - /** - * Get theme by name and type - * @param {string} name - Theme name - * @param {string} type - Theme type (default, user, downloaded) - * @returns {Object|null} Theme data or null - */ - getTheme(name, type = 'default') { - const themes = this.getAllThemes(); - return themes[type] && themes[type][name] ? themes[type][name] : null; - } - - /** - * Apply theme to application (for use in main process) - * @param {Object} theme - Theme to apply - */ - applyThemeToApp(theme) { - // This would be used to apply themes at the application level - // For example, updating window chrome colors, etc. - console.log('Applying theme to application:', theme.name); - } -} - -module.exports = ThemeManager; diff --git a/themes/arctic-ice.json b/themes/arctic-ice.json deleted file mode 100644 index 5488ac0..0000000 --- a/themes/arctic-ice.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Arctic Ice", - "colors": { - "bg": "#f0f8ff", - "darkBlue": "#e6f3ff", - "darkPurple": "#d1e7ff", - "primary": "#4169e1", - "accent": "#87ceeb", - "text": "#2f4f4f", - "urlBarBg": "#e6f3ff", - "urlBarText": "#2f4f4f", - "urlBarBorder": "#4169e1", - "tabBg": "#e6f3ff", - "tabText": "#4169e1", - "tabActive": "#d1e7ff", - "tabActiveText": "#2f4f4f", - "tabBorder": "#f0f8ff" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #f0f8ff 0%, #d1e7ff 100%)", - "version": "1.0", - "description": "A clean, cool theme inspired by arctic ice with crisp blue tones" -} diff --git a/themes/cherry-blossom.json b/themes/cherry-blossom.json deleted file mode 100644 index 8c33ee2..0000000 --- a/themes/cherry-blossom.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Cherry Blossom", - "colors": { - "bg": "#fff5f8", - "darkBlue": "#ffe4e8", - "darkPurple": "#ffd4db", - "primary": "#ff69b4", - "accent": "#ffb6c1", - "text": "#8b4513", - "urlBarBg": "#ffe4e8", - "urlBarText": "#8b4513", - "urlBarBorder": "#ff69b4", - "tabBg": "#ffe4e8", - "tabText": "#ff69b4", - "tabActive": "#ffd4db", - "tabActiveText": "#8b4513", - "tabBorder": "#fff5f8" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #fff5f8 0%, #ffd4db 100%)", - "version": "1.0", - "description": "A soft, warm theme inspired by cherry blossoms with gentle pink tones" -} diff --git a/themes/cosmic-purple.json b/themes/cosmic-purple.json deleted file mode 100644 index bfc81c7..0000000 --- a/themes/cosmic-purple.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Cosmic Purple", - "colors": { - "bg": "#0f0524", - "darkBlue": "#1a0b3d", - "darkPurple": "#2d1b69", - "primary": "#8a2be2", - "accent": "#da70d6", - "text": "#e6e6fa", - "urlBarBg": "#1a0b3d", - "urlBarText": "#e6e6fa", - "urlBarBorder": "#8a2be2", - "tabBg": "#1a0b3d", - "tabText": "#da70d6", - "tabActive": "#2d1b69", - "tabActiveText": "#e6e6fa", - "tabBorder": "#0f0524" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #0f0524 0%, #2d1b69 50%, #4b0082 100%)", - "version": "1.0", - "description": "A space-inspired theme with deep cosmic purples and starlight accents" -} diff --git a/themes/cyberpunk.json b/themes/cyberpunk.json deleted file mode 100644 index 2e6ddc7..0000000 --- a/themes/cyberpunk.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Cyberpunk Neon", - "colors": { - "bg": "#0a0a0a", - "darkBlue": "#1a0520", - "darkPurple": "#2a0a3a", - "primary": "#ff0080", - "accent": "#00ffff", - "text": "#ffffff", - "urlBarBg": "#1a0520", - "urlBarText": "#ffffff", - "urlBarBorder": "#ff0080", - "tabBg": "#1a0520", - "tabText": "#00ffff", - "tabActive": "#2a0a3a", - "tabActiveText": "#ff0080", - "tabBorder": "#ff0080" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #0a0a0a 0%, #2a0a3a 50%, #1a0520 100%)", - "version": "1.0", - "description": "A futuristic cyberpunk theme with neon pink and cyan highlights" -} diff --git a/themes/default.json b/themes/default.json deleted file mode 100644 index 162b2b7..0000000 --- a/themes/default.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Default Nebula", - "colors": { - "bg": "#121418", - "darkBlue": "#0B1C2B", - "darkPurple": "#1B1035", - "primary": "#7B2EFF", - "accent": "#00C6FF", - "text": "#E0E0E0", - "urlBarBg": "#1C2030", - "urlBarText": "#E0E0E0", - "urlBarBorder": "#3E4652", - "tabBg": "#161925", - "tabText": "#A4A7B3", - "tabActive": "#1C2030", - "tabActiveText": "#E0E0E0", - "tabBorder": "#2B3040" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #121418 0%, #1B1035 100%)", - "version": "1.0", - "description": "The original Nebula Browser theme with purple and blue gradients" -} diff --git a/themes/emerald-dream.json b/themes/emerald-dream.json deleted file mode 100644 index 26cbc53..0000000 --- a/themes/emerald-dream.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Emerald Dream", - "colors": { - "bg": "#0d2818", - "darkBlue": "#1a3a2e", - "darkPurple": "#2d5a44", - "primary": "#50c878", - "accent": "#98fb98", - "text": "#f0fff0", - "urlBarBg": "#1a3a2e", - "urlBarText": "#f0fff0", - "urlBarBorder": "#50c878", - "tabBg": "#1a3a2e", - "tabText": "#98fb98", - "tabActive": "#2d5a44", - "tabActiveText": "#f0fff0", - "tabBorder": "#0d2818" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #0d2818 0%, #2d5a44 100%)", - "version": "1.0", - "description": "A sophisticated theme with rich emerald greens and jewel-like accents" -} diff --git a/themes/forest.json b/themes/forest.json deleted file mode 100644 index d05c4d0..0000000 --- a/themes/forest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Forest Night", - "colors": { - "bg": "#1a202c", - "darkBlue": "#2d3748", - "darkPurple": "#4a5568", - "primary": "#68d391", - "accent": "#9ae6b4", - "text": "#f7fafc", - "urlBarBg": "#2d3748", - "urlBarText": "#f7fafc", - "urlBarBorder": "#4a5568", - "tabBg": "#2d3748", - "tabText": "#cbd5e0", - "tabActive": "#4a5568", - "tabActiveText": "#f7fafc", - "tabBorder": "#1a202c" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #1a202c 0%, #2d3748 100%)", - "version": "1.0", - "description": "A nature-inspired theme with forest greens and earth tones" -} diff --git a/themes/lavender-fields.json b/themes/lavender-fields.json deleted file mode 100644 index 768e015..0000000 --- a/themes/lavender-fields.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Lavender Fields", - "colors": { - "bg": "#f8f4ff", - "darkBlue": "#ede4ff", - "darkPurple": "#e6d8ff", - "primary": "#9370db", - "accent": "#dda0dd", - "text": "#4b0082", - "urlBarBg": "#ede4ff", - "urlBarText": "#4b0082", - "urlBarBorder": "#9370db", - "tabBg": "#ede4ff", - "tabText": "#9370db", - "tabActive": "#e6d8ff", - "tabActiveText": "#4b0082", - "tabBorder": "#f8f4ff" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #f8f4ff 0%, #e6d8ff 100%)", - "version": "1.0", - "description": "A peaceful light theme inspired by lavender fields with soft purple hues" -} diff --git a/themes/midnight-rose.json b/themes/midnight-rose.json deleted file mode 100644 index f82f17d..0000000 --- a/themes/midnight-rose.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Midnight Rose", - "colors": { - "bg": "#1c1820", - "darkBlue": "#2d2433", - "darkPurple": "#3d3046", - "primary": "#d4af37", - "accent": "#ffd700", - "text": "#f5f5dc", - "urlBarBg": "#3d3046", - "urlBarText": "#f5f5dc", - "urlBarBorder": "#d4af37", - "tabBg": "#2d2433", - "tabText": "#d4af37", - "tabActive": "#3d3046", - "tabActiveText": "#ffd700", - "tabBorder": "#1c1820" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #1c1820 0%, #3d3046 100%)", - "version": "1.0", - "description": "An elegant dark theme with rose gold accents and warm undertones" -} diff --git a/themes/mocha-coffee.json b/themes/mocha-coffee.json deleted file mode 100644 index 9b2c888..0000000 --- a/themes/mocha-coffee.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Mocha Coffee", - "colors": { - "bg": "#3c2414", - "darkBlue": "#4a2c1a", - "darkPurple": "#5d3a26", - "primary": "#d2691e", - "accent": "#daa520", - "text": "#faf0e6", - "urlBarBg": "#4a2c1a", - "urlBarText": "#faf0e6", - "urlBarBorder": "#d2691e", - "tabBg": "#4a2c1a", - "tabText": "#daa520", - "tabActive": "#5d3a26", - "tabActiveText": "#faf0e6", - "tabBorder": "#3c2414" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #3c2414 0%, #5d3a26 100%)", - "version": "1.0", - "description": "A warm, cozy theme inspired by coffee and chocolate tones" -} diff --git a/themes/ocean.json b/themes/ocean.json deleted file mode 100644 index 81f5966..0000000 --- a/themes/ocean.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Ocean Depths", - "colors": { - "bg": "#1a365d", - "darkBlue": "#2a4365", - "darkPurple": "#2c5282", - "primary": "#3182ce", - "accent": "#00d9ff", - "text": "#e2e8f0", - "urlBarBg": "#2d5282", - "urlBarText": "#e2e8f0", - "urlBarBorder": "#1e3a5f", - "tabBg": "#2a4365", - "tabText": "#cbd5e0", - "tabActive": "#2d5282", - "tabActiveText": "#e2e8f0", - "tabBorder": "#1a365d" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #1a365d 0%, #2c5282 100%)", - "version": "1.0", - "description": "A calming ocean-inspired theme with blues and teals" -} diff --git a/themes/sunset.json b/themes/sunset.json deleted file mode 100644 index ed53803..0000000 --- a/themes/sunset.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Sunset Glow", - "colors": { - "bg": "#744210", - "darkBlue": "#975a16", - "darkPurple": "#c05621", - "primary": "#ed8936", - "accent": "#fbb040", - "text": "#fffaf0", - "urlBarBg": "#975a16", - "urlBarText": "#fffaf0", - "urlBarBorder": "#c05621", - "tabBg": "#975a16", - "tabText": "#fde4b6", - "tabActive": "#c05621", - "tabActiveText": "#fffaf0", - "tabBorder": "#744210" - }, - "layout": "centered", - "showLogo": true, - "customTitle": "Nebula Browser", - "gradient": "linear-gradient(145deg, #744210 0%, #c05621 100%)", - "version": "1.0", - "description": "A warm sunset theme with oranges and golden hues" -} diff --git a/update-appdir.sh b/update-appdir.sh deleted file mode 100755 index 9c81d36..0000000 --- a/update-appdir.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/bash -# Update nebula-appdir with local source changes -# Run this after making changes to sync them to the AppDir - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -APPDIR_ROOT="$SCRIPT_DIR/nebula-appdir" -# Support both layouts: -# 1) nebula-appdir/resources/app -# 2) nebula-appdir/nebula-appdir/resources/app (example layout) -if [ -d "$APPDIR_ROOT/resources" ] || [ -f "$APPDIR_ROOT/resources/app.asar" ]; then - RESOURCES_DIR="$APPDIR_ROOT/resources" -else - RESOURCES_DIR="$APPDIR_ROOT/nebula-appdir/resources" -fi -APP_DIR="$RESOURCES_DIR/app" -ASAR_FILE="$RESOURCES_DIR/app.asar" -ASAR_ORIG_FILE="$RESOURCES_DIR/app.asar.orig" - -echo "๐Ÿš€ Updating Nebula AppDir..." -echo " Source: $SCRIPT_DIR" -echo " Target: $APP_DIR" -echo "" - -# Check if target exists (or extract from app.asar if present) -if [ ! -d "$APP_DIR" ]; then - if [ -f "$ASAR_FILE" ] || [ -f "$ASAR_ORIG_FILE" ]; then - if command -v npx &> /dev/null; then - if [ -f "$ASAR_FILE" ]; then - echo "โ„น๏ธ app.asar detected. Extracting to $APP_DIR..." - (cd "$RESOURCES_DIR" && npx asar extract "app.asar" "app") - mv "$ASAR_FILE" "$ASAR_ORIG_FILE" 2>/dev/null || true - else - echo "โ„น๏ธ app.asar.orig detected. Extracting to $APP_DIR..." - (cd "$RESOURCES_DIR" && npx asar extract "app.asar.orig" "app") - fi - else - echo "โŒ Error: $APP_DIR not found and npx is not available to extract app.asar." - echo " Install Node.js/npm, then run:" - echo " cd $RESOURCES_DIR && npx asar extract app.asar app" - exit 1 - fi - else - echo "โŒ Error: AppDir not found at $APP_DIR" - exit 1 - fi -fi - -# Files to sync (main app files) -FILES=( - "main.js" - "preload.js" - "package.json" - "portable-data.js" - "gpu-config.js" - "gpu-fallback.js" - "performance-monitor.js" - "plugin-manager.js" - "theme-manager.js" - "bookmarks.json" -) - -# Directories to sync -DIRS=( - "renderer" - "themes" - "assets" - "plugins" - "documentation" -) - -# Sync individual files -echo "๐Ÿ“„ Syncing files..." -for file in "${FILES[@]}"; do - if [ -f "$SCRIPT_DIR/$file" ]; then - cp "$SCRIPT_DIR/$file" "$APP_DIR/$file" - echo " โœ“ $file" - else - echo " โš  $file (not found, skipping)" - fi -done - -# Update main launcher scripts if present -echo "" -echo "๐Ÿ”„ Syncing launcher scripts..." -if [ -f "$SCRIPT_DIR/appdir-example/run-nebula.sh" ]; then - cp "$SCRIPT_DIR/appdir-example/run-nebula.sh" "$APPDIR_ROOT/run-nebula.sh" - sed -i.bak 's|usr/data|usr/user-data|g' "$APPDIR_ROOT/run-nebula.sh" && rm -f "$APPDIR_ROOT/run-nebula.sh.bak" - chmod +x "$APPDIR_ROOT/run-nebula.sh" || true - echo " โœ“ run-nebula.sh" -fi -for launcher in "Nebula-Desktop" "Nebula-Controller"; do - if [ -f "$SCRIPT_DIR/nebula-appdir/$launcher" ]; then - cp "$SCRIPT_DIR/nebula-appdir/$launcher" "$APPDIR_ROOT/$launcher" - chmod +x "$APPDIR_ROOT/$launcher" || true - echo " โœ“ $launcher" - fi -done -# Sync Nebula symlink if it exists -if [ -L "$SCRIPT_DIR/nebula-appdir/Nebula" ]; then - rm -f "$APPDIR_ROOT/Nebula" - ln -sf "Nebula-Desktop" "$APPDIR_ROOT/Nebula" - echo " โœ“ Nebula (symlink)" -fi - -# Sync directories -echo "" -echo "๐Ÿ“ Syncing directories..." -for dir in "${DIRS[@]}"; do - if [ -d "$SCRIPT_DIR/$dir" ]; then - # Use rsync if available, otherwise use cp - if command -v rsync &> /dev/null; then - rsync -a --delete "$SCRIPT_DIR/$dir/" "$APP_DIR/$dir/" - else - rm -rf "$APP_DIR/$dir" - cp -r "$SCRIPT_DIR/$dir" "$APP_DIR/$dir" - fi - echo " โœ“ $dir/" - else - echo " โš  $dir/ (not found, skipping)" - fi -done - -# Ensure portable user-data directory exists for Linux AppDir builds -mkdir -p "$APPDIR_ROOT/usr/user-data" -chmod 700 "$APPDIR_ROOT/usr/user-data" || true -echo " โœ“ usr/user-data/" - -echo "" -echo "โœ… AppDir updated successfully!" -echo "" -echo "To run Nebula, use:" -echo " ./nebula-appdir/run-nebula.sh"