Files
NebulaBrowser/src/ui/paths.cpp
T
andrew b7596674ab Add host platform detection and macOS styles
Expose host platform to the UI and apply platform-specific styling. Backend: add CurrentPlatformName() and include "platform" in the JSON sent from NebulaController so the frontend can know the host OS. Frontend: detectHostPlatform() initializes state.platform, applyPlatform() sets a platform-* body class, and applyState() applies it. CSS: add .platform-macos rules to adjust title padding and hide window controls on macOS. Also fix FilePathToUrl to avoid producing an extra slash when the encoded path already starts with '/'.
2026-05-22 10:55:16 +12:00

282 lines
7.9 KiB
C++

#include "ui/paths.h"
#include "platform/paths_platform.h"
#include <algorithm>
#include <cctype>
#include <string_view>
namespace nebula::ui {
namespace {
constexpr std::string_view kNebulaScheme = "nebula://";
constexpr std::string_view kInternalFallbackPage = "404.html";
struct InternalPage {
std::string_view slug;
std::string_view file_name;
};
constexpr InternalPage kInternalPages[] = {
{"home", "home.html"},
{"settings", "settings.html"},
{"downloads", "downloads.html"},
{"bigpicture", "bigpicture.html"},
{"big-picture", "bigpicture.html"},
{"gpu-diagnostics", "gpu-diagnostics.html"},
{"setup", "setup.html"},
{"404", "404.html"},
{"insecure", "insecure.html"},
};
std::string GetUrlWithoutDecoration(std::string url) {
const size_t split = url.find_first_of("?#");
if (split != std::string::npos) {
url.resize(split);
}
return url;
}
std::string GetUrlDecoration(const std::string& url) {
const size_t split = url.find_first_of("?#");
return split == std::string::npos ? std::string{} : url.substr(split);
}
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;
}
std::string PageFileUrl(std::string_view page_name) {
const auto path = GetUiPagePath(std::string(page_name));
return path.empty() ? std::string{} : FilePathToUrl(path);
}
std::string PercentEncode(const std::string& value) {
constexpr char kHex[] = "0123456789ABCDEF";
std::string encoded;
encoded.reserve(value.size());
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
encoded += static_cast<char>(ch);
} else {
encoded += '%';
encoded += kHex[ch >> 4];
encoded += kHex[ch & 0x0F];
}
}
return encoded;
}
std::string InternalPageName(const std::string& url) {
std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
if (!target.starts_with(kNebulaScheme)) {
return {};
}
target.erase(0, kNebulaScheme.size());
while (!target.empty() && target.front() == '/') {
target.erase(target.begin());
}
while (!target.empty() && target.back() == '/') {
target.pop_back();
}
return target.empty() ? "home" : target;
}
std::string InternalUrlForSlug(std::string_view slug) {
return std::string(kNebulaScheme) + std::string(slug);
}
const InternalPage* FindInternalPageBySlug(std::string_view slug) {
for (const auto& page : kInternalPages) {
if (page.slug == slug) {
return &page;
}
}
return nullptr;
}
const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
const std::string base_url = GetUrlWithoutDecoration(url);
for (const auto& page : kInternalPages) {
if (PageFileUrl(page.file_name) == base_url) {
return &page;
}
}
return nullptr;
}
} // namespace
std::filesystem::path GetExecutableDirectory() {
return nebula::platform::ExecutableDirectory();
}
std::filesystem::path GetUserDataDirectory() {
auto root = nebula::platform::DefaultUserDataRoot();
if (root.empty()) {
root = GetExecutableDirectory();
}
if (root.empty()) {
return {};
}
std::filesystem::path user_data = root / "Nebula" / "User Data";
std::error_code ec;
std::filesystem::create_directories(user_data, ec);
return user_data;
}
std::filesystem::path GetCacheDirectory() {
auto user_data = GetUserDataDirectory();
if (user_data.empty()) {
return {};
}
std::filesystem::path cache = user_data / "Cache";
std::error_code ec;
std::filesystem::create_directories(cache, ec);
return cache;
}
std::filesystem::path GetSessionStatePath() {
auto user_data = GetUserDataDirectory();
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) {
const auto exe_dir = GetExecutableDirectory();
if (exe_dir.empty()) {
return {};
}
return exe_dir / "ui" / "pages" / page_name;
}
std::string FilePathToUrl(std::filesystem::path path) {
std::string value = nebula::platform::PathToUtf8(path);
for (char& ch : value) {
if (ch == '\\') {
ch = '/';
}
}
std::string encoded;
encoded.reserve(value.size());
for (char ch : value) {
encoded += ch == ' ' ? "%20" : std::string(1, ch);
}
return encoded.starts_with('/') ? "file://" + encoded : "file:///" + encoded;
}
std::string GetChromeUrl() {
const auto path = GetUiPagePath("chrome.html");
const std::string fallback = PageFileUrl("home.html");
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
}
std::string GetHomeUrl() {
return InternalUrlForSlug("home");
}
std::string GetSetupUrl() {
return InternalUrlForSlug("setup");
}
std::string GetSettingsUrl() {
return InternalUrlForSlug("settings");
}
std::string GetDownloadsUrl() {
return InternalUrlForSlug("downloads");
}
std::string GetBigPictureUrl() {
return InternalUrlForSlug("bigpicture");
}
std::string GetGpuDiagnosticsUrl() {
return InternalUrlForSlug("gpu-diagnostics");
}
std::string GetMenuPopupUrl() {
const auto path = GetUiPagePath("menu-popup.html");
const std::string fallback = PageFileUrl("home.html");
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
}
std::string GetInsecureWarningUrl(const std::string& target_url) {
return InternalUrlForSlug("insecure") + "?target=" + PercentEncode(target_url);
}
std::string GetNotFoundUrl(const std::string& target_url) {
return InternalUrlForSlug("404") + "?url=" + PercentEncode(target_url);
}
std::string ResolveInternalUrl(const std::string& url) {
const std::string page_name = InternalPageName(url);
if (page_name.empty()) {
return url;
}
if (const auto* page = FindInternalPageBySlug(page_name)) {
const std::string file_url = PageFileUrl(page->file_name);
return file_url.empty() ? url : file_url + GetUrlDecoration(url);
}
const std::string fallback_url = PageFileUrl(kInternalFallbackPage);
return fallback_url.empty() ? url : fallback_url + "?url=" + PercentEncode(url);
}
std::string ToInternalUrl(const std::string& url) {
const std::string page_name = InternalPageName(url);
if (!page_name.empty()) {
if (const auto* page = FindInternalPageBySlug(page_name)) {
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
}
return url;
}
if (const auto* page = FindInternalPageByFileUrl(url)) {
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
}
return url;
}
bool IsInternalHomeUrl(const std::string& url) {
return GetUrlWithoutDecoration(ToInternalUrl(url)) == GetHomeUrl();
}
bool IsNebulaInternalUrl(const std::string& url) {
return ToLowerAscii(url).starts_with(kNebulaScheme);
}
bool IsHttpUrl(const std::string& url) {
return ToLowerAscii(url).starts_with("http://");
}
bool IsChromiumNewTabUrl(const std::string& url) {
const std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
return target == "about:blank" ||
target == "chrome://newtab" ||
target == "chrome://newtab/" ||
target == "chrome://new-tab-page" ||
target == "chrome://new-tab-page/" ||
target == "chrome-search://local-ntp/local-ntp.html";
}
bool IsEmptyOrChromiumNewTabUrl(const std::string& url) {
return url.empty() || IsChromiumNewTabUrl(url);
}
} // namespace nebula::ui