mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
palette: better community palettes management
This commit is contained in:
@@ -1091,7 +1091,7 @@
|
||||
},
|
||||
"community-palette": {
|
||||
"label": "Community Palette",
|
||||
"description": "Palette name to fetch from the community catalog"
|
||||
"description": "Choose a palette from the community catalog"
|
||||
},
|
||||
"ui-scale": {
|
||||
"label": "UI Scale",
|
||||
|
||||
@@ -589,6 +589,7 @@ _noctalia_sources = files(
|
||||
'src/theme/custom_schemes.cpp',
|
||||
'src/theme/builtin_palettes.cpp',
|
||||
'src/theme/builtin_templates.cpp',
|
||||
'src/theme/community_palettes.cpp',
|
||||
'src/theme/community_templates.cpp',
|
||||
'src/theme/fixed_palette.cpp',
|
||||
'src/theme/template_apply_service.cpp',
|
||||
|
||||
@@ -310,6 +310,9 @@ void Application::initServices() {
|
||||
m_configService.addReloadCallback(applyPasswordMaskStyle);
|
||||
m_configService.addReloadCallback(
|
||||
[this]() { m_httpClient.setOfflineMode(m_configService.config().shell.offlineMode); });
|
||||
m_communityPaletteService.setReadyCallback([this]() { m_settingsWindow.onExternalOptionsChanged(); });
|
||||
m_communityPaletteService.sync();
|
||||
m_configService.addReloadCallback([this]() { m_communityPaletteService.sync(); });
|
||||
m_communityTemplateService.setReadyCallback([this]() {
|
||||
if (m_configService.config().theme.templates.enableCommunityTemplates) {
|
||||
m_themeService.onConfigReload();
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
#include "system/telemetry_service.h"
|
||||
#include "system/weather_poll_source.h"
|
||||
#include "system/weather_service.h"
|
||||
#include "theme/community_palettes.h"
|
||||
#include "theme/community_templates.h"
|
||||
#include "theme/template_apply_service.h"
|
||||
#include "theme/theme_service.h"
|
||||
@@ -118,6 +119,7 @@ private:
|
||||
VirtualKeyboardService m_virtualKeyboardService;
|
||||
ConfigService m_configService;
|
||||
HttpClient m_httpClient;
|
||||
noctalia::theme::CommunityPaletteService m_communityPaletteService{m_httpClient};
|
||||
noctalia::theme::CommunityTemplateService m_communityTemplateService{m_httpClient};
|
||||
noctalia::theme::ThemeService m_themeService{m_configService, m_httpClient};
|
||||
noctalia::theme::TemplateApplyService m_templateApplyService{m_configService};
|
||||
|
||||
@@ -272,9 +272,13 @@ namespace settings {
|
||||
{"theme", "wallpaper_scheme"}, wallpaperSchemeSelect(cfg.theme.wallpaperScheme),
|
||||
"wallpaper palette generator scheme material you m3 colors"));
|
||||
} else if (cfg.theme.source == ThemeSource::Community) {
|
||||
SettingControl communityPaletteControl = TextSetting{cfg.theme.communityPalette, "Oxocarbon"};
|
||||
if (!env.communityPalettes.empty()) {
|
||||
communityPaletteControl = SelectSetting{env.communityPalettes, cfg.theme.communityPalette};
|
||||
}
|
||||
entries.push_back(makeEntry("appearance", "theme", tr("settings.schema.appearance.community-palette.label"),
|
||||
tr("settings.schema.appearance.community-palette.description"),
|
||||
{"theme", "community_palette"}, TextSetting{cfg.theme.communityPalette, "Noctalia"},
|
||||
{"theme", "community_palette"}, std::move(communityPaletteControl),
|
||||
"community palette colors"));
|
||||
}
|
||||
entries.push_back(makeEntry("appearance", "interface", tr("settings.schema.appearance.ui-scale.label"),
|
||||
|
||||
@@ -97,6 +97,7 @@ namespace settings {
|
||||
struct RegistryEnvironment {
|
||||
bool niriBackdropSupported = false; // hide the [backdrop] section when false
|
||||
std::vector<SelectOption> availableOutputs; // monitor selectors available on this machine
|
||||
std::vector<SelectOption> communityPalettes;
|
||||
std::vector<SelectOption> communityTemplates;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "shell/settings/settings_entity_editor.h"
|
||||
#include "shell/settings/settings_registry.h"
|
||||
#include "shell/settings/settings_sidebar.h"
|
||||
#include "theme/community_palettes.h"
|
||||
#include "theme/community_templates.h"
|
||||
#include "ui/controls/box.h"
|
||||
#include "ui/controls/button.h"
|
||||
@@ -936,6 +937,9 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
}
|
||||
settings::RegistryEnvironment env;
|
||||
env.niriBackdropSupported = (m_wayland != nullptr && compositors::isNiri());
|
||||
for (const auto& paletteInfo : noctalia::theme::availableCommunityPalettes()) {
|
||||
env.communityPalettes.push_back(settings::SelectOption{paletteInfo.name, paletteInfo.name});
|
||||
}
|
||||
for (const auto& t : noctalia::theme::CommunityTemplateService::availableTemplates()) {
|
||||
env.communityTemplates.push_back(settings::SelectOption{t.id, t.displayName});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "theme/community_palettes.h"
|
||||
|
||||
#include "core/deferred_call.h"
|
||||
#include "core/log.h"
|
||||
#include "net/http_client.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <json.hpp>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace noctalia::theme {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("community_palettes");
|
||||
constexpr std::string_view kCatalogUrl = "https://api.noctalia.dev/palettes";
|
||||
constexpr std::string_view kPaletteUrlBase = "https://api.noctalia.dev/palette";
|
||||
|
||||
std::filesystem::path catalogCachePath() { return communityPaletteCacheDir() / ".catalog" / "palettes.json"; }
|
||||
|
||||
std::string urlEncode(std::string_view text) {
|
||||
std::string encoded;
|
||||
encoded.reserve(text.size() * 3);
|
||||
auto isUnreserved = [](unsigned char ch) {
|
||||
return std::isalnum(ch) != 0 || ch == '-' || ch == '_' || ch == '.' || ch == '~';
|
||||
};
|
||||
for (char rawCh : text) {
|
||||
const auto ch = static_cast<unsigned char>(rawCh);
|
||||
if (isUnreserved(ch)) {
|
||||
encoded.push_back(static_cast<char>(ch));
|
||||
} else {
|
||||
encoded += std::format("%{:02X}", static_cast<unsigned int>(ch));
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::string stringField(const nlohmann::json& obj, std::string_view key) {
|
||||
auto it = obj.find(std::string(key));
|
||||
if (it == obj.end() || !it->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return it->get<std::string>();
|
||||
}
|
||||
|
||||
std::string paletteNameFromJson(const nlohmann::json& item) {
|
||||
if (item.is_string()) {
|
||||
return item.get<std::string>();
|
||||
}
|
||||
if (!item.is_object()) {
|
||||
return {};
|
||||
}
|
||||
return stringField(item, "name");
|
||||
}
|
||||
|
||||
std::vector<AvailablePalette> parseCatalogFile(const std::filesystem::path& path) {
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
std::stringstream buf;
|
||||
buf << in.rdbuf();
|
||||
const auto root = nlohmann::json::parse(buf.str());
|
||||
const nlohmann::json* entries = &root;
|
||||
if (root.is_object()) {
|
||||
if (auto it = root.find("palettes"); it != root.end()) {
|
||||
entries = &*it;
|
||||
}
|
||||
}
|
||||
if (!entries->is_array()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<AvailablePalette> out;
|
||||
out.reserve(entries->size());
|
||||
for (const auto& item : *entries) {
|
||||
AvailablePalette palette;
|
||||
palette.name = paletteNameFromJson(item);
|
||||
if (!palette.name.empty()) {
|
||||
out.push_back(std::move(palette));
|
||||
}
|
||||
}
|
||||
std::sort(out.begin(), out.end(),
|
||||
[](const AvailablePalette& a, const AvailablePalette& b) { return a.name < b.name; });
|
||||
out.erase(std::unique(out.begin(), out.end(),
|
||||
[](const AvailablePalette& a, const AvailablePalette& b) { return a.name == b.name; }),
|
||||
out.end());
|
||||
return out;
|
||||
} catch (const std::exception& e) {
|
||||
kLog.warn("failed to parse community palette catalog {}: {}", path.string(), e.what());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CommunityPaletteService::CommunityPaletteService(HttpClient& httpClient) : m_httpClient(httpClient) {}
|
||||
|
||||
void CommunityPaletteService::setReadyCallback(ReadyCallback callback) { m_readyCallback = std::move(callback); }
|
||||
|
||||
void CommunityPaletteService::sync() {
|
||||
const std::uint64_t generation = ++m_generation;
|
||||
const std::filesystem::path path = catalogCachePath();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
|
||||
m_httpClient.download(kCatalogUrl, path, [this, generation, path](bool success) {
|
||||
if (generation != m_generation) {
|
||||
return;
|
||||
}
|
||||
if (!success) {
|
||||
kLog.warn("failed to refresh community palette catalog; using cached metadata when available");
|
||||
return;
|
||||
}
|
||||
if (parseCatalogFile(path).empty()) {
|
||||
kLog.warn("community palette catalog downloaded but contained no palettes");
|
||||
}
|
||||
if (m_readyCallback) {
|
||||
DeferredCall::callLater([callback = m_readyCallback]() { callback(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<AvailablePalette> availableCommunityPalettes() { return parseCatalogFile(catalogCachePath()); }
|
||||
|
||||
std::filesystem::path communityPaletteCacheDir() {
|
||||
if (const char* xdg = std::getenv("XDG_CACHE_HOME"); xdg != nullptr && xdg[0] != '\0') {
|
||||
return std::filesystem::path(xdg) / "noctalia" / "community-palettes";
|
||||
}
|
||||
if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') {
|
||||
return std::filesystem::path(home) / ".cache" / "noctalia" / "community-palettes";
|
||||
}
|
||||
return std::filesystem::path("/tmp") / "noctalia" / "community-palettes";
|
||||
}
|
||||
|
||||
std::filesystem::path communityPaletteCachePath(std::string_view name) {
|
||||
return communityPaletteCacheDir() / (urlEncode(name) + ".json");
|
||||
}
|
||||
|
||||
std::string communityPaletteDownloadUrl(std::string_view name) {
|
||||
return std::string(kPaletteUrlBase) + "/" + urlEncode(name);
|
||||
}
|
||||
|
||||
} // namespace noctalia::theme
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class HttpClient;
|
||||
|
||||
namespace noctalia::theme {
|
||||
|
||||
struct AvailablePalette {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class CommunityPaletteService {
|
||||
public:
|
||||
using ReadyCallback = std::function<void()>;
|
||||
|
||||
explicit CommunityPaletteService(HttpClient& httpClient);
|
||||
|
||||
void setReadyCallback(ReadyCallback callback);
|
||||
void sync();
|
||||
|
||||
private:
|
||||
HttpClient& m_httpClient;
|
||||
ReadyCallback m_readyCallback;
|
||||
std::uint64_t m_generation = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<AvailablePalette> availableCommunityPalettes();
|
||||
[[nodiscard]] std::filesystem::path communityPaletteCacheDir();
|
||||
[[nodiscard]] std::filesystem::path communityPaletteCachePath(std::string_view name);
|
||||
[[nodiscard]] std::string communityPaletteDownloadUrl(std::string_view name);
|
||||
|
||||
} // namespace noctalia::theme
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "net/http_client.h"
|
||||
#include "theme/builtin_palettes.h"
|
||||
#include "theme/community_palettes.h"
|
||||
#include "theme/fixed_palette.h"
|
||||
#include "theme/image_loader.h"
|
||||
#include "theme/palette_generator.h"
|
||||
@@ -13,10 +14,8 @@
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <json.hpp>
|
||||
#include <sstream>
|
||||
@@ -85,37 +84,6 @@ namespace noctalia::theme {
|
||||
};
|
||||
}
|
||||
|
||||
std::string urlEncode(std::string_view text) {
|
||||
std::string encoded;
|
||||
encoded.reserve(text.size() * 3);
|
||||
auto isUnreserved = [](unsigned char ch) {
|
||||
return std::isalnum(ch) != 0 || ch == '-' || ch == '_' || ch == '.' || ch == '~';
|
||||
};
|
||||
for (char rawCh : text) {
|
||||
const auto ch = static_cast<unsigned char>(rawCh);
|
||||
if (isUnreserved(ch)) {
|
||||
encoded.push_back(static_cast<char>(ch));
|
||||
} else {
|
||||
encoded += std::format("%{:02X}", static_cast<unsigned int>(ch));
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::filesystem::path communityPaletteCacheDir() {
|
||||
if (const char* xdg = std::getenv("XDG_CACHE_HOME"); xdg != nullptr && xdg[0] != '\0') {
|
||||
return std::filesystem::path(xdg) / "noctalia" / "community-palettes";
|
||||
}
|
||||
if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') {
|
||||
return std::filesystem::path(home) / ".cache" / "noctalia" / "community-palettes";
|
||||
}
|
||||
return std::filesystem::path("/tmp") / "noctalia" / "community-palettes";
|
||||
}
|
||||
|
||||
std::filesystem::path communityPaletteCachePath(std::string_view encodedName) {
|
||||
return communityPaletteCacheDir() / (std::string(encodedName) + ".json");
|
||||
}
|
||||
|
||||
// Reads a color key from a JSON object, looking first for the `m`-prefixed form
|
||||
// (e.g. `mPrimary`) and falling back to the unprefixed name. Returns fallback
|
||||
// (transparent black) if the key is missing or the value is not a hex string.
|
||||
@@ -334,11 +302,10 @@ namespace noctalia::theme {
|
||||
return;
|
||||
}
|
||||
m_inflightCommunityName = name;
|
||||
const std::string encoded = urlEncode(name);
|
||||
const auto cachePath = communityPaletteCachePath(encoded);
|
||||
const auto cachePath = communityPaletteCachePath(name);
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(cachePath.parent_path(), ec);
|
||||
const std::string url = "https://api.noctalia.dev/palette/" + encoded;
|
||||
const std::string url = communityPaletteDownloadUrl(name);
|
||||
kLog.info("fetching community palette '{}' from {}", name, url);
|
||||
m_httpClient.download(url, cachePath, [this, name, cachePath](bool success) {
|
||||
if (m_inflightCommunityName == name) {
|
||||
@@ -365,7 +332,7 @@ namespace noctalia::theme {
|
||||
if (cfg.source == ThemeSource::Wallpaper) {
|
||||
resolved = resolveWallpaper(cfg, m_config.getDefaultWallpaperPath());
|
||||
} else if (cfg.source == ThemeSource::Community && !cfg.communityPalette.empty()) {
|
||||
const auto cachePath = communityPaletteCachePath(urlEncode(cfg.communityPalette));
|
||||
const auto cachePath = communityPaletteCachePath(cfg.communityPalette);
|
||||
if (std::filesystem::exists(cachePath)) {
|
||||
if (auto parsed = parseCommunityPaletteJson(cachePath)) {
|
||||
resolved = makeResolvedFromParsed(*parsed, cfg);
|
||||
|
||||
Reference in New Issue
Block a user