Website Status
This commit is contained in:
@@ -12,6 +12,32 @@
|
||||
let mobileMenuOpen = $state(false);
|
||||
let currentTime = $state(new Date());
|
||||
|
||||
// Discord presence from layout data
|
||||
let discordStatus = $state('offline');
|
||||
let discordDisplayName: string | null = $state(null);
|
||||
let discordActivities: any[] = $state([]);
|
||||
let discordAvatarUrl: string | null = $state(null);
|
||||
let discordGuildTag: string | null = $state(null);
|
||||
let guildTagBadgeImage: string | null = $state(null);
|
||||
let discordPopoverOpen = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
discordStatus = $page.data?.status ?? 'offline';
|
||||
discordDisplayName = $page.data?.displayName ?? null;
|
||||
discordAvatarUrl = $page.data?.avatarUrl ?? null;
|
||||
discordGuildTag = $page.data?.guildTag ?? null;
|
||||
guildTagBadgeImage = $page.data?.guildTagBadgeImage ?? null;
|
||||
// Filter out Custom Status (type 4)
|
||||
discordActivities = ($page.data?.activities ?? []).filter((a: any) => a.type !== 4);
|
||||
});
|
||||
|
||||
// Derived: first displayable activity (if any)
|
||||
let currentActivity = $derived(discordActivities.length > 0 ? discordActivities[0] : null);
|
||||
|
||||
// Battery state (computed from EST time)
|
||||
let batteryLevel = $state(50);
|
||||
let batteryCharging = $state(false);
|
||||
|
||||
// Derived values (Svelte 5 runes)
|
||||
let currentPath = $derived($page.url.pathname);
|
||||
|
||||
@@ -22,6 +48,51 @@
|
||||
// Update time every second
|
||||
timeInterval = setInterval(() => {
|
||||
currentTime = new Date();
|
||||
|
||||
// Compute battery using America/New_York timezone (DST-aware)
|
||||
function zoneEpochFromMs(ms: number, timeZone: string) {
|
||||
const date = new Date(ms);
|
||||
const fmt = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
const parts = fmt.formatToParts(date).reduce((acc: any, p: any) => {
|
||||
acc[p.type] = p.value;
|
||||
return acc;
|
||||
}, {});
|
||||
const iso = `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}Z`;
|
||||
return Date.parse(iso);
|
||||
}
|
||||
|
||||
function computeBatteryFromMs(ms: number) {
|
||||
const dischargeMs = 5 * 3600 * 1000; // 5 hours
|
||||
const chargeMs = 0.5 * 3600 * 1000; // 30 minutes
|
||||
const cycleMs = dischargeMs + chargeMs; // 5.5 hours
|
||||
// position in cycle using America/New_York wall-clock epoch (DST-aware)
|
||||
const estMs = zoneEpochFromMs(ms, 'America/New_York');
|
||||
let pos = estMs % cycleMs;
|
||||
if (pos < 0) pos += cycleMs;
|
||||
if (pos < dischargeMs) {
|
||||
// discharging: 100 -> 0
|
||||
const pct = 100 - (pos / dischargeMs) * 100;
|
||||
return { level: Math.max(0, Math.min(100, Math.round(pct))), charging: false };
|
||||
} else {
|
||||
// charging: 0 -> 100
|
||||
const chargePos = pos - dischargeMs;
|
||||
const pct = (chargePos / chargeMs) * 100;
|
||||
return { level: Math.max(0, Math.min(100, Math.round(pct))), charging: true };
|
||||
}
|
||||
}
|
||||
|
||||
const { level, charging } = computeBatteryFromMs(Date.now());
|
||||
batteryLevel = level;
|
||||
batteryCharging = charging;
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
@@ -72,6 +143,22 @@
|
||||
if (event.key === 'Escape') {
|
||||
themeDropdownOpen = false;
|
||||
mobileMenuOpen = false;
|
||||
discordPopoverOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDiscordPopover() {
|
||||
discordPopoverOpen = !discordPopoverOpen;
|
||||
}
|
||||
|
||||
function getActivityTypeLabel(type: number): string {
|
||||
switch (type) {
|
||||
case 0: return 'Playing';
|
||||
case 1: return 'Streaming';
|
||||
case 2: return 'Listening to';
|
||||
case 3: return 'Watching';
|
||||
case 5: return 'Competing in';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +271,123 @@
|
||||
<div class="bar-right">
|
||||
<!-- (No system modules — minimal Waybar look) -->
|
||||
|
||||
<!-- Discord status with popover -->
|
||||
<div class="module discord-status">
|
||||
<button
|
||||
class="discord-status-btn"
|
||||
onclick={toggleDiscordPopover}
|
||||
title={discordDisplayName ? `${discordDisplayName} — ${discordStatus}` : `Discord: ${discordStatus}`}
|
||||
aria-expanded={discordPopoverOpen}
|
||||
>
|
||||
<span
|
||||
class="status-dot"
|
||||
class:online={discordStatus === 'online'}
|
||||
class:idle={discordStatus === 'idle'}
|
||||
class:dnd={discordStatus === 'dnd'}
|
||||
class:offline={discordStatus === 'offline'}
|
||||
></span>
|
||||
{#if discordDisplayName}
|
||||
<span class="status-text">{discordDisplayName}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if discordPopoverOpen}
|
||||
<div
|
||||
class="discord-popover"
|
||||
transition:fly={{ y: -10, duration: 150 }}
|
||||
>
|
||||
<!-- Header with avatar and name -->
|
||||
<div class="popover-header">
|
||||
<div class="popover-avatar">
|
||||
{#if discordAvatarUrl}
|
||||
<img src={discordAvatarUrl} alt="" class="avatar-img" />
|
||||
{:else}
|
||||
<Icon icon="mdi:account-circle" width="48" />
|
||||
{/if}
|
||||
<span
|
||||
class="popover-status-dot"
|
||||
class:online={discordStatus === 'online'}
|
||||
class:idle={discordStatus === 'idle'}
|
||||
class:dnd={discordStatus === 'dnd'}
|
||||
class:offline={discordStatus === 'offline'}
|
||||
></span>
|
||||
</div>
|
||||
<div class="popover-user-info">
|
||||
<div class="popover-name-row">
|
||||
<span class="popover-username">{discordDisplayName ?? 'Unknown'}</span>
|
||||
{#if discordGuildTag}
|
||||
<span class="popover-clan-tag">
|
||||
<img src={guildTagBadgeImage} alt={discordGuildTag ?? 'Guild'} class="popover-clan-badge" />
|
||||
{discordGuildTag}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="popover-status-label">{discordStatus}</span>
|
||||
</div>
|
||||
<button class="popover-close" onclick={() => discordPopoverOpen = false} aria-label="Close">
|
||||
<Icon icon="mdi:close" width="16" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="popover-divider"></div>
|
||||
|
||||
<!-- Activities -->
|
||||
{#if discordActivities.length > 0}
|
||||
<div class="popover-activities">
|
||||
{#each discordActivities as activity}
|
||||
<div class="activity-card">
|
||||
<div class="activity-type-label">{getActivityTypeLabel(activity.type)}</div>
|
||||
<div class="activity-content">
|
||||
{#if activity.assets?.large_image}
|
||||
<div class="activity-images">
|
||||
<img
|
||||
src={activity.assets.large_image}
|
||||
alt=""
|
||||
class="activity-large-image"
|
||||
/>
|
||||
{#if activity.assets?.small_image}
|
||||
<img
|
||||
src={activity.assets.small_image}
|
||||
alt=""
|
||||
class="activity-small-image"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="activity-info">
|
||||
<span class="activity-name">{activity.name}</span>
|
||||
{#if activity.details}
|
||||
<span class="activity-details">{activity.details}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="popover-no-activity">
|
||||
<Icon icon="mdi:sleep" width="24" />
|
||||
<span>No current activity</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Battery module (EST-based simulated battery cycle) -->
|
||||
<div class="module battery" title="Battery">
|
||||
<button
|
||||
class="battery-btn"
|
||||
aria-label={`Battery ${batteryLevel}% ${batteryCharging ? 'charging' : 'discharging'}`}
|
||||
class:low={batteryLevel < 20 && !batteryCharging}
|
||||
class:charging={batteryCharging}
|
||||
>
|
||||
<Icon icon={getBatteryIcon(batteryLevel, batteryCharging)} width="16" />
|
||||
<span class="battery-level" class:battery-alert={batteryLevel < 20 && !batteryCharging} class:charging={batteryCharging}>{batteryLevel}%</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Theme selector -->
|
||||
<div class="module theme-selector">
|
||||
<button
|
||||
@@ -227,12 +431,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backdrop for theme dropdown only -->
|
||||
{#if themeDropdownOpen}
|
||||
<!-- Backdrop for dropdowns -->
|
||||
{#if themeDropdownOpen || discordPopoverOpen}
|
||||
<button
|
||||
class="backdrop"
|
||||
transition:fade={{ duration: 100 }}
|
||||
onclick={() => { themeDropdownOpen = false; }}
|
||||
onclick={() => { themeDropdownOpen = false; discordPopoverOpen = false; }}
|
||||
aria-label="Close"
|
||||
></button>
|
||||
{/if}
|
||||
@@ -320,10 +524,9 @@
|
||||
class="mobile-theme-btn"
|
||||
class:active={$colorTheme === option.value}
|
||||
onclick={() => { handleThemeSelect(option.value); closeMobileMenu(); }}
|
||||
title={`Press T+${i+1} to switch to ${option.label}`}
|
||||
>
|
||||
<Icon icon={getThemeIcon(option.value)} width="18" />
|
||||
<span class="theme-label"><span class="theme-number">{i+1}.</span> {option.label}</span>
|
||||
<span class="theme-label">{option.label} <span class="theme-number">[{i+1}]</span></span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user