fixes lol
All checks were successful
Build and Push / build-all (push) Successful in 9m32s

This commit is contained in:
doomtube 2026-01-07 16:27:43 -05:00
parent c2bfa06faa
commit a56ca40204
16 changed files with 816 additions and 234 deletions

View 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>

View file

@ -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>

View file

@ -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;

View file

@ -666,6 +666,7 @@
bind:this={chatInputRef}
disabled={!isConnected}
username={$chatUserInfo.username}
isGuest={$chatUserInfo.isGuest}
on:send={handleSendMessage}
/>
{/if}

View file

@ -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;
}

View 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);