Add first-run setup and theme synchronization
Introduce first-run setup flow and live chrome theme syncing. - Add first_run_state.cpp/.h to read/write a first_run_state.json under user data and decide whether to show the setup UI. - Wire first-run logic into NebulaController: track first_run_setup_active_, create initial setup tab, defer/bring up chrome browser accordingly, and add CompleteFirstRunSetup() to persist state and finish setup. - Add SendThemeToChromeSurfaces() and handle "theme-update" and "complete-first-run" chrome commands; restrict setup completion to setup frame. - Expose GetFirstRunStatePath() and GetSetupUrl() in UI path helpers and include the state file in the build list (CMakeLists.txt). - Update chrome UI: new CSS variables and styles for tabs/url-bar; chrome.js can apply themes (applyTheme), persist/load theme, and listen for storage updates to apply theme changes live. - Update customization.js, settings.js, and setup.js to normalize/persist themes, send theme updates to the native host (or fallback), and communicate completion via the native bridge when available; include customization.js in setup.html. These changes allow the app to run an interactive first-run setup and keep the separate chrome UI in sync with user-selected themes.
This commit is contained in:
@@ -40,6 +40,7 @@ SET_CEF_TARGET_OUT_DIR()
|
|||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
set(NEBULA_COMMON_SOURCES
|
set(NEBULA_COMMON_SOURCES
|
||||||
|
src/app/first_run_state.cpp
|
||||||
src/app/nebula_controller.cpp
|
src/app/nebula_controller.cpp
|
||||||
src/app/run.cpp
|
src/app/run.cpp
|
||||||
src/browser/session_state.cpp
|
src/browser/session_state.cpp
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
|
||||||
|
#include "app/first_run_state.h"
|
||||||
#include "browser/session_state.h"
|
#include "browser/session_state.h"
|
||||||
#include "browser/url_utils.h"
|
#include "browser/url_utils.h"
|
||||||
#include "include/cef_app.h"
|
#include "include/cef_app.h"
|
||||||
@@ -194,14 +195,19 @@ void NebulaController::OnWindowCreated() {
|
|||||||
window_->SetFullscreen(true);
|
window_->SetFullscreen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initial_url_.empty()) {
|
first_run_setup_active_ =
|
||||||
|
!big_picture_mode_ && initial_url_.empty() && ShouldShowFirstRunSetup();
|
||||||
|
|
||||||
|
if (first_run_setup_active_) {
|
||||||
|
tabs_.CreateInitialTab(nebula::ui::GetSetupUrl());
|
||||||
|
} else if (initial_url_.empty()) {
|
||||||
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
|
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||||
} else {
|
} else {
|
||||||
tabs_.CreateInitialTab(initial_url_);
|
tabs_.CreateInitialTab(initial_url_);
|
||||||
}
|
}
|
||||||
PersistSession();
|
PersistSession();
|
||||||
|
|
||||||
if (!big_picture_mode_) {
|
if (!big_picture_mode_ && !first_run_setup_active_) {
|
||||||
CreateChromeBrowser();
|
CreateChromeBrowser();
|
||||||
}
|
}
|
||||||
CreateContentBrowser();
|
CreateContentBrowser();
|
||||||
@@ -378,6 +384,10 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
|||||||
CloseMenuPopup();
|
CloseMenuPopup();
|
||||||
} else if (command == "home") {
|
} else if (command == "home") {
|
||||||
tabs_.LoadURL(nebula::ui::GetHomeUrl());
|
tabs_.LoadURL(nebula::ui::GetHomeUrl());
|
||||||
|
} else if (command == "theme-update") {
|
||||||
|
SendThemeToChromeSurfaces(payload);
|
||||||
|
} else if (command == "complete-first-run") {
|
||||||
|
CompleteFirstRunSetup();
|
||||||
} else if (command == "clear-site-history") {
|
} else if (command == "clear-site-history") {
|
||||||
site_history_.clear();
|
site_history_.clear();
|
||||||
SaveSiteHistory(site_history_);
|
SaveSiteHistory(site_history_);
|
||||||
@@ -656,6 +666,10 @@ nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (first_run_setup_active_) {
|
||||||
|
return window_->CurrentLayout(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!big_picture_mode_) {
|
if (!big_picture_mode_) {
|
||||||
return window_->CurrentLayout(!content_fullscreen_);
|
return window_->CurrentLayout(!content_fullscreen_);
|
||||||
}
|
}
|
||||||
@@ -761,6 +775,29 @@ void NebulaController::SendMenuPopupZoom() {
|
|||||||
menu_popup_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetMenuPopupUrl(), 0);
|
menu_popup_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetMenuPopupUrl(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NebulaController::SendThemeToChromeSurfaces(const std::string& theme_json) {
|
||||||
|
if (theme_json.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string escaped_theme = nebula::browser::JsonEscape(theme_json);
|
||||||
|
const std::string script =
|
||||||
|
"(function(){"
|
||||||
|
"try{"
|
||||||
|
"const theme=JSON.parse(\"" + escaped_theme + "\");"
|
||||||
|
"if(window.NebulaChrome&&window.NebulaChrome.applyTheme){window.NebulaChrome.applyTheme(theme);}"
|
||||||
|
"if(window.NebulaMenuPopup&&window.NebulaMenuPopup.applyTheme){window.NebulaMenuPopup.applyTheme(theme);}"
|
||||||
|
"}catch(e){console.warn('[Theme] Failed to apply chrome theme',e);}"
|
||||||
|
"})();";
|
||||||
|
|
||||||
|
if (chrome_browser_) {
|
||||||
|
chrome_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetChromeUrl(), 0);
|
||||||
|
}
|
||||||
|
if (menu_popup_browser_) {
|
||||||
|
menu_popup_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetMenuPopupUrl(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void NebulaController::ToggleDevTools() {
|
void NebulaController::ToggleDevTools() {
|
||||||
auto* tab = tabs_.ActiveTab();
|
auto* tab = tabs_.ActiveTab();
|
||||||
if (!tab || !tab->browser || !window_ || !window_->native_handle()) {
|
if (!tab || !tab->browser || !window_ || !window_->native_handle()) {
|
||||||
@@ -924,6 +961,25 @@ void NebulaController::SetContentFullscreen(bool fullscreen) {
|
|||||||
ResizeBrowsers();
|
ResizeBrowsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NebulaController::CompleteFirstRunSetup() {
|
||||||
|
WriteFirstRunState(false);
|
||||||
|
|
||||||
|
if (!first_run_setup_active_) {
|
||||||
|
tabs_.LoadURL(nebula::ui::GetHomeUrl());
|
||||||
|
PersistSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
first_run_setup_active_ = false;
|
||||||
|
tabs_.LoadURL(nebula::ui::GetHomeUrl());
|
||||||
|
PersistSession();
|
||||||
|
|
||||||
|
if (!big_picture_mode_ && !chrome_browser_) {
|
||||||
|
CreateChromeBrowser();
|
||||||
|
}
|
||||||
|
ResizeBrowsers();
|
||||||
|
}
|
||||||
|
|
||||||
void NebulaController::ResizeBrowsers() {
|
void NebulaController::ResizeBrowsers() {
|
||||||
if (!window_) {
|
if (!window_) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ private:
|
|||||||
void CreateMenuPopupBrowser();
|
void CreateMenuPopupBrowser();
|
||||||
void PositionMenuPopup();
|
void PositionMenuPopup();
|
||||||
void SendMenuPopupZoom();
|
void SendMenuPopupZoom();
|
||||||
|
void SendThemeToChromeSurfaces(const std::string& theme_json);
|
||||||
void ToggleDevTools();
|
void ToggleDevTools();
|
||||||
void AdjustZoom(double delta);
|
void AdjustZoom(double delta);
|
||||||
void FreshReload();
|
void FreshReload();
|
||||||
@@ -67,6 +68,7 @@ private:
|
|||||||
void SendBigPictureText(const std::string& payload);
|
void SendBigPictureText(const std::string& payload);
|
||||||
void SetBigPictureBrowseVisible(bool visible);
|
void SetBigPictureBrowseVisible(bool visible);
|
||||||
void SetContentFullscreen(bool fullscreen);
|
void SetContentFullscreen(bool fullscreen);
|
||||||
|
void CompleteFirstRunSetup();
|
||||||
void ResizeBrowsers();
|
void ResizeBrowsers();
|
||||||
void SendChromeState(const nebula::browser::NebulaTab& tab);
|
void SendChromeState(const nebula::browser::NebulaTab& tab);
|
||||||
void SendBigPictureState(const nebula::browser::NebulaTab& tab);
|
void SendBigPictureState(const nebula::browser::NebulaTab& tab);
|
||||||
@@ -87,6 +89,7 @@ private:
|
|||||||
bool big_picture_mode_ = false;
|
bool big_picture_mode_ = false;
|
||||||
bool big_picture_browse_visible_ = false;
|
bool big_picture_browse_visible_ = false;
|
||||||
bool content_fullscreen_ = false;
|
bool content_fullscreen_ = false;
|
||||||
|
bool first_run_setup_active_ = false;
|
||||||
bool menu_popup_visible_ = false;
|
bool menu_popup_visible_ = false;
|
||||||
|
|
||||||
std::unique_ptr<nebula::window::NebulaWindow> window_;
|
std::unique_ptr<nebula::window::NebulaWindow> window_;
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ bool IsSettingsFrame(CefRefPtr<CefFrame> frame) {
|
|||||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings");
|
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) {
|
bool IsBigPictureFrame(CefRefPtr<CefFrame> frame) {
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return false;
|
return false;
|
||||||
@@ -105,10 +113,12 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
|
|||||||
command == "new-tab" ||
|
command == "new-tab" ||
|
||||||
command == "clear-site-history" ||
|
command == "clear-site-history" ||
|
||||||
command == "clear-search-history");
|
command == "clear-search-history");
|
||||||
|
const bool allowed_setup_command =
|
||||||
|
command == "complete-first-run" && IsSetupFrame(frame);
|
||||||
const bool allowed_big_picture_command =
|
const bool allowed_big_picture_command =
|
||||||
IsBigPictureFrame(frame) && command == "exit-bigpicture";
|
IsBigPictureFrame(frame) && command == "exit-bigpicture";
|
||||||
if (!allowed_insecure_command && !allowed_settings_command &&
|
if (!allowed_insecure_command && !allowed_settings_command &&
|
||||||
!allowed_big_picture_command) {
|
!allowed_setup_command && !allowed_big_picture_command) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (role_ == BrowserRole::BigPicture) {
|
} else if (role_ == BrowserRole::BigPicture) {
|
||||||
|
|||||||
@@ -147,6 +147,11 @@ std::filesystem::path GetSessionStatePath() {
|
|||||||
return user_data.empty() ? std::filesystem::path{} : user_data / "session_state.json";
|
return user_data.empty() ? std::filesystem::path{} : user_data / "session_state.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::filesystem::path GetFirstRunStatePath() {
|
||||||
|
auto user_data = GetUserDataDirectory();
|
||||||
|
return user_data.empty() ? std::filesystem::path{} : user_data / "first_run_state.json";
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path GetUiPagePath(const std::string& page_name) {
|
std::filesystem::path GetUiPagePath(const std::string& page_name) {
|
||||||
const auto exe_dir = GetExecutableDirectory();
|
const auto exe_dir = GetExecutableDirectory();
|
||||||
if (exe_dir.empty()) {
|
if (exe_dir.empty()) {
|
||||||
@@ -182,6 +187,10 @@ std::string GetHomeUrl() {
|
|||||||
return InternalUrlForSlug("home");
|
return InternalUrlForSlug("home");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string GetSetupUrl() {
|
||||||
|
return InternalUrlForSlug("setup");
|
||||||
|
}
|
||||||
|
|
||||||
std::string GetSettingsUrl() {
|
std::string GetSettingsUrl() {
|
||||||
return InternalUrlForSlug("settings");
|
return InternalUrlForSlug("settings");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ std::filesystem::path GetExecutableDirectory();
|
|||||||
std::filesystem::path GetUserDataDirectory();
|
std::filesystem::path GetUserDataDirectory();
|
||||||
std::filesystem::path GetCacheDirectory();
|
std::filesystem::path GetCacheDirectory();
|
||||||
std::filesystem::path GetSessionStatePath();
|
std::filesystem::path GetSessionStatePath();
|
||||||
|
std::filesystem::path GetFirstRunStatePath();
|
||||||
std::filesystem::path GetUiPagePath(const std::string& page_name);
|
std::filesystem::path GetUiPagePath(const std::string& page_name);
|
||||||
std::string FilePathToUrl(std::filesystem::path path);
|
std::string FilePathToUrl(std::filesystem::path path);
|
||||||
std::string GetChromeUrl();
|
std::string GetChromeUrl();
|
||||||
std::string GetHomeUrl();
|
std::string GetHomeUrl();
|
||||||
|
std::string GetSetupUrl();
|
||||||
std::string GetSettingsUrl();
|
std::string GetSettingsUrl();
|
||||||
std::string GetDownloadsUrl();
|
std::string GetDownloadsUrl();
|
||||||
std::string GetBigPictureUrl();
|
std::string GetBigPictureUrl();
|
||||||
|
|||||||
+41
-31
@@ -6,10 +6,20 @@
|
|||||||
--text: #e8e8f0;
|
--text: #e8e8f0;
|
||||||
--muted: #7a7e90;
|
--muted: #7a7e90;
|
||||||
--accent: #7b2eff;
|
--accent: #7b2eff;
|
||||||
|
--primary: #7b2eff;
|
||||||
--accent-2: #00c6ff;
|
--accent-2: #00c6ff;
|
||||||
--outline: #1f2533;
|
--outline: #1f2533;
|
||||||
--outline-soft: rgba(255, 255, 255, 0.06);
|
--outline-soft: rgba(255, 255, 255, 0.06);
|
||||||
--danger: #e0445c;
|
--danger: #e0445c;
|
||||||
|
--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;
|
||||||
|
--chrome-hover: color-mix(in srgb, var(--text) 10%, transparent);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,21 +112,21 @@ button:disabled {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--tab-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
transition: background 120ms, color 120ms;
|
transition: background 120ms, color 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover:not(.active) {
|
.tab:hover:not(.active) {
|
||||||
background: var(--surface);
|
background: var(--tab-bg);
|
||||||
color: var(--text);
|
color: var(--tab-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: var(--surface-raised);
|
background: var(--tab-active);
|
||||||
border-color: var(--outline);
|
border-color: var(--tab-border);
|
||||||
color: var(--text);
|
color: var(--tab-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-title {
|
.tab-title {
|
||||||
@@ -139,7 +149,7 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--accent);
|
background: var(--primary);
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -166,8 +176,8 @@ button:disabled {
|
|||||||
.tab-loading {
|
.tab-loading {
|
||||||
width: 13px;
|
width: 13px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
border: 2px solid rgba(0, 198, 255, 0.2);
|
border: 2px solid color-mix(in srgb, var(--accent) 20%, transparent);
|
||||||
border-top-color: var(--accent-2);
|
border-top-color: var(--accent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -181,7 +191,7 @@ button:disabled {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--tab-text);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: background 120ms, color 120ms, opacity 120ms;
|
transition: background 120ms, color 120ms, opacity 120ms;
|
||||||
}
|
}
|
||||||
@@ -193,8 +203,8 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-close:hover {
|
.tab-close:hover {
|
||||||
background: var(--surface-hover);
|
background: var(--chrome-hover);
|
||||||
color: var(--text);
|
color: var(--tab-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-add {
|
.tab-add {
|
||||||
@@ -207,14 +217,14 @@ button:disabled {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--tab-text);
|
||||||
transition: background 120ms, color 120ms, border-color 120ms;
|
transition: background 120ms, color 120ms, border-color 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-add:hover {
|
.tab-add:hover {
|
||||||
background: var(--surface-hover);
|
background: var(--chrome-hover);
|
||||||
border-color: var(--outline);
|
border-color: var(--tab-border);
|
||||||
color: var(--text);
|
color: var(--tab-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Window controls ────────────────────────────────────────── */
|
/* ── Window controls ────────────────────────────────────────── */
|
||||||
@@ -233,13 +243,13 @@ button:disabled {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--tab-text);
|
||||||
transition: background 100ms, color 100ms;
|
transition: background 100ms, color 100ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-controls button:hover {
|
.window-controls button:hover {
|
||||||
background: var(--surface-hover);
|
background: var(--chrome-hover);
|
||||||
color: var(--text);
|
color: var(--tab-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-controls .close:hover {
|
.window-controls .close:hover {
|
||||||
@@ -252,8 +262,8 @@ button:disabled {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
background: var(--surface-raised);
|
background: var(--tab-active);
|
||||||
border-top: 1px solid var(--outline);
|
border-top: 1px solid var(--tab-border);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,15 +293,15 @@ button:disabled {
|
|||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: var(--muted);
|
color: var(--tab-text);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: background 120ms, color 120ms, border-color 120ms;
|
transition: background 120ms, color 120ms, border-color 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button:hover:not(:disabled) {
|
.icon-button:hover:not(:disabled) {
|
||||||
background: var(--surface-hover);
|
background: var(--chrome-hover);
|
||||||
border-color: var(--outline);
|
border-color: var(--tab-border);
|
||||||
color: var(--text);
|
color: var(--tab-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Address bar ────────────────────────────────────────────── */
|
/* ── Address bar ────────────────────────────────────────────── */
|
||||||
@@ -305,15 +315,15 @@ button:disabled {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--outline);
|
border: 1px solid var(--url-bar-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: var(--surface);
|
background: var(--url-bar-bg);
|
||||||
transition: border-color 140ms, box-shadow 140ms;
|
transition: border-color 140ms, box-shadow 140ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-shell:focus-within {
|
.address-shell:focus-within {
|
||||||
border-color: rgba(123, 46, 255, 0.55);
|
border-color: color-mix(in srgb, var(--primary) 70%, var(--url-bar-border));
|
||||||
box-shadow: 0 0 0 3px rgba(123, 46, 255, 0.12);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-shell input {
|
.address-shell input {
|
||||||
@@ -323,12 +333,12 @@ button:disabled {
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--url-bar-text);
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-shell input::placeholder {
|
.address-shell input::placeholder {
|
||||||
color: var(--muted);
|
color: color-mix(in srgb, var(--url-bar-text) 55%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
|
|||||||
+103
-1
@@ -1,5 +1,24 @@
|
|||||||
const SEARCH_URL = 'https://www.google.com/search?q=';
|
const SEARCH_URL = 'https://www.google.com/search?q=';
|
||||||
|
|
||||||
|
const DEFAULT_THEME = {
|
||||||
|
colors: {
|
||||||
|
bg: '#080a0f',
|
||||||
|
darkBlue: '#0e1119',
|
||||||
|
darkPurple: '#141824',
|
||||||
|
primary: '#7b2eff',
|
||||||
|
accent: '#00c6ff',
|
||||||
|
text: '#e8e8f0',
|
||||||
|
urlBarBg: '#1c2030',
|
||||||
|
urlBarText: '#e0e0e0',
|
||||||
|
urlBarBorder: '#3e4652',
|
||||||
|
tabBg: '#161925',
|
||||||
|
tabText: '#a4a7b3',
|
||||||
|
tabActive: '#1c2030',
|
||||||
|
tabActiveText: '#e0e0e0',
|
||||||
|
tabBorder: '#2b3040'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
id: 1,
|
id: 1,
|
||||||
url: '',
|
url: '',
|
||||||
@@ -12,6 +31,79 @@ const state = {
|
|||||||
tabs: []
|
tabs: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
if (!hex || typeof hex !== 'string') return null;
|
||||||
|
let normalized = hex.trim().replace(/^#/, '');
|
||||||
|
if (normalized.length === 3) {
|
||||||
|
normalized = normalized.split('').map(char => char + char).join('');
|
||||||
|
}
|
||||||
|
if (!/^[a-fA-F\d]{6}$/.test(normalized)) return null;
|
||||||
|
|
||||||
|
const value = parseInt(normalized, 16);
|
||||||
|
return {
|
||||||
|
r: (value >> 16) & 255,
|
||||||
|
g: (value >> 8) & 255,
|
||||||
|
b: value & 255
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDarkColor(hex) {
|
||||||
|
const rgb = hexToRgb(hex);
|
||||||
|
if (!rgb) return true;
|
||||||
|
const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
|
||||||
|
return luminance < 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCssVar(name, value, fallback) {
|
||||||
|
document.documentElement.style.setProperty(name, value || fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTheme(theme) {
|
||||||
|
const colors = theme?.colors || {};
|
||||||
|
return {
|
||||||
|
...DEFAULT_THEME,
|
||||||
|
...(theme || {}),
|
||||||
|
colors: {
|
||||||
|
...DEFAULT_THEME.colors,
|
||||||
|
...colors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const normalized = normalizeTheme(theme);
|
||||||
|
const colors = normalized.colors;
|
||||||
|
|
||||||
|
setCssVar('--bg', colors.bg, DEFAULT_THEME.colors.bg);
|
||||||
|
setCssVar('--surface', colors.darkBlue, DEFAULT_THEME.colors.darkBlue);
|
||||||
|
setCssVar('--surface-raised', colors.darkPurple, DEFAULT_THEME.colors.darkPurple);
|
||||||
|
setCssVar('--text', colors.text, DEFAULT_THEME.colors.text);
|
||||||
|
setCssVar('--muted', colors.tabText, DEFAULT_THEME.colors.tabText);
|
||||||
|
setCssVar('--primary', colors.primary, DEFAULT_THEME.colors.primary);
|
||||||
|
setCssVar('--accent', colors.accent, DEFAULT_THEME.colors.accent);
|
||||||
|
setCssVar('--accent-2', colors.accent, DEFAULT_THEME.colors.accent);
|
||||||
|
setCssVar('--outline', colors.tabBorder, DEFAULT_THEME.colors.tabBorder);
|
||||||
|
setCssVar('--url-bar-bg', colors.urlBarBg, colors.darkBlue);
|
||||||
|
setCssVar('--url-bar-text', colors.urlBarText, colors.text);
|
||||||
|
setCssVar('--url-bar-border', colors.urlBarBorder, colors.primary);
|
||||||
|
setCssVar('--tab-bg', colors.tabBg, colors.darkBlue);
|
||||||
|
setCssVar('--tab-text', colors.tabText, colors.text);
|
||||||
|
setCssVar('--tab-active', colors.tabActive, colors.darkPurple);
|
||||||
|
setCssVar('--tab-active-text', colors.tabActiveText, colors.text);
|
||||||
|
setCssVar('--tab-border', colors.tabBorder, colors.darkBlue);
|
||||||
|
|
||||||
|
document.documentElement.style.colorScheme = isDarkColor(colors.bg) ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySavedTheme() {
|
||||||
|
try {
|
||||||
|
const savedTheme = localStorage.getItem('currentTheme');
|
||||||
|
if (savedTheme) applyTheme(JSON.parse(savedTheme));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Chrome] Failed to apply saved theme:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toNavigationUrl(input) {
|
function toNavigationUrl(input) {
|
||||||
const value = (input || '').trim();
|
const value = (input || '').trim();
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
@@ -175,9 +267,19 @@ function wireCommands() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.NebulaChrome = { applyState, postCommand, toNavigationUrl };
|
window.NebulaChrome = { applyState, applyTheme, postCommand, toNavigationUrl };
|
||||||
|
|
||||||
|
window.addEventListener('storage', event => {
|
||||||
|
if (event.key !== 'currentTheme' || !event.newValue) return;
|
||||||
|
try {
|
||||||
|
applyTheme(JSON.parse(event.newValue));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Chrome] Failed to apply updated theme:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
applySavedTheme();
|
||||||
wireCommands();
|
wireCommands();
|
||||||
applyState(state);
|
applyState(state);
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-4
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class BrowserCustomizer {
|
class BrowserCustomizer {
|
||||||
constructor() {
|
constructor(options = {}) {
|
||||||
this.defaultTheme = {
|
this.defaultTheme = {
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
colors: {
|
colors: {
|
||||||
@@ -286,6 +286,10 @@ class BrowserCustomizer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options.skipInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.currentTheme = this.loadTheme();
|
this.currentTheme = this.loadTheme();
|
||||||
this.activeThemeName = this.loadActiveThemeName();
|
this.activeThemeName = this.loadActiveThemeName();
|
||||||
this.init();
|
this.init();
|
||||||
@@ -584,9 +588,13 @@ class BrowserCustomizer {
|
|||||||
// This will be called to apply theme to home.html and other pages
|
// This will be called to apply theme to home.html and other pages
|
||||||
this.saveTheme();
|
this.saveTheme();
|
||||||
|
|
||||||
// Send theme update to host (for settings webview)
|
const themePayload = JSON.stringify(this.currentTheme);
|
||||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
|
||||||
window.electronAPI.sendToHost('theme-update', this.currentTheme);
|
// Send theme update to host so the separate chrome browser can update live.
|
||||||
|
if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
|
||||||
|
window.nebulaNative.postMessage('theme-update', themePayload);
|
||||||
|
} else if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||||
|
window.electronAPI.sendToHost('theme-update', themePayload);
|
||||||
}
|
}
|
||||||
// Fallback: send via postMessage (for iframe embedding)
|
// Fallback: send via postMessage (for iframe embedding)
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ function attachClearHandler(btn) {
|
|||||||
} finally {
|
} finally {
|
||||||
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
|
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
|
||||||
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||||
window.electronAPI.sendToHost('theme-update', currentTheme);
|
window.electronAPI.sendToHost('theme-update', JSON.stringify(currentTheme));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+70
-19
@@ -12,17 +12,58 @@ const setupState = {
|
|||||||
themes: []
|
themes: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function hasNebulaNativeBridge() {
|
||||||
|
return !!(window.nebulaNative && typeof window.nebulaNative.postMessage === 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPresetThemes() {
|
||||||
|
if (typeof BrowserCustomizer === 'function') {
|
||||||
|
const customizer = new BrowserCustomizer({ skipInit: true });
|
||||||
|
return customizer.predefinedThemes || { default: customizer.defaultTheme };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: {
|
||||||
|
name: 'Default',
|
||||||
|
colors: {
|
||||||
|
bg: '#121418',
|
||||||
|
darkBlue: '#0B1C2B',
|
||||||
|
darkPurple: '#1B1035',
|
||||||
|
primary: '#7B2EFF',
|
||||||
|
accent: '#00C6FF',
|
||||||
|
text: '#E0E0E0',
|
||||||
|
urlBarBg: '#1C2030',
|
||||||
|
urlBarText: '#E0E0E0',
|
||||||
|
urlBarBorder: '#3E4652',
|
||||||
|
tabBg: '#161925',
|
||||||
|
tabText: '#A4A7B3',
|
||||||
|
tabActive: '#1C2030',
|
||||||
|
tabActiveText: '#E0E0E0',
|
||||||
|
tabBorder: '#2B3040'
|
||||||
|
},
|
||||||
|
layout: 'centered',
|
||||||
|
showLogo: true,
|
||||||
|
customTitle: 'Nebula Browser',
|
||||||
|
gradient: 'linear-gradient(145deg, #121418 0%, #1B1035 100%)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTheme(theme) {
|
||||||
|
const fallback = getPresetThemes().default;
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
...(theme || {}),
|
||||||
|
colors: {
|
||||||
|
...fallback.colors,
|
||||||
|
...((theme && theme.colors) || {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const nativeApi = window.api || {
|
const nativeApi = window.api || {
|
||||||
async getAllThemes() {
|
async getAllThemes() {
|
||||||
return {
|
return { default: getPresetThemes() };
|
||||||
default: {
|
|
||||||
default: {
|
|
||||||
name: 'Default',
|
|
||||||
description: 'Classic Nebula theme',
|
|
||||||
colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
async isDefaultBrowser() {
|
async isDefaultBrowser() {
|
||||||
return false;
|
return false;
|
||||||
@@ -31,10 +72,17 @@ const nativeApi = window.api || {
|
|||||||
return { success: false, error: 'Default browser setup is handled by the native CEF app.' };
|
return { success: false, error: 'Default browser setup is handled by the native CEF app.' };
|
||||||
},
|
},
|
||||||
async applyTheme(themeId) {
|
async applyTheme(themeId) {
|
||||||
|
const theme = getThemeById(themeId);
|
||||||
|
if (theme) {
|
||||||
|
localStorage.setItem('currentTheme', JSON.stringify(normalizeTheme(theme)));
|
||||||
|
}
|
||||||
localStorage.setItem('activeThemeName', themeId);
|
localStorage.setItem('activeThemeName', themeId);
|
||||||
},
|
},
|
||||||
async completeFirstRun(data) {
|
async completeFirstRun(data) {
|
||||||
localStorage.setItem('nebula-first-run-complete', JSON.stringify(data));
|
localStorage.setItem('nebula-first-run-complete', JSON.stringify(data));
|
||||||
|
if (hasNebulaNativeBridge()) {
|
||||||
|
window.nebulaNative.postMessage('complete-first-run', JSON.stringify(data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,9 +115,7 @@ async function loadThemes() {
|
|||||||
console.error('[Setup] Error loading themes:', error);
|
console.error('[Setup] Error loading themes:', error);
|
||||||
// Fallback to a default theme
|
// Fallback to a default theme
|
||||||
setupState.themes = {
|
setupState.themes = {
|
||||||
default: {
|
default: getPresetThemes()
|
||||||
default: { name: 'Default', description: 'Classic Nebula theme', colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF' } }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
renderThemeGrid(setupState.themes);
|
renderThemeGrid(setupState.themes);
|
||||||
}
|
}
|
||||||
@@ -167,8 +213,9 @@ function hexToRgb(hex) {
|
|||||||
* Apply theme to the setup page UI and persist selection
|
* Apply theme to the setup page UI and persist selection
|
||||||
*/
|
*/
|
||||||
function applyThemeToSetupPage(theme, themeId = null) {
|
function applyThemeToSetupPage(theme, themeId = null) {
|
||||||
if (!theme || !theme.colors) return;
|
const completeTheme = normalizeTheme(theme);
|
||||||
const colors = theme.colors;
|
if (!completeTheme || !completeTheme.colors) return;
|
||||||
|
const colors = completeTheme.colors;
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
const setVar = (cssVar, value, fallback) => {
|
const setVar = (cssVar, value, fallback) => {
|
||||||
@@ -202,15 +249,15 @@ function applyThemeToSetupPage(theme, themeId = null) {
|
|||||||
setVar('--warning-rgb', `${warningRgb.r}, ${warningRgb.g}, ${warningRgb.b}`);
|
setVar('--warning-rgb', `${warningRgb.r}, ${warningRgb.g}, ${warningRgb.b}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme.gradient) {
|
if (completeTheme.gradient) {
|
||||||
document.body.style.background = theme.gradient;
|
document.body.style.background = completeTheme.gradient;
|
||||||
} else if (colors.bg) {
|
} else if (colors.bg) {
|
||||||
document.body.style.background = colors.bg;
|
document.body.style.background = colors.bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist for main UI to pick up on first load
|
// Persist for main UI to pick up on first load
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('currentTheme', JSON.stringify(theme));
|
localStorage.setItem('currentTheme', JSON.stringify(completeTheme));
|
||||||
if (themeId) localStorage.setItem('activeThemeName', themeId);
|
if (themeId) localStorage.setItem('activeThemeName', themeId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Setup] Failed to persist theme:', err);
|
console.warn('[Setup] Failed to persist theme:', err);
|
||||||
@@ -499,7 +546,9 @@ async function completeSetup() {
|
|||||||
|
|
||||||
console.log('[Setup] First-time setup completed successfully');
|
console.log('[Setup] First-time setup completed successfully');
|
||||||
|
|
||||||
window.location.href = 'home.html';
|
if (!hasNebulaNativeBridge()) {
|
||||||
|
window.location.href = 'home.html';
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Setup] Error completing setup:', error);
|
console.error('[Setup] Error completing setup:', error);
|
||||||
alert('There was an error saving your preferences. Please try again.');
|
alert('There was an error saving your preferences. Please try again.');
|
||||||
@@ -522,7 +571,9 @@ async function skipSetup() {
|
|||||||
|
|
||||||
console.log('[Setup] Setup skipped, using defaults');
|
console.log('[Setup] Setup skipped, using defaults');
|
||||||
|
|
||||||
window.location.href = 'home.html';
|
if (!hasNebulaNativeBridge()) {
|
||||||
|
window.location.href = 'home.html';
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Setup] Error skipping setup:', error);
|
console.error('[Setup] Error skipping setup:', error);
|
||||||
window.location.href = 'home.html';
|
window.location.href = 'home.html';
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/customization.js"></script>
|
||||||
<script src="../js/setup.js"></script>
|
<script src="../js/setup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user