This commit is contained in:
parent
c2bfa06faa
commit
a56ca40204
16 changed files with 816 additions and 234 deletions
|
|
@ -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<void(const HttpResponsePtr &)> &&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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
// Screensaver settings
|
||||
void updateScreensaver(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
private:
|
||||
// Übercoin helper: Calculate burn rate based on account age
|
||||
// Formula: max(1, 99 * e^(-account_age_days / 180))
|
||||
|
|
|
|||
|
|
@ -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<std::string>();
|
||||
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
|
||||
user.screensaverEnabled = r[0]["screensaver_enabled"].isNull() ? false : r[0]["screensaver_enabled"].as<bool>();
|
||||
user.screensaverTimeoutMinutes = r[0]["screensaver_timeout_minutes"].isNull() ? 5 : r[0]["screensaver_timeout_minutes"].as<int>();
|
||||
|
||||
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<std::string>();
|
||||
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
|
||||
user.screensaverEnabled = r[0]["screensaver_enabled"].isNull() ? false : r[0]["screensaver_enabled"].as<bool>();
|
||||
user.screensaverTimeoutMinutes = r[0]["screensaver_timeout_minutes"].isNull() ? 5 : r[0]["screensaver_timeout_minutes"].as<int>();
|
||||
|
||||
std::string token = generateToken(user);
|
||||
callback(true, token, user);
|
||||
|
|
@ -917,7 +921,7 @@ void AuthService::fetchUserInfo(int64_t userId, std::function<void(bool, const U
|
|||
return;
|
||||
}
|
||||
|
||||
*dbClient << "SELECT id, username, is_admin, is_moderator, is_streamer, is_restreamer, is_bot, is_texter, is_pgp_only, bio, avatar_url, banner_url, banner_position, banner_zoom, banner_position_x, graffiti_url, pgp_only_enabled_at, user_color "
|
||||
*dbClient << "SELECT id, username, is_admin, is_moderator, is_streamer, is_restreamer, is_bot, is_texter, is_pgp_only, 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 id = $1 LIMIT 1"
|
||||
<< userId
|
||||
>> [callback](const Result& r) {
|
||||
|
|
@ -946,6 +950,8 @@ void AuthService::fetchUserInfo(int64_t userId, std::function<void(bool, const U
|
|||
user.graffitiUrl = r[0]["graffiti_url"].isNull() ? "" : r[0]["graffiti_url"].as<std::string>();
|
||||
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
|
||||
user.screensaverEnabled = r[0]["screensaver_enabled"].isNull() ? false : r[0]["screensaver_enabled"].as<bool>();
|
||||
user.screensaverTimeoutMinutes = r[0]["screensaver_timeout_minutes"].isNull() ? 5 : r[0]["screensaver_timeout_minutes"].as<int>();
|
||||
|
||||
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<std::string>();
|
||||
user.avatarUrl = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
user.tokenVersion = row["token_version"].isNull() ? 1 : row["token_version"].as<int>();
|
||||
user.screensaverEnabled = row["screensaver_enabled"].isNull() ? false : row["screensaver_enabled"].as<bool>();
|
||||
user.screensaverTimeoutMinutes = row["screensaver_timeout_minutes"].isNull() ? 5 : row["screensaver_timeout_minutes"].as<int>();
|
||||
|
||||
// Generate new tokens (rotation)
|
||||
std::string newRefreshToken = generateRefreshToken();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<std::mutex> 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<std::mutex> lock(connectionsMutex_);
|
||||
auto it = connections_.find(wsConnPtr);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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 $$;
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
120
frontend/src/lib/components/ScreensaverOverlay.svelte
Normal file
120
frontend/src/lib/components/ScreensaverOverlay.svelte
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { screensaver, isScreensaverActive } from '$lib/stores/screensaver';
|
||||
|
||||
let snowflakes = [];
|
||||
const SNOWFLAKE_COUNT = 100;
|
||||
|
||||
// Generate initial snowflakes
|
||||
function initSnowflakes() {
|
||||
snowflakes = Array.from({ length: SNOWFLAKE_COUNT }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 100, // % position
|
||||
size: Math.random() * 4 + 2, // 2-6px
|
||||
speed: Math.random() * 1 + 0.5, // Fall speed multiplier
|
||||
drift: Math.random() * 2 - 1, // Horizontal drift
|
||||
opacity: Math.random() * 0.5 + 0.5,
|
||||
delay: Math.random() * 10 // Animation delay
|
||||
}));
|
||||
}
|
||||
|
||||
// Dismiss on any interaction
|
||||
function handleDismiss() {
|
||||
screensaver.dismiss();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
initSnowflakes();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $isScreensaverActive}
|
||||
<div
|
||||
class="screensaver-overlay"
|
||||
on:click={handleDismiss}
|
||||
on:keydown={handleDismiss}
|
||||
on:mousemove={handleDismiss}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Click or press any key to dismiss screensaver"
|
||||
>
|
||||
<div class="snowfall">
|
||||
{#each snowflakes as flake (flake.id)}
|
||||
<div
|
||||
class="snowflake"
|
||||
style="
|
||||
--x: {flake.x}%;
|
||||
--size: {flake.size}px;
|
||||
--speed: {flake.speed};
|
||||
--drift: {flake.drift};
|
||||
--opacity: {flake.opacity};
|
||||
--delay: {flake.delay}s;
|
||||
"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="screensaver-hint">
|
||||
Click or press any key to dismiss
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.screensaver-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
z-index: 9997; /* Below other overlays (9998) but above content */
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snowfall {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
top: -10px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
opacity: var(--opacity);
|
||||
animation: fall linear infinite;
|
||||
animation-duration: calc(10s / var(--speed));
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
0% {
|
||||
transform: translateY(-10px) translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) translateX(calc(var(--drift) * 100px));
|
||||
}
|
||||
}
|
||||
|
||||
.screensaver-hint {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 0.875rem;
|
||||
pointer-events: none;
|
||||
animation: fadeInOut 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export let disabled = false;
|
||||
export let username = '';
|
||||
export let isGuest = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -266,38 +267,40 @@
|
|||
<form class="chat-input" on:submit={handleSubmit}>
|
||||
<div class="input-wrapper">
|
||||
<div class="input-icons">
|
||||
<div class="timer-container">
|
||||
<button
|
||||
type="button"
|
||||
class="timer-btn"
|
||||
class:active={selfDestructSeconds > 0}
|
||||
on:click={() => showTimerMenu = !showTimerMenu}
|
||||
title="Self-destruct timer"
|
||||
{disabled}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{#if selfDestructSeconds > 0}
|
||||
<span class="timer-value">{activeTimerLabel}</span>
|
||||
{#if !isGuest}
|
||||
<div class="timer-container">
|
||||
<button
|
||||
type="button"
|
||||
class="timer-btn"
|
||||
class:active={selfDestructSeconds > 0}
|
||||
on:click={() => showTimerMenu = !showTimerMenu}
|
||||
title="Self-destruct timer"
|
||||
{disabled}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{#if selfDestructSeconds > 0}
|
||||
<span class="timer-value">{activeTimerLabel}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if showTimerMenu}
|
||||
<div class="timer-menu">
|
||||
{#each timerOptions as option}
|
||||
<button
|
||||
type="button"
|
||||
class="timer-option"
|
||||
class:selected={selfDestructSeconds === option.value}
|
||||
on:click={() => selectTimer(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if showTimerMenu}
|
||||
<div class="timer-menu">
|
||||
{#each timerOptions as option}
|
||||
<button
|
||||
type="button"
|
||||
class="timer-option"
|
||||
class:selected={selfDestructSeconds === option.value}
|
||||
on:click={() => selectTimer(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $stickerFavorites.length > 0}
|
||||
<div class="favorites-container">
|
||||
<button
|
||||
|
|
@ -405,8 +408,8 @@
|
|||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
border-top: 1px solid #333;
|
||||
background: #0d0d0d;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-surface);
|
||||
flex-shrink: 0; /* Prevent input from shrinking */
|
||||
}
|
||||
|
||||
|
|
@ -415,14 +418,14 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #222;
|
||||
border: 1px solid #333;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: #4a9eff;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.input-icons {
|
||||
|
|
@ -439,7 +442,7 @@
|
|||
padding-right: 4rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
|
|
@ -458,12 +461,12 @@
|
|||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.char-count.warning {
|
||||
color: #ff9800;
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
/* Sticker autocomplete dropdown */
|
||||
|
|
@ -473,8 +476,8 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
|
|
@ -490,7 +493,7 @@
|
|||
padding: 0.5rem 0.75rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
|
@ -500,7 +503,7 @@
|
|||
.autocomplete-item:hover,
|
||||
.autocomplete-item.selected {
|
||||
background: rgba(74, 158, 255, 0.15);
|
||||
color: #fff;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.autocomplete-preview {
|
||||
|
|
@ -530,19 +533,19 @@
|
|||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.timer-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #999;
|
||||
background: var(--bg-hover-light);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.timer-btn.active {
|
||||
background: rgba(255, 152, 0, 0.15);
|
||||
color: #ff9800;
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.timer-btn:disabled {
|
||||
|
|
@ -560,8 +563,8 @@
|
|||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
|
|
@ -574,7 +577,7 @@
|
|||
padding: 0.5rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
|
@ -582,13 +585,13 @@
|
|||
}
|
||||
|
||||
.timer-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
background: var(--bg-hover-light);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.timer-option.selected {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.favorites-container {
|
||||
|
|
@ -603,14 +606,14 @@
|
|||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.favorites-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f5c518;
|
||||
background: var(--bg-hover-light);
|
||||
color: var(--accent-gold);
|
||||
}
|
||||
|
||||
.favorites-btn:disabled {
|
||||
|
|
@ -623,8 +626,8 @@
|
|||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
|
|
@ -648,8 +651,8 @@
|
|||
}
|
||||
|
||||
.favorite-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: #444;
|
||||
background: var(--bg-hover-light);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.favorite-item img {
|
||||
|
|
@ -660,13 +663,13 @@
|
|||
|
||||
.favorite-item .sticker-name {
|
||||
font-size: 0.7rem;
|
||||
color: #999;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.favorite-context-menu {
|
||||
position: fixed;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
|
|
@ -682,7 +685,7 @@
|
|||
padding: 0.6rem 0.75rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
|
@ -690,22 +693,22 @@
|
|||
}
|
||||
|
||||
.favorite-context-menu button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
background: var(--bg-hover-light);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.favorite-context-menu button.danger {
|
||||
color: #ff6b6b;
|
||||
color: var(--error-light);
|
||||
}
|
||||
|
||||
.favorite-context-menu button.danger:hover {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
color: #ff6b6b;
|
||||
background: rgba(248, 81, 73, 0.15);
|
||||
color: var(--error-light);
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
background: #333;
|
||||
background: var(--border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -485,17 +485,17 @@
|
|||
flex-direction: column;
|
||||
padding: 0.1rem 0.2rem;
|
||||
border-radius: 2px;
|
||||
background: #000;
|
||||
background: var(--bg-base);
|
||||
transition: background 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-message:hover {
|
||||
background: #111;
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.chat-message.own-message {
|
||||
background: #000;
|
||||
background: var(--bg-base);
|
||||
}
|
||||
|
||||
.chat-message.compact {
|
||||
|
|
@ -546,9 +546,9 @@
|
|||
justify-content: center;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text-primary);
|
||||
flex-shrink: 0;
|
||||
background: #666;
|
||||
background: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
|
|
@ -605,8 +605,8 @@
|
|||
}
|
||||
|
||||
.badge.guest {
|
||||
background: #666;
|
||||
color: #fff;
|
||||
background: var(--text-faint);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.cmd-tag {
|
||||
|
|
@ -614,12 +614,12 @@
|
|||
padding: 0.05rem 0.25rem;
|
||||
border-radius: 2px;
|
||||
font-weight: 700;
|
||||
background: #888;
|
||||
color: #000;
|
||||
background: var(--text-muted);
|
||||
color: var(--bg-base);
|
||||
}
|
||||
|
||||
.realm-link {
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.6rem;
|
||||
text-decoration: none;
|
||||
padding: 0.05rem 0.25rem;
|
||||
|
|
@ -643,12 +643,12 @@
|
|||
}
|
||||
|
||||
.timestamp {
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.epoch {
|
||||
color: #555;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.6rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
@ -657,7 +657,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: #ff9800;
|
||||
color: var(--accent-orange);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 0.125rem 0.375rem;
|
||||
|
|
@ -679,7 +679,7 @@
|
|||
.mod-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
padding: 0 0.25rem;
|
||||
font-size: 1rem;
|
||||
|
|
@ -693,7 +693,7 @@
|
|||
}
|
||||
|
||||
.mod-button:hover {
|
||||
color: #fff;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.message-row {
|
||||
|
|
@ -711,7 +711,7 @@
|
|||
}
|
||||
|
||||
.message-content {
|
||||
color: #ddd;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.25;
|
||||
word-wrap: break-word;
|
||||
flex: 1;
|
||||
|
|
@ -771,7 +771,7 @@
|
|||
|
||||
.message-content :global(blockquote p) {
|
||||
font-weight: 500;
|
||||
color: #789922;
|
||||
color: var(--greentext);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
|
|
@ -785,7 +785,7 @@
|
|||
|
||||
.message-content :global(blockquote h1) {
|
||||
font-weight: bold;
|
||||
color: #789922;
|
||||
color: var(--greentext);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
|
|
@ -800,7 +800,7 @@
|
|||
/* Redtext - custom syntax */
|
||||
.message-content :global(.redtext) {
|
||||
font-weight: 500;
|
||||
color: #cc1105;
|
||||
color: var(--redtext);
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
@ -904,8 +904,8 @@
|
|||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background: #333;
|
||||
border: 1px solid #444;
|
||||
background: var(--border);
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem;
|
||||
z-index: 10;
|
||||
|
|
@ -916,9 +916,9 @@
|
|||
}
|
||||
|
||||
.mod-menu button {
|
||||
background: #444;
|
||||
background: var(--border-hover);
|
||||
border: none;
|
||||
color: #fff;
|
||||
color: var(--text-primary);
|
||||
padding: 0.5rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
|
@ -934,7 +934,7 @@
|
|||
.tts-mute-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
|
|
@ -950,24 +950,24 @@
|
|||
}
|
||||
|
||||
.tts-mute-btn:hover {
|
||||
color: #999;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-hover-light);
|
||||
}
|
||||
|
||||
.tts-mute-btn.muted {
|
||||
color: #f44336;
|
||||
color: var(--error);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tts-mute-btn.muted:hover {
|
||||
color: #ff6659;
|
||||
color: var(--error-light);
|
||||
}
|
||||
|
||||
/* Self-delete button for mods */
|
||||
.self-delete-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
|
|
@ -983,15 +983,15 @@
|
|||
}
|
||||
|
||||
.self-delete-btn:hover {
|
||||
color: #f44336;
|
||||
color: var(--error);
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
|
||||
/* Sticker context menu - must be global since it's outside the component div */
|
||||
:global(.sticker-context-menu) {
|
||||
position: fixed;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
|
|
@ -1006,7 +1006,7 @@
|
|||
padding: 0.6rem 0.75rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
|
@ -1014,14 +1014,14 @@
|
|||
}
|
||||
|
||||
:global(.sticker-context-menu button:hover) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
background: var(--bg-hover-light);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Mentioned message styling */
|
||||
.chat-message.mentioned {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
border-left: 2px solid #ffd700;
|
||||
border-left: 2px solid var(--mention);
|
||||
}
|
||||
|
||||
.chat-message.mentioned:hover {
|
||||
|
|
@ -1029,13 +1029,13 @@
|
|||
}
|
||||
|
||||
.chat-message.mentioned .message-content {
|
||||
color: #fff;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* @mention highlighting */
|
||||
.message-content :global(.mention) {
|
||||
color: #ffd700;
|
||||
color: var(--mention);
|
||||
font-weight: 700;
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
padding: 0 0.25rem;
|
||||
|
|
|
|||
|
|
@ -666,6 +666,7 @@
|
|||
bind:this={chatInputRef}
|
||||
disabled={!isConnected}
|
||||
username={$chatUserInfo.username}
|
||||
isGuest={$chatUserInfo.isGuest}
|
||||
on:send={handleSendMessage}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,25 @@
|
|||
* This file contains CSS shared between ChatTerminal overlay and terminal popout page
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
TERMINAL CSS VARIABLES (inherit from app.css)
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Terminal-specific tokens that reference global variables */
|
||||
--terminal-bg: var(--bg-surface, #0d0d0d);
|
||||
--terminal-header-bg: var(--bg-elevated, #161b22);
|
||||
--terminal-text: var(--text-secondary, #c9d1d9);
|
||||
--terminal-text-muted: var(--text-muted, #8b949e);
|
||||
--terminal-text-faint: var(--text-faint, #6e7681);
|
||||
--terminal-border: var(--border-light, #30363d);
|
||||
--terminal-border-subtle: #21262d;
|
||||
--terminal-active: var(--accent-green, #0f0);
|
||||
--terminal-audio: var(--accent-pink, #ec4899);
|
||||
--terminal-audio-light: var(--accent-pink-light, #f472b6);
|
||||
--terminal-error: var(--error-light, #f85149);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HEADER & TAB NAVIGATION
|
||||
============================================ */
|
||||
|
|
@ -10,8 +29,8 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #161b22;
|
||||
border-bottom: 1px solid #30363d;
|
||||
background: var(--terminal-header-bg);
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
|
|
@ -25,10 +44,10 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
cursor: pointer;
|
||||
|
|
@ -36,19 +55,19 @@
|
|||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #c9d1d9;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--terminal-text);
|
||||
background: var(--bg-hover-light);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #0f0;
|
||||
color: var(--terminal-active);
|
||||
background: rgba(0, 255, 0, 0.1);
|
||||
border-color: rgba(0, 255, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Audio tab special color */
|
||||
.tab-button.audio.active {
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
background: rgba(236, 72, 153, 0.15);
|
||||
border-color: rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
|
@ -66,11 +85,11 @@
|
|||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #f85149;
|
||||
background: var(--terminal-error);
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background: #0f0;
|
||||
background: var(--terminal-active);
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
|
|
@ -82,7 +101,7 @@
|
|||
.control-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
width: 1.5rem;
|
||||
|
|
@ -95,14 +114,14 @@
|
|||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: #30363d;
|
||||
color: #c9d1d9;
|
||||
background: var(--terminal-border);
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
|
@ -115,8 +134,8 @@
|
|||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #30363d;
|
||||
color: #c9d1d9;
|
||||
background: var(--terminal-border);
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
@ -139,8 +158,8 @@
|
|||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
color: #c9d1d9;
|
||||
background: #0d1117;
|
||||
color: var(--terminal-text);
|
||||
background: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.terminal-messages::-webkit-scrollbar {
|
||||
|
|
@ -148,16 +167,16 @@
|
|||
}
|
||||
|
||||
.terminal-messages::-webkit-scrollbar-track {
|
||||
background: #0d1117;
|
||||
background: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.terminal-messages::-webkit-scrollbar-thumb {
|
||||
background: #30363d;
|
||||
background: var(--terminal-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terminal-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #484f58;
|
||||
background: var(--border-hover);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
@ -173,7 +192,7 @@
|
|||
}
|
||||
|
||||
.prompt {
|
||||
color: #0f0;
|
||||
color: var(--terminal-active);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
|
|
@ -183,15 +202,15 @@
|
|||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
outline: none;
|
||||
caret-color: #0f0;
|
||||
caret-color: var(--terminal-active);
|
||||
}
|
||||
|
||||
.terminal-input::placeholder {
|
||||
color: #484f58;
|
||||
color: var(--border-hover);
|
||||
}
|
||||
|
||||
.terminal-input:disabled {
|
||||
|
|
@ -204,7 +223,7 @@
|
|||
|
||||
.system-message {
|
||||
padding: 0.125rem 0;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
border-left: none;
|
||||
|
|
@ -213,13 +232,13 @@
|
|||
}
|
||||
|
||||
.system-prefix {
|
||||
color: #6e7681;
|
||||
color: var(--terminal-text-faint);
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.system-text {
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
@ -294,7 +313,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #0d1117;
|
||||
background: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.streams-header {
|
||||
|
|
@ -302,11 +321,11 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid #30363d;
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.streams-title {
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
|
@ -317,7 +336,7 @@
|
|||
background: rgba(0, 255, 0, 0.1);
|
||||
border: 1px solid rgba(0, 255, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
color: #0f0;
|
||||
color: var(--terminal-active);
|
||||
font-size: 0.7rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
|
|
@ -339,17 +358,17 @@
|
|||
}
|
||||
|
||||
.streams-list::-webkit-scrollbar-track {
|
||||
background: #0d1117;
|
||||
background: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.streams-list::-webkit-scrollbar-thumb {
|
||||
background: #30363d;
|
||||
background: var(--terminal-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.streams-loading,
|
||||
.streams-empty {
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
|
|
@ -357,11 +376,11 @@
|
|||
|
||||
.streams-section-header {
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.5rem 0.25rem 0.25rem;
|
||||
border-bottom: 1px solid #21262d;
|
||||
border-bottom: 1px solid var(--terminal-border-subtle);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +424,7 @@
|
|||
height: 27px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
background: var(--bg-input);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +463,7 @@
|
|||
}
|
||||
|
||||
.stream-name {
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
|
@ -457,11 +476,11 @@
|
|||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
}
|
||||
|
||||
.viewer-count {
|
||||
color: #f85149;
|
||||
color: var(--terminal-error);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
|
@ -479,7 +498,7 @@
|
|||
}
|
||||
|
||||
.offline-badge {
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.65rem;
|
||||
background: rgba(139, 148, 158, 0.2);
|
||||
padding: 0.1rem 0.3rem;
|
||||
|
|
@ -490,9 +509,9 @@
|
|||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #30363d;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #8b949e;
|
||||
border: 1px solid var(--terminal-border);
|
||||
background: var(--bg-hover);
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
@ -505,13 +524,13 @@
|
|||
.tile-btn:hover {
|
||||
background: rgba(0, 255, 0, 0.15);
|
||||
border-color: rgba(0, 255, 0, 0.3);
|
||||
color: #0f0;
|
||||
color: var(--terminal-active);
|
||||
}
|
||||
|
||||
.tile-btn.active {
|
||||
background: rgba(0, 255, 0, 0.2);
|
||||
border-color: #0f0;
|
||||
color: #0f0;
|
||||
border-color: var(--terminal-active);
|
||||
color: var(--terminal-active);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
@ -523,7 +542,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #0d1117;
|
||||
background: var(--terminal-bg);
|
||||
}
|
||||
|
||||
/* Player section (foobar/winamp style) */
|
||||
|
|
@ -545,7 +564,7 @@
|
|||
height: 48px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #222;
|
||||
background: var(--bg-input);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -572,7 +591,7 @@
|
|||
|
||||
.player-title {
|
||||
font-size: 0.85rem;
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -581,13 +600,13 @@
|
|||
|
||||
.player-artist {
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.player-progress {
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: var(--bg-hover-light);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.25rem;
|
||||
|
|
@ -596,7 +615,7 @@
|
|||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ec4899, #f472b6);
|
||||
background: linear-gradient(90deg, var(--terminal-audio), var(--terminal-audio-light));
|
||||
border-radius: 3px;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
|
@ -605,7 +624,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.65rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-family: monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
|
@ -624,8 +643,8 @@
|
|||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #c9d1d9;
|
||||
background: var(--bg-hover-light);
|
||||
color: var(--terminal-text);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
@ -640,18 +659,18 @@
|
|||
|
||||
.ctrl-btn.active {
|
||||
background: rgba(236, 72, 153, 0.3);
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
}
|
||||
|
||||
.ctrl-btn.play {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #ec4899;
|
||||
background: var(--terminal-audio);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ctrl-btn.play:hover {
|
||||
background: #f472b6;
|
||||
background: var(--terminal-audio-light);
|
||||
}
|
||||
|
||||
/* Volume control */
|
||||
|
|
@ -667,7 +686,7 @@
|
|||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
@ -676,7 +695,7 @@
|
|||
}
|
||||
|
||||
.vol-btn:hover {
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
|
|
@ -684,7 +703,7 @@
|
|||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: var(--bg-hover-light);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -694,7 +713,7 @@
|
|||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #ec4899;
|
||||
background: var(--terminal-audio);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -702,7 +721,7 @@
|
|||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #ec4899;
|
||||
background: var(--terminal-audio);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -714,19 +733,19 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.player-hint {
|
||||
font-size: 0.7rem;
|
||||
color: #6e7681;
|
||||
color: var(--terminal-text-faint);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Queue section */
|
||||
.queue-section {
|
||||
border-bottom: 1px solid #30363d;
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.queue-header {
|
||||
|
|
@ -736,7 +755,7 @@
|
|||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
|
|
@ -744,7 +763,7 @@
|
|||
background: rgba(248, 81, 73, 0.2);
|
||||
border: 1px solid rgba(248, 81, 73, 0.3);
|
||||
border-radius: 3px;
|
||||
color: #f85149;
|
||||
color: var(--terminal-error);
|
||||
font-size: 0.65rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
|
|
@ -765,18 +784,18 @@
|
|||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.queue-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.queue-item.active {
|
||||
background: rgba(236, 72, 153, 0.1);
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
}
|
||||
|
||||
.queue-index {
|
||||
|
|
@ -790,11 +809,11 @@
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.queue-item.active .queue-title {
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
}
|
||||
|
||||
.queue-duration {
|
||||
|
|
@ -807,7 +826,7 @@
|
|||
height: 18px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
@ -824,17 +843,17 @@
|
|||
|
||||
.remove-btn:hover {
|
||||
background: rgba(248, 81, 73, 0.2);
|
||||
color: #f85149;
|
||||
color: var(--terminal-error);
|
||||
}
|
||||
|
||||
/* Browse section */
|
||||
.audio-browse-header {
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid #21262d;
|
||||
border-bottom: 1px solid var(--terminal-border-subtle);
|
||||
}
|
||||
|
||||
.audio-list {
|
||||
|
|
@ -848,17 +867,17 @@
|
|||
}
|
||||
|
||||
.audio-list::-webkit-scrollbar-track {
|
||||
background: #0d1117;
|
||||
background: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.audio-list::-webkit-scrollbar-thumb {
|
||||
background: #30363d;
|
||||
background: var(--terminal-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.audio-loading,
|
||||
.audio-empty {
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
|
|
@ -882,7 +901,7 @@
|
|||
height: 40px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
background: var(--bg-input);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -921,7 +940,7 @@
|
|||
}
|
||||
|
||||
.audio-name {
|
||||
color: #c9d1d9;
|
||||
color: var(--terminal-text);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
|
@ -934,11 +953,11 @@
|
|||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
color: var(--terminal-text-muted);
|
||||
}
|
||||
|
||||
.audio-plays {
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
}
|
||||
|
||||
.audio-user {
|
||||
|
|
@ -962,9 +981,9 @@
|
|||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #30363d;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #8b949e;
|
||||
border: 1px solid var(--terminal-border);
|
||||
background: var(--bg-hover);
|
||||
color: var(--terminal-text-muted);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
@ -976,16 +995,16 @@
|
|||
.audio-btn:hover {
|
||||
background: rgba(236, 72, 153, 0.15);
|
||||
border-color: rgba(236, 72, 153, 0.3);
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
}
|
||||
|
||||
.audio-btn.play {
|
||||
background: rgba(236, 72, 153, 0.2);
|
||||
border-color: rgba(236, 72, 153, 0.3);
|
||||
color: #ec4899;
|
||||
color: var(--terminal-audio);
|
||||
}
|
||||
|
||||
.audio-btn.play:hover {
|
||||
background: #ec4899;
|
||||
background: var(--terminal-audio);
|
||||
color: white;
|
||||
}
|
||||
|
|
|
|||
170
frontend/src/lib/stores/screensaver.js
Normal file
170
frontend/src/lib/stores/screensaver.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { writable, derived } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const defaultState = {
|
||||
// User settings (from backend)
|
||||
enabled: false,
|
||||
timeoutMinutes: 5,
|
||||
|
||||
// Runtime state
|
||||
active: false, // Is screensaver currently showing
|
||||
idleTime: 0, // Current idle time in seconds
|
||||
tabVisible: true, // Is tab currently visible
|
||||
mediaPlaying: false // Is any media currently playing
|
||||
};
|
||||
|
||||
function createScreensaverStore() {
|
||||
const { subscribe, set, update } = writable(defaultState);
|
||||
|
||||
let idleTimer = null;
|
||||
let activityListenersAdded = false;
|
||||
|
||||
// Activity events to track
|
||||
const activityEvents = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'];
|
||||
|
||||
function resetIdleTimer() {
|
||||
update(state => {
|
||||
if (state.active) {
|
||||
// Dismiss screensaver on any activity
|
||||
return { ...state, active: false, idleTime: 0 };
|
||||
}
|
||||
return { ...state, idleTime: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
function checkMediaPlaying() {
|
||||
if (!browser) return false;
|
||||
|
||||
// Check for playing video elements
|
||||
const videos = document.querySelectorAll('video');
|
||||
for (const video of videos) {
|
||||
if (!video.paused && !video.ended) return true;
|
||||
}
|
||||
|
||||
// Check for playing audio elements
|
||||
const audios = document.querySelectorAll('audio');
|
||||
for (const audio of audios) {
|
||||
if (!audio.paused && !audio.ended) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
update(state => ({
|
||||
...state,
|
||||
tabVisible: document.visibilityState === 'visible',
|
||||
idleTime: 0 // Reset idle time on visibility change
|
||||
}));
|
||||
}
|
||||
|
||||
function startTracking() {
|
||||
if (!browser || activityListenersAdded) return;
|
||||
|
||||
// Handle visibility changes
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Handle activity events
|
||||
activityEvents.forEach(event => {
|
||||
document.addEventListener(event, resetIdleTimer, { passive: true });
|
||||
});
|
||||
|
||||
activityListenersAdded = true;
|
||||
|
||||
// Start idle timer (check every second)
|
||||
if (idleTimer) clearInterval(idleTimer);
|
||||
idleTimer = setInterval(() => {
|
||||
update(state => {
|
||||
// Don't do anything if disabled
|
||||
if (!state.enabled) return state;
|
||||
|
||||
const mediaPlaying = checkMediaPlaying();
|
||||
const newIdleTime = state.idleTime + 1;
|
||||
const newState = { ...state, idleTime: newIdleTime, mediaPlaying };
|
||||
|
||||
// Check if should activate
|
||||
// Don't activate if: disabled, tab not visible, media playing, already active
|
||||
if (!state.enabled || !state.tabVisible || mediaPlaying || state.active) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
// Check if idle time exceeds timeout
|
||||
if (newIdleTime >= state.timeoutMinutes * 60) {
|
||||
return { ...newState, active: true };
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTracking() {
|
||||
if (!browser) return;
|
||||
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
activityEvents.forEach(event => {
|
||||
document.removeEventListener(event, resetIdleTimer);
|
||||
});
|
||||
|
||||
activityListenersAdded = false;
|
||||
|
||||
if (idleTimer) {
|
||||
clearInterval(idleTimer);
|
||||
idleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
// Initialize from user settings
|
||||
init(settings) {
|
||||
const enabled = settings?.screensaverEnabled || false;
|
||||
const timeoutMinutes = settings?.screensaverTimeoutMinutes || 5;
|
||||
|
||||
update(state => ({
|
||||
...state,
|
||||
enabled,
|
||||
timeoutMinutes
|
||||
}));
|
||||
|
||||
if (browser && enabled) {
|
||||
startTracking();
|
||||
}
|
||||
},
|
||||
|
||||
// Update settings from API response
|
||||
updateSettings(enabled, timeoutMinutes) {
|
||||
update(state => ({
|
||||
...state,
|
||||
enabled,
|
||||
timeoutMinutes
|
||||
}));
|
||||
|
||||
if (browser) {
|
||||
if (enabled) {
|
||||
startTracking();
|
||||
} else {
|
||||
stopTracking();
|
||||
update(state => ({ ...state, active: false }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Manually dismiss the screensaver
|
||||
dismiss() {
|
||||
update(state => ({ ...state, active: false, idleTime: 0 }));
|
||||
},
|
||||
|
||||
// Cleanup on component destroy
|
||||
cleanup() {
|
||||
stopTracking();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const screensaver = createScreensaverStore();
|
||||
|
||||
// Derived store for whether screensaver is active
|
||||
export const isScreensaverActive = derived(screensaver, $s => $s.active);
|
||||
|
|
@ -9,6 +9,8 @@
|
|||
import EbookReaderOverlay from '$lib/components/EbookReaderOverlay.svelte';
|
||||
import ChessGameOverlay from '$lib/components/ChessGameOverlay.svelte';
|
||||
import GlobalAudioPlayer from '$lib/components/GlobalAudioPlayer.svelte';
|
||||
import ScreensaverOverlay from '$lib/components/ScreensaverOverlay.svelte';
|
||||
import { screensaver } from '$lib/stores/screensaver';
|
||||
import '../app.css';
|
||||
|
||||
let showDropdown = false;
|
||||
|
|
@ -43,6 +45,22 @@
|
|||
onMount(() => {
|
||||
auth.init().then(() => {
|
||||
fetchBalance();
|
||||
// Initialize screensaver if user is authenticated
|
||||
const unsubscribe = auth.subscribe(state => {
|
||||
if (state.user) {
|
||||
screensaver.init({
|
||||
screensaverEnabled: state.user.screensaverEnabled,
|
||||
screensaverTimeoutMinutes: state.user.screensaverTimeoutMinutes
|
||||
});
|
||||
}
|
||||
});
|
||||
// Initial check
|
||||
if ($auth.user) {
|
||||
screensaver.init({
|
||||
screensaverEnabled: $auth.user.screensaverEnabled,
|
||||
screensaverTimeoutMinutes: $auth.user.screensaverTimeoutMinutes
|
||||
});
|
||||
}
|
||||
});
|
||||
loadSiteSettings();
|
||||
|
||||
|
|
@ -291,42 +309,42 @@
|
|||
|
||||
.role-badge.admin {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
color: var(--role-admin);
|
||||
}
|
||||
|
||||
.role-badge.moderator {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
color: #a855f7;
|
||||
color: var(--role-moderator);
|
||||
}
|
||||
|
||||
.role-badge.streamer {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
color: var(--role-streamer);
|
||||
}
|
||||
|
||||
.role-badge.restreamer {
|
||||
background: rgba(20, 184, 166, 0.2);
|
||||
color: #14b8a6;
|
||||
color: var(--role-restreamer);
|
||||
}
|
||||
|
||||
.role-badge.uploader {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22c55e;
|
||||
color: var(--role-uploader);
|
||||
}
|
||||
|
||||
.role-badge.texter {
|
||||
background: rgba(251, 146, 60, 0.2);
|
||||
color: #fb923c;
|
||||
color: var(--role-texter);
|
||||
}
|
||||
|
||||
.role-badge.sticker-creator {
|
||||
background: rgba(236, 72, 153, 0.2);
|
||||
color: #ec4899;
|
||||
color: var(--role-sticker);
|
||||
}
|
||||
|
||||
.role-badge.watch-creator {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #6366f1;
|
||||
color: var(--role-watch);
|
||||
}
|
||||
|
||||
.dropdown-username {
|
||||
|
|
@ -582,6 +600,7 @@
|
|||
<ChatTerminal />
|
||||
<EbookReaderOverlay />
|
||||
<ChessGameOverlay />
|
||||
<ScreensaverOverlay />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { auth, isAuthenticated, isBot, isStickerCreator } from '$lib/stores/auth';
|
||||
import { siteSettings } from '$lib/stores/siteSettings';
|
||||
import { screensaver } from '$lib/stores/screensaver';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as pgp from '$lib/pgp';
|
||||
import GraffitiEditor from '$lib/components/GraffitiEditor.svelte';
|
||||
|
|
@ -108,6 +109,13 @@
|
|||
let referralPurchasing = false;
|
||||
let referralSystemEnabled = false;
|
||||
|
||||
// Screensaver settings
|
||||
let screensaverEnabled = false;
|
||||
let screensaverTimeoutMinutes = 5;
|
||||
let screensaverLoading = false;
|
||||
let screensaverMessage = '';
|
||||
let screensaverError = '';
|
||||
|
||||
// User data for safe access
|
||||
let currentUser = null;
|
||||
|
||||
|
|
@ -151,6 +159,8 @@
|
|||
graffitiUrl = data.user.graffitiUrl || '';
|
||||
userColor = data.user.colorCode || '#561D5E';
|
||||
newColor = userColor;
|
||||
screensaverEnabled = data.user.screensaverEnabled || false;
|
||||
screensaverTimeoutMinutes = data.user.screensaverTimeoutMinutes || 5;
|
||||
currentUser = data.user;
|
||||
|
||||
// Update auth store with fresh data
|
||||
|
|
@ -564,6 +574,40 @@
|
|||
setTimeout(() => { referralMessage = ''; }, 3000);
|
||||
}
|
||||
|
||||
async function updateScreensaverSettings() {
|
||||
screensaverLoading = true;
|
||||
screensaverError = '';
|
||||
screensaverMessage = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/screensaver', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
enabled: screensaverEnabled,
|
||||
timeout_minutes: screensaverTimeoutMinutes
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
screensaverMessage = 'Screensaver settings saved';
|
||||
// Update the screensaver store
|
||||
screensaver.updateSettings(screensaverEnabled, screensaverTimeoutMinutes);
|
||||
setTimeout(() => { screensaverMessage = ''; }, 3000);
|
||||
} else {
|
||||
screensaverError = data.error || 'Failed to save settings';
|
||||
}
|
||||
} catch (e) {
|
||||
screensaverError = 'Error saving settings';
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
screensaverLoading = false;
|
||||
}
|
||||
|
||||
function validatePassword(pass) {
|
||||
if (pass.length < 8) {
|
||||
return 'Password must be at least 8 characters';
|
||||
|
|
@ -2196,6 +2240,13 @@
|
|||
Referrals
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="tab-button"
|
||||
class:active={activeTab === 'screensaver'}
|
||||
on:click={() => activeTab = 'screensaver'}
|
||||
>
|
||||
Screensaver
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if activeTab === 'profile'}
|
||||
|
|
@ -3211,6 +3262,74 @@ bot.connect();</code></pre>
|
|||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'screensaver'}
|
||||
<div class="card">
|
||||
<h2>Screensaver Settings</h2>
|
||||
<p style="color: var(--gray); font-size: 0.9rem; margin-bottom: 1.5rem;">
|
||||
Enable a snowfall screensaver that activates when you're idle.
|
||||
</p>
|
||||
|
||||
{#if screensaverMessage}
|
||||
<div class="success" style="margin-bottom: 1rem;">{screensaverMessage}</div>
|
||||
{/if}
|
||||
|
||||
{#if screensaverError}
|
||||
<div class="error" style="margin-bottom: 1rem;">{screensaverError}</div>
|
||||
{/if}
|
||||
|
||||
<form on:submit|preventDefault={updateScreensaverSettings}>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={screensaverEnabled}
|
||||
style="width: 18px; height: 18px; cursor: pointer;"
|
||||
/>
|
||||
<span>Enable screensaver</span>
|
||||
</label>
|
||||
<p style="font-size: 0.85rem; color: var(--gray); margin-top: 0.5rem;">
|
||||
When enabled, a snowfall animation will appear after the idle timeout.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if screensaverEnabled}
|
||||
<div class="form-group" style="margin-top: 1.5rem;">
|
||||
<label for="screensaver-timeout">Idle timeout (minutes)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="screensaver-timeout"
|
||||
bind:value={screensaverTimeoutMinutes}
|
||||
min="1"
|
||||
max="30"
|
||||
style="width: 100px;"
|
||||
/>
|
||||
<p style="font-size: 0.85rem; color: var(--gray); margin-top: 0.5rem;">
|
||||
Time of inactivity before the screensaver activates (1-30 minutes).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: rgba(139, 92, 246, 0.1); border-radius: 8px; border: 1px solid rgba(139, 92, 246, 0.2);">
|
||||
<p style="font-size: 0.85rem; color: var(--gray); margin: 0;">
|
||||
<strong>Note:</strong> The screensaver will not activate while:
|
||||
</p>
|
||||
<ul style="font-size: 0.85rem; color: var(--gray); margin: 0.5rem 0 0 1.5rem; padding: 0;">
|
||||
<li>Video or audio is playing</li>
|
||||
<li>The browser tab is not visible</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={screensaverLoading}
|
||||
>
|
||||
{screensaverLoading ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue