Skip to content

Commit c10ad9a

Browse files
committed
wip
1 parent 74238e1 commit c10ad9a

File tree

12 files changed

+508
-32
lines changed

12 files changed

+508
-32
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"gql.tada": "^1.9.0",
4646
"graphql-web-lite": "16.6.0-4",
4747
"mode-watcher": "^1.1.0",
48+
"paneforge": "^1.0.2",
4849
"tailwind-merge": "^3.4.0",
4950
"tailwindcss": "^4.1.17",
5051
"tauri-plugin-cache-api": "^0.1.5",

pnpm-lock.yaml

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/app.svelte.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { History } from "./history.svelte";
55
import { log } from "./log";
66
import { ChannelManager } from "./managers/channel-manager";
77
import { EmoteManager } from "./managers/emote-manager";
8+
import { SplitManager } from "./managers/split-manager.svelte";
89
import { TwitchClient } from "./twitch/client";
910
import type { EmoteSet } from "./emotes";
1011
import type { Badge } from "./graphql/twitch";
@@ -40,6 +41,11 @@ class App {
4041
*/
4142
public readonly channels = new ChannelManager(this.twitch);
4243

44+
/**
45+
* The current split layout.
46+
*/
47+
public readonly splits = new SplitManager();
48+
4349
/**
4450
* Route history.
4551
*/

src/lib/components/Channel.svelte

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script lang="ts">
2+
import { listen } from "@tauri-apps/api/event";
3+
import type { UnlistenFn } from "@tauri-apps/api/event";
4+
import { onDestroy, onMount } from "svelte";
5+
import { app } from "$lib/app.svelte";
6+
import Chat from "$lib/components/chat/Chat.svelte";
7+
import ChatInput from "$lib/components/chat/Input.svelte";
8+
import StreamInfo from "$lib/components/StreamInfo.svelte";
9+
import { handlers } from "$lib/handlers";
10+
import type { Channel } from "$lib/models/channel.svelte";
11+
import type { IrcMessage } from "$lib/twitch/irc";
12+
13+
interface Props {
14+
channel: Channel;
15+
}
16+
17+
const { channel }: Props = $props();
18+
19+
let unlisten: UnlistenFn | undefined;
20+
21+
onMount(async () => {
22+
unlisten = await listen<IrcMessage[]>("recentmessages", async (event) => {
23+
for (const message of event.payload) {
24+
await handlers.get(message.type)?.handle(message);
25+
}
26+
});
27+
});
28+
29+
onDestroy(() => unlisten?.());
30+
</script>
31+
32+
<!-- svelte-ignore a11y_click_events_have_key_events -->
33+
<!-- svelte-ignore a11y_no_static_element_interactions -->
34+
<div class="flex h-full flex-col" onclick={() => (app.focused = channel)}>
35+
{#if channel.stream}
36+
<StreamInfo stream={channel.stream} />
37+
{/if}
38+
39+
<Chat class="grow" chat={channel.chat} />
40+
41+
<div class="p-2">
42+
<ChatInput chat={channel.chat} />
43+
</div>
44+
</div>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<script lang="ts">
2+
import type { Attachment } from "svelte/attachments";
3+
import DotsSixVertical from "~icons/ph/dots-six-vertical";
4+
import SquareHalfBottom from "~icons/ph/square-half-bottom-fill";
5+
import SquareHalf from "~icons/ph/square-half-fill";
6+
import X from "~icons/ph/x";
7+
import { app } from "$lib/app.svelte";
8+
import type { Channel } from "$lib/models/channel.svelte";
9+
import { Button } from "../ui/button";
10+
11+
interface Props {
12+
channel: Channel;
13+
handleRef: Attachment<Element>;
14+
}
15+
16+
const { channel, handleRef }: Props = $props();
17+
</script>
18+
19+
<div class="z-20 flex items-center justify-between border-b p-1">
20+
<div class="flex h-full flex-1 items-center gap-x-2 overflow-hidden px-1">
21+
<DotsSixVertical class="cursor-grab active:cursor-grabbing" {@attach handleRef} />
22+
<span class="truncate text-sm font-medium select-none">{channel.user.displayName}</span>
23+
</div>
24+
25+
<div class="flex shrink-0 items-center gap-x-1">
26+
<Button
27+
class="size-min p-1"
28+
size="icon-sm"
29+
variant="ghost"
30+
onclick={() => {
31+
app.splits.insert(channel.id, `Window ${Date.now()}`, {
32+
direction: "horizontal",
33+
first: channel.id,
34+
second: `Window ${Date.now()}`,
35+
});
36+
}}
37+
>
38+
<SquareHalf />
39+
</Button>
40+
41+
<Button
42+
class="size-min p-1"
43+
size="icon-sm"
44+
variant="ghost"
45+
onclick={() => {
46+
app.splits.insert(channel.id, `Window ${Date.now()}`, {
47+
direction: "vertical",
48+
first: channel.id,
49+
second: `Window ${Date.now()}`,
50+
});
51+
}}
52+
>
53+
<SquareHalfBottom />
54+
</Button>
55+
56+
<Button
57+
class="size-min p-1"
58+
size="icon-sm"
59+
variant="ghost"
60+
onclick={async () => {
61+
app.splits.remove(channel.id);
62+
await channel?.leave();
63+
}}
64+
>
65+
<X />
66+
</Button>
67+
</div>
68+
</div>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script lang="ts">
2+
import { Pane, PaneGroup, PaneResizer } from "paneforge";
3+
import type { SplitNode } from "$lib/managers/split-manager.svelte";
4+
import Self from "./SplitNode.svelte";
5+
import SplitView from "./SplitView.svelte";
6+
7+
interface Props {
8+
node: SplitNode;
9+
}
10+
11+
const { node }: Props = $props();
12+
</script>
13+
14+
{#if typeof node === "string"}
15+
<SplitView id={node} />
16+
{:else}
17+
<PaneGroup class="size-full" direction={node.direction}>
18+
<Pane defaultSize={node.size ?? 50}>
19+
<Self node={node.first} />
20+
</Pane>
21+
22+
<PaneResizer
23+
class={[
24+
"bg-muted relative flex items-center justify-center transition-colors hover:bg-blue-400",
25+
node.direction === "horizontal"
26+
? "w-0.5 cursor-col-resize"
27+
: "h-0.5 cursor-row-resize",
28+
]}
29+
/>
30+
31+
<Pane defaultSize={100 - (node.size ?? 50)}>
32+
<Self node={node.second} />
33+
</Pane>
34+
</PaneGroup>
35+
{/if}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script lang="ts">
2+
import { useDraggable, useDroppable } from "@dnd-kit-svelte/svelte";
3+
import { app } from "$lib/app.svelte";
4+
import Channel from "../Channel.svelte";
5+
import SplitHeader from "./SplitHeader.svelte";
6+
7+
interface Props {
8+
id: string;
9+
}
10+
11+
const { id }: Props = $props();
12+
13+
const { ref, handleRef, isDragging } = useDraggable({ id: () => id });
14+
15+
const dropCenter = useDroppable({ id: () => `${id}:center` });
16+
const dropUp = useDroppable({ id: () => `${id}:up` });
17+
const dropDown = useDroppable({ id: () => `${id}:down` });
18+
const dropLeft = useDroppable({ id: () => `${id}:left` });
19+
const dropRight = useDroppable({ id: () => `${id}:right` });
20+
21+
const channel = $derived(app.channels.get(id));
22+
</script>
23+
24+
<div class="relative flex size-full flex-col overflow-hidden">
25+
{#if channel}
26+
<SplitHeader {channel} {handleRef} />
27+
{/if}
28+
29+
<div class={["relative flex-1", isDragging.current && "opacity-50"]} {@attach ref}>
30+
{#if channel}
31+
<Channel {channel} />
32+
{:else}
33+
<div class="text-muted-foreground flex h-full items-center justify-center">
34+
Empty Split
35+
</div>
36+
{/if}
37+
38+
{@render dropZone(dropCenter, "inset-0")}
39+
{@render dropZone(dropUp, "top-0 inset-x-0 h-1/3")}
40+
{@render dropZone(dropDown, "bottom-0 inset-x-0 h-1/3")}
41+
{@render dropZone(dropLeft, "left-0 inset-y-0 w-1/3")}
42+
{@render dropZone(dropRight, "right-0 inset-y-0 w-1/3")}
43+
</div>
44+
</div>
45+
46+
{#snippet dropZone(dropper: ReturnType<typeof useDroppable>, className: string)}
47+
<div
48+
class={["absolute z-10", className, dropper.isDropTarget.current && "bg-primary/50"]}
49+
{@attach dropper.ref}
50+
></div>
51+
{/snippet}

0 commit comments

Comments
 (0)