diff --git a/.env.dev b/.env.dev index 0ab8828..9a2727d 100644 --- a/.env.dev +++ b/.env.dev @@ -1,2 +1,10 @@ SIZE_LIMIT=10MiB VERBOSITY=debug +THEME_CUSTOM_CSS_FILE=/app/custom.css +# THEME_SVG=https://mway.io/images/logo/mway_logo_black.svg +# THEME_SVG=https://mway.digital/favicon.svg +# THEME_SVG=https://bluerange.io/img/assets/favicon.svg +# THEME_IMAGE=https://mway.io/images/logo/BlueRange_Logo_black.svg +# THEME_IMAGE=https://repo.mwaysolutions.com/uploads/-/system/appearance/header_logo/1/mway_logo_white_outline_small.png +THEME_IMAGE=/app/logo.jpeg +# THEME_IMAGE="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjMuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA2NjkuNCAxMTcuNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNjY5LjQgMTE3LjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7Y2xpcC1wYXRoOnVybCgjU1ZHSURfMl8pO30KPC9zdHlsZT4KPGc+Cgk8ZGVmcz4KCQk8cmVjdCBpZD0iU1ZHSURfMV8iIHg9IjEuMyIgeT0iMy42IiB3aWR0aD0iNjY2LjEiIGhlaWdodD0iMTExLjgiLz4KCTwvZGVmcz4KCTxjbGlwUGF0aCBpZD0iU1ZHSURfMl8iPgoJCTx1c2UgeGxpbms6aHJlZj0iI1NWR0lEXzFfIiAgc3R5bGU9Im92ZXJmbG93OnZpc2libGU7Ii8+Cgk8L2NsaXBQYXRoPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTEyMi4yLDQ4LjdjMCwxOS4xLTE1LjcsMzQuNi0zNS4yLDM0LjZjLTMuOSwwLTcuNi0wLjYtMTEuMS0xLjhjLTMuMS0xLTYtMi40LTguNi00LjIKCQljLTIuNiwxLjgtNS41LDMuMi04LjYsNC4yYy0zLjUsMS4xLTcuMiwxLjgtMTEuMSwxLjhjLTE5LjQsMC0zNS4yLTE1LjUtMzUuMi0zNC42YzAtMTkuMSwxNS43LTM0LjYsMzUuMi0zNC42CgkJYzMuOSwwLDcuNiwwLjYsMTEuMSwxLjhjMy4xLDEsNiwyLjQsOC42LDQuMmMyLjYtMS44LDUuNS0zLjIsOC42LTQuMmMzLjUtMS4xLDcuMi0xLjgsMTEuMS0xLjhDMTA2LjUsMTQuMSwxMjIuMiwyOS42LDEyMi4yLDQ4LjcKCQkgTTEzMy40LDQ4LjdjMC0yNC45LTIwLjUtNDUuMS00NS43LTQ1LjFjLTcuMSwwLTE0LDEuNi0yMC4zLDQuN0M2MSw1LjIsNTQuMSwzLjYsNDcsMy42QzIxLjgsMy42LDEuMywyMy44LDEuMyw0OC43CgkJYzAsMjQuOSwyMC41LDQ1LjEsNDUuNyw0NS4xYzcuMSwwLDE0LTEuNiwyMC4zLTQuN2M2LjQsMy4xLDEzLjIsNC43LDIwLjMsNC43QzExMi45LDkzLjksMTMzLjQsNzMuNiwxMzMuNCw0OC43Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNODcuMSw3My40YzEzLjcsMCwyNC44LTExLDI0LjgtMjQuNWMwLTEzLjUtMTEuMS0yNC41LTI0LjgtMjQuNWMtMSwwLTIsMC4xLTMuMSwwLjJjLTMuMSwwLjQtNiwxLjMtOC43LDIuOAoJCWMtMy4xLDEuNy01LjksNC4xLTgsNi45Yy0zLjEsNC4xLTQuOCw5LjEtNC45LDE0LjNsMCwwLjNsMCwwLjNjLTAuMSw1LjEtMyw5LjctNy42LDEyLjFjLTIsMS4xLTQuMiwxLjYtNi41LDEuNgoJCWMtMC41LDAtMS4xLDAtMS43LTAuMUMzOS4zLDYyLDM0LDU2LDM0LDQ4LjljMC03LjEsNS40LTEzLDEyLjUtMTMuOWMwLjYtMC4xLDEuMi0wLjEsMS43LTAuMWMyLjMsMCw0LjUsMC41LDYuNSwxLjZsMC41LDAuMgoJCWwwLjItMC41YzEuMi0yLjksMi44LTUuNiw0LjctOC4xbDAuNC0wLjVMNjAsMjcuNGMtMi43LTEuNC01LjYtMi40LTguNy0yLjhjLTEuMS0wLjEtMi4yLTAuMi0zLjEtMC4yYy0xMy43LDAtMjQuOCwxMS0yNC44LDI0LjUKCQljMCwxMy41LDExLjEsMjQuNSwyNC44LDI0LjVjMC45LDAsMS45LTAuMSwzLjEtMC4yYzMuMS0wLjQsNi0xLjMsOC43LTIuOGMzLjEtMS43LDUuOS00LjEsOC02LjljMy4xLTQuMSw0LjgtOS4xLDQuOS0xNC4zbDAtMC4zCgkJbDAtMC4zYzAuMS01LjEsMy05LjcsNy42LTEyLjFjMi0xLjEsNC4yLTEuNiw2LjUtMS42YzAuNSwwLDEuMSwwLDEuNywwLjFjNy4xLDAuOSwxMi41LDYuOCwxMi41LDEzLjljMCw3LjEtNS40LDEzLTEyLjUsMTMuOQoJCWMtMC43LDAuMS0xLjIsMC4xLTEuNywwLjFjLTIuMywwLTQuNS0wLjUtNi41LTEuNmwtMC41LTAuMmwtMC4yLDAuNWMtMS4yLDIuOS0yLjgsNS42LTQuNyw4LjFsLTAuNCwwLjVsMC41LDAuMwoJCWMyLjcsMS40LDUuNiwyLjQsOC43LDIuOEM4NS4yLDczLjMsODYuMSw3My40LDg3LjEsNzMuNCIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTQ4LjEsNTQuMmMtMi45LDAtNS4zLTIuNC01LjMtNS4zYzAtMi45LDIuNC01LjMsNS4zLTUuM2MyLjksMCw1LjMsMi40LDUuMyw1LjNDNTMuNCw1MS44LDUxLDU0LjIsNDguMSw1NC4yIgoJCS8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNTk4LjIsNTYuN2MwLDgtNi41LDE0LjYtMTQuNSwxNC42Yy04LDAtMTQuNS02LjUtMTQuNS0xNC42YzAtOCw2LjUtMTQuNiwxNC41LTE0LjYKCQlDNTkxLjgsNDIuMSw1OTguMiw0OC43LDU5OC4yLDU2LjcgTTYxMC4xLDM1LjdsLTkuNS03LjJsLTQuNSw2LjRjLTMuNi0yLjEtNy44LTMuMy0xMi4zLTMuM2MtMTMuOCwwLTI0LjksMTEuMy0yNC45LDI1LjEKCQljMCwxMy43LDExLDI0LjksMjQuNiwyNS4xYzAsMCwwLDAsMCwwYzcuMywwLDE1LjIsMi44LDE1LjIsMTEuMmMwLDguNS02LjYsMTIuMS0xNSwxMi4xYy04LjQsMC0xNS41LTUuMi0xNS41LTEzLjIKCQljMCwwLTEwLjEsMC0xMC4xLDBjMCwxNC4xLDEwLjksMjMuNCwyNS41LDIzLjRjMTQuNiwwLDI1LjItNy44LDI1LjItMjIuMmMwLTYuNi0yLjItMTEuNy0xMC40LTE2LjFjNi4zLTQuNiwxMC40LTEyLDEwLjQtMjAuNAoJCWMwLTUuMS0xLjUtOS45LTQuMi0xMy45TDYxMC4xLDM1Ljd6IE00NjMuMyw0MS44Yy05LDAtMTYuMyw3LjMtMTYuMywxNi4zYzAsOSw3LjMsMTYuMywxNi4zLDE2LjNjOSwwLDE2LjMtNy4zLDE2LjMtMTYuMwoJCUM0NzkuNiw0OS4xLDQ3Mi4zLDQxLjgsNDYzLjMsNDEuOCBNNDgxLjQsODMuMmwtMS4xLTQuOGMtNC42LDMuOS0xMC42LDYuMi0xNy4xLDYuMmMtMTQuNiwwLTI2LjUtMTEuOS0yNi41LTI2LjUKCQljMC0xNC42LDExLjktMjYuNSwyNi41LTI2LjVjNi41LDAsMTIuNCwyLjMsMTcsNi4ydi0wLjFsMS4xLTQuOGg5LjR2NTAuMkg0ODEuNHogTTIzMS42LDgzLjJoMTAuNXYtNjloLTEwLjVWODMuMnogTTI1My42LDMzdjMwLjYKCQljMCwxMy41LDkuOCwyMSwxOS45LDIxYzUuNCwwLDEzLjItMS45LDE3LjUtNi4ybDAuNyw0LjhoOS41VjMzLjFoLTEwLjVWNjNjMCw2LjUtOC4zLDEyLTE0LjksMTJjLTYuOCwwLTExLjYtNS0xMS42LTExLjVWMzNIMjUzLjYKCQl6IE01NDkuOCw4My4yVjUyLjZjMC0xMy41LTkuOC0yMS0xOS45LTIxYy01LjQsMC0xMy4yLDIuMi0xNy41LDYuNGwtMC43LTVoLTkuNXY1MC4yaDEwLjVWNTMuMmMwLTYuNSw4LjMtMTIsMTQuOS0xMgoJCWM2LjgsMCwxMS42LDUsMTEuNiwxMS41djMwLjVINTQ5Ljh6IE00MTkuNCwzNi43YzAtNC45LTMuMi0xMS44LTEwLjQtMTEuOGgtMjMuN3YyMS41SDQwOUM0MTYuMyw0Ni40LDQxOS40LDQxLjYsNDE5LjQsMzYuNwoJCSBNNDMzLjksODMuM2gtMTMuN2wtMTcuNy0yNi41aC0xNy4ydjI2LjVoLTExLjVWMTQuMWM5LjcsMCwyNS41LDAuMSwzNS4xLDAuMWMxNC4zLDAuMSwyMS45LDExLjcsMjEuOSwyMi4yCgkJYzAsOC4zLTMuOCwxNi44LTE1LjQsMTkuMWwxOC40LDI3VjgzLjN6IE0zNTEuNCw1M2gtMjljMi4xLTYuNSw3LjMtMTEuMiwxNC41LTExLjJTMzQ5LjIsNDYuNSwzNTEuNCw1MyBNMzYyLjQsNjIuMnYtNC4xCgkJYzAtMTQuNi0xMC45LTI2LjUtMjUuNS0yNi41cy0yNS41LDExLjktMjUuNSwyNi41czExLjksMjYuNSwyNi41LDI2LjVjOC4xLDAsMTUuNi0zLjIsMjAuNC04LjZsLTcuNi02LjljLTMsMy4zLTcuNiw1LjItMTIuOSw1LjIKCQljLTcuNSwwLTE0LjUtNS40LTE1LjUtMTIuM0gzNjIuNHogTTY0MS45LDQxLjhjLTcuMiwwLTEyLjMsNC43LTE0LjUsMTEuMmgyOUM2NTQuMyw0Ni41LDY0OS4yLDQxLjgsNjQxLjksNDEuOCBNNjI3LjQsNjIuMgoJCWMxLDYuOCw4LDEyLjMsMTUuNSwxMi4zYzUuMywwLDkuOS0xLjksMTIuOS01LjJsNy42LDYuOWMtNC44LDUuNC0xMi4zLDguNi0yMC40LDguNmMtMTQuNiwwLTI2LjUtMTEuOS0yNi41LTI2LjUKCQlzMTAuOS0yNi41LDI1LjUtMjYuNWMxNC42LDAsMjUuNSwxMS45LDI1LjUsMjYuNXY0LjFINjI3LjR6IE0yMTAuNyw2Mi45YzAsNS41LTMuMiw5LjQtMTAuNiw5LjRoLTI0LjZWNTIuOGgyMy42CgkJQzIwOCw1Mi44LDIxMC43LDU3LjYsMjEwLjcsNjIuOSBNMjA5LDMzLjdjMCwzLjMtMS44LDkuNS0xMC4zLDkuNWgtMjMuM1YyNWgyMy4zQzIwNy41LDI1LDIwOSwzMSwyMDksMzMuNyBNMjE5LjYsMzQuMQoJCWMwLTExLjctOC40LTE5LjktMTkuNS0xOS45aC0zNS4ydjY5aDM1LjJjMTEuOCwwLDIxLjItOC4zLDIxLjItMjEuMmMwLTMuNy0yLjQtMTEuOS05LjEtMTRDMjE3LjUsNDUuNSwyMTkuNiwzOC42LDIxOS42LDM0LjEiLz4KPC9nPgo8L3N2Zz4K" diff --git a/README.md b/README.md index 81d697e..c47338b 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ of the notes even if it tried to. | `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable | | `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. | | `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. Defaults to `true`. | +| `THEME_CUSTOM_CSS_FILE` | `""` | Path to a CSS file inside the container. When set, the file is served at `/custom.css` and loaded by the frontend, allowing full visual customisation via CSS variables or selectors. Mount the file as a volume: `-v ./my-theme.css:/custom.css:ro` and set `THEME_CUSTOM_CSS_FILE=/custom.css`. | | `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. | | `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. | diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 4f21c0d..f1d31d0 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -29,4 +29,8 @@ services: interval: 1m timeout: 3s retries: 2 - start_period: 5s \ No newline at end of file + start_period: 5s + + volumes: + - './custom.css:/app/custom.css' + - './logo.jpeg:/app/logo.jpeg' diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml index 2646a8f..926fff8 100644 --- a/packages/backend/Cargo.toml +++ b/packages/backend/Cargo.toml @@ -25,4 +25,6 @@ lazy_static = "1" ring = "0.17" bs62 = "0.1" byte-unit = "4" -dotenv = "0.15" \ No newline at end of file +dotenv = "0.15" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +base64 = "0.22" diff --git a/packages/backend/src/config.rs b/packages/backend/src/config.rs index b9b4468..b91542c 100644 --- a/packages/backend/src/config.rs +++ b/packages/backend/src/config.rs @@ -1,4 +1,8 @@ use byte_unit::Byte; +use std::sync::OnceLock; + +pub static THEME_RESOLVED_SVG: OnceLock = OnceLock::new(); +pub static THEME_RESOLVED_IMAGE: OnceLock = OnceLock::new(); // Internal lazy_static! { @@ -82,4 +86,6 @@ lazy_static! { .unwrap_or("true".to_string()) .parse() .unwrap(); -} \ No newline at end of file + pub static ref THEME_CUSTOM_CSS_FILE: String = std::env::var("THEME_CUSTOM_CSS_FILE") + .unwrap_or("".to_string()); +} diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs index ff27313..b09f1df 100644 --- a/packages/backend/src/main.rs +++ b/packages/backend/src/main.rs @@ -1,3 +1,7 @@ +use std::{collections::HashMap, sync::Arc}; +use base64::Engine; +use reqwest; + use axum::{ Router, ServiceExt, extract::{DefaultBodyLimit, Request}, @@ -20,15 +24,90 @@ mod note; mod status; mod store; +fn is_svg_content(s: &str) -> bool { + let t = s.trim_start(); + t.starts_with(" &'static str { + let lower = path.to_lowercase(); + if lower.ends_with(".png") { "image/png" } + else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") { "image/jpeg" } + else if lower.ends_with(".gif") { "image/gif" } + else if lower.ends_with(".webp") { "image/webp" } + else if lower.ends_with(".ico") { "image/x-icon" } + else if lower.ends_with(".bmp") { "image/bmp" } + else { "application/octet-stream" } +} + +async fn resolve_theme_image(value: &str) { + if value.is_empty() { + return; + } + + if let Some(rest) = value.strip_prefix("data:") { + // Base64 or raw data URL: data:[mediatype][;base64], + let (header, data) = rest.split_once(',') + .unwrap_or_else(|| panic!("Invalid data URL format in THEME_IMAGE")); + if header.contains("svg") { + let svg = if header.contains(";base64") { + let bytes = base64::engine::general_purpose::STANDARD.decode(data) + .unwrap_or_else(|e| panic!("Invalid base64 in THEME_IMAGE: {e}")); + String::from_utf8(bytes) + .unwrap_or_else(|e| panic!("Invalid UTF-8 in THEME_IMAGE SVG data: {e}")) + } else { + data.to_string() + }; + config::THEME_RESOLVED_SVG.set(svg).unwrap(); + } else { + config::THEME_RESOLVED_IMAGE.set(value.to_string()).unwrap(); + } + } else if value.starts_with("http://") || value.starts_with("https://") { + let response = reqwest::get(value) + .await + .unwrap_or_else(|e| panic!("Failed to fetch THEME_IMAGE from {value}: {e}")); + let is_svg_ct = response.headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .map(|ct| ct.contains("svg")) + .unwrap_or(false); + let content = response.text() + .await + .unwrap_or_else(|e| panic!("Failed to read THEME_IMAGE response: {e}")); + if is_svg_ct || is_svg_content(&content) { + config::THEME_RESOLVED_SVG.set(content).unwrap(); + } else { + config::THEME_RESOLVED_IMAGE.set(value.to_string()).unwrap(); + } + } else { + // Local file path + let bytes = std::fs::read(value) + .unwrap_or_else(|e| panic!("Cannot read THEME_IMAGE file {value}: {e}")); + let is_svg = value.to_lowercase().ends_with(".svg") + || std::str::from_utf8(&bytes).map(is_svg_content).unwrap_or(false); + if is_svg { + let content = String::from_utf8(bytes) + .unwrap_or_else(|e| panic!("Invalid UTF-8 in THEME_IMAGE SVG file: {e}")); + config::THEME_RESOLVED_SVG.set(content).unwrap(); + } else { + let mime = mime_from_path(value); + let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); + config::THEME_RESOLVED_IMAGE.set(format!("data:{mime};base64,{encoded}")).unwrap(); + } + } +} + #[tokio::main] async fn main() { dotenv().ok(); if !store::can_reach_cache() { - println!("cannot reach cache"); - panic!("cannot reach cache"); + println!("cannot reach redis"); + panic!("cannot reach redis"); } - + + resolve_theme_image(config::THEME_IMAGE.as_str()).await; + let notes_routes = Router::new() .route("/", post(note::create)) .route("/{id}", delete(note::view)) @@ -44,9 +123,13 @@ async fn main() { let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); let serve_dir = ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index)); - let app = Router::new() + let mut app = Router::new() .nest("/api", api_routes) - .merge(health_routes) + .merge(health_routes); + if !config::THEME_CUSTOM_CSS_FILE.is_empty() { + app = app.route_service("/custom.css", ServeFile::new(config::THEME_CUSTOM_CSS_FILE.as_str())); + } + let app = app .fallback_service(serve_dir) .layer(DefaultBodyLimit::max(*config::LIMIT)) .layer( @@ -66,4 +149,4 @@ async fn main() { axum::serve(listener, ServiceExt::::into_make_service(app)) .await .unwrap(); -} \ No newline at end of file +} diff --git a/packages/backend/src/status/mod.rs b/packages/backend/src/status/mod.rs index e15941a..45dc11c 100644 --- a/packages/backend/src/status/mod.rs +++ b/packages/backend/src/status/mod.rs @@ -16,11 +16,13 @@ pub struct Status { pub imprint_html: String, // Theme pub theme_image: String, + pub theme_svg: String, pub theme_text: String, pub theme_page_title: String, pub theme_favicon: String, pub theme_new_note_notice: bool, pub theme_home_link: bool, + pub theme_custom_css: bool, } pub async fn get_status() -> (StatusCode, Json) { @@ -35,7 +37,9 @@ pub async fn get_status() -> (StatusCode, Json) { imprint_html: config::IMPRINT_HTML.to_string(), theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE, theme_home_link: *config::THEME_HOME_LINK, - theme_image: config::THEME_IMAGE.to_string(), + theme_custom_css: !config::THEME_CUSTOM_CSS_FILE.is_empty(), + theme_image: config::THEME_RESOLVED_IMAGE.get().cloned().unwrap_or_default(), + theme_svg: config::THEME_RESOLVED_SVG.get().cloned().unwrap_or_default(), theme_text: config::THEME_TEXT.to_string(), theme_page_title: config::THEME_PAGE_TITLE.to_string(), theme_favicon: config::THEME_FAVICON.to_string(), diff --git a/packages/cli/src/shared/api.ts b/packages/cli/src/shared/api.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/lib/views/Header.svelte b/packages/frontend/src/lib/views/Header.svelte index dd6330b..4bdf099 100644 --- a/packages/frontend/src/lib/views/Header.svelte +++ b/packages/frontend/src/lib/views/Header.svelte @@ -10,6 +10,8 @@ {#if $status === null} + {:else if $status.theme_svg} + {@html $status.theme_svg} {:else if $status.theme_image} logo {:else} diff --git a/packages/frontend/src/routes/+layout.svelte b/packages/frontend/src/routes/+layout.svelte index d35c0a3..a551ac1 100644 --- a/packages/frontend/src/routes/+layout.svelte +++ b/packages/frontend/src/routes/+layout.svelte @@ -22,6 +22,9 @@ {$status?.theme_page_title || 'cryptgeon'} + {#if $status?.theme_custom_css} + + {/if} {#await waitLocale() then _} diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 6ce718b..6bcc217 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -30,9 +30,11 @@ export type Status = { imprint_url: string; imprint_html: string; theme_image: string; + theme_svg: string theme_text: string; theme_page_title: string; theme_favicon: string; theme_new_note_notice: boolean; theme_home_link: boolean; -}; \ No newline at end of file + theme_custom_css: boolean +};