Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7596674ab | |||
| ce92b3841f | |||
| 659d1530b0 | |||
| 302753cd3d | |||
| bbba5b2927 | |||
| 29908646ea | |||
| 8cf9b50690 | |||
| d6f15c5dce | |||
| b4d93f24cd | |||
| c514e4faec | |||
| e51594a010 | |||
| 18bc607d93 | |||
| 54216aa133 | |||
| 8eb5c1a3b2 | |||
| 406d73c10f | |||
| 6fac7e320b | |||
| a32940a3f3 | |||
| 10180b7109 | |||
| dd6b3fa70d | |||
| a8786b4c1c | |||
| 207a849f06 | |||
| 79565f2ef3 | |||
| adefa1706e | |||
| 8b87a07d1b | |||
| a101899d9c | |||
| 0b51d133a4 | |||
| 6b2a7c8404 | |||
| 618ea7d12d | |||
| 0137df60dd | |||
| b725d5a672 | |||
| 86f3b10e80 |
@@ -0,0 +1,17 @@
|
||||
# clangd configuration for Nebula Browser
|
||||
#
|
||||
# Full diagnostics require:
|
||||
# 1. CEF binary distribution unpacked to thirdparty/cef/
|
||||
# 2. Running: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
# 3. Symlink or copy: ln -s build/compile_commands.json .
|
||||
#
|
||||
# Without compile_commands.json, clangd uses fallback flags below.
|
||||
|
||||
CompileFlags:
|
||||
Add:
|
||||
- -std=c++20
|
||||
- -xobjective-c++
|
||||
- -Isrc
|
||||
- -Ithirdparty/cef
|
||||
- -Ithirdparty/cef/include
|
||||
- -Wno-c++17-extensions
|
||||
@@ -1,116 +1,21 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
# Build output
|
||||
/build/
|
||||
/out/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# CEF binaries
|
||||
/thirdparty/cef/
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# Visual Studio
|
||||
.vs/
|
||||
*.vcxproj.user
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
# CMake
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-temporary-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Mac files
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Electron build output
|
||||
/dist
|
||||
/out
|
||||
/release
|
||||
/build
|
||||
*.nupkg
|
||||
*.AppImage
|
||||
*.dmg
|
||||
*.exe
|
||||
*.pkg
|
||||
|
||||
# IDE config files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
site-history.json
|
||||
bookmarks.json
|
||||
bookmarks.backup.json
|
||||
search-history.json
|
||||
|
||||
# Portable user data folder
|
||||
user-data/
|
||||
|
||||
# AppImage / SteamOS
|
||||
squashfs-root/
|
||||
*.AppImage
|
||||
|
||||
# Electron build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
release/
|
||||
|
||||
# Native binaries
|
||||
nebula
|
||||
nebula.exe
|
||||
|
||||
# Node/Electron
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Build artifacts
|
||||
nebula-appdir/
|
||||
*.asar
|
||||
/.cache
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Nebula Browser Asset License
|
||||
|
||||
The source code of Nebula Browser is licensed under the Mozilla Public License 2.0, as described in the LICENSE file.
|
||||
|
||||
However, the Nebula Browser name, logo, icons, artwork, screenshots, visual identity, branding materials, and other non-code assets are not automatically covered by the MPL-2.0 license.
|
||||
|
||||
Unless otherwise stated, these assets are All Rights Reserved.
|
||||
|
||||
You may not use the Nebula Browser name, logo, or branding in a way that suggests endorsement, official status, or affiliation with the Nebula Project without permission.
|
||||
|
||||
Community forks are welcome, but they should use their own name and branding unless permission is granted.
|
||||
@@ -0,0 +1,258 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
# Enable OBJCXX early on macOS for Cocoa integration
|
||||
if(APPLE)
|
||||
project(NebulaBrowser LANGUAGES CXX OBJCXX)
|
||||
else()
|
||||
project(NebulaBrowser LANGUAGES CXX)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CEF location
|
||||
# ------------------------------------------------------------
|
||||
|
||||
set(CEF_ROOT "${CMAKE_SOURCE_DIR}/thirdparty/cef" CACHE PATH "Path to the CEF binary distribution")
|
||||
|
||||
if(NOT EXISTS "${CEF_ROOT}/cmake/FindCEF.cmake")
|
||||
message(FATAL_ERROR
|
||||
"CEF was not found.\n"
|
||||
"Expected CEF here:\n"
|
||||
" ${CEF_ROOT}\n\n"
|
||||
"Unpack the CEF binary distribution for your OS into thirdparty/cef."
|
||||
)
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CEF_ROOT}/cmake")
|
||||
find_package(CEF REQUIRED)
|
||||
|
||||
add_subdirectory(
|
||||
"${CEF_LIBCEF_DLL_WRAPPER_PATH}"
|
||||
"${CMAKE_BINARY_DIR}/libcef_dll_wrapper"
|
||||
)
|
||||
|
||||
SET_CEF_TARGET_OUT_DIR()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Sources
|
||||
# ------------------------------------------------------------
|
||||
|
||||
set(NEBULA_COMMON_SOURCES
|
||||
src/app/first_run_state.cpp
|
||||
src/app/nebula_controller.cpp
|
||||
src/app/run.cpp
|
||||
src/browser/session_state.cpp
|
||||
src/browser/tab.cpp
|
||||
src/browser/tab_manager.cpp
|
||||
src/browser/url_utils.cpp
|
||||
src/cef/browser_client.cpp
|
||||
src/cef/nebula_app.cpp
|
||||
src/ui/paths.cpp
|
||||
)
|
||||
|
||||
if(OS_WINDOWS)
|
||||
set(NEBULA_PLATFORM_SOURCES
|
||||
src/platform/win/default_browser_win.cpp
|
||||
src/platform/win/paths_win.cpp
|
||||
src/platform/win/startup_win.cpp
|
||||
src/platform/win/browser_host_win.cpp
|
||||
src/platform/win/nebula_window_win.cpp
|
||||
)
|
||||
elseif(OS_MACOSX)
|
||||
set(NEBULA_PLATFORM_SOURCES
|
||||
src/platform/mac/default_browser_mac.mm
|
||||
src/platform/mac/paths_mac.cpp
|
||||
src/platform/mac/startup_mac.mm
|
||||
src/platform/mac/browser_host_mac.mm
|
||||
src/platform/mac/nebula_window_mac.mm
|
||||
)
|
||||
set_source_files_properties(
|
||||
src/platform/mac/startup_mac.mm
|
||||
src/platform/mac/browser_host_mac.mm
|
||||
src/platform/mac/nebula_window_mac.mm
|
||||
PROPERTIES
|
||||
COMPILE_FLAGS "-fobjc-arc"
|
||||
)
|
||||
elseif(OS_LINUX)
|
||||
set(NEBULA_PLATFORM_SOURCES
|
||||
src/platform/linux/default_browser_linux.cpp
|
||||
src/platform/linux/paths_linux.cpp
|
||||
src/platform/linux/startup_linux.cpp
|
||||
src/platform/linux/browser_host_linux.cpp
|
||||
src/platform/linux/nebula_window_linux.cpp
|
||||
)
|
||||
else()
|
||||
message(FATAL_ERROR "Unsupported platform.")
|
||||
endif()
|
||||
|
||||
# On macOS, CEF is a framework linked via CEF_STANDARD_LIBS.
|
||||
# On Windows/Linux, we create a logical target for libcef.
|
||||
if(NOT OS_MACOSX)
|
||||
ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_RELEASE}" "${CEF_LIB_DEBUG}")
|
||||
endif()
|
||||
|
||||
if(OS_LINUX)
|
||||
FIND_LINUX_LIBRARIES("X11")
|
||||
endif()
|
||||
|
||||
function(add_nebula_app_target nebula_target entry_source)
|
||||
if(OS_WINDOWS)
|
||||
add_executable(${nebula_target} WIN32
|
||||
${entry_source}
|
||||
${NEBULA_COMMON_SOURCES}
|
||||
${NEBULA_PLATFORM_SOURCES}
|
||||
)
|
||||
elseif(OS_MACOSX)
|
||||
add_executable(${nebula_target} MACOSX_BUNDLE
|
||||
${entry_source}
|
||||
${NEBULA_COMMON_SOURCES}
|
||||
${NEBULA_PLATFORM_SOURCES}
|
||||
)
|
||||
elseif(OS_LINUX)
|
||||
add_executable(${nebula_target}
|
||||
${entry_source}
|
||||
${NEBULA_COMMON_SOURCES}
|
||||
${NEBULA_PLATFORM_SOURCES}
|
||||
)
|
||||
endif()
|
||||
|
||||
SET_EXECUTABLE_TARGET_PROPERTIES(${nebula_target})
|
||||
add_dependencies(${nebula_target} libcef_dll_wrapper)
|
||||
|
||||
target_include_directories(${nebula_target} PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
"${CEF_ROOT}"
|
||||
"${CEF_ROOT}/include"
|
||||
)
|
||||
|
||||
if(OS_MACOSX)
|
||||
# On macOS, CEF is a framework; don't link libcef_lib
|
||||
target_link_libraries(${nebula_target} PRIVATE
|
||||
libcef_dll_wrapper
|
||||
${CEF_STANDARD_LIBS}
|
||||
)
|
||||
else()
|
||||
target_link_libraries(${nebula_target} PRIVATE
|
||||
libcef_lib
|
||||
libcef_dll_wrapper
|
||||
${CEF_STANDARD_LIBS}
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
set_property(TARGET ${nebula_target} PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
|
||||
)
|
||||
endif()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Platform-specific CEF runtime deployment
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if(OS_WINDOWS)
|
||||
target_link_libraries(${nebula_target} PRIVATE dwmapi)
|
||||
target_compile_definitions(${nebula_target} PRIVATE
|
||||
NOMINMAX
|
||||
WIN32_LEAN_AND_MEAN
|
||||
)
|
||||
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
|
||||
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
|
||||
elseif(OS_LINUX)
|
||||
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
|
||||
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
|
||||
elseif(OS_MACOSX)
|
||||
target_link_libraries(${nebula_target} PRIVATE
|
||||
"-framework Cocoa"
|
||||
"-framework ApplicationServices"
|
||||
)
|
||||
set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/${nebula_target}.app")
|
||||
set(NEBULA_HELPER_TARGET "${nebula_target}_Helper")
|
||||
set(NEBULA_HELPER_OUTPUT_NAME "${nebula_target} Helper")
|
||||
string(TOLOWER "${nebula_target}" NEBULA_HELPER_BUNDLE_NAME)
|
||||
|
||||
COPY_MAC_FRAMEWORK(
|
||||
"${nebula_target}"
|
||||
"${CEF_BINARY_DIR_RELEASE}"
|
||||
"${NEBULA_APP}"
|
||||
)
|
||||
COPY_FILES(
|
||||
"${nebula_target}"
|
||||
"${CEF_BINARY_FILES}"
|
||||
"${CEF_BINARY_DIR_RELEASE}"
|
||||
"${NEBULA_APP}/Contents/Frameworks"
|
||||
)
|
||||
COPY_FILES(
|
||||
"${nebula_target}"
|
||||
"${CEF_RESOURCE_FILES}"
|
||||
"${CEF_RESOURCE_DIR}"
|
||||
"${NEBULA_APP}/Contents/Resources"
|
||||
)
|
||||
|
||||
foreach(_suffix_list ${CEF_HELPER_APP_SUFFIXES})
|
||||
string(REPLACE ":" ";" _suffix_list ${_suffix_list})
|
||||
list(GET _suffix_list 0 _name_suffix)
|
||||
list(GET _suffix_list 1 _target_suffix)
|
||||
list(GET _suffix_list 2 _plist_suffix)
|
||||
|
||||
set(_helper_target "${NEBULA_HELPER_TARGET}${_target_suffix}")
|
||||
set(_helper_output_name "${NEBULA_HELPER_OUTPUT_NAME}${_name_suffix}")
|
||||
set(_helper_info_plist "${CMAKE_CURRENT_BINARY_DIR}/${_helper_target}-Info.plist")
|
||||
|
||||
file(READ "${CMAKE_SOURCE_DIR}/cmake/mac-helper-Info.plist.in" _plist_contents)
|
||||
string(REPLACE "\${EXECUTABLE_NAME}" "${_helper_output_name}" _plist_contents "${_plist_contents}")
|
||||
string(REPLACE "\${PRODUCT_NAME}" "${_helper_output_name}" _plist_contents "${_plist_contents}")
|
||||
string(REPLACE "\${HELPER_BUNDLE_NAME}" "${NEBULA_HELPER_BUNDLE_NAME}" _plist_contents "${_plist_contents}")
|
||||
string(REPLACE "\${BUNDLE_ID_SUFFIX}" "${_plist_suffix}" _plist_contents "${_plist_contents}")
|
||||
file(WRITE "${_helper_info_plist}" "${_plist_contents}")
|
||||
|
||||
add_executable(${_helper_target} MACOSX_BUNDLE
|
||||
app/process_helper_mac.cc
|
||||
src/cef/nebula_app.cpp
|
||||
)
|
||||
SET_EXECUTABLE_TARGET_PROPERTIES(${_helper_target})
|
||||
add_dependencies(${_helper_target} libcef_dll_wrapper)
|
||||
target_include_directories(${_helper_target} PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
"${CEF_ROOT}"
|
||||
"${CEF_ROOT}/include"
|
||||
)
|
||||
target_link_libraries(${_helper_target} PRIVATE
|
||||
libcef_dll_wrapper
|
||||
${CEF_STANDARD_LIBS}
|
||||
)
|
||||
set_target_properties(${_helper_target} PROPERTIES
|
||||
MACOSX_BUNDLE_INFO_PLIST "${_helper_info_plist}"
|
||||
OUTPUT_NAME "${_helper_output_name}"
|
||||
)
|
||||
|
||||
add_dependencies(${nebula_target} "${_helper_target}")
|
||||
add_custom_command(TARGET ${nebula_target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CEF_TARGET_OUT_DIR}/${_helper_output_name}.app"
|
||||
"${NEBULA_APP}/Contents/Frameworks/${_helper_output_name}.app"
|
||||
VERBATIM
|
||||
)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Copy Nebula UI files after build
|
||||
# ------------------------------------------------------------
|
||||
|
||||
add_custom_command(TARGET ${nebula_target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/ui"
|
||||
"$<TARGET_FILE_DIR:${nebula_target}>/ui"
|
||||
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/assets"
|
||||
"$<TARGET_FILE_DIR:${nebula_target}>/ui/assets"
|
||||
|
||||
COMMENT "Copying Nebula UI files and assets for ${nebula_target}..."
|
||||
)
|
||||
endfunction()
|
||||
|
||||
add_nebula_app_target(NebulaBrowser app/main.cpp)
|
||||
add_nebula_app_target(NebulaBigPicture app/main_bigpicture.cpp)
|
||||
@@ -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`
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -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: `<app>/plugins/<plugin-id>/`
|
||||
- User folder: `%APPDATA%/Nebula/plugins/<plugin-id>/` (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 limited APIs.
|
||||
- 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:<id>]` 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
|
||||
@@ -1,234 +0,0 @@
|
||||
Converting extracted AppImage (`squashfs-root`) into a distributable AppDir for Steam
|
||||
|
||||
If your environment lacks `rsync`, use `cp -a` to copy the extracted AppImage into a clean AppDir and prepare it for upload to Steam.
|
||||
|
||||
1) Copy the extracted AppImage to an AppDir folder
|
||||
```bash
|
||||
cp -a squashfs-root/ nebula-appdir
|
||||
```
|
||||
|
||||
2) Unpack `app.asar` to edit or include app sources (optional; requires `npx asar`)
|
||||
```bash
|
||||
cd nebula-appdir/resources
|
||||
npx asar extract app.asar app
|
||||
# keep a backup if you want
|
||||
mv app app.orig && rm app.asar
|
||||
cd ../../
|
||||
```
|
||||
|
||||
3) Add/verify launcher (we added `nebula-appdir/Nebula`):
|
||||
```bash
|
||||
chmod +x nebula-appdir/Nebula
|
||||
```
|
||||
Run locally:
|
||||
```bash
|
||||
cd nebula-appdir
|
||||
./Nebula
|
||||
```
|
||||
|
||||
4) Ensure binary & permissions are correct
|
||||
```bash
|
||||
chmod +x nebula-appdir/nebula
|
||||
```
|
||||
|
||||
5) Package or upload to Steam
|
||||
- Create a tarball to upload as game files, or upload the AppDir contents as the depot.
|
||||
```bash
|
||||
tar -czf nebula-appdir.tar.gz -C nebula-appdir .
|
||||
```
|
||||
- In Steamworks, set the launch command to `./Nebula` (or `./nebula`).
|
||||
|
||||
Notes
|
||||
- `--no-sandbox` reduces Chromium sandboxing; prefer fixing `chrome-sandbox` and enabling sandboxing when possible.
|
||||
- Using the AppDir avoids AppImage/FUSE dependency on target systems.
|
||||
- Test on a clean SteamOS/Deck image before publishing.
|
||||
|
||||
Big Picture auto-start (SteamOS Gaming Mode)
|
||||
- If Nebula is launched from SteamOS Gaming Mode, it will auto-start in Big Picture Mode.
|
||||
- To force/disable via Steam Launch Options: `--big-picture` or `--no-big-picture`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- This stops Steam from translating controller input into keyboard/mouse events (“Desktop Layout” behavior).
|
||||
|
||||
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
|
||||
```
|
||||
@@ -1,208 +0,0 @@
|
||||
# Linux / SteamOS Build Upload Guide (SteamCMD)
|
||||
|
||||
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**.
|
||||
@@ -1,149 +1,351 @@
|
||||
# NEBULA BROWSER
|
||||
# Nebula Browser
|
||||
|
||||
*A controller-first browser originally designed for SteamOS*
|
||||
A Chromium Embedded Framework (CEF) browser with a custom HTML chrome UI.
|
||||
|
||||
---
|
||||
**Nebula Browser** is a controller-first web browser designed for Steam Deck, SteamOS-style systems, handheld PCs, living room setups, and gamepad-driven desktop environments.
|
||||
|
||||
### ⚠️ Final Release • Project Archived ⚠️
|
||||
Originally developed as an Electron-based browser for SteamOS users, Nebula Browser is now being rebuilt with **CEF / Chromium Embedded Framework** to provide deeper control over the browser experience, improved native integration, and a cleaner long-term foundation for custom UI, controller navigation, and platform-specific builds.
|
||||
|
||||
Nebula Browser has reached end of support and is no longer under active development.
|
||||
This repository represents the final archived state of the project.
|
||||
Development on Nebula Browser has somewhat resumed, with the project now moving toward a more native custom browser architecture.
|
||||
|
||||
---
|
||||
Nebula Browser is intended to remain open source. The browser source code is licensed under MPL-2.0, while Nebula branding and visual assets are protected separately.
|
||||
|
||||
# Nebula
|
||||
## Documentation
|
||||
|
||||

|
||||
|
||||
Nebula is a customizable and privacy-focused web browser built with Electron. It was designed to be a lightweight, secure, and user-friendly browser with a strong emphasis on controller-first interaction, performance, and privacy.
|
||||
- **[Cross-platform build & architecture](docs/cross-platform.md)** — how to use one repo for Windows, macOS, and Linux; CEF setup; source layout; porting status.
|
||||
|
||||
---
|
||||
|
||||
## Project Status
|
||||
|
||||
**Status:** Archived
|
||||
**Maintenance:** Ended
|
||||
**Future Updates:** None planned
|
||||
Nebula Browser is currently in early active redevelopment.
|
||||
|
||||
Nebula is no longer actively maintained. The source code remains available for reference, learning, and archival purposes.
|
||||
The original Electron version proved the core idea: a browser built around controller use, large-screen navigation, and a UI that does not feel like a standard desktop browser awkwardly placed on a handheld gaming device.
|
||||
|
||||
The new CEF version is the next major step.
|
||||
|
||||
Current focus areas include:
|
||||
|
||||
* Rebuilding the browser around CEF
|
||||
* Creating a fully custom browser interface
|
||||
* Moving away from default Chromium UI elements
|
||||
* Reusing and adapting existing Nebula UI concepts where possible
|
||||
* Supporting controller-first navigation
|
||||
* Preparing for cross-platform builds across Windows, Linux, and macOS
|
||||
* Creating a stronger foundation for future NebulaOS integration
|
||||
|
||||
---
|
||||
|
||||
## Why Nebula Was Archived
|
||||
## Why CEF?
|
||||
|
||||
Nebula was created with a very specific goal in mind: to be a **controller-first browser that lived inside the Steam ecosystem**, particularly for Steam Deck and SteamOS users who wanted a web experience without relying on desktop mode, keyboards, or external workarounds.
|
||||
Nebula Browser originally used Electron because it was fast to prototype, easy to package, and allowed the project to move quickly. However, Electron also comes with limits when building a browser that is meant to feel fully custom and deeply integrated with the operating system.
|
||||
|
||||
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 application was permanently retired from Steam and cannot be distributed on the platform.
|
||||
The move to **CEF** allows Nebula Browser to become more than a web app wrapped in a browser shell.
|
||||
|
||||
While Nebula can function as a desktop browser, distributing it outside of Steam fundamentally changes the experience it was designed to provide. Requiring third-party installation methods or desktop mode defeats the core problem Nebula was built to solve.
|
||||
CEF gives the project:
|
||||
|
||||
Rather than ship a compromised version that no longer aligns with its original purpose, the decision was made to formally conclude development and archive the project.
|
||||
* More control over the native window and browser lifecycle
|
||||
* Better separation between the browser engine and the custom UI
|
||||
* Greater flexibility for building a non-Chrome-like interface
|
||||
* A more suitable foundation for SteamOS, Linux, and future NebulaOS use
|
||||
* The ability to build a browser that feels purpose-built rather than wrapped
|
||||
|
||||
This repository preserves Nebula in its final state as a complete exploration of controller-first browser design.
|
||||
The goal is not to create another Chromium clone. The goal is to create a browser interface designed around how people actually use handhelds, controllers, TVs, and gaming-first systems.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
## Distribution Direction
|
||||
|
||||
* **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.
|
||||
Nebula Browser was originally aimed at Steam. However, after exploring Steam as a release platform, it became clear that Steam may not be the best fit for this kind of application.
|
||||
|
||||
[**Learn more about Nebula's features.**](documentation/FEATURES.md)
|
||||
The current distribution direction is:
|
||||
|
||||
## Getting Started
|
||||
### Primary Target
|
||||
|
||||
### Prerequisites
|
||||
* **Itch.io**
|
||||
|
||||
* [Node.js](https://nodejs.org/) installed.
|
||||
Itch.io is currently the most practical and flexible platform for distributing Nebula Browser. It allows the project to be released as a utility-style app without needing to fit into a traditional game store category.
|
||||
|
||||
### Installation
|
||||
### Potential Future Targets
|
||||
|
||||
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
|
||||
```
|
||||
* **Epic Games Store**
|
||||
* **GOG**
|
||||
* **Flatpak / Linux package distribution**
|
||||
* **Direct downloads from the Nebula website**
|
||||
|
||||
### Running the Application
|
||||
Epic Games and GOG are being considered as possible future distribution platforms, especially if Nebula Browser grows into a more polished desktop or gaming utility. These are not confirmed release platforms yet, but they remain possible options.
|
||||
|
||||
To start the browser, run the following command:
|
||||
---
|
||||
|
||||
```sh
|
||||
npm start
|
||||
## Open Source Direction
|
||||
|
||||
Nebula Browser is intended to remain open source because browsers are privacy-sensitive software. Users should be able to inspect how the browser works, verify that it is not doing anything suspicious, and contribute fixes or improvements when they find problems.
|
||||
|
||||
Open source is especially important for:
|
||||
|
||||
* User trust
|
||||
* Privacy transparency
|
||||
* Linux and Steam Deck community adoption
|
||||
* Cross-platform testing
|
||||
* Community contributions
|
||||
* Long-term sustainability
|
||||
|
||||
The core Nebula Browser project should remain open source, including:
|
||||
|
||||
* Browser shell
|
||||
* CEF integration code
|
||||
* Custom browser UI
|
||||
* Controller navigation systems
|
||||
* Settings and theme systems
|
||||
* Build scripts and documentation
|
||||
|
||||
Not every Nebula-related component has to be open source. Some future or external components may remain private or be licensed separately, such as:
|
||||
|
||||
* NebulaOS-specific services
|
||||
* Account systems
|
||||
* Cloud sync
|
||||
* Hosted backend services
|
||||
* Internal analytics or crash reporting infrastructure
|
||||
* Private distribution tooling
|
||||
* Experimental unreleased features
|
||||
* Protected branding assets
|
||||
|
||||
Suggested openness model:
|
||||
|
||||
```text
|
||||
Nebula Browser: open source
|
||||
Nebula Core: open source or source-available
|
||||
NebulaOS: potentially partially open source
|
||||
Nebula online services/backend: private
|
||||
Branding assets: protected
|
||||
```
|
||||
|
||||
## Building the Application
|
||||
---
|
||||
|
||||
To build the application for your platform, run:
|
||||
## Core Vision
|
||||
|
||||
```sh
|
||||
npm run dist
|
||||
Nebula Browser is built around one simple idea:
|
||||
|
||||
> The web should be comfortable to use from a couch, handheld, controller, or gaming-focused operating system.
|
||||
|
||||
Most browsers are designed around keyboards, mice, and desktop workflows. Nebula Browser is designed around a different experience.
|
||||
|
||||
Nebula Browser aims to support:
|
||||
|
||||
* Controller-first navigation
|
||||
* Large, readable interface elements
|
||||
* Gamepad-friendly menus and shortcuts
|
||||
* Steam Deck and handheld PC usability
|
||||
* Custom home pages and launcher-style navigation
|
||||
* Better integration with gaming-focused environments
|
||||
* A UI that feels closer to a console app than a traditional browser
|
||||
|
||||
---
|
||||
|
||||
## Planned Features
|
||||
|
||||
The CEF rebuild is still early, but the intended feature set includes:
|
||||
|
||||
* Fully custom browser chrome
|
||||
* Custom tab bar
|
||||
* Custom address bar
|
||||
* Controller-friendly navigation system
|
||||
* On-screen keyboard support
|
||||
* Bookmarks and quick access tiles
|
||||
* Custom home page
|
||||
* Desktop mode and controller mode
|
||||
* Fullscreen and couch-friendly browsing
|
||||
* Custom context menus
|
||||
* Download management
|
||||
* Settings system
|
||||
* Theme support
|
||||
* Steam Deck / handheld-focused layout options
|
||||
* Optional integration with NebulaOS
|
||||
|
||||
Some features may be ported or adapted from the earlier Electron version where it makes sense.
|
||||
|
||||
---
|
||||
|
||||
## Nebula Core
|
||||
|
||||
Nebula Browser may also make use of **Nebula Core**, a shared package system for Nebula-related projects.
|
||||
|
||||
Nebula Core is intended to provide reusable systems such as:
|
||||
|
||||
* Controller input handling
|
||||
* Navigation helpers
|
||||
* UI components
|
||||
* Glyphs and button prompts
|
||||
* Theming utilities
|
||||
* On-screen keyboard systems
|
||||
* Shared Nebula project logic
|
||||
|
||||
Because the new browser is CEF-based rather than Electron-based, some Nebula Core modules may need to be adapted for native integration or used through the browser UI layer rather than directly inside the C++ application layer.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to NebulaOS
|
||||
|
||||
Nebula Browser is part of the wider **Nebula Project** ecosystem.
|
||||
|
||||
NebulaOS is a controller-focused operating system / shell concept designed around gaming, handheld PCs, and living room devices. Nebula Browser is intended to become one of the key built-in applications for that ecosystem.
|
||||
|
||||
While Nebula Browser can exist as a standalone app, its long-term role is to provide a web browsing experience that feels native inside NebulaOS.
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
|
||||
The current redevelopment direction uses:
|
||||
|
||||
* **C++** for the native browser shell
|
||||
* **CEF / Chromium Embedded Framework** for web rendering
|
||||
* **HTML, CSS, and JavaScript** for the custom browser UI
|
||||
* **CMake** for project configuration
|
||||
* Platform-specific CEF builds for Windows, Linux, and macOS
|
||||
|
||||
The previous version used Electron, but the current direction is focused on CEF.
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
|
||||
The repository structure may change as the CEF rebuild develops, but the project is expected to follow a layout similar to:
|
||||
|
||||
```text
|
||||
NebulaBrowser/
|
||||
├── app/
|
||||
│ ├── browser/
|
||||
│ ├── ui/
|
||||
│ └── platform/
|
||||
├── cef/
|
||||
│ └── README.md
|
||||
├── resources/
|
||||
│ ├── icons/
|
||||
│ ├── themes/
|
||||
│ └── pages/
|
||||
├── scripts/
|
||||
├── third_party/
|
||||
├── CMakeLists.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
This will create a distributable file in the `dist` directory.
|
||||
CEF itself should generally not be committed directly into the repository. Instead, platform-specific CEF builds should be downloaded separately and placed into the expected local folder during development.
|
||||
|
||||
Tip (Windows): If you encounter GPU issues, try starting with `start-gpu-safe.bat` to launch in a safer rendering mode.
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## Development Notes
|
||||
|
||||
An overview of the project's structure. For a more detailed explanation, please see the [Project Structure documentation](documentation/PROJECT_STRUCTURE.md).
|
||||
Nebula Browser is still experimental and in transition from the original Electron version to the new CEF version.
|
||||
|
||||
- `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.
|
||||
Expect changes in:
|
||||
|
||||
## Core Concepts
|
||||
* Build setup
|
||||
* File structure
|
||||
* UI architecture
|
||||
* Controller input systems
|
||||
* Platform support
|
||||
* Packaging and distribution
|
||||
|
||||
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).
|
||||
The project is not yet considered production-ready.
|
||||
|
||||
- **Main and Renderer Processes**
|
||||
- **Inter-Process Communication (IPC)**
|
||||
- **Performance and GPU Management**
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
Build instructions will be expanded as the CEF version becomes more stable.
|
||||
|
||||
At a high level, development requires:
|
||||
|
||||
1. A supported C++ compiler
|
||||
2. CMake
|
||||
3. A downloaded CEF binary distribution for your operating system
|
||||
4. The Nebula Browser source code
|
||||
5. A local build folder
|
||||
|
||||
Example build flow:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
More detailed platform-specific instructions will be added later. For macOS/Linux prerequisites, directory structure, and current port status, see [docs/cross-platform.md](docs/cross-platform.md).
|
||||
|
||||
### Quick start (Windows)
|
||||
|
||||
1. Download the [CEF standard binary distribution](https://cef-builds.spotifycdn.com/index.html) for Windows 64-bit.
|
||||
2. Extract into `thirdparty/cef/` so `thirdparty/cef/cmake/FindCEF.cmake` exists.
|
||||
3. Build:
|
||||
|
||||
```powershell
|
||||
cmake -B build
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
4. Run `build\Release\NebulaBrowser.exe`.
|
||||
|
||||
---
|
||||
|
||||
## Platform Goals
|
||||
|
||||
Nebula Browser is being designed with cross-platform support in mind.
|
||||
|
||||
Target platforms include:
|
||||
|
||||
* Windows
|
||||
* Linux
|
||||
* SteamOS / Steam Deck
|
||||
* macOS
|
||||
|
||||
Linux and SteamOS are especially important because Nebula Browser was originally created to solve a gap in the handheld and couch-browsing experience.
|
||||
|
||||
---
|
||||
|
||||
## Release Plans
|
||||
|
||||
Nebula Browser does not currently have a fixed release date.
|
||||
|
||||
The current release plan is:
|
||||
|
||||
1. Rebuild the core browser shell in CEF
|
||||
2. Replace default Chromium UI with a custom Nebula interface
|
||||
3. Restore core browser functionality
|
||||
4. Add controller-first navigation
|
||||
5. Prepare an early public build
|
||||
6. Release through Itch.io first
|
||||
7. Explore additional distribution options later
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our [contributing guidelines](documentation/CONTRIBUTING.md) to get started.
|
||||
Nebula Browser is currently a personal / experimental project and is still finding its new technical direction.
|
||||
|
||||
## Technologies Used
|
||||
Contributions, ideas, testing, and feedback may become more open once the CEF rebuild has a more stable foundation.
|
||||
|
||||
* [Electron](https://www.electronjs.org/)
|
||||
* HTML, CSS, JavaScript
|
||||
---
|
||||
|
||||
## License
|
||||
## Licensing
|
||||
|
||||
This project is licensed under the MIT License. [Read More](documentation/MIT.md)
|
||||
Nebula Browser separates source code licensing from branding and visual assets.
|
||||
|
||||
The preferred licensing direction is:
|
||||
|
||||
## Documentation
|
||||
* **Source code:** Mozilla Public License 2.0
|
||||
* **Documentation:** CC BY 4.0, unless otherwise specified
|
||||
* **Branding, logos, icons, names, and visual identity:** All Rights Reserved, unless explicitly stated otherwise
|
||||
|
||||
* [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)
|
||||
MPL-2.0 is being used because it is a practical open-source license for a browser-style project. It allows people to use, modify, and contribute to the code while requiring changes to MPL-licensed files to remain open under the same license.
|
||||
|
||||
The Nebula name, logo, and brand identity are not automatically granted for reuse just because the code is open source. See [ASSETS-LICENSE.md](ASSETS-LICENSE.md) for the separate asset and branding terms.
|
||||
|
||||
See the CEF distribution and Chromium license terms for third-party runtime components.
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
Nebula Browser is created as part of the wider Nebula Project, a collection of software experiments focused on controller-first computing, gaming-focused interfaces, and custom desktop experiences.
|
||||
|
||||
The project began as a browser for SteamOS and Steam Deck users, but is now evolving into a more flexible, native, and custom browser experience built around CEF.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "app/run.h"
|
||||
#include "platform/types.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include "include/wrapper/cef_library_loader.h"
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
int APIENTRY wWinMain(HINSTANCE instance,
|
||||
HINSTANCE previous_instance,
|
||||
LPWSTR command_line,
|
||||
int show_command) {
|
||||
NEBULA_UNUSED(previous_instance);
|
||||
NEBULA_UNUSED(command_line);
|
||||
|
||||
const nebula::platform::AppStartup startup{instance, show_command};
|
||||
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
|
||||
}
|
||||
#else
|
||||
int main(int argc, char* argv[]) {
|
||||
#if defined(__APPLE__)
|
||||
CefScopedLibraryLoader library_loader;
|
||||
if (!library_loader.LoadInMain()) {
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
const nebula::platform::AppStartup startup{argc, argv};
|
||||
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "app/run.h"
|
||||
#include "platform/types.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include "include/wrapper/cef_library_loader.h"
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
int APIENTRY wWinMain(HINSTANCE instance,
|
||||
HINSTANCE previous_instance,
|
||||
LPWSTR command_line,
|
||||
int show_command) {
|
||||
NEBULA_UNUSED(previous_instance);
|
||||
NEBULA_UNUSED(command_line);
|
||||
|
||||
const nebula::platform::AppStartup startup{instance, show_command};
|
||||
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
|
||||
}
|
||||
#else
|
||||
int main(int argc, char* argv[]) {
|
||||
#if defined(__APPLE__)
|
||||
CefScopedLibraryLoader library_loader;
|
||||
if (!library_loader.LoadInMain()) {
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
const nebula::platform::AppStartup startup{argc, argv};
|
||||
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "cef/nebula_app.h"
|
||||
#include "include/cef_app.h"
|
||||
#include "include/wrapper/cef_library_loader.h"
|
||||
|
||||
#if defined(CEF_USE_SANDBOX)
|
||||
#include "include/cef_sandbox_mac.h"
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#if defined(CEF_USE_SANDBOX)
|
||||
CefScopedSandboxContext sandbox_context;
|
||||
if (!sandbox_context.Initialize(argc, argv)) {
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
CefScopedLibraryLoader library_loader;
|
||||
if (!library_loader.LoadInHelper()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const CefMainArgs main_args(argc, argv);
|
||||
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
|
||||
return CefExecuteProcess(main_args, app, nullptr);
|
||||
}
|
||||
@@ -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[@]}" "$@"
|
||||
@@ -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
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Run Nebula with portable data storage
|
||||
# User data (cookies, history, bookmarks) is stored in usr/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/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"
|
||||
@@ -1 +0,0 @@
|
||||
4290110
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 699 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 890 KiB After Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +0,0 @@
|
||||
[
|
||||
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.nebula.browser.${HELPER_BUNDLE_NAME}.helper${BUNDLE_ID_SUFFIX}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSEnvironment</key>
|
||||
<dict>
|
||||
<key>MallocNanoZone</key>
|
||||
<string>0</string>
|
||||
</dict>
|
||||
<key>LSFileQuarantineEnabled</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
build/compile_commands.json
|
||||
@@ -0,0 +1,215 @@
|
||||
# Cross-platform build guide
|
||||
|
||||
Nebula Browser uses a **single codebase** and **one git branch** for Windows, macOS, and Linux. You do not maintain separate platform branches. Instead, each machine unpacks the correct [CEF](https://bitbucket.org/chromiumembedded/cef) binary distribution into `thirdparty/cef/` and builds with CMake.
|
||||
|
||||
## Overview
|
||||
|
||||
| Layer | Location | Platform-specific? |
|
||||
|-------|----------|-------------------|
|
||||
| CEF binaries | `thirdparty/cef/` (gitignored) | Yes — one distribution per OS |
|
||||
| CMake / deploy | `CMakeLists.txt` | Yes — uses CEF’s `COPY_FILES`, `COPY_MAC_FRAMEWORK`, etc. |
|
||||
| App entry & CEF init | `src/app/run.cpp`, `src/platform/*/startup_*.cpp` | Partly |
|
||||
| Native window shell | `src/platform/win\|mac\|linux/` | Yes |
|
||||
| Browser embedding | `src/platform/*/browser_host_*.cpp` | Partly |
|
||||
| Tabs, URLs, UI routing | `src/browser/`, `src/ui/`, `src/cef/` | No (shared) |
|
||||
|
||||
**Windows** has a full native shell (custom frame, DWM, embedded CEF views).
|
||||
|
||||
**macOS** has a Cocoa native shell (`NSWindow` / `NSView`) with CEF child-view embedding.
|
||||
|
||||
**Linux** compiles and links today, but `NebulaWindow::Create()` is still a stub that returns `false` until a real X11 (or GTK) host is implemented.
|
||||
|
||||
## Directory layout
|
||||
|
||||
```
|
||||
thirdparty/cef/ # CEF standard distribution for your OS (not in git)
|
||||
|
||||
src/platform/
|
||||
types.h # Rect, BrowserLayout, NativeWindow, AppStartup
|
||||
startup.h # PrepareApp, single-instance, CefMainArgs, CefSettings paths
|
||||
browser_host.h # CefWindowInfo helpers, resize/show/destroy browser HWNDs
|
||||
paths_platform.h # ExecutableDirectory, DefaultUserDataRoot, PathToUtf8
|
||||
|
||||
win/ # Windows implementation
|
||||
mac/ # macOS Cocoa implementation
|
||||
linux/ # Linux stubs + partial CEF glue
|
||||
|
||||
src/window/
|
||||
nebula_window.h # Platform-agnostic window API (PIMPL)
|
||||
|
||||
src/app/ # Shared controller and run loop
|
||||
src/browser/ # Shared tab/session logic
|
||||
src/cef/ # Shared CEF app and browser client
|
||||
src/ui/ # Shared nebula:// URLs and path helpers
|
||||
ui/ # HTML/JS UI (copied next to the binary at build time)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **CMake** 3.21+
|
||||
- **C++20** compiler
|
||||
- Windows: Visual Studio 2022
|
||||
- macOS: Xcode 16+ (Clang)
|
||||
- Linux: GCC 10+ or Clang, plus development packages for X11 (CEF’s CMake runs `pkg-config` for X11 on Linux)
|
||||
- **CEF standard binary distribution** for your OS and CPU architecture from the [CEF builds](https://cef-builds.spotifycdn.com/index.html) page (or your usual CEF source)
|
||||
|
||||
## CEF setup
|
||||
|
||||
1. Download the **standard distribution** for your platform (e.g. `cef_binary_*_windows64`, `*_macos*`, `*_linux64`).
|
||||
2. Extract so that this path exists:
|
||||
|
||||
```
|
||||
thirdparty/cef/cmake/FindCEF.cmake
|
||||
```
|
||||
|
||||
3. The rest of the distribution (`Release/`, `Resources/`, `libcef_dll/`, etc.) should sit directly under `thirdparty/cef/` as shipped by CEF.
|
||||
|
||||
`thirdparty/cef/` is listed in `.gitignore` so binaries are never committed. Each developer and CI machine supplies its own copy.
|
||||
|
||||
## IDE / clangd setup
|
||||
|
||||
Without CEF headers, clangd will show many errors (unknown types like `CefWindowInfo`, `CefMainArgs`, etc.). To get full IDE support:
|
||||
|
||||
1. Unpack CEF into `thirdparty/cef/` as described above
|
||||
2. Generate `compile_commands.json`:
|
||||
```bash
|
||||
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
```
|
||||
3. Symlink it to the project root:
|
||||
```bash
|
||||
ln -s build/compile_commands.json .
|
||||
```
|
||||
|
||||
The included `.clangd` file provides fallback flags, but full diagnostics require the compile database.
|
||||
|
||||
Optional: override the location at configure time:
|
||||
|
||||
```bash
|
||||
cmake -B build -DCEF_ROOT=/path/to/cef_binary
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
cmake -B build
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
Output: `build/Release/NebulaBrowser.exe` (plus CEF DLLs and `ui/` copied beside it).
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
cmake -B build -DPROJECT_ARCH=x86_64 # or arm64 on Apple Silicon
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
CMake builds a `NebulaBrowser.app` bundle and copies the Chromium Embedded Framework into `Contents/Frameworks`. The macOS port includes a Cocoa `NSWindow` / `NSView` host and child-view embedding for CEF browsers.
|
||||
|
||||
CEF **helper app** subprocess targets may still need to be added if the selected CEF distribution does not provide a compatible default subprocess layout. Use CEF’s `cefsimple` sample as the reference for the complete Mac bundle layout.
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
cmake -B build
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
Output: `build/NebulaBrowser` (or `build/<Config>/NebulaBrowser` depending on generator). CEF `.so` files and resources are copied next to the executable.
|
||||
|
||||
You may need to set **SUID** on `chrome-sandbox` as printed by CMake post-build (CEF documents this in its Linux README).
|
||||
|
||||
Until a Linux host window exists, the binary will not show a UI for the same reason as macOS.
|
||||
|
||||
## How CMake picks a platform
|
||||
|
||||
`find_package(CEF)` loads CEF’s `cef_variables.cmake`, which sets:
|
||||
|
||||
- `OS_WINDOWS`
|
||||
- `OS_MACOSX`
|
||||
- `OS_LINUX`
|
||||
|
||||
`CMakeLists.txt` then:
|
||||
|
||||
1. Adds the matching `src/platform/{win,mac,linux}/*.cpp` sources
|
||||
2. Sets the executable type (`WIN32`, `MACOSX_BUNDLE`, or plain executable)
|
||||
3. Links `libcef_dll_wrapper`, `libcef_lib`, and `${CEF_STANDARD_LIBS}`
|
||||
4. Runs CEF’s copy macros so runtime files land beside the app
|
||||
|
||||
Shared sources are always compiled; only the `src/platform/...` tree changes per OS.
|
||||
|
||||
## Application flow
|
||||
|
||||
```
|
||||
main (app/main.cpp)
|
||||
└─ RunNebula(AppStartup) src/app/run.cpp
|
||||
├─ platform::PrepareApp()
|
||||
├─ CefExecuteProcess() (subprocesses)
|
||||
├─ platform::TryAcquireSingleInstance()
|
||||
├─ CefInitialize()
|
||||
├─ NebulaController::Create()
|
||||
│ └─ NebulaWindow::Create() platform-specific
|
||||
└─ CefRunMessageLoop()
|
||||
```
|
||||
|
||||
`AppStartup` carries platform entry data:
|
||||
|
||||
- **Windows:** `HINSTANCE` + show command (`wWinMain`)
|
||||
- **macOS / Linux:** `argc` / `argv` (`main`)
|
||||
|
||||
## User data and profile paths
|
||||
|
||||
Shared logic in `src/ui/paths.cpp` writes under:
|
||||
|
||||
| OS | Default root |
|
||||
|----|----------------|
|
||||
| Windows | `%LOCALAPPDATA%\Nebula\User Data` (falls back to exe directory) |
|
||||
| macOS | `~/Library/Application Support/Nebula/User Data` |
|
||||
| Linux | `$XDG_DATA_HOME/Nebula/User Data` or `~/.local/share/Nebula/User Data` |
|
||||
|
||||
Platform-specific discovery lives in `src/platform/*/paths_*.cpp`.
|
||||
|
||||
## GPU / Chromium flags
|
||||
|
||||
`src/cef/nebula_app.cpp` applies shared switches (`no-sandbox`, `ignore-gpu-blocklist`, etc.) and platform graphics backends:
|
||||
|
||||
| OS | Backend |
|
||||
|----|---------|
|
||||
| Windows | ANGLE + D3D11 |
|
||||
| macOS | ANGLE + Metal |
|
||||
| Linux | EGL |
|
||||
|
||||
## Adding or changing platform code
|
||||
|
||||
1. **Shared behavior** (tabs, `nebula://` URLs, CEF clients) → edit under `src/browser/`, `src/ui/`, `src/cef/`, `src/app/` only if it is truly cross-platform.
|
||||
2. **Native window or OS API** → edit `src/platform/<os>/` and keep `src/window/nebula_window.h` free of `#include <windows.h>` (or Cocoa/X11 headers).
|
||||
3. **CEF child window embedding** → `src/platform/<os>/browser_host_*.cpp` (`MakeChildWindowInfo`, resize, visibility).
|
||||
4. **Startup / paths** → `startup_*.cpp` and `paths_*.cpp` in the same folder.
|
||||
5. **New OS** → add `src/platform/<os>/`, extend the `if(OS_...)` blocks in `CMakeLists.txt`, and document CEF distribution layout here.
|
||||
|
||||
## Porting checklist (macOS / Linux)
|
||||
|
||||
- [x] macOS: implement `NebulaWindow` in `nebula_window_mac.mm`
|
||||
- [x] macOS: wire `browser_host_mac.mm` resize/show/raise to Cocoa views
|
||||
- [ ] Linux: implement `NebulaWindow` in `nebula_window_linux.cpp`
|
||||
- [ ] Linux: wire `browser_host_linux.cpp` resize/show/raise to the real toolkit
|
||||
- [ ] macOS: add CEF helper app targets and bundle layout (copy from `cefsimple`)
|
||||
- [ ] Linux: confirm X11 (or chosen toolkit) parent handle for `CefWindowInfo::SetAsChild`
|
||||
- [ ] Test GPU diagnostics page and hardware acceleration on target hardware
|
||||
- [ ] Package or script runtime layout (`ui/`, CEF frameworks/libs, ICU locales, etc.)
|
||||
|
||||
## FAQ
|
||||
|
||||
**Do I need a separate git branch per OS?**
|
||||
No. Use one branch; swap `thirdparty/cef` and rebuild on each machine.
|
||||
|
||||
**Can I commit CEF into the repo?**
|
||||
Not recommended (size, licensing, per-arch binaries). Keeping `thirdparty/cef/` gitignored is intentional.
|
||||
|
||||
**Why does Linux build but not run?**
|
||||
The Linux window stub intentionally returns `false` from `Create()` until a native shell exists. Shared CEF and browser code are ready; the missing piece is the host window.
|
||||
|
||||
**Where is the Windows UI code?**
|
||||
`src/platform/win/nebula_window_win.cpp` — custom chrome, hit-testing, fullscreen, and child browser placement.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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/<version>` marker.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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/<version>` 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
|
||||
@@ -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');
|
||||
|
||||
})();
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,89 +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
|
||||
if [ -f "$DEST/run-nebula.sh" ]; then
|
||||
mv "$DEST/run-nebula.sh" "$DEST/Nebula" 2>/dev/null || true
|
||||
fi
|
||||
chmod +x "$DEST/Nebula" || true
|
||||
|
||||
# Ensure directories for icons and desktop entries
|
||||
mkdir -p "$DEST/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$DEST/usr/share/applications"
|
||||
|
||||
# Copy icon if present at top level of extracted AppImage
|
||||
if [ -f "$SRC/nebula.png" ]; then
|
||||
cp "$SRC/nebula.png" "$DEST/usr/share/icons/hicolor/256x256/apps/nebula.png"
|
||||
fi
|
||||
|
||||
# Also embed project icon if present in repo assets
|
||||
PROJECT_ICON="$(cd "$(dirname "$0")" && pwd)/assets/images/Logos/Nebula-Favicon.png"
|
||||
if [ -f "$PROJECT_ICON" ]; then
|
||||
echo "Embedding project icon into AppDir: $PROJECT_ICON"
|
||||
cp "$PROJECT_ICON" "$DEST/usr/share/icons/hicolor/256x256/apps/nebula.png"
|
||||
fi
|
||||
|
||||
# Install desktop file into AppDir
|
||||
if [ -f "$DEST/nebula.desktop" ]; then
|
||||
cp "$DEST/nebula.desktop" "$DEST/usr/share/applications/nebula.desktop"
|
||||
else
|
||||
cat > "$DEST/usr/share/applications/nebula.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Nebula
|
||||
Comment=Nebula Browser
|
||||
Exec=./Nebula %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=nebula
|
||||
Categories=Network;WebBrowser;
|
||||
StartupWMClass=Nebula
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 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"
|
||||
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 launcher exists (from example if needed)
|
||||
if [ -f "$SCRIPT_DIR/appdir-example/Nebula" ]; then
|
||||
cp "$SCRIPT_DIR/appdir-example/Nebula" "$DEST/Nebula"
|
||||
chmod +x "$DEST/Nebula" || true
|
||||
fi
|
||||
|
||||
# Fix permissions
|
||||
chmod -R a+r "$DEST/usr/share/icons/hicolor/256x256/apps" || true
|
||||
chmod +x "$DEST/Nebula" || true
|
||||
|
||||
echo "AppDir assembled at $DEST. Run with: $DEST/run-nebula.sh"
|
||||
@@ -1,64 +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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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://<id>)
|
||||
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;
|
||||
@@ -1,377 +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 'user-data' in Documents/My Games/<AppName>
|
||||
* with a safe fallback to the app directory.
|
||||
*/
|
||||
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: prefer Documents/My Games/<AppName>/user-data
|
||||
let dataPath = '';
|
||||
try {
|
||||
const docsDir = app.getPath('documents');
|
||||
const appName = app.getName() || 'NebulaBrowser';
|
||||
dataPath = path.join(docsDir, 'My Games', appName, 'user-data');
|
||||
} catch (err) {
|
||||
console.warn('[Portable] Failed to resolve Documents path, using app directory');
|
||||
}
|
||||
|
||||
if (!dataPath) {
|
||||
const appRoot = this._getAppRootDir();
|
||||
dataPath = path.join(appRoot, '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;
|
||||
@@ -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 <webview preload="..."> attributes in the DOM early so
|
||||
// guests receive an absolute path instead of a relative one that may fail
|
||||
// to resolve inside the guest process.
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
if (webviewPreloadAbsolutePath) {
|
||||
const els = document.querySelectorAll('webview[preload]');
|
||||
for (const el of els) {
|
||||
try { el.setAttribute('preload', webviewPreloadAbsolutePath); } catch {};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// non-fatal
|
||||
}
|
||||
});
|
||||
|
||||
// Cache for bookmarks to reduce IPC calls
|
||||
let bookmarksCache = null;
|
||||
let bookmarksCacheTime = 0;
|
||||
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);
|
||||
}
|
||||
})();
|
||||
@@ -1,215 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GPU Diagnostics - Nebula Browser</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.status.good { background: #d4edda; color: #155724; }
|
||||
.status.warning { background: #fff3cd; color: #856404; }
|
||||
.status.error { background: #f8d7da; color: #721c24; }
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover { background: #0056b3; }
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.canvas-test {
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>GPU Diagnostics</h1>
|
||||
|
||||
<div id="gpu-status" class="status">
|
||||
<h3>GPU Status</h3>
|
||||
<p>Loading GPU information...</p>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h3>WebGL Test</h3>
|
||||
<canvas id="webgl-canvas" class="canvas-test" width="300" height="150"></canvas>
|
||||
<p id="webgl-status">Testing WebGL...</p>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h3>Canvas 2D Acceleration Test</h3>
|
||||
<canvas id="canvas2d" class="canvas-test" width="300" height="150"></canvas>
|
||||
<p id="canvas2d-status">Testing Canvas 2D...</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Actions</h3>
|
||||
<button onclick="refreshGPUInfo()">Refresh GPU Info</button>
|
||||
<button onclick="forceGC()">Force Garbage Collection</button>
|
||||
<button onclick="applyFallback(1)">Apply GPU Fallback Level 1</button>
|
||||
<button onclick="applyFallback(2)">Apply GPU Fallback Level 2</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Detailed GPU Information</h3>
|
||||
<pre id="gpu-details">Loading...</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function refreshGPUInfo() {
|
||||
try {
|
||||
const gpuInfo = await window.electronAPI.invoke('get-gpu-info');
|
||||
const statusDiv = document.getElementById('gpu-status');
|
||||
const detailsDiv = document.getElementById('gpu-details');
|
||||
|
||||
if (gpuInfo.error) {
|
||||
statusDiv.className = 'status error';
|
||||
statusDiv.innerHTML = `<h3>GPU Status</h3><p>Error: ${gpuInfo.error}</p>`;
|
||||
} else {
|
||||
const isGPUWorking = checkGPUFeatures(gpuInfo.featureStatus);
|
||||
statusDiv.className = `status ${isGPUWorking ? 'good' : 'warning'}`;
|
||||
statusDiv.innerHTML = `
|
||||
<h3>GPU Status</h3>
|
||||
<p><strong>Hardware Acceleration:</strong> ${isGPUWorking ? 'Enabled' : 'Disabled/Limited'}</p>
|
||||
<p><strong>Fallback Level:</strong> ${gpuInfo.fallbackStatus?.fallbackLevel || 0}</p>
|
||||
<p><strong>GPU Enabled:</strong> ${gpuInfo.fallbackStatus?.gpuEnabled ? 'Yes' : 'No'}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
detailsDiv.textContent = JSON.stringify(gpuInfo, null, 2);
|
||||
} catch (err) {
|
||||
console.error('Failed to get GPU info:', err);
|
||||
document.getElementById('gpu-status').innerHTML = `<h3>GPU Status</h3><p>Error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function checkGPUFeatures(features) {
|
||||
const criticalFeatures = ['gpu_compositing', 'webgl', 'webgl2'];
|
||||
return criticalFeatures.some(feature =>
|
||||
features[feature] && !features[feature].includes('disabled')
|
||||
);
|
||||
}
|
||||
|
||||
async function forceGC() {
|
||||
try {
|
||||
await window.electronAPI.invoke('force-gc');
|
||||
alert('Garbage collection completed');
|
||||
} catch (err) {
|
||||
alert('Failed to force GC: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFallback(level) {
|
||||
try {
|
||||
const result = await window.electronAPI.invoke('apply-gpu-fallback', level);
|
||||
if (result.success) {
|
||||
alert(`Applied GPU fallback level ${level}. App restart may be required.`);
|
||||
} else {
|
||||
alert('Failed to apply fallback: ' + result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to apply fallback: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Test WebGL
|
||||
function testWebGL() {
|
||||
const canvas = document.getElementById('webgl-canvas');
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
const status = document.getElementById('webgl-status');
|
||||
|
||||
if (gl) {
|
||||
// Draw a simple triangle
|
||||
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(vertexShader, `
|
||||
attribute vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`);
|
||||
gl.compileShader(vertexShader);
|
||||
|
||||
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(fragmentShader, `
|
||||
precision mediump float;
|
||||
void main() {
|
||||
gl_Color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
`);
|
||||
gl.compileShader(fragmentShader);
|
||||
|
||||
status.textContent = 'WebGL: Available ✓';
|
||||
status.parentElement.className = 'status good';
|
||||
|
||||
// Clear with green color to show it's working
|
||||
gl.clearColor(0.0, 0.8, 0.0, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
} else {
|
||||
status.textContent = 'WebGL: Not Available ✗';
|
||||
status.parentElement.className = 'status error';
|
||||
}
|
||||
}
|
||||
|
||||
// Test Canvas 2D
|
||||
function testCanvas2D() {
|
||||
const canvas = document.getElementById('canvas2d');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const status = document.getElementById('canvas2d-status');
|
||||
|
||||
try {
|
||||
// Draw some graphics to test acceleration
|
||||
const gradient = ctx.createLinearGradient(0, 0, 300, 0);
|
||||
gradient.addColorStop(0, '#ff0000');
|
||||
gradient.addColorStop(1, '#0000ff');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, 300, 150);
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '20px Arial';
|
||||
ctx.fillText('Canvas 2D Working!', 50, 80);
|
||||
|
||||
status.textContent = 'Canvas 2D: Working ✓';
|
||||
status.parentElement.className = 'status good';
|
||||
} catch (err) {
|
||||
status.textContent = 'Canvas 2D: Error - ' + err.message;
|
||||
status.parentElement.className = 'status error';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize tests
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
refreshGPUInfo();
|
||||
testWebGL();
|
||||
testCanvas2D();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,112 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nebula Browser</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
/* Removed custom draggable bar CSS to allow use of native title bar */
|
||||
|
||||
:root { --resize-border: 8px; }
|
||||
|
||||
body {
|
||||
padding: var(--resize-border);
|
||||
margin: 0;
|
||||
height: calc(100vh - 2 * var(--resize-border));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
/* Adjust the color and transparency as needed */
|
||||
}
|
||||
|
||||
#view-host {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Windows title bar controls wrapper - sits above tab bar -->
|
||||
<div id="titlebar-container">
|
||||
<div id="tab-bar"></div>
|
||||
<div id="window-controls">
|
||||
<button id="min-btn" title="Minimize" aria-label="Minimize">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M0 5h10" stroke="currentColor" stroke-width="1" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="max-btn" title="Maximize" aria-label="Maximize">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" class="maximize-icon">
|
||||
<rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" stroke-width="1" fill="none"/>
|
||||
</svg>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" class="restore-icon" style="display:none">
|
||||
<path d="M2.5 0.5h7v7M0.5 2.5h7v7h-7z" stroke="currentColor" stroke-width="1" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="close-btn" title="Close" aria-label="Close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M0 0l10 10M10 0l-10 10" stroke="currentColor" stroke-width="1" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nav">
|
||||
<div class="nav-left">
|
||||
<button onclick="goBack()">←</button>
|
||||
<button onclick="goForward()">→</button>
|
||||
<button id="reload-btn">⟳</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-center">
|
||||
<input id="url" type="text" placeholder="Type URL here" />
|
||||
<button onclick="navigate()">Go</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<div class="downloads-wrapper">
|
||||
<button id="downloads-btn" title="Downloads" aria-label="Downloads">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 3v12"/>
|
||||
<path d="M7 10l5 5 5-5"/>
|
||||
<path d="M5 21h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="downloads-popup" class="hidden">
|
||||
<div class="downloads-pop-header">
|
||||
<span>Downloads</span>
|
||||
<button id="downloads-show-all">Show all</button>
|
||||
</div>
|
||||
<div id="downloads-list" class="downloads-pop-list"></div>
|
||||
<div id="downloads-empty" class="downloads-empty">No downloads</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-wrapper">
|
||||
<button id="menu-btn">☰</button>
|
||||
<div id="menu-popup" class="hidden">
|
||||
<button onclick="openSettings()">Settings</button>
|
||||
<!-- You can add more options here -->
|
||||
<button id="bigpicture-btn" title="Launch Big Picture Mode (Controller/Steam Deck UI)">🎮 Big Picture Mode</button>
|
||||
<button id="devtools-btn" title="Open / Close Developer Tools">Toggle Developer Tools</button>
|
||||
<div class="zoom-controls">
|
||||
<button id="zoom-out-btn">-</button>
|
||||
<span id="zoom-percent">100%</span>
|
||||
<button id="zoom-in-btn">+</button>
|
||||
</div>
|
||||
<button id="hard-reload-btn">Hard Reload (Ignore Cache)</button>
|
||||
<button id="fresh-reload-btn">Reload Fresh (Add Cache-Buster)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="view-host"></div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Menu</title>
|
||||
<link rel="stylesheet" href="menu-popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="menu-popup" role="menu">
|
||||
<button data-cmd="open-settings" role="menuitem">Settings</button>
|
||||
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</button>
|
||||
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
|
||||
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
||||
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
|
||||
<span id="zoom-percent">100%</span>
|
||||
<button data-cmd="zoom-in" aria-label="Zoom in">+</button>
|
||||
</div>
|
||||
<button data-cmd="hard-reload" role="menuitem">Hard Reload (Ignore Cache)</button>
|
||||
<button data-cmd="fresh-reload" role="menuitem">Reload Fresh (Add Cache-Buster)</button>
|
||||
</div>
|
||||
<script src="menu-popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Nebot</title>
|
||||
<link rel="stylesheet" href="../plugins/nebot/page.css" onerror="this.remove()"/>
|
||||
<style>
|
||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:#12141c; color:#e6e8ef; }
|
||||
.fallback { max-width:620px; margin:60px auto; padding:32px 36px; background:#1d222e; border:1px solid rgba(255,255,255,0.08); border-radius:18px; }
|
||||
.fallback h1 { margin:0 0 12px; font-size:28px; background:linear-gradient(90deg,#a48bff,#6cb6ff); -webkit-background-clip:text; color:transparent; }
|
||||
.fallback p { line-height:1.55; }
|
||||
.tip { background:#232b38; padding:10px 14px; border-radius:10px; font-size:13px; margin-top:18px; border:1px solid rgba(255,255,255,0.08); }
|
||||
.err { color:#ff6d7d; font-weight:600; }
|
||||
#mount { min-height:400px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mount"></div>
|
||||
<script>
|
||||
(async function(){
|
||||
// Wait a tick so plugin preload (renderer-preload) runs and exposes window.ollamaChat
|
||||
const mount = document.getElementById('mount');
|
||||
function showFallback(reason){
|
||||
mount.innerHTML = `<div class="fallback">`+
|
||||
`<h1>Nebot</h1>`+
|
||||
`<p>The Nebot plugin page could not load automatically.</p>`+
|
||||
(reason?`<p class='err'>${reason}</p>`:'')+
|
||||
`<div class="tip"><strong>How to fix</strong><br/>1. Ensure the Nebot plugin folder exists at plugins/nebot.<br/>2. Confirm plugin is enabled (manifest enabled: true).<br/>3. Restart the app so the plugin manager registers pages.</div>`+
|
||||
`</div>`;
|
||||
}
|
||||
try {
|
||||
// Try to fetch plugin page HTML directly
|
||||
const res = await fetch('../plugins/nebot/page.html');
|
||||
if(!res.ok){ showFallback('Missing page.html (status '+res.status+').'); return; }
|
||||
const html = await res.text();
|
||||
// Simple sandboxed injection
|
||||
mount.innerHTML = html;
|
||||
// The injected page expects its CSS & JS relative to itself; adjust asset paths
|
||||
const fixLinks = mount.querySelectorAll('link[rel="stylesheet"], script[src]');
|
||||
fixLinks.forEach(el=>{
|
||||
const attr = el.tagName==='SCRIPT'?'src':'href';
|
||||
if(el.getAttribute(attr) && !/plugins\/nebot\//.test(el.getAttribute(attr))){
|
||||
el.setAttribute(attr,'../plugins/nebot/'+el.getAttribute(attr));
|
||||
}
|
||||
});
|
||||
// Inject JS if not already present
|
||||
if(!mount.querySelector('script[data-nebot-page]')){
|
||||
const s=document.createElement('script'); s.dataset.nebotPage='1';
|
||||
// Pass the current URL hash to the page script for debug mode
|
||||
s.src='../plugins/nebot/page.js' + window.location.hash;
|
||||
mount.appendChild(s);
|
||||
}
|
||||
} catch(e){
|
||||
showFallback(e.message||'Unknown error');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 <webview> 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 <webview> 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);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -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
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "app/first_run_state.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
namespace {
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
bool ReadFirstStartValue(const std::string& json, bool& first_start) {
|
||||
constexpr std::string_view key = "\"first-start\"";
|
||||
const size_t key_pos = json.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t colon = json.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
++colon;
|
||||
while (colon < json.size() && std::isspace(static_cast<unsigned char>(json[colon]))) {
|
||||
++colon;
|
||||
}
|
||||
|
||||
if (json.compare(colon, 4, "true") == 0) {
|
||||
first_start = true;
|
||||
return true;
|
||||
}
|
||||
if (json.compare(colon, 5, "false") == 0) {
|
||||
first_start = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ShouldShowFirstRunSetup() {
|
||||
const auto path = nebula::ui::GetFirstRunStatePath();
|
||||
if (path.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string json = ReadFile(path);
|
||||
if (json.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool first_start = true;
|
||||
return ReadFirstStartValue(json, first_start) ? first_start : true;
|
||||
}
|
||||
|
||||
bool WriteFirstRunState(bool first_start) {
|
||||
const auto path = nebula::ui::GetFirstRunStatePath();
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path temp_path = path;
|
||||
temp_path += L".tmp";
|
||||
{
|
||||
std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
output << "{\n \"first-start\": " << (first_start ? "true" : "false") << "\n}\n";
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
ec.clear();
|
||||
std::filesystem::rename(temp_path, path, ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
bool ShouldShowFirstRunSetup();
|
||||
bool WriteFirstRunState(bool first_start);
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "app/run.h"
|
||||
#include "browser/tab_manager.h"
|
||||
#include "cef/browser_client.h"
|
||||
#include "platform/types.h"
|
||||
#include "window/nebula_window.h"
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
class NebulaController final : public nebula::window::WindowDelegate,
|
||||
public nebula::browser::TabObserver,
|
||||
public nebula::cef::BrowserClientDelegate {
|
||||
public:
|
||||
NebulaController(nebula::platform::AppStartup startup,
|
||||
std::string initial_url,
|
||||
LaunchOptions launch_options = {});
|
||||
~NebulaController() override;
|
||||
|
||||
bool Create();
|
||||
|
||||
void OnWindowCreated() override;
|
||||
void OnWindowResized(const nebula::window::BrowserLayout& layout) override;
|
||||
void OnWindowCloseRequested() override;
|
||||
void OnExternalOpenRequested(const std::string& target) override;
|
||||
|
||||
void OnActiveTabChanged(const nebula::browser::NebulaTab& tab) override;
|
||||
|
||||
void OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
|
||||
void OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
|
||||
void OnChromeCommand(const std::string& command, const std::string& payload) override;
|
||||
void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) override;
|
||||
void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) override;
|
||||
void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) override;
|
||||
void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override;
|
||||
void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) override;
|
||||
void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override;
|
||||
void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
|
||||
void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
|
||||
bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
|
||||
|
||||
private:
|
||||
void CreateNewTab(std::string url = {});
|
||||
void ActivateTab(int tab_id);
|
||||
void CloseTab(int tab_id);
|
||||
void CreateChromeBrowser();
|
||||
void CreateBigPictureBrowser();
|
||||
void CreateContentBrowser();
|
||||
void EnterBigPictureMode();
|
||||
void ExitBigPictureMode();
|
||||
nebula::window::BrowserLayout CurrentBrowserLayout() const;
|
||||
void ToggleMenuPopup();
|
||||
void CloseMenuPopup();
|
||||
void CreateMenuPopupBrowser();
|
||||
void PositionMenuPopup();
|
||||
void SendMenuPopupZoom();
|
||||
void SendThemeToChromeSurfaces(const std::string& theme_json);
|
||||
void ToggleDevTools();
|
||||
void AdjustZoom(double delta);
|
||||
void FreshReload();
|
||||
void SendBigPictureMouseMove(const std::string& payload);
|
||||
void SendBigPictureMouseClick(const std::string& payload, bool right_click);
|
||||
void SendBigPictureMouseWheel(const std::string& payload);
|
||||
void SendBigPictureText(const std::string& payload);
|
||||
void SetBigPictureBrowseVisible(bool visible);
|
||||
void SetContentFullscreen(bool fullscreen);
|
||||
void CompleteFirstRunSetup();
|
||||
void SendDefaultBrowserResult(const std::string& request_id,
|
||||
bool success,
|
||||
bool needs_user_action,
|
||||
const std::string& error = {});
|
||||
void ResizeBrowsers();
|
||||
void SendChromeState(const nebula::browser::NebulaTab& tab);
|
||||
void SendBigPictureState(const nebula::browser::NebulaTab& tab);
|
||||
void RecordSiteHistory(const std::string& url);
|
||||
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
|
||||
void InjectBigPictureCursor(CefRefPtr<CefBrowser> browser);
|
||||
void RemoveBigPictureCursor(CefRefPtr<CefBrowser> browser);
|
||||
void PersistSession() const;
|
||||
void BeginShutdown();
|
||||
void MaybeFinishShutdown();
|
||||
bool ForgetClosingTabBrowser(CefRefPtr<CefBrowser> browser);
|
||||
void LoadPendingNavigationDelayed();
|
||||
|
||||
nebula::platform::AppStartup startup_;
|
||||
std::string initial_url_;
|
||||
std::string pending_initial_navigation_;
|
||||
LaunchOptions launch_options_;
|
||||
bool closing_ = false;
|
||||
bool chrome_ready_ = false;
|
||||
bool big_picture_ready_ = false;
|
||||
bool big_picture_mode_ = false;
|
||||
bool big_picture_browse_visible_ = false;
|
||||
bool content_fullscreen_ = false;
|
||||
bool first_run_setup_active_ = false;
|
||||
bool menu_popup_visible_ = false;
|
||||
|
||||
std::unique_ptr<nebula::window::NebulaWindow> window_;
|
||||
nebula::browser::TabManager tabs_;
|
||||
CefRefPtr<CefBrowser> chrome_browser_;
|
||||
CefRefPtr<CefBrowser> big_picture_browser_;
|
||||
CefRefPtr<CefBrowser> menu_popup_browser_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> big_picture_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
|
||||
std::vector<CefRefPtr<CefBrowser>> closing_tab_browsers_;
|
||||
std::unordered_set<std::string> insecure_warning_bypasses_;
|
||||
std::vector<std::string> site_history_;
|
||||
};
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "app/run.h"
|
||||
|
||||
#include "app/nebula_controller.h"
|
||||
#include "browser/url_utils.h"
|
||||
#include "cef/nebula_app.h"
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_command_line.h"
|
||||
#include "platform/default_browser.h"
|
||||
#include "platform/startup.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace nebula::app {
|
||||
namespace {
|
||||
|
||||
std::string Trim(std::string value) {
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) {
|
||||
value.erase(value.begin());
|
||||
}
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) {
|
||||
value.pop_back();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
bool StartsWithKnownScheme(const std::string& value) {
|
||||
const std::string lower = ToLowerAscii(value);
|
||||
return lower.starts_with("http://") ||
|
||||
lower.starts_with("https://") ||
|
||||
lower.starts_with("file:") ||
|
||||
lower.starts_with("data:") ||
|
||||
lower.starts_with("blob:") ||
|
||||
lower.starts_with("chrome:") ||
|
||||
lower.starts_with("nebula://");
|
||||
}
|
||||
|
||||
std::filesystem::path PathFromUtf8(const std::string& value) {
|
||||
#if defined(_WIN32)
|
||||
const int size = MultiByteToWideChar(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
|
||||
if (size <= 0) {
|
||||
return std::filesystem::path(value);
|
||||
}
|
||||
|
||||
std::wstring wide(size, L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), wide.data(), size);
|
||||
return std::filesystem::path(wide);
|
||||
#else
|
||||
return std::filesystem::path(value);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string NormalizeLaunchTarget(std::string target) {
|
||||
target = Trim(std::move(target));
|
||||
if (target.empty() || nebula::ui::IsChromiumNewTabUrl(target)) {
|
||||
return target.empty() ? std::string{} : nebula::ui::GetHomeUrl();
|
||||
}
|
||||
|
||||
if (StartsWithKnownScheme(target)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = PathFromUtf8(target);
|
||||
std::error_code ec;
|
||||
if (!path.empty() && std::filesystem::exists(path, ec) && !ec) {
|
||||
const std::filesystem::path absolute_path = std::filesystem::absolute(path, ec);
|
||||
return nebula::ui::FilePathToUrl(ec ? path : absolute_path);
|
||||
}
|
||||
|
||||
return nebula::browser::NormalizeNavigationInput(target);
|
||||
}
|
||||
|
||||
std::string GetLaunchTarget(CefRefPtr<CefCommandLine> command_line) {
|
||||
if (!command_line) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string target = command_line->GetSwitchValue("url");
|
||||
if (!target.empty()) {
|
||||
return NormalizeLaunchTarget(std::move(target));
|
||||
}
|
||||
|
||||
std::vector<CefString> arguments;
|
||||
command_line->GetArguments(arguments);
|
||||
for (const auto& argument : arguments) {
|
||||
target = argument.ToString();
|
||||
if (!Trim(target).empty()) {
|
||||
return NormalizeLaunchTarget(std::move(target));
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options) {
|
||||
nebula::platform::PrepareApp();
|
||||
|
||||
const CefMainArgs main_args = nebula::platform::MakeMainArgs(startup);
|
||||
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
|
||||
|
||||
const int subprocess_exit_code = CefExecuteProcess(main_args, app, nullptr);
|
||||
if (subprocess_exit_code >= 0) {
|
||||
return subprocess_exit_code;
|
||||
}
|
||||
|
||||
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
||||
nebula::platform::InitCommandLine(command_line, startup);
|
||||
std::string initial_url = GetLaunchTarget(command_line);
|
||||
|
||||
if (!nebula::platform::TryAcquireSingleInstance(initial_url)) {
|
||||
return 0;
|
||||
}
|
||||
nebula::platform::EnsureDefaultBrowserRegistration();
|
||||
|
||||
CefSettings settings;
|
||||
settings.no_sandbox = true;
|
||||
settings.persist_session_cookies = true;
|
||||
nebula::platform::ConfigureCefSettings(settings);
|
||||
|
||||
if (!CefInitialize(main_args, settings, app, nullptr)) {
|
||||
return CefGetExitCode();
|
||||
}
|
||||
|
||||
NebulaController controller(startup, std::move(initial_url), options);
|
||||
const bool created = controller.Create();
|
||||
if (created) {
|
||||
CefRunMessageLoop();
|
||||
}
|
||||
|
||||
CefShutdown();
|
||||
return created ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "platform/types.h"
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
enum class AppMode {
|
||||
Desktop,
|
||||
BigPicture,
|
||||
};
|
||||
|
||||
struct LaunchOptions {
|
||||
AppMode mode = AppMode::Desktop;
|
||||
};
|
||||
|
||||
int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options = {});
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "browser/session_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxRestoredTabs = 50;
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::optional<size_t> ReadUnsignedValue(const std::string& json, std::string_view key) {
|
||||
const size_t key_pos = json.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t colon = json.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
++colon;
|
||||
while (colon < json.size() && std::isspace(static_cast<unsigned char>(json[colon]))) {
|
||||
++colon;
|
||||
}
|
||||
|
||||
size_t end = colon;
|
||||
while (end < json.size() && std::isdigit(static_cast<unsigned char>(json[end]))) {
|
||||
++end;
|
||||
}
|
||||
|
||||
size_t value = 0;
|
||||
const auto result = std::from_chars(json.data() + colon, json.data() + end, value);
|
||||
if (result.ec != std::errc{} || result.ptr != json.data() + end) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
std::optional<std::string> ReadStringValue(const std::string& object, std::string_view key) {
|
||||
const size_t key_pos = object.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t colon = object.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t quote = object.find('"', colon + 1);
|
||||
if (quote == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
for (size_t i = quote + 1; i < object.size(); ++i) {
|
||||
const char ch = object[i];
|
||||
if (ch == '"') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (ch != '\\') {
|
||||
value += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++i >= object.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (object[i]) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
value += object[i];
|
||||
break;
|
||||
case 'b':
|
||||
value += '\b';
|
||||
break;
|
||||
case 'f':
|
||||
value += '\f';
|
||||
break;
|
||||
case 'n':
|
||||
value += '\n';
|
||||
break;
|
||||
case 'r':
|
||||
value += '\r';
|
||||
break;
|
||||
case 't':
|
||||
value += '\t';
|
||||
break;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<PersistedTab> ReadTabs(const std::string& json) {
|
||||
std::vector<PersistedTab> tabs;
|
||||
const size_t tabs_pos = json.find("\"tabs\"");
|
||||
if (tabs_pos == std::string::npos) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
const size_t array_start = json.find('[', tabs_pos);
|
||||
const size_t array_end = json.find(']', array_start == std::string::npos ? tabs_pos : array_start);
|
||||
if (array_start == std::string::npos || array_end == std::string::npos) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
size_t cursor = array_start + 1;
|
||||
while (cursor < array_end && tabs.size() < kMaxRestoredTabs) {
|
||||
const size_t object_start = json.find('{', cursor);
|
||||
if (object_start == std::string::npos || object_start >= array_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t object_end = json.find('}', object_start + 1);
|
||||
if (object_end == std::string::npos || object_end > array_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const std::string object = json.substr(object_start, object_end - object_start + 1);
|
||||
const auto url = ReadStringValue(object, "\"url\"");
|
||||
if (url && !url->empty()) {
|
||||
PersistedTab tab;
|
||||
tab.url = *url;
|
||||
if (const auto title = ReadStringValue(object, "\"title\""); title && !title->empty()) {
|
||||
tab.title = *title;
|
||||
}
|
||||
tabs.push_back(std::move(tab));
|
||||
}
|
||||
|
||||
cursor = object_end + 1;
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionState LoadSessionState() {
|
||||
SessionState state;
|
||||
const std::string json = ReadFile(nebula::ui::GetSessionStatePath());
|
||||
if (json.empty()) {
|
||||
return state;
|
||||
}
|
||||
|
||||
state.tabs = ReadTabs(json);
|
||||
if (const auto active_index = ReadUnsignedValue(json, "\"activeTabIndex\"")) {
|
||||
state.active_tab_index = *active_index;
|
||||
}
|
||||
|
||||
if (!state.tabs.empty()) {
|
||||
state.active_tab_index = std::min(state.active_tab_index, state.tabs.size() - 1);
|
||||
} else {
|
||||
state.active_tab_index = 0;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index) {
|
||||
const auto path = nebula::ui::GetSessionStatePath();
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\n \"activeTabIndex\": " << active_tab_index << ",\n \"tabs\": [\n";
|
||||
|
||||
bool wrote_tab = false;
|
||||
for (const auto& tab : tabs) {
|
||||
if (tab.url.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wrote_tab) {
|
||||
json << ",\n";
|
||||
}
|
||||
|
||||
json << " {\"url\": \"" << JsonEscape(tab.url)
|
||||
<< "\", \"title\": \"" << JsonEscape(tab.title) << "\"}";
|
||||
wrote_tab = true;
|
||||
}
|
||||
|
||||
json << "\n ]\n}\n";
|
||||
|
||||
std::filesystem::path temp_path = path;
|
||||
temp_path += L".tmp";
|
||||
{
|
||||
std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
output << json.str();
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
ec.clear();
|
||||
std::filesystem::rename(temp_path, path, ec);
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
struct PersistedTab {
|
||||
std::string url;
|
||||
std::string title = "New Tab";
|
||||
};
|
||||
|
||||
struct SessionState {
|
||||
std::vector<PersistedTab> tabs;
|
||||
size_t active_tab_index = 0;
|
||||
};
|
||||
|
||||
SessionState LoadSessionState();
|
||||
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index);
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "browser/tab.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
bool NebulaTab::CanGoBack() const {
|
||||
return browser && browser->CanGoBack();
|
||||
}
|
||||
|
||||
bool NebulaTab::CanGoForward() const {
|
||||
return browser && browser->CanGoForward();
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "include/cef_browser.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
struct NebulaTab {
|
||||
int id = 1;
|
||||
std::string url;
|
||||
std::string title = "New Tab";
|
||||
bool is_loading = false;
|
||||
double load_progress = 0.0;
|
||||
std::string favicon_url;
|
||||
CefRefPtr<CefBrowser> browser;
|
||||
|
||||
bool CanGoBack() const;
|
||||
bool CanGoForward() const;
|
||||
};
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "browser/tab_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
TabManager::TabManager(TabObserver* observer) : observer_(observer) {}
|
||||
|
||||
NebulaTab& TabManager::CreateInitialTab(std::string initial_url) {
|
||||
tabs_.clear();
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = std::move(initial_url);
|
||||
tabs_.push_back(std::move(tab));
|
||||
active_tab_id_ = tabs_.front().id;
|
||||
Notify();
|
||||
return tabs_.front();
|
||||
}
|
||||
|
||||
NebulaTab& TabManager::CreateTab(std::string url) {
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = std::move(url);
|
||||
tabs_.push_back(std::move(tab));
|
||||
active_tab_id_ = tabs_.back().id;
|
||||
Notify();
|
||||
return tabs_.back();
|
||||
}
|
||||
|
||||
void TabManager::RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index) {
|
||||
tabs_.clear();
|
||||
active_tab_id_ = 0;
|
||||
|
||||
for (const auto& restored_tab : tabs) {
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = restored_tab.url.empty() ? nebula::ui::GetHomeUrl() : restored_tab.url;
|
||||
tab.title = restored_tab.title.empty() ? "New Tab" : restored_tab.title;
|
||||
tabs_.push_back(std::move(tab));
|
||||
}
|
||||
|
||||
if (tabs_.empty()) {
|
||||
CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
active_tab_index = std::min(active_tab_index, tabs_.size() - 1);
|
||||
active_tab_id_ = tabs_[active_tab_index].id;
|
||||
Notify();
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::ActiveTab() {
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.id == active_tab_id_) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const NebulaTab* TabManager::ActiveTab() const {
|
||||
for (const auto& tab : tabs_) {
|
||||
if (tab.id == active_tab_id_) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<NebulaTab>& TabManager::Tabs() const {
|
||||
return tabs_;
|
||||
}
|
||||
|
||||
size_t TabManager::ActiveTabIndex() const {
|
||||
for (size_t i = 0; i < tabs_.size(); ++i) {
|
||||
if (tabs_[i].id == active_tab_id_) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool TabManager::ActivateTab(int tab_id) {
|
||||
if (!FindTab(tab_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
active_tab_id_ = tab_id;
|
||||
Notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowser> TabManager::CloseTab(int tab_id) {
|
||||
for (auto it = tabs_.begin(); it != tabs_.end(); ++it) {
|
||||
if (it->id != tab_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CefRefPtr<CefBrowser> browser = it->browser;
|
||||
const bool was_active = it->id == active_tab_id_;
|
||||
const auto next_it = tabs_.erase(it);
|
||||
|
||||
if (tabs_.empty()) {
|
||||
active_tab_id_ = 0;
|
||||
} else if (was_active) {
|
||||
active_tab_id_ = next_it != tabs_.end() ? next_it->id : tabs_.back().id;
|
||||
}
|
||||
|
||||
Notify();
|
||||
return browser;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TabManager::SetActiveBrowser(CefRefPtr<CefBrowser> browser) {
|
||||
if (NebulaTab* tab = ActiveTab()) {
|
||||
tab->browser = browser;
|
||||
if (browser && tab->url.empty()) {
|
||||
tab->url = browser->GetMainFrame()->GetURL();
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
bool TabManager::OwnsBrowser(CefRefPtr<CefBrowser> browser) const {
|
||||
if (!browser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& tab : tabs_) {
|
||||
if (tab.browser && tab.browser->IsSame(browser)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TabManager::HasOpenBrowsers() const {
|
||||
for (const auto& tab : tabs_) {
|
||||
if (tab.browser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TabManager::ClearBrowser(CefRefPtr<CefBrowser> browser) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->browser = nullptr;
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::LoadURL(const std::string& input) {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (!tab || !tab->browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string target = NormalizeNavigationInput(input);
|
||||
if (target.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tab->url = target;
|
||||
tab->favicon_url.clear();
|
||||
tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
|
||||
Notify();
|
||||
}
|
||||
|
||||
void TabManager::GoBack() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser && tab->browser->CanGoBack()) {
|
||||
tab->browser->GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::GoForward() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser && tab->browser->CanGoForward()) {
|
||||
tab->browser->GoForward();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::Reload() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser) {
|
||||
tab->browser->Reload();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::StopLoad() {
|
||||
NebulaTab* tab = ActiveTab();
|
||||
if (tab && tab->browser) {
|
||||
tab->browser->StopLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateURL(CefRefPtr<CefBrowser> browser, std::string url) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->url = std::move(url);
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->title = title.empty() ? "New Tab" : std::move(title);
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->is_loading = is_loading;
|
||||
if (is_loading) {
|
||||
tab->favicon_url.clear();
|
||||
}
|
||||
if (!is_loading) {
|
||||
tab->load_progress = 1.0;
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->load_progress = progress;
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
|
||||
if (NebulaTab* tab = FindTab(browser)) {
|
||||
tab->favicon_url = urls.empty() ? std::string{} : urls.front();
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
|
||||
void TabManager::Notify() {
|
||||
const NebulaTab* tab = ActiveTab();
|
||||
if (observer_ && tab) {
|
||||
observer_->OnActiveTabChanged(*tab);
|
||||
}
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::FindTab(int tab_id) {
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.id == tab_id) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::FindTab(CefRefPtr<CefBrowser> browser) {
|
||||
if (!browser) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.browser && tab.browser->IsSame(browser)) {
|
||||
return &tab;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.h"
|
||||
#include "browser/session_state.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
class TabObserver {
|
||||
public:
|
||||
virtual ~TabObserver() = default;
|
||||
virtual void OnActiveTabChanged(const NebulaTab& tab) = 0;
|
||||
};
|
||||
|
||||
class TabManager {
|
||||
public:
|
||||
explicit TabManager(TabObserver* observer);
|
||||
|
||||
NebulaTab& CreateInitialTab(std::string initial_url);
|
||||
NebulaTab& CreateTab(std::string url);
|
||||
void RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index);
|
||||
NebulaTab* ActiveTab();
|
||||
const NebulaTab* ActiveTab() const;
|
||||
const std::vector<NebulaTab>& Tabs() const;
|
||||
size_t ActiveTabIndex() const;
|
||||
|
||||
bool ActivateTab(int tab_id);
|
||||
CefRefPtr<CefBrowser> CloseTab(int tab_id);
|
||||
void SetActiveBrowser(CefRefPtr<CefBrowser> browser);
|
||||
bool OwnsBrowser(CefRefPtr<CefBrowser> browser) const;
|
||||
void ClearBrowser(CefRefPtr<CefBrowser> browser);
|
||||
bool HasOpenBrowsers() const;
|
||||
|
||||
void LoadURL(const std::string& input);
|
||||
void GoBack();
|
||||
void GoForward();
|
||||
void Reload();
|
||||
void StopLoad();
|
||||
|
||||
void UpdateURL(CefRefPtr<CefBrowser> browser, std::string url);
|
||||
void UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title);
|
||||
void UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading);
|
||||
void UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress);
|
||||
void UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls);
|
||||
|
||||
private:
|
||||
void Notify();
|
||||
NebulaTab* FindTab(int tab_id);
|
||||
NebulaTab* FindTab(CefRefPtr<CefBrowser> browser);
|
||||
|
||||
TabObserver* observer_ = nullptr;
|
||||
std::vector<NebulaTab> tabs_;
|
||||
int active_tab_id_ = 0;
|
||||
int next_tab_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "browser/url_utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace nebula::browser {
|
||||
namespace {
|
||||
|
||||
constexpr char kSearchUrl[] = "https://www.google.com/search?q=";
|
||||
|
||||
std::string Trim(std::string value) {
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) {
|
||||
value.erase(value.begin());
|
||||
}
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) {
|
||||
value.pop_back();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool StartsWithScheme(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value.starts_with("http://") ||
|
||||
value.starts_with("https://") ||
|
||||
value.starts_with("file:") ||
|
||||
value.starts_with("data:") ||
|
||||
value.starts_with("blob:") ||
|
||||
value.starts_with("chrome:") ||
|
||||
value.starts_with("nebula://");
|
||||
}
|
||||
|
||||
bool LooksLikeHostName(const std::string& value) {
|
||||
return value.find('.') != std::string::npos &&
|
||||
value.find_first_of(" \t\r\n") == std::string::npos;
|
||||
}
|
||||
|
||||
std::string UrlEncodeSearch(const std::string& value) {
|
||||
std::ostringstream encoded;
|
||||
encoded << std::hex << std::uppercase;
|
||||
|
||||
for (unsigned char ch : value) {
|
||||
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
|
||||
encoded << static_cast<char>(ch);
|
||||
} else if (ch == ' ') {
|
||||
encoded << '+';
|
||||
} else {
|
||||
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
|
||||
}
|
||||
}
|
||||
|
||||
return encoded.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string NormalizeNavigationInput(const std::string& input) {
|
||||
const std::string value = Trim(input);
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (StartsWithScheme(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (LooksLikeHostName(value)) {
|
||||
return "https://" + value;
|
||||
}
|
||||
|
||||
return std::string(kSearchUrl) + UrlEncodeSearch(value);
|
||||
}
|
||||
|
||||
std::string JsonEscape(const std::string& value) {
|
||||
std::ostringstream escaped;
|
||||
for (unsigned char ch : value) {
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
escaped << "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escaped << "\\\"";
|
||||
break;
|
||||
case '\b':
|
||||
escaped << "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
escaped << "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
escaped << "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escaped << "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escaped << "\\t";
|
||||
break;
|
||||
default:
|
||||
if (ch < 0x20) {
|
||||
escaped << "\\u" << std::hex << std::uppercase << std::setw(4)
|
||||
<< std::setfill('0') << static_cast<int>(ch);
|
||||
} else {
|
||||
escaped << static_cast<char>(ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return escaped.str();
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
std::string NormalizeNavigationInput(const std::string& input);
|
||||
std::string JsonEscape(const std::string& value);
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "cef/browser_client.h"
|
||||
|
||||
#include "include/cef_request.h"
|
||||
#include "platform/types.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
namespace {
|
||||
|
||||
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
|
||||
|
||||
bool IsInsecureInterstitialFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure");
|
||||
}
|
||||
|
||||
bool IsSettingsFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings");
|
||||
}
|
||||
|
||||
bool IsSetupFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://setup");
|
||||
}
|
||||
|
||||
bool IsBigPictureFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string url = nebula::ui::ToInternalUrl(frame->GetURL().ToString());
|
||||
return url.starts_with("nebula://bigpicture") ||
|
||||
url.starts_with("nebula://big-picture");
|
||||
}
|
||||
|
||||
bool IsBigPictureCommand(const std::string& command) {
|
||||
return command == "navigate" ||
|
||||
command == "new-tab" ||
|
||||
command == "activate-tab" ||
|
||||
command == "close-tab" ||
|
||||
command == "back" ||
|
||||
command == "forward" ||
|
||||
command == "reload" ||
|
||||
command == "stop" ||
|
||||
command == "home" ||
|
||||
command == "settings" ||
|
||||
command == "open-settings" ||
|
||||
command == "big-picture" ||
|
||||
command == "exit-bigpicture" ||
|
||||
command == "gpu-diagnostics" ||
|
||||
command == "zoom-out" ||
|
||||
command == "zoom-in" ||
|
||||
command == "clear-site-history" ||
|
||||
command == "clear-search-history" ||
|
||||
command == "bigpicture-mouse-move" ||
|
||||
command == "bigpicture-click" ||
|
||||
command == "bigpicture-right-click" ||
|
||||
command == "bigpicture-scroll" ||
|
||||
command == "bigpicture-text" ||
|
||||
command == "bigpicture-browse-visible";
|
||||
}
|
||||
|
||||
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
|
||||
std::vector<std::string> result;
|
||||
result.reserve(values.size());
|
||||
for (const auto& value : values) {
|
||||
result.push_back(value.ToString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NebulaBrowserClient::NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate)
|
||||
: role_(role), delegate_(delegate) {}
|
||||
|
||||
bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefProcessId source_process,
|
||||
CefRefPtr<CefProcessMessage> message) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(browser);
|
||||
NEBULA_UNUSED(source_process);
|
||||
|
||||
if (!message || message->GetName().ToString() != kChromeCommandMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::Content &&
|
||||
role_ != BrowserRole::BigPicture && role_ != BrowserRole::MenuPopup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CefRefPtr<CefListValue> args = message->GetArgumentList();
|
||||
const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : "";
|
||||
const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : "";
|
||||
if (role_ == BrowserRole::Content) {
|
||||
const bool allowed_insecure_command =
|
||||
command == "navigate-insecure" && IsInsecureInterstitialFrame(frame);
|
||||
const bool allowed_settings_command =
|
||||
IsSettingsFrame(frame) && (command == "navigate" ||
|
||||
command == "new-tab" ||
|
||||
command == "check-default-browser" ||
|
||||
command == "set-default-browser" ||
|
||||
command == "clear-site-history" ||
|
||||
command == "clear-search-history");
|
||||
const bool allowed_setup_command =
|
||||
IsSetupFrame(frame) && (command == "complete-first-run" ||
|
||||
command == "check-default-browser" ||
|
||||
command == "set-default-browser");
|
||||
const bool allowed_big_picture_command =
|
||||
IsBigPictureFrame(frame) && command == "exit-bigpicture";
|
||||
if (!allowed_insecure_command && !allowed_settings_command &&
|
||||
!allowed_setup_command && !allowed_big_picture_command) {
|
||||
return false;
|
||||
}
|
||||
} else if (role_ == BrowserRole::BigPicture) {
|
||||
if (!IsBigPictureFrame(frame) || !IsBigPictureCommand(command)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (delegate_ && !command.empty()) {
|
||||
delegate_->OnChromeCommand(command, payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
const CefString& url) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
delegate_->OnContentAddressChanged(browser, url.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||
const CefString& title) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentTitleChanged(browser, title.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||
const std::vector<CefString>& icon_urls) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentFaviconChanged(browser, ToStringVector(icon_urls));
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentFullscreenChanged(browser, fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
CefEventHandle os_event,
|
||||
bool* is_keyboard_shortcut) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(os_event);
|
||||
|
||||
if (role_ == BrowserRole::Content &&
|
||||
event.type == KEYEVENT_RAWKEYDOWN &&
|
||||
(event.modifiers & EVENTFLAG_CONTROL_DOWN) != 0 &&
|
||||
event.windows_key_code == 'T') {
|
||||
if (is_keyboard_shortcut) {
|
||||
*is_keyboard_shortcut = true;
|
||||
}
|
||||
if (delegate_) {
|
||||
delegate_->OnPopupRequested(browser, nebula::ui::GetHomeUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int popup_id,
|
||||
const CefString& target_url,
|
||||
const CefString& target_frame_name,
|
||||
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||
bool user_gesture,
|
||||
const CefPopupFeatures& popupFeatures,
|
||||
CefWindowInfo& windowInfo,
|
||||
CefRefPtr<CefClient>& client,
|
||||
CefBrowserSettings& settings,
|
||||
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||
bool* no_javascript_access) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(frame);
|
||||
NEBULA_UNUSED(popup_id);
|
||||
NEBULA_UNUSED(target_frame_name);
|
||||
NEBULA_UNUSED(target_disposition);
|
||||
NEBULA_UNUSED(user_gesture);
|
||||
NEBULA_UNUSED(popupFeatures);
|
||||
NEBULA_UNUSED(windowInfo);
|
||||
NEBULA_UNUSED(client);
|
||||
NEBULA_UNUSED(settings);
|
||||
NEBULA_UNUSED(extra_info);
|
||||
NEBULA_UNUSED(no_javascript_access);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnPopupRequested(browser, target_url.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (delegate_) {
|
||||
delegate_->OnBrowserCreated(role_, browser);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (delegate_) {
|
||||
delegate_->OnBrowserClosing(role_, browser);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
|
||||
bool isLoading,
|
||||
bool canGoBack,
|
||||
bool canGoForward) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(canGoBack);
|
||||
NEBULA_UNUSED(canGoForward);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentLoadingStateChanged(browser, isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnLoadStart(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
TransitionType transition_type) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(transition_type);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
delegate_->OnContentLoadProgressChanged(browser, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int httpStatusCode) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
if (httpStatusCode == 404) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(
|
||||
nebula::ui::GetNotFoundUrl(frame->GetURL().ToString())));
|
||||
return;
|
||||
}
|
||||
|
||||
delegate_->OnContentLoadProgressChanged(browser, 1.0);
|
||||
delegate_->OnContentLoadFinished(browser, frame->GetURL().ToString());
|
||||
}
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefRequest> request,
|
||||
bool user_gesture,
|
||||
bool is_redirect) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(browser);
|
||||
NEBULA_UNUSED(user_gesture);
|
||||
NEBULA_UNUSED(is_redirect);
|
||||
|
||||
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request) {
|
||||
const std::string url = request->GetURL().ToString();
|
||||
if (nebula::ui::IsChromiumNewTabUrl(url)) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(nebula::ui::GetHomeUrl()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nebula::ui::IsNebulaInternalUrl(url)) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(url));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nebula::ui::IsHttpUrl(url) &&
|
||||
(!delegate_ || !delegate_->ShouldBypassInsecureWarning(browser, url))) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(
|
||||
nebula::ui::GetInsecureWarningUrl(url)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnShowPermissionPrompt(
|
||||
CefRefPtr<CefBrowser> browser,
|
||||
uint64_t prompt_id,
|
||||
const CefString& requesting_origin,
|
||||
uint32_t requested_permissions,
|
||||
CefRefPtr<CefPermissionPromptCallback> callback) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
NEBULA_UNUSED(prompt_id);
|
||||
NEBULA_UNUSED(requesting_origin);
|
||||
|
||||
if (role_ == BrowserRole::Content &&
|
||||
(requested_permissions & CEF_PERMISSION_TYPE_GEOLOCATION) != 0 &&
|
||||
browser && callback &&
|
||||
nebula::ui::IsInternalHomeUrl(browser->GetMainFrame()->GetURL().ToString())) {
|
||||
callback->Continue(CEF_PERMISSION_RESULT_ACCEPT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "include/cef_client.h"
|
||||
#include "include/cef_display_handler.h"
|
||||
#include "include/cef_keyboard_handler.h"
|
||||
#include "include/cef_life_span_handler.h"
|
||||
#include "include/cef_load_handler.h"
|
||||
#include "include/cef_permission_handler.h"
|
||||
#include "include/cef_request_handler.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
|
||||
enum class BrowserRole {
|
||||
Chrome,
|
||||
Content,
|
||||
BigPicture,
|
||||
MenuPopup,
|
||||
};
|
||||
|
||||
class BrowserClientDelegate {
|
||||
public:
|
||||
virtual ~BrowserClientDelegate() = default;
|
||||
virtual void OnBrowserCreated(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
|
||||
virtual void OnBrowserClosing(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
|
||||
virtual void OnChromeCommand(const std::string& command, const std::string& payload) = 0;
|
||||
virtual void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
|
||||
virtual void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) = 0;
|
||||
virtual void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) = 0;
|
||||
virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0;
|
||||
virtual void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
|
||||
virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0;
|
||||
virtual void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) = 0;
|
||||
virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
|
||||
virtual bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
|
||||
};
|
||||
|
||||
class NebulaBrowserClient final : public CefClient,
|
||||
public CefDisplayHandler,
|
||||
public CefKeyboardHandler,
|
||||
public CefLifeSpanHandler,
|
||||
public CefLoadHandler,
|
||||
public CefPermissionHandler,
|
||||
public CefRequestHandler {
|
||||
public:
|
||||
NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate);
|
||||
|
||||
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
|
||||
CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override { return this; }
|
||||
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
|
||||
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
|
||||
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override { return this; }
|
||||
CefRefPtr<CefRequestHandler> GetRequestHandler() override { return this; }
|
||||
|
||||
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefProcessId source_process,
|
||||
CefRefPtr<CefProcessMessage> message) override;
|
||||
|
||||
void OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
const CefString& url) override;
|
||||
void OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||
const CefString& title) override;
|
||||
void OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||
const std::vector<CefString>& icon_urls) override;
|
||||
void OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
|
||||
|
||||
bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
CefEventHandle os_event,
|
||||
bool* is_keyboard_shortcut) override;
|
||||
|
||||
bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int popup_id,
|
||||
const CefString& target_url,
|
||||
const CefString& target_frame_name,
|
||||
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||
bool user_gesture,
|
||||
const CefPopupFeatures& popupFeatures,
|
||||
CefWindowInfo& windowInfo,
|
||||
CefRefPtr<CefClient>& client,
|
||||
CefBrowserSettings& settings,
|
||||
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||
bool* no_javascript_access) override;
|
||||
|
||||
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
|
||||
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
|
||||
|
||||
void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
|
||||
bool isLoading,
|
||||
bool canGoBack,
|
||||
bool canGoForward) override;
|
||||
void OnLoadStart(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
TransitionType transition_type) override;
|
||||
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int httpStatusCode) override;
|
||||
|
||||
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefRequest> request,
|
||||
bool user_gesture,
|
||||
bool is_redirect) override;
|
||||
|
||||
bool OnShowPermissionPrompt(CefRefPtr<CefBrowser> browser,
|
||||
uint64_t prompt_id,
|
||||
const CefString& requesting_origin,
|
||||
uint32_t requested_permissions,
|
||||
CefRefPtr<CefPermissionPromptCallback> callback) override;
|
||||
|
||||
private:
|
||||
BrowserRole role_;
|
||||
BrowserClientDelegate* delegate_ = nullptr;
|
||||
|
||||
IMPLEMENT_REFCOUNTING(NebulaBrowserClient);
|
||||
};
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "cef/nebula_app.h"
|
||||
|
||||
#include "include/cef_process_message.h"
|
||||
#include "platform/types.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
namespace {
|
||||
|
||||
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
|
||||
|
||||
class NativeBridgeHandler final : public CefV8Handler {
|
||||
public:
|
||||
bool Execute(const CefString& name,
|
||||
CefRefPtr<CefV8Value> object,
|
||||
const CefV8ValueList& arguments,
|
||||
CefRefPtr<CefV8Value>& retval,
|
||||
CefString& exception) override {
|
||||
NEBULA_UNUSED(object);
|
||||
NEBULA_UNUSED(retval);
|
||||
|
||||
if (name != "postMessage" && name != "sendToHost" && name != "send") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arguments.empty() || !arguments[0]->IsString()) {
|
||||
exception = "nebulaNative.postMessage requires a command string.";
|
||||
return true;
|
||||
}
|
||||
|
||||
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
|
||||
CefRefPtr<CefBrowser> browser = context ? context->GetBrowser() : nullptr;
|
||||
CefRefPtr<CefFrame> frame = context ? context->GetFrame() : nullptr;
|
||||
if (!browser || !frame) {
|
||||
exception = "No CEF frame is available for native messaging.";
|
||||
return true;
|
||||
}
|
||||
|
||||
CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kChromeCommandMessage);
|
||||
CefRefPtr<CefListValue> args = message->GetArgumentList();
|
||||
args->SetString(0, arguments[0]->GetStringValue());
|
||||
args->SetString(1, arguments.size() > 1 && arguments[1]->IsString()
|
||||
? arguments[1]->GetStringValue()
|
||||
: CefString());
|
||||
|
||||
frame->SendProcessMessage(PID_BROWSER, message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
IMPLEMENT_REFCOUNTING(NativeBridgeHandler);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||
CefRefPtr<CefCommandLine> command_line) {
|
||||
NEBULA_UNUSED(process_type);
|
||||
|
||||
// The bundled UI is loaded from file:// and uses ES modules.
|
||||
command_line->AppendSwitch("allow-file-access-from-files");
|
||||
|
||||
// CefSettings.no_sandbox disables the browser-level sandbox, but Chromium
|
||||
// still attempts to bring up a separate GPU sandbox inside the GPU process.
|
||||
// Without the host-side sandbox plumbing this fails with STATUS_BREAKPOINT
|
||||
// (-2147483645) immediately on startup, which is exactly what the GPU
|
||||
// diagnostics page was showing - the GPU process crashed three times and
|
||||
// Chromium then fell back to software rendering. Disabling the GPU sandbox
|
||||
// matches the rest of our no_sandbox configuration and lets the GPU
|
||||
// process initialize.
|
||||
command_line->AppendSwitch("no-sandbox");
|
||||
command_line->AppendSwitch("disable-gpu-sandbox");
|
||||
command_line->AppendSwitch("in-process-gpu");
|
||||
|
||||
// Avoid Chromium's conservative GPU blocklist, but let Chromium choose the
|
||||
// safest graphics backend for this machine. Forcing raster/zero-copy paths
|
||||
// can prevent WebGL shared contexts from initializing on some drivers.
|
||||
command_line->AppendSwitch("ignore-gpu-blocklist");
|
||||
command_line->AppendSwitch("enable-accelerated-video-decode");
|
||||
|
||||
#if defined(_WIN32)
|
||||
command_line->AppendSwitchWithValue("use-gl", "angle");
|
||||
command_line->AppendSwitchWithValue("use-angle", "d3d11");
|
||||
#elif defined(__APPLE__)
|
||||
command_line->AppendSwitchWithValue("use-angle", "metal");
|
||||
#else
|
||||
command_line->AppendSwitchWithValue("use-gl", "egl");
|
||||
#endif
|
||||
}
|
||||
|
||||
void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefV8Context> context) {
|
||||
CEF_REQUIRE_RENDERER_THREAD();
|
||||
NEBULA_UNUSED(browser);
|
||||
NEBULA_UNUSED(frame);
|
||||
|
||||
CefRefPtr<CefV8Value> global = context->GetGlobal();
|
||||
CefRefPtr<NativeBridgeHandler> handler = new NativeBridgeHandler();
|
||||
CefRefPtr<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr);
|
||||
native->SetValue(
|
||||
"postMessage",
|
||||
CefV8Value::CreateFunction("postMessage", handler),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
global->SetValue("nebulaNative", native, V8_PROPERTY_ATTRIBUTE_READONLY);
|
||||
|
||||
CefRefPtr<CefV8Value> electron_api = CefV8Value::CreateObject(nullptr, nullptr);
|
||||
electron_api->SetValue(
|
||||
"sendToHost",
|
||||
CefV8Value::CreateFunction("sendToHost", handler),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
electron_api->SetValue(
|
||||
"send",
|
||||
CefV8Value::CreateFunction("send", handler),
|
||||
V8_PROPERTY_ATTRIBUTE_NONE);
|
||||
global->SetValue("electronAPI", electron_api, V8_PROPERTY_ATTRIBUTE_READONLY);
|
||||
}
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_render_process_handler.h"
|
||||
#include "include/cef_v8.h"
|
||||
|
||||
namespace nebula::cef {
|
||||
|
||||
class NebulaApp final : public CefApp,
|
||||
public CefRenderProcessHandler {
|
||||
public:
|
||||
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override { return this; }
|
||||
|
||||
void OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||
CefRefPtr<CefCommandLine> command_line) override;
|
||||
|
||||
void OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefV8Context> context) override;
|
||||
|
||||
private:
|
||||
IMPLEMENT_REFCOUNTING(NebulaApp);
|
||||
};
|
||||
|
||||
} // namespace nebula::cef
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "include/cef_browser.h"
|
||||
#include "platform/types.h"
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect);
|
||||
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title);
|
||||
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect);
|
||||
void SetBrowserVisible(NativeWindow browser_window, bool visible);
|
||||
void RaiseBrowserWindow(NativeWindow browser_window);
|
||||
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y);
|
||||
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout);
|
||||
std::string CacheBusterToken();
|
||||
void DestroyTopLevelWindow(NativeWindow window);
|
||||
int ScaleForParentWindow(NativeWindow parent, int value);
|
||||
std::pair<int, int> ParentClientSize(NativeWindow parent);
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
bool IsDefaultBrowser();
|
||||
bool EnsureDefaultBrowserRegistration();
|
||||
bool RequestDefaultBrowser();
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "platform/browser_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
|
||||
CefWindowInfo info;
|
||||
info.SetAsChild(
|
||||
reinterpret_cast<CefWindowHandle>(parent),
|
||||
CefRect(rect.x, rect.y, rect.width, rect.height));
|
||||
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
return info;
|
||||
}
|
||||
|
||||
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title) {
|
||||
CefWindowInfo info;
|
||||
info.SetAsPopup(reinterpret_cast<CefWindowHandle>(parent), title);
|
||||
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
return info;
|
||||
}
|
||||
|
||||
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) {
|
||||
NEBULA_UNUSED(browser_window);
|
||||
NEBULA_UNUSED(rect);
|
||||
}
|
||||
|
||||
void SetBrowserVisible(NativeWindow browser_window, bool visible) {
|
||||
NEBULA_UNUSED(browser_window);
|
||||
NEBULA_UNUSED(visible);
|
||||
}
|
||||
|
||||
void RaiseBrowserWindow(NativeWindow browser_window) {
|
||||
NEBULA_UNUSED(browser_window);
|
||||
}
|
||||
|
||||
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
|
||||
NEBULA_UNUSED(browser_window);
|
||||
NEBULA_UNUSED(x);
|
||||
NEBULA_UNUSED(y);
|
||||
}
|
||||
|
||||
int ScaleForParentWindow(NativeWindow parent, int value) {
|
||||
NEBULA_UNUSED(parent);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::pair<int, int> ParentClientSize(NativeWindow parent) {
|
||||
NEBULA_UNUSED(parent);
|
||||
return {1280, 720};
|
||||
}
|
||||
|
||||
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) {
|
||||
const auto client_size = ParentClientSize(parent);
|
||||
const int width = 260;
|
||||
const int height = 258;
|
||||
const int margin = 12;
|
||||
const int overlap = 2;
|
||||
const int x = std::max(0, client_size.first - width - margin);
|
||||
const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap);
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
std::min(client_size.first, x + width) - x,
|
||||
std::min(client_size.second, y + height) - y,
|
||||
};
|
||||
}
|
||||
|
||||
std::string CacheBusterToken() {
|
||||
return "0";
|
||||
}
|
||||
|
||||
void DestroyTopLevelWindow(NativeWindow window) {
|
||||
NEBULA_UNUSED(window);
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "platform/default_browser.h"
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
bool IsDefaultBrowser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EnsureDefaultBrowserRegistration() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RequestDefaultBrowser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "window/nebula_window.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace nebula::window {
|
||||
|
||||
struct nebula::window::NebulaWindowImpl {
|
||||
WindowDelegate* delegate = nullptr;
|
||||
};
|
||||
|
||||
NebulaWindow::NebulaWindow(WindowDelegate* delegate)
|
||||
: impl_(std::make_unique<NebulaWindowImpl>()) {
|
||||
impl_->delegate = delegate;
|
||||
}
|
||||
|
||||
NebulaWindow::~NebulaWindow() = default;
|
||||
|
||||
bool NebulaWindow::Create(const platform::AppStartup& startup) {
|
||||
NEBULA_UNUSED(startup);
|
||||
return false;
|
||||
}
|
||||
|
||||
platform::NativeWindow NebulaWindow::native_handle() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
|
||||
NEBULA_UNUSED(show_chrome);
|
||||
return {};
|
||||
}
|
||||
|
||||
void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const {
|
||||
NEBULA_UNUSED(child);
|
||||
NEBULA_UNUSED(rect);
|
||||
}
|
||||
|
||||
void NebulaWindow::Minimize() {}
|
||||
void NebulaWindow::ToggleMaximize() {}
|
||||
void NebulaWindow::SetFullscreen(bool fullscreen) { NEBULA_UNUSED(fullscreen); }
|
||||
void NebulaWindow::Close() {}
|
||||
void NebulaWindow::BeginDrag() {}
|
||||
void NebulaWindow::SetTitle(const std::string& title) { NEBULA_UNUSED(title); }
|
||||
void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const { NEBULA_UNUSED(child); }
|
||||
|
||||
} // namespace nebula::window
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
std::filesystem::path ExecutableDirectory() {
|
||||
char buffer[4096] = {};
|
||||
const ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
buffer[length] = '\0';
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
}
|
||||
|
||||
std::filesystem::path DefaultUserDataRoot() {
|
||||
if (const char* xdg_data = std::getenv("XDG_DATA_HOME"); xdg_data && *xdg_data) {
|
||||
return std::filesystem::path(xdg_data);
|
||||
}
|
||||
|
||||
if (const char* home = std::getenv("HOME")) {
|
||||
return std::filesystem::path(home) / ".local" / "share";
|
||||
}
|
||||
|
||||
if (passwd* pw = getpwuid(getuid())) {
|
||||
return std::filesystem::path(pw->pw_dir) / ".local" / "share";
|
||||
}
|
||||
|
||||
return ExecutableDirectory();
|
||||
}
|
||||
|
||||
std::string PathToUtf8(const std::filesystem::path& path) {
|
||||
return path.string();
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "platform/startup.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "include/cef_command_line.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
int g_single_instance_lock = -1;
|
||||
|
||||
} // namespace
|
||||
|
||||
void PrepareApp() {}
|
||||
|
||||
bool TryAcquireSingleInstance(const std::string& launch_target) {
|
||||
NEBULA_UNUSED(launch_target);
|
||||
|
||||
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(lock_path.parent_path(), ec);
|
||||
|
||||
g_single_instance_lock = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644);
|
||||
if (g_single_instance_lock < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return flock(g_single_instance_lock, LOCK_EX | LOCK_NB) == 0;
|
||||
}
|
||||
|
||||
CefMainArgs MakeMainArgs(const AppStartup& startup) {
|
||||
return CefMainArgs(startup.argc, startup.argv);
|
||||
}
|
||||
|
||||
void InitCommandLine(CefRefPtr<CefCommandLine> command_line, const AppStartup& startup) {
|
||||
command_line->InitFromArgv(startup.argc, startup.argv);
|
||||
}
|
||||
|
||||
void ConfigureCefSettings(CefSettings& settings) {
|
||||
const std::string user_data_dir = nebula::ui::GetUserDataDirectory().string();
|
||||
const std::string cache_dir = nebula::ui::GetCacheDirectory().string();
|
||||
if (!user_data_dir.empty()) {
|
||||
CefString(&settings.root_cache_path).FromString(user_data_dir);
|
||||
}
|
||||
if (!cache_dir.empty()) {
|
||||
CefString(&settings.cache_path).FromString(cache_dir);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "platform/browser_host.h"
|
||||
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
NSView* AsView(NativeWindow window) {
|
||||
return (__bridge NSView*)window;
|
||||
}
|
||||
|
||||
NSRect ToNativeRect(const Rect& rect) {
|
||||
return NSMakeRect(rect.x, rect.y, std::max(0, rect.width), std::max(0, rect.height));
|
||||
}
|
||||
|
||||
Rect ToPlatformRect(NSRect rect) {
|
||||
return {
|
||||
static_cast<int>(std::round(NSMinX(rect))),
|
||||
static_cast<int>(std::round(NSMinY(rect))),
|
||||
std::max(0, static_cast<int>(std::round(NSWidth(rect)))),
|
||||
std::max(0, static_cast<int>(std::round(NSHeight(rect)))),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
|
||||
CefWindowInfo info;
|
||||
info.SetAsChild((__bridge CefWindowHandle)AsView(parent),
|
||||
CefRect(rect.x, rect.y, rect.width, rect.height));
|
||||
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
return info;
|
||||
}
|
||||
|
||||
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title) {
|
||||
(void)title;
|
||||
CefWindowInfo info;
|
||||
info.SetAsChild((__bridge CefWindowHandle)AsView(parent), CefRect(0, 0, 800, 600));
|
||||
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
return info;
|
||||
}
|
||||
|
||||
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) {
|
||||
NSView* view = AsView(browser_window);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
[view setFrame:ToNativeRect(rect)];
|
||||
}
|
||||
|
||||
void SetBrowserVisible(NativeWindow browser_window, bool visible) {
|
||||
NSView* view = AsView(browser_window);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
[view setHidden:!visible];
|
||||
if (visible && [view superview]) {
|
||||
[[view superview] addSubview:view positioned:NSWindowAbove relativeTo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
void RaiseBrowserWindow(NativeWindow browser_window) {
|
||||
NSView* view = AsView(browser_window);
|
||||
if (view && [view superview]) {
|
||||
[[view superview] addSubview:view positioned:NSWindowAbove relativeTo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
|
||||
NSView* view = AsView(browser_window);
|
||||
if (!view || ![view window]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const NSPoint window_point = [view convertPoint:NSMakePoint(x, y) toView:nil];
|
||||
const NSPoint screen_point = [[view window] convertPointToScreen:window_point];
|
||||
const CGFloat screen_height = NSMaxY([[NSScreen mainScreen] frame]);
|
||||
CGWarpMouseCursorPosition(CGPointMake(screen_point.x, screen_height - screen_point.y));
|
||||
}
|
||||
|
||||
int ScaleForParentWindow(NativeWindow parent, int value) {
|
||||
(void)parent;
|
||||
return value;
|
||||
}
|
||||
|
||||
std::pair<int, int> ParentClientSize(NativeWindow parent) {
|
||||
NSView* view = AsView(parent);
|
||||
if (!view) {
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
const Rect rect = ToPlatformRect([view bounds]);
|
||||
return {rect.width, rect.height};
|
||||
}
|
||||
|
||||
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) {
|
||||
const auto client_size = ParentClientSize(parent);
|
||||
const int width = 260;
|
||||
const int height = 258;
|
||||
const int margin = 12;
|
||||
const int overlap = 2;
|
||||
const int x = std::max(0, client_size.first - width - margin);
|
||||
const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap);
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
std::min(client_size.first, x + width) - x,
|
||||
std::min(client_size.second, y + height) - y,
|
||||
};
|
||||
}
|
||||
|
||||
std::string CacheBusterToken() {
|
||||
const auto now = std::chrono::system_clock::now().time_since_epoch();
|
||||
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
|
||||
return std::to_string(millis);
|
||||
}
|
||||
|
||||
void DestroyTopLevelWindow(NativeWindow window) {
|
||||
NSView* view = AsView(window);
|
||||
NSWindow* native_window = [view window];
|
||||
if (native_window) {
|
||||
[native_window setDelegate:nil];
|
||||
[native_window close];
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "platform/default_browser.h"
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
bool IsDefaultBrowser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EnsureDefaultBrowserRegistration() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RequestDefaultBrowser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,222 @@
|
||||
#include "window/nebula_window.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace nebula::window {
|
||||
struct NebulaWindowImpl;
|
||||
} // namespace nebula::window
|
||||
|
||||
@interface NebulaContentView : NSView
|
||||
@end
|
||||
|
||||
@implementation NebulaContentView
|
||||
- (BOOL)isFlipped {
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface NebulaWindowDelegate : NSObject <NSWindowDelegate> {
|
||||
@private
|
||||
nebula::window::NebulaWindowImpl* owner_;
|
||||
}
|
||||
- (instancetype)initWithOwner:(nebula::window::NebulaWindowImpl*)owner;
|
||||
@end
|
||||
|
||||
namespace nebula::window {
|
||||
namespace {
|
||||
|
||||
constexpr CGFloat kChromeHeight = 104.0;
|
||||
|
||||
NSRect ToNativeRect(const platform::Rect& rect) {
|
||||
return NSMakeRect(rect.x, rect.y, std::max(0, rect.width), std::max(0, rect.height));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct NebulaWindowImpl {
|
||||
WindowDelegate* delegate = nullptr;
|
||||
NSWindow* window = nil;
|
||||
NebulaContentView* content_view = nil;
|
||||
NebulaWindowDelegate* window_delegate = nil;
|
||||
bool fullscreen = false;
|
||||
|
||||
BrowserLayout CurrentLayout(bool show_chrome) const {
|
||||
const NSRect bounds = content_view ? [content_view bounds] : NSZeroRect;
|
||||
const int width = std::max(0, static_cast<int>(std::round(NSWidth(bounds))));
|
||||
const int height = std::max(0, static_cast<int>(std::round(NSHeight(bounds))));
|
||||
const int chrome_height = show_chrome ? std::min(height, static_cast<int>(kChromeHeight)) : 0;
|
||||
|
||||
BrowserLayout layout;
|
||||
layout.chrome = show_chrome ? platform::Rect{0, 0, width, chrome_height} : platform::Rect{};
|
||||
layout.content = {0, chrome_height, width, std::max(0, height - chrome_height)};
|
||||
return layout;
|
||||
}
|
||||
|
||||
void NotifyCreated() const {
|
||||
if (delegate) {
|
||||
delegate->OnWindowCreated();
|
||||
}
|
||||
}
|
||||
|
||||
void NotifyResized() const {
|
||||
if (delegate) {
|
||||
delegate->OnWindowResized(CurrentLayout(true));
|
||||
}
|
||||
}
|
||||
|
||||
void NotifyCloseRequested() const {
|
||||
if (delegate) {
|
||||
delegate->OnWindowCloseRequested();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace nebula::window
|
||||
|
||||
@implementation NebulaWindowDelegate
|
||||
- (instancetype)initWithOwner:(nebula::window::NebulaWindowImpl*)owner {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
owner_ = owner;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(NSNotification*)notification {
|
||||
(void)notification;
|
||||
if (owner_) {
|
||||
owner_->NotifyResized();
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)windowShouldClose:(id)sender {
|
||||
(void)sender;
|
||||
if (owner_) {
|
||||
owner_->NotifyCloseRequested();
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
@end
|
||||
|
||||
namespace nebula::window {
|
||||
|
||||
NebulaWindow::NebulaWindow(WindowDelegate* delegate)
|
||||
: impl_(std::make_unique<NebulaWindowImpl>()) {
|
||||
impl_->delegate = delegate;
|
||||
}
|
||||
|
||||
NebulaWindow::~NebulaWindow() = default;
|
||||
|
||||
bool NebulaWindow::Create(const platform::AppStartup& startup) {
|
||||
(void)startup;
|
||||
|
||||
@autoreleasepool {
|
||||
[NSApplication sharedApplication];
|
||||
|
||||
const NSRect visible_frame = [[NSScreen mainScreen] visibleFrame];
|
||||
const CGFloat width = std::min<CGFloat>(1400.0, NSWidth(visible_frame));
|
||||
const CGFloat height = std::min<CGFloat>(900.0, NSHeight(visible_frame));
|
||||
const CGFloat x = NSMinX(visible_frame) + (NSWidth(visible_frame) - width) / 2.0;
|
||||
const CGFloat y = NSMinY(visible_frame) + (NSHeight(visible_frame) - height) / 2.0;
|
||||
|
||||
impl_->content_view = [[NebulaContentView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
|
||||
impl_->window_delegate = [[NebulaWindowDelegate alloc] initWithOwner:impl_.get()];
|
||||
impl_->window = [[NSWindow alloc] initWithContentRect:NSMakeRect(x, y, width, height)
|
||||
styleMask:NSWindowStyleMaskTitled |
|
||||
NSWindowStyleMaskClosable |
|
||||
NSWindowStyleMaskMiniaturizable |
|
||||
NSWindowStyleMaskResizable |
|
||||
NSWindowStyleMaskFullSizeContentView
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
if (!impl_->window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[impl_->window setTitle:@"Nebula Browser"];
|
||||
[impl_->window setTitleVisibility:NSWindowTitleHidden];
|
||||
[impl_->window setTitlebarAppearsTransparent:YES];
|
||||
[impl_->window setContentView:impl_->content_view];
|
||||
[impl_->window setDelegate:impl_->window_delegate];
|
||||
[impl_->window center];
|
||||
[impl_->window makeKeyAndOrderFront:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
|
||||
impl_->NotifyCreated();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
platform::NativeWindow NebulaWindow::native_handle() const {
|
||||
return (__bridge void*)impl_->content_view;
|
||||
}
|
||||
|
||||
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
|
||||
return impl_->CurrentLayout(show_chrome);
|
||||
}
|
||||
|
||||
void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const {
|
||||
NSView* view = (__bridge NSView*)child;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
[view setFrame:ToNativeRect(rect)];
|
||||
}
|
||||
|
||||
void NebulaWindow::Minimize() {
|
||||
[impl_->window miniaturize:nil];
|
||||
}
|
||||
|
||||
void NebulaWindow::ToggleMaximize() {
|
||||
if (impl_->window && !impl_->fullscreen) {
|
||||
[impl_->window zoom:nil];
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::SetFullscreen(bool fullscreen) {
|
||||
if (!impl_->window || impl_->fullscreen == fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->fullscreen = fullscreen;
|
||||
[impl_->window toggleFullScreen:nil];
|
||||
}
|
||||
|
||||
void NebulaWindow::Close() {
|
||||
if (impl_->delegate) {
|
||||
impl_->delegate->OnWindowCloseRequested();
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::BeginDrag() {
|
||||
if (!impl_->window) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSEvent* event = [NSApp currentEvent];
|
||||
if (event) {
|
||||
[impl_->window performWindowDragWithEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaWindow::SetTitle(const std::string& title) {
|
||||
if (!impl_->window) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* native_title = title.empty()
|
||||
? @"Nebula Browser"
|
||||
: [[NSString alloc] initWithUTF8String:title.c_str()];
|
||||
[impl_->window setTitle:native_title ?: @"Nebula Browser"];
|
||||
}
|
||||
|
||||
void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const {
|
||||
(void)child;
|
||||
}
|
||||
|
||||
} // namespace nebula::window
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <mach-o/dyld.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
std::filesystem::path ExecutableDirectory() {
|
||||
char buffer[4096] = {};
|
||||
uint32_t size = sizeof(buffer);
|
||||
if (_NSGetExecutablePath(buffer, &size) != 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
}
|
||||
|
||||
std::filesystem::path DefaultUserDataRoot() {
|
||||
if (const char* home = std::getenv("HOME")) {
|
||||
return std::filesystem::path(home) / "Library" / "Application Support";
|
||||
}
|
||||
|
||||
if (passwd* pw = getpwuid(getuid())) {
|
||||
return std::filesystem::path(pw->pw_dir) / "Library" / "Application Support";
|
||||
}
|
||||
|
||||
return ExecutableDirectory();
|
||||
}
|
||||
|
||||
std::string PathToUtf8(const std::filesystem::path& path) {
|
||||
return path.string();
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "platform/startup.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "include/cef_command_line.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
int g_single_instance_lock = -1;
|
||||
|
||||
} // namespace
|
||||
|
||||
void PrepareApp() {
|
||||
@autoreleasepool {
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
[NSApp finishLaunching];
|
||||
}
|
||||
}
|
||||
|
||||
bool TryAcquireSingleInstance(const std::string& launch_target) {
|
||||
NEBULA_UNUSED(launch_target);
|
||||
|
||||
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(lock_path.parent_path(), ec);
|
||||
|
||||
g_single_instance_lock = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644);
|
||||
if (g_single_instance_lock < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return flock(g_single_instance_lock, LOCK_EX | LOCK_NB) == 0;
|
||||
}
|
||||
|
||||
CefMainArgs MakeMainArgs(const AppStartup& startup) {
|
||||
return CefMainArgs(startup.argc, startup.argv);
|
||||
}
|
||||
|
||||
void InitCommandLine(CefRefPtr<CefCommandLine> command_line, const AppStartup& startup) {
|
||||
command_line->InitFromArgv(startup.argc, startup.argv);
|
||||
}
|
||||
|
||||
void ConfigureCefSettings(CefSettings& settings) {
|
||||
const std::string user_data_dir = nebula::ui::GetUserDataDirectory().string();
|
||||
const std::string cache_dir = nebula::ui::GetCacheDirectory().string();
|
||||
if (!user_data_dir.empty()) {
|
||||
CefString(&settings.root_cache_path).FromString(user_data_dir);
|
||||
}
|
||||
if (!cache_dir.empty()) {
|
||||
CefString(&settings.cache_path).FromString(cache_dir);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||