fix: split LiveShell into Astro layout + individual islands
- Rewrite live.astro to use Astro Sidebar/Header/Card components - VoiceControls becomes self-contained (fetches guilds/channels internally) - Remove LiveShell.tsx (no longer needed) - Each React island uses appropriate client:load/idle directive
This commit is contained in:
-13
@@ -1,13 +0,0 @@
|
|||||||
[submodule "vendor/discord.js-selfbot-v13"]
|
|
||||||
path = vendor/discord.js-selfbot-v13
|
|
||||||
url = https://github.com/MythEclipse/discord.js-selfbot-v13.git
|
|
||||||
|
|
||||||
[submodule "vendor/discord-video-stream"]
|
|
||||||
path = vendor/discord-video-stream
|
|
||||||
url = https://github.com/MythEclipse/discord-video-stream.git
|
|
||||||
[submodule "vendor/drizzle-orm"]
|
|
||||||
path = vendor/drizzle-orm
|
|
||||||
url = https://github.com/MythEclipse/drizzle-orm.git
|
|
||||||
[submodule "vendor/better-sqlite3"]
|
|
||||||
path = vendor/better-sqlite3
|
|
||||||
url = https://github.com/MythEclipse/better-sqlite3.git
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// ─── LiveShell.tsx — Page shell for /live ────────────────────────────────────
|
|
||||||
// Composes Sidebar, Header, ParticleBackground, and the four live islands
|
|
||||||
// (VoiceControls, ActiveSpeakers, AudioVisualizer, NowPlaying) into a full-page
|
|
||||||
// responsive layout.
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
getGuilds,
|
|
||||||
getVoiceChannels,
|
|
||||||
getVoiceStatus,
|
|
||||||
} from "../shared/api/client.js";
|
|
||||||
import { Header } from "../shared/components/Header.js";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "../shared/components/index.js";
|
|
||||||
import { ParticleBackground } from "../shared/components/particles/ParticleBackground.js";
|
|
||||||
import { Sidebar } from "../shared/components/Sidebar.js";
|
|
||||||
import { useTheme } from "../shared/hooks/useTheme.js";
|
|
||||||
import type { Channel, Guild } from "../shared/types/guild.js";
|
|
||||||
import type { VoiceStatus } from "../shared/types/voice.js";
|
|
||||||
import { useVoiceStore } from "../stores/voice-store.js";
|
|
||||||
import ActiveSpeakers from "./ActiveSpeakers.js";
|
|
||||||
import AudioVisualizer from "./AudioVisualizer.js";
|
|
||||||
import NowPlaying from "./NowPlaying.js";
|
|
||||||
import VoiceControls from "./VoiceControls.js";
|
|
||||||
|
|
||||||
const emptyVoiceStatus: VoiceStatus = {
|
|
||||||
connected: false,
|
|
||||||
activeGuildId: null,
|
|
||||||
activeChannelId: null,
|
|
||||||
activeChannelName: null,
|
|
||||||
connections: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LiveShell() {
|
|
||||||
const { mode, isDark, toggle: toggleTheme } = useTheme();
|
|
||||||
const [voiceStatus, setVoiceStatus] = useState<VoiceStatus>(emptyVoiceStatus);
|
|
||||||
const [guilds, setGuilds] = useState<Guild[]>([]);
|
|
||||||
const [voiceChannels, setVoiceChannels] = useState<Channel[]>([]);
|
|
||||||
|
|
||||||
// Watch store for guild changes so we can fetch voice channels
|
|
||||||
const guildId = useVoiceStore((state) => state.guildId);
|
|
||||||
|
|
||||||
// Fetch initial data on mount
|
|
||||||
useEffect(() => {
|
|
||||||
getVoiceStatus()
|
|
||||||
.then(setVoiceStatus)
|
|
||||||
.catch(() => {
|
|
||||||
/* offline — leave default */
|
|
||||||
});
|
|
||||||
getGuilds()
|
|
||||||
.then(setGuilds)
|
|
||||||
.catch(() => {
|
|
||||||
/* offline — leave default */
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Refetch voice channels when guild selection changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (guildId) {
|
|
||||||
getVoiceChannels(guildId)
|
|
||||||
.then(setVoiceChannels)
|
|
||||||
.catch(() => setVoiceChannels([]));
|
|
||||||
} else {
|
|
||||||
setVoiceChannels([]);
|
|
||||||
}
|
|
||||||
}, [guildId]);
|
|
||||||
|
|
||||||
const handleTabChange = useCallback((tab: string) => {
|
|
||||||
if (tab !== "live") {
|
|
||||||
window.location.href = "/";
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative min-h-screen bg-background text-foreground">
|
|
||||||
{/* Background layers */}
|
|
||||||
<ParticleBackground />
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 pointer-events-none grid-pattern opacity-[0.03]"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative flex min-h-screen">
|
|
||||||
<Sidebar
|
|
||||||
activeTab="live"
|
|
||||||
onTabChange={handleTabChange}
|
|
||||||
collapsed={false}
|
|
||||||
recentMessages={[]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main className="flex min-w-0 flex-1 flex-col">
|
|
||||||
<Header
|
|
||||||
activeTab="live"
|
|
||||||
wsStatus="disconnected"
|
|
||||||
voiceStatus={voiceStatus}
|
|
||||||
themeMode={mode}
|
|
||||||
isDark={isDark}
|
|
||||||
onThemeToggle={toggleTheme}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto p-4 md:p-6 lg:p-8 pb-16 md:pb-0">
|
|
||||||
{/* Responsive grid: stacks on <1024px */}
|
|
||||||
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
|
||||||
{/* Main content column */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<VoiceControls guilds={guilds} voiceChannels={voiceChannels} />
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="text-base">Live Audio</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<AudioVisualizer barCount={48} height={32} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<NowPlaying />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar column */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="text-base">Active Speakers</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ActiveSpeakers />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,47 @@
|
|||||||
// ─── VoiceControls.tsx — Voice bridge island ────────────────────────────────
|
// ─── VoiceControls.tsx — Voice bridge island ────────────────────────────────
|
||||||
// Self-contained: reads/writes useVoiceStore, calls API for connect/disconnect.
|
// Self-contained: fetches guilds/channels internally, reads/writes useVoiceStore,
|
||||||
|
// calls API for connect/disconnect.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
import { Radio } from "lucide-react";
|
import { Radio } from "lucide-react";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { connectVoice, disconnectVoice } from "../shared/api/client.js";
|
import {
|
||||||
|
connectVoice,
|
||||||
|
disconnectVoice,
|
||||||
|
getGuilds,
|
||||||
|
getVoiceChannels,
|
||||||
|
} from "../shared/api/client.js";
|
||||||
import { Button, Select } from "../shared/components/index.js";
|
import { Button, Select } from "../shared/components/index.js";
|
||||||
import type { Channel, Guild } from "../shared/types/guild.js";
|
import type { Channel, Guild } from "../shared/types/guild.js";
|
||||||
import { useVoiceStore } from "../stores/voice-store.js";
|
import { useVoiceStore } from "../stores/voice-store.js";
|
||||||
|
|
||||||
interface VoiceControlsProps {
|
export default function VoiceControls() {
|
||||||
guilds: Guild[];
|
|
||||||
voiceChannels: Channel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function VoiceControls({
|
|
||||||
guilds,
|
|
||||||
voiceChannels,
|
|
||||||
}: VoiceControlsProps) {
|
|
||||||
const connected = useVoiceStore((state) => state.connected);
|
const connected = useVoiceStore((state) => state.connected);
|
||||||
const guildId = useVoiceStore((state) => state.guildId);
|
const guildId = useVoiceStore((state) => state.guildId);
|
||||||
const channelId = useVoiceStore((state) => state.channelId);
|
const channelId = useVoiceStore((state) => state.channelId);
|
||||||
const setGuildChannel = useVoiceStore((state) => state.setGuildChannel);
|
const setGuildChannel = useVoiceStore((state) => state.setGuildChannel);
|
||||||
const setConnected = useVoiceStore((state) => state.setConnected);
|
const setConnected = useVoiceStore((state) => state.setConnected);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [guilds, setGuilds] = useState<Guild[]>([]);
|
||||||
|
const [voiceChannels, setVoiceChannels] = useState<Channel[]>([]);
|
||||||
|
|
||||||
|
// Fetch guilds on mount
|
||||||
|
useEffect(() => {
|
||||||
|
getGuilds()
|
||||||
|
.then(setGuilds)
|
||||||
|
.catch(() => { /* offline */ });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch voice channels when guild selection changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (guildId) {
|
||||||
|
getVoiceChannels(guildId)
|
||||||
|
.then(setVoiceChannels)
|
||||||
|
.catch(() => setVoiceChannels([]));
|
||||||
|
} else {
|
||||||
|
setVoiceChannels([]);
|
||||||
|
}
|
||||||
|
}, [guildId]);
|
||||||
|
|
||||||
const handleConnect = useCallback(async () => {
|
const handleConnect = useCallback(async () => {
|
||||||
if (!guildId || !channelId) return;
|
if (!guildId || !channelId) return;
|
||||||
@@ -51,7 +69,7 @@ export default function VoiceControls({
|
|||||||
}, [setConnected]);
|
}, [setConnected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="glass rounded-xl p-6">
|
<div className="space-y-4">
|
||||||
<h3 className="flex items-center gap-2 text-lg font-semibold tracking-tight">
|
<h3 className="flex items-center gap-2 text-lg font-semibold tracking-tight">
|
||||||
<Radio className="h-5 w-5 text-primary" /> Voice Bridge
|
<Radio className="h-5 w-5 text-primary" /> Voice Bridge
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -1,14 +1,108 @@
|
|||||||
---
|
---
|
||||||
// ─── live.astro — Voice & Media live monitoring page ────────────────────────
|
// ─── live.astro — Voice & Media live monitoring page ────────────────────────
|
||||||
// Full-page layout with Sidebar navigation, Header, and the LiveShell React
|
// Layout shell (Sidebar, Header) rendered as zero-JS Astro components.
|
||||||
// island that composes all live components (VoiceControls, ActiveSpeakers,
|
// Only the truly interactive parts are React islands with client directives.
|
||||||
// AudioVisualizer, NowPlaying).
|
|
||||||
// Particles and MascotChat are embedded inside the LiveShell island.
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||||
import LiveShell from "../islands/LiveShell";
|
import Sidebar from "../components/sidebar/Sidebar.astro";
|
||||||
|
import Header from "../components/header/Header.astro";
|
||||||
|
import Card from "../components/ui/Card.astro";
|
||||||
|
|
||||||
|
// React islands — interactive only
|
||||||
|
import ThemeToggle from "../islands/ThemeToggle";
|
||||||
|
import VoiceControls from "../islands/VoiceControls";
|
||||||
|
import ActiveSpeakers from "../islands/ActiveSpeakers";
|
||||||
|
import AudioVisualizer from "../islands/AudioVisualizer";
|
||||||
|
import NowPlaying from "../islands/NowPlaying";
|
||||||
|
import Particles from "../islands/Particles";
|
||||||
|
import MascotChat from "../islands/MascotChat";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<LiveShell client:only="react" />
|
<div class="live-page-layout">
|
||||||
|
<!-- Sidebar — zero JS Astro component -->
|
||||||
|
<Sidebar activeTab="live" />
|
||||||
|
|
||||||
|
<main class="live-main-area">
|
||||||
|
<!-- Header — zero JS, ThemeToggle is a tiny React island -->
|
||||||
|
<Header title="Live">
|
||||||
|
<ThemeToggle slot="actions" client:idle />
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<div class="live-content p-4 md:p-6 lg:p-8">
|
||||||
|
<!-- Responsive grid: stacks on <1024px -->
|
||||||
|
<div class="live-grid">
|
||||||
|
<!-- Main column -->
|
||||||
|
<div class="live-main-col space-y-4 md:space-y-6">
|
||||||
|
<!-- VoiceControls — core functionality, load immediately -->
|
||||||
|
<Card variant="glass">
|
||||||
|
<h2 class="text-base font-semibold mb-3">Voice Connection</h2>
|
||||||
|
<VoiceControls client:load />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- AudioVisualizer — deferred, not critical -->
|
||||||
|
<Card>
|
||||||
|
<h2 class="text-base font-semibold mb-3">Live Audio</h2>
|
||||||
|
<AudioVisualizer client:idle barCount={48} height={32} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- NowPlaying — deferred -->
|
||||||
|
<Card>
|
||||||
|
<h2 class="text-base font-semibold mb-3">Now Playing</h2>
|
||||||
|
<NowPlaying client:idle />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar column -->
|
||||||
|
<aside class="live-sidebar-col space-y-4 md:space-y-6">
|
||||||
|
<Card>
|
||||||
|
<h2 class="text-base font-semibold mb-3">Active Speakers</h2>
|
||||||
|
<ActiveSpeakers client:idle />
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deferred background & mascot -->
|
||||||
|
<Particles client:idle />
|
||||||
|
<MascotChat client:idle />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
.live-page-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-main-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.live-grid {
|
||||||
|
grid-template-columns: 1fr 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-sidebar-col {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(56px + 2rem);
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user