From c2bfa06faaa118346c0a5052906aa75db7ddeee1 Mon Sep 17 00:00:00 2001 From: doomtube Date: Wed, 7 Jan 2026 03:38:34 -0500 Subject: [PATCH 1/2] fixes lol --- backend/src/controllers/AdminController.cpp | 38 ++++++++- database/init.sql | 4 +- frontend/src/lib/stores/siteSettings.js | 4 +- frontend/src/routes/+layout.svelte | 20 ++++- frontend/src/routes/admin/+page.svelte | 87 +++++++++++++++++++++ 5 files changed, 149 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/AdminController.cpp b/backend/src/controllers/AdminController.cpp index eaa4305..0c1fa02 100644 --- a/backend/src/controllers/AdminController.cpp +++ b/backend/src/controllers/AdminController.cpp @@ -2336,6 +2336,41 @@ void AdminController::updateSiteSettings(const HttpRequestPtr &req, }; } + // Update announcement_enabled if provided + if (json->isMember("announcement_enabled")) { + bool enabled = (*json)["announcement_enabled"].asBool(); + std::string value = enabled ? "true" : "false"; + *dbClient << "INSERT INTO site_settings (setting_key, setting_value) VALUES ('announcement_enabled', $1) " + "ON CONFLICT (setting_key) DO UPDATE SET setting_value = $1, updated_at = CURRENT_TIMESTAMP" + << value + >> [](const Result&) { + LOG_INFO << "Announcement enabled setting updated successfully"; + } + >> [](const DrogonDbException& e) { + LOG_ERROR << "Failed to update announcement_enabled: " << e.base().what(); + }; + } + + // Update announcement_text if provided + if (json->isMember("announcement_text")) { + std::string text = (*json)["announcement_text"].asString(); + // Limit to 500 characters + if (text.length() > 500) { + text = text.substr(0, 500); + } + // Sanitize to prevent XSS + text = htmlEscape(text); + *dbClient << "INSERT INTO site_settings (setting_key, setting_value) VALUES ('announcement_text', $1) " + "ON CONFLICT (setting_key) DO UPDATE SET setting_value = $1, updated_at = CURRENT_TIMESTAMP" + << text + >> [](const Result&) { + LOG_INFO << "Announcement text updated successfully"; + } + >> [](const DrogonDbException& e) { + LOG_ERROR << "Failed to update announcement_text: " << e.base().what(); + }; + } + // Update censored_words if provided (comma-separated list) if (json->isMember("censored_words")) { // Rate limit: 10 updates per minute per admin @@ -2464,7 +2499,8 @@ void AdminController::getPublicSiteSettings(const HttpRequestPtr &, >> [callback](const Result& r) { // Whitelist of publicly-safe settings static const std::unordered_set publicKeys = { - "site_title", "logo_path", "logo_display_mode" + "site_title", "logo_path", "logo_display_mode", + "announcement_enabled", "announcement_text" }; Json::Value resp; diff --git a/database/init.sql b/database/init.sql index 3875dba..9bb3864 100644 --- a/database/init.sql +++ b/database/init.sql @@ -555,7 +555,9 @@ CREATE TABLE IF NOT EXISTS site_settings ( INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_title', 'Stream'), ('logo_path', ''), - ('logo_display_mode', 'text') -- 'text', 'image', 'both' + ('logo_display_mode', 'text'), -- 'text', 'image', 'both' + ('announcement_enabled', 'false'), + ('announcement_text', '') ON CONFLICT (setting_key) DO NOTHING; -- Trigger for site_settings updated_at diff --git a/frontend/src/lib/stores/siteSettings.js b/frontend/src/lib/stores/siteSettings.js index 4dd91f4..4053420 100644 --- a/frontend/src/lib/stores/siteSettings.js +++ b/frontend/src/lib/stores/siteSettings.js @@ -3,5 +3,7 @@ import { writable } from 'svelte/store'; export const siteSettings = writable({ site_title: 'Stream', logo_path: '', - logo_display_mode: 'text' + logo_display_mode: 'text', + announcement_enabled: false, + announcement_text: '' }); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index ad7da15..34a4b4b 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -30,7 +30,9 @@ siteSettings.set({ site_title: settings.site_title || 'Stream', logo_path: settings.logo_path || '', - logo_display_mode: settings.logo_display_mode || 'text' + logo_display_mode: settings.logo_display_mode || 'text', + announcement_enabled: settings.announcement_enabled === 'true', + announcement_text: settings.announcement_text || '' }); } } catch (e) { @@ -413,6 +415,16 @@ .dropdown-item.logout:hover :global(svg) { color: var(--error); } + + .announcement-banner { + background: linear-gradient(135deg, #8b5cf6, #a855f7); + color: white; + padding: 0.75rem 1rem; + text-align: center; + font-size: 0.95rem; + font-weight: 500; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } {#if !isPopoutPage} @@ -559,6 +571,12 @@ +{#if $siteSettings.announcement_enabled && $siteSettings.announcement_text} +
+ {$siteSettings.announcement_text} +
+{/if} + {#if browser} diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 34978f1..0836134 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -57,6 +57,11 @@ lastRenewalError: null, autoRenewalEnabled: true }; + + // Announcement Settings + let announcementEnabled = false; + let announcementText = ''; + let announcementSaving = false; let sslLoading = false; let sslSaving = false; let sslRequesting = false; @@ -1142,6 +1147,8 @@ logo_display_mode: settings.logo_display_mode || 'text' }; censoredWords = settings.censored_words || ''; + announcementEnabled = settings.announcement_enabled === 'true'; + announcementText = settings.announcement_text || ''; } else { console.error('Failed to load site settings'); } @@ -1221,6 +1228,35 @@ setTimeout(() => { message = ''; error = ''; }, 3000); } + async function saveAnnouncementSettings() { + announcementSaving = true; + try { + const response = await fetch('/api/admin/settings/site', { + method: 'PUT', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + announcement_enabled: announcementEnabled, + announcement_text: announcementText + }) + }); + + if (response.ok) { + message = 'Announcement settings updated successfully'; + } else { + error = 'Failed to update announcement settings'; + } + } catch (e) { + error = 'Error updating announcement settings'; + console.error(e); + } + announcementSaving = false; + + setTimeout(() => { message = ''; error = ''; }, 3000); + } + async function loadDefaultAvatars() { try { const response = await fetch('/api/admin/default-avatars', { @@ -3603,6 +3639,57 @@ {/if} + +
+

Site Announcement

+

+ Display a site-wide announcement banner at the top of all pages +

+ +
+
+ + + When enabled, the announcement will be displayed at the top of all pages + +
+ +
+ + + + {announcementText.length}/500 characters. This message will be displayed to all visitors. + +
+ +
+ +
+
+
+ {:else if activeTab === 'botkeys'}

Bot API Keys

From a56ca40204976e3a18570ee0deab63334b276668 Mon Sep 17 00:00:00 2001 From: doomtube Date: Wed, 7 Jan 2026 16:27:43 -0500 Subject: [PATCH 2/2] fixes lol --- backend/src/controllers/UserController.cpp | 39 ++++ backend/src/controllers/UserController.h | 6 + backend/src/services/AuthService.cpp | 16 +- backend/src/services/AuthService.h | 2 + .../controllers/ChatWebSocketController.cpp | 17 +- database/init.sql | 28 ++- frontend/package.json | 2 - frontend/src/app.css | 73 +++++-- .../lib/components/ScreensaverOverlay.svelte | 120 +++++++++++ .../src/lib/components/chat/ChatInput.svelte | 143 ++++++------ .../lib/components/chat/ChatMessage.svelte | 76 +++---- .../src/lib/components/chat/ChatPanel.svelte | 1 + .../components/terminal/terminalStyles.css | 203 ++++++++++-------- frontend/src/lib/stores/screensaver.js | 170 +++++++++++++++ frontend/src/routes/+layout.svelte | 35 ++- frontend/src/routes/settings/+page.svelte | 119 ++++++++++ 16 files changed, 816 insertions(+), 234 deletions(-) create mode 100644 frontend/src/lib/components/ScreensaverOverlay.svelte create mode 100644 frontend/src/lib/stores/screensaver.js diff --git a/backend/src/controllers/UserController.cpp b/backend/src/controllers/UserController.cpp index 4ad4fe6..8abe78a 100644 --- a/backend/src/controllers/UserController.cpp +++ b/backend/src/controllers/UserController.cpp @@ -2684,4 +2684,43 @@ void UserController::getReferralSettings(const HttpRequestPtr &req, callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "get settings", "Failed to get settings"); +} + +void UserController::updateScreensaver(const HttpRequestPtr &req, + std::function &&callback) { + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + bool enabled = (*json).isMember("enabled") ? (*json)["enabled"].asBool() : false; + int timeoutMinutes = (*json).isMember("timeout_minutes") ? (*json)["timeout_minutes"].asInt() : 5; + + // Validate timeout range (1-30 minutes) + if (timeoutMinutes < 1) timeoutMinutes = 1; + if (timeoutMinutes > 30) timeoutMinutes = 30; + + auto dbClient = app().getDbClient(); + *dbClient << "UPDATE users SET screensaver_enabled = $1, screensaver_timeout_minutes = $2 WHERE id = $3" + << enabled << timeoutMinutes << user.id + >> [callback, enabled, timeoutMinutes](const Result&) { + Json::Value resp; + resp["success"] = true; + resp["screensaver"]["enabled"] = enabled; + resp["screensaver"]["timeout_minutes"] = timeoutMinutes; + callback(jsonResp(resp)); + } + >> DB_ERROR_MSG(callback, "update screensaver settings", "Failed to update screensaver settings"); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in updateScreensaver: " << e.what(); + callback(jsonError("Internal server error")); + } } \ No newline at end of file diff --git a/backend/src/controllers/UserController.h b/backend/src/controllers/UserController.h index 0a41059..6084947 100644 --- a/backend/src/controllers/UserController.h +++ b/backend/src/controllers/UserController.h @@ -48,6 +48,8 @@ public: ADD_METHOD_TO(UserController::validateReferralCode, "/api/auth/validate-referral", Post); ADD_METHOD_TO(UserController::registerWithReferral, "/api/auth/register-referral", Post); ADD_METHOD_TO(UserController::getReferralSettings, "/api/settings/referral", Get); + // Screensaver settings + ADD_METHOD_TO(UserController::updateScreensaver, "/api/user/screensaver", Put); METHOD_LIST_END void register_(const HttpRequestPtr &req, @@ -171,6 +173,10 @@ public: void getReferralSettings(const HttpRequestPtr &req, std::function &&callback); + // Screensaver settings + void updateScreensaver(const HttpRequestPtr &req, + std::function &&callback); + private: // Übercoin helper: Calculate burn rate based on account age // Formula: max(1, 99 * e^(-account_age_days / 180)) diff --git a/backend/src/services/AuthService.cpp b/backend/src/services/AuthService.cpp index 5788bee..b387433 100644 --- a/backend/src/services/AuthService.cpp +++ b/backend/src/services/AuthService.cpp @@ -443,7 +443,7 @@ void AuthService::loginUser(const std::string& username, const std::string& pass return; } - *dbClient << "SELECT id, username, password_hash, is_admin, is_moderator, is_streamer, is_restreamer, is_bot, is_texter, is_pgp_only, is_disabled, bio, avatar_url, banner_url, banner_position, banner_zoom, banner_position_x, graffiti_url, pgp_only_enabled_at, user_color " + *dbClient << "SELECT id, username, password_hash, is_admin, is_moderator, is_streamer, is_restreamer, is_bot, is_texter, is_pgp_only, is_disabled, bio, avatar_url, banner_url, banner_position, banner_zoom, banner_position_x, graffiti_url, pgp_only_enabled_at, user_color, screensaver_enabled, screensaver_timeout_minutes " "FROM users WHERE username = $1 LIMIT 1" << username >> [password, callback, this](const Result& r) { @@ -504,6 +504,8 @@ void AuthService::loginUser(const std::string& username, const std::string& pass user.graffitiUrl = r[0]["graffiti_url"].isNull() ? "" : r[0]["graffiti_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + user.screensaverEnabled = r[0]["screensaver_enabled"].isNull() ? false : r[0]["screensaver_enabled"].as(); + user.screensaverTimeoutMinutes = r[0]["screensaver_timeout_minutes"].isNull() ? 5 : r[0]["screensaver_timeout_minutes"].as(); std::string token = generateToken(user); callback(true, token, user); @@ -599,7 +601,7 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string& } *dbClient << "SELECT pk.public_key, u.id, u.username, u.is_admin, u.is_moderator, u.is_streamer, u.is_restreamer, u.is_bot, u.is_texter, " - "u.is_pgp_only, u.is_disabled, u.bio, u.avatar_url, u.banner_url, u.banner_position, u.banner_zoom, u.banner_position_x, u.graffiti_url, u.pgp_only_enabled_at, u.user_color " + "u.is_pgp_only, u.is_disabled, u.bio, u.avatar_url, u.banner_url, u.banner_position, u.banner_zoom, u.banner_position_x, u.graffiti_url, u.pgp_only_enabled_at, u.user_color, u.screensaver_enabled, u.screensaver_timeout_minutes " "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " "WHERE u.username = $1 ORDER BY pk.created_at DESC LIMIT 1" << username @@ -650,6 +652,8 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string& user.graffitiUrl = r[0]["graffiti_url"].isNull() ? "" : r[0]["graffiti_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + user.screensaverEnabled = r[0]["screensaver_enabled"].isNull() ? false : r[0]["screensaver_enabled"].as(); + user.screensaverTimeoutMinutes = r[0]["screensaver_timeout_minutes"].isNull() ? 5 : r[0]["screensaver_timeout_minutes"].as(); std::string token = generateToken(user); callback(true, token, user); @@ -917,7 +921,7 @@ void AuthService::fetchUserInfo(int64_t userId, std::function> [callback](const Result& r) { @@ -946,6 +950,8 @@ void AuthService::fetchUserInfo(int64_t userId, std::function(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + user.screensaverEnabled = r[0]["screensaver_enabled"].isNull() ? false : r[0]["screensaver_enabled"].as(); + user.screensaverTimeoutMinutes = r[0]["screensaver_timeout_minutes"].isNull() ? 5 : r[0]["screensaver_timeout_minutes"].as(); callback(true, user); } catch (const std::exception& e) { @@ -1069,7 +1075,7 @@ void AuthService::validateAndRotateRefreshToken(const std::string& refreshToken, *dbClient << "SELECT rtf.id, rtf.user_id, rtf.family_id, rtf.expires_at, rtf.revoked, " "u.username, u.is_admin, u.is_moderator, u.is_streamer, u.is_restreamer, " "u.is_bot, u.is_texter, u.is_pgp_only, u.is_disabled, u.user_color, u.avatar_url, " - "u.token_version " + "u.token_version, u.screensaver_enabled, u.screensaver_timeout_minutes " "FROM refresh_token_families rtf " "JOIN users u ON rtf.user_id = u.id " "WHERE rtf.current_token_hash = $1" @@ -1130,6 +1136,8 @@ void AuthService::validateAndRotateRefreshToken(const std::string& refreshToken, user.colorCode = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as(); user.avatarUrl = row["avatar_url"].isNull() ? "" : row["avatar_url"].as(); user.tokenVersion = row["token_version"].isNull() ? 1 : row["token_version"].as(); + user.screensaverEnabled = row["screensaver_enabled"].isNull() ? false : row["screensaver_enabled"].as(); + user.screensaverTimeoutMinutes = row["screensaver_timeout_minutes"].isNull() ? 5 : row["screensaver_timeout_minutes"].as(); // Generate new tokens (rotation) std::string newRefreshToken = generateRefreshToken(); diff --git a/backend/src/services/AuthService.h b/backend/src/services/AuthService.h index 0214c10..88749de 100644 --- a/backend/src/services/AuthService.h +++ b/backend/src/services/AuthService.h @@ -29,6 +29,8 @@ struct UserInfo { double ubercoinBalance = 0.0; // Übercoin balance (3 decimal places) std::string createdAt; // Account creation date (for burn rate calculation) int tokenVersion = 1; // SECURITY FIX #10: Token version for revocation + bool screensaverEnabled = false; // Screensaver feature enabled + int screensaverTimeoutMinutes = 5; // Idle timeout before screensaver activates (1-30) }; // Result structure for refresh token operations diff --git a/chat-service/src/controllers/ChatWebSocketController.cpp b/chat-service/src/controllers/ChatWebSocketController.cpp index c47eb1d..3ce4d58 100644 --- a/chat-service/src/controllers/ChatWebSocketController.cpp +++ b/chat-service/src/controllers/ChatWebSocketController.cpp @@ -464,9 +464,24 @@ void ChatWebSocketController::handleChatMessage(const WebSocketConnectionPtr& ws selfDestructSeconds = 300; } + // Get connection info first to check if guest + ConnectionInfo currentInfo; + { + std::lock_guard lock(connectionsMutex_); + auto it = connections_.find(wsConnPtr); + if (it == connections_.end()) { + return; + } + currentInfo = it->second; + } + + // Guests cannot send self-destructing messages + if (currentInfo.isGuest && selfDestructSeconds > 0) { + selfDestructSeconds = 0; + } + // Re-fetch current connection info to prevent TOCTOU race conditions // (user could have been banned/disconnected since the initial copy) - ConnectionInfo currentInfo; { std::lock_guard lock(connectionsMutex_); auto it = connections_.find(wsConnPtr); diff --git a/database/init.sql b/database/init.sql index 9bb3864..e9ee0d2 100644 --- a/database/init.sql +++ b/database/init.sql @@ -1224,4 +1224,30 @@ CREATE TABLE IF NOT EXISTS refresh_token_families ( CREATE INDEX IF NOT EXISTS idx_refresh_families_user_id ON refresh_token_families(user_id); CREATE INDEX IF NOT EXISTS idx_refresh_families_family_id ON refresh_token_families(family_id); CREATE INDEX IF NOT EXISTS idx_refresh_families_active ON refresh_token_families(user_id, revoked) WHERE revoked = FALSE; -CREATE INDEX IF NOT EXISTS idx_refresh_families_expires ON refresh_token_families(expires_at) WHERE revoked = FALSE; \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_refresh_families_expires ON refresh_token_families(expires_at) WHERE revoked = FALSE; + +-- ============================================ +-- SCREENSAVER SETTINGS +-- ============================================ + +-- Add screensaver_enabled column to users table (default disabled) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'screensaver_enabled' + ) THEN + ALTER TABLE users ADD COLUMN screensaver_enabled BOOLEAN DEFAULT false; + END IF; +END $$; + +-- Add screensaver_timeout_minutes column to users table (default 5 minutes, range 1-30) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'screensaver_timeout_minutes' + ) THEN + ALTER TABLE users ADD COLUMN screensaver_timeout_minutes INTEGER DEFAULT 5; + END IF; +END $$; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 4689e6d..85bd0a4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,13 +24,11 @@ "@heroiclabs/nakama-js": "^2.8.0", "@fingerprintjs/fingerprintjs": "^4.5.1", "chess.js": "^1.0.0-beta.8", - "@fortawesome/fontawesome-free": "^6.7.2", "@types/dompurify": "^3.0.5", "dompurify": "^3.3.0", "foliate-js": "^1.0.1", "hls.js": "^1.6.7", "marked": "^17.0.1", - "mdb-ui-kit": "^9.1.0", "openpgp": "^6.0.0-alpha.0", "ovenplayer": "^0.10.43" }, diff --git a/frontend/src/app.css b/frontend/src/app.css index cd27d7c..ed8a61b 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5,14 +5,63 @@ } :root { + /* Primary */ --primary: #561d5e; + --primary-light: #8b3a92; + + /* Legacy aliases (for backwards compatibility) */ --black: #000; --white: #fff; --gray: #888; --light-gray: #f5f5f5; - --error: #dc3545; - --success: #28a745; + + /* Backgrounds (dark to light) */ + --bg-base: #000; + --bg-surface: #0d0d0d; + --bg-elevated: #111; + --bg-input: #1a1a1a; + --bg-hover: rgba(255, 255, 255, 0.05); + --bg-hover-light: rgba(255, 255, 255, 0.1); + + /* Text */ + --text-primary: #fff; + --text-secondary: #c9d1d9; + --text-muted: #888; + --text-faint: #666; + + /* Borders */ --border: #333; + --border-light: #30363d; + --border-hover: #484f58; + + /* Accents */ + --accent-blue: #4a9eff; + --accent-pink: #ec4899; + --accent-pink-light: #f472b6; + --accent-green: #0f0; + --accent-orange: #ff9800; + --accent-gold: #ffd700; + + /* Semantic */ + --success: #28a745; + --error: #dc3545; + --error-light: #f85149; + --warning: #ff9800; + + /* Chat-specific */ + --mention: #ffd700; + --greentext: #789922; + --redtext: #cc1105; + + /* Role Colors */ + --role-admin: #ef4444; + --role-moderator: #a855f7; + --role-streamer: #3b82f6; + --role-restreamer: #14b8a6; + --role-uploader: #22c55e; + --role-texter: #fb923c; + --role-sticker: #ec4899; + --role-watch: #6366f1; } html { @@ -51,7 +100,7 @@ body::-webkit-scrollbar { max-width: 400px; margin: 4rem auto; padding: 2rem; - background: #111; + background: var(--bg-elevated); border-radius: 8px; border: 1px solid var(--border); } @@ -141,7 +190,7 @@ button:disabled { } .nav { - background: #111; + background: var(--bg-elevated); border-bottom: 1px solid var(--border); padding: 1rem 0; margin-bottom: 2rem; @@ -165,7 +214,7 @@ button:disabled { .card { - background: #111; + background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; @@ -178,18 +227,6 @@ button:disabled { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } -.avatar { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; -} - -.avatar-small { - width: 40px; - height: 40px; -} - .file-input-wrapper { position: relative; overflow: hidden; @@ -260,7 +297,7 @@ button:disabled { } .modal-content { - background: #111; + background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px; padding: 2rem; diff --git a/frontend/src/lib/components/ScreensaverOverlay.svelte b/frontend/src/lib/components/ScreensaverOverlay.svelte new file mode 100644 index 0000000..56e5122 --- /dev/null +++ b/frontend/src/lib/components/ScreensaverOverlay.svelte @@ -0,0 +1,120 @@ + + +{#if $isScreensaverActive} +
+
+ {#each snowflakes as flake (flake.id)} +
+ {/each} +
+ +
+ Click or press any key to dismiss +
+
+{/if} + + diff --git a/frontend/src/lib/components/chat/ChatInput.svelte b/frontend/src/lib/components/chat/ChatInput.svelte index 5176d0e..9995735 100644 --- a/frontend/src/lib/components/chat/ChatInput.svelte +++ b/frontend/src/lib/components/chat/ChatInput.svelte @@ -5,6 +5,7 @@ export let disabled = false; export let username = ''; + export let isGuest = false; const dispatch = createEventDispatcher(); @@ -266,38 +267,40 @@
-
- + {#if showTimerMenu} +
+ {#each timerOptions as option} + + {/each} +
{/if} - - {#if showTimerMenu} -
- {#each timerOptions as option} - - {/each} -
- {/if} -
+
+ {/if} {#if $stickerFavorites.length > 0}
{/if} +
{#if activeTab === 'profile'} @@ -3211,6 +3262,74 @@ bot.connect(); {/if} {/if}
+ + {:else if activeTab === 'screensaver'} +
+

Screensaver Settings

+

+ Enable a snowfall screensaver that activates when you're idle. +

+ + {#if screensaverMessage} +
{screensaverMessage}
+ {/if} + + {#if screensaverError} +
{screensaverError}
+ {/if} + + +
+ +

+ When enabled, a snowfall animation will appear after the idle timeout. +

+
+ + {#if screensaverEnabled} +
+ + +

+ Time of inactivity before the screensaver activates (1-30 minutes). +

+
+ +
+

+ Note: The screensaver will not activate while: +

+
    +
  • Video or audio is playing
  • +
  • The browser tab is not visible
  • +
+
+ {/if} + +
+ +
+ +
{/if}