Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7f2c394
wip
olivermrose Dec 8, 2025
413c4e0
fix interactivity
olivermrose Dec 8, 2025
04d24eb
don't remove self
olivermrose Dec 9, 2025
b0db0c3
move handle
olivermrose Dec 9, 2025
347dfcf
move ref out
olivermrose Dec 9, 2025
bc87ddc
fix inverted logic
olivermrose Dec 9, 2025
6620ffb
remove old code
olivermrose Dec 9, 2025
385d55e
channel list interactions
olivermrose Dec 9, 2025
aecee69
persist layout
olivermrose Dec 9, 2025
0b48c33
update routing
olivermrose Dec 9, 2025
f30ecbb
more routing logic
olivermrose Dec 9, 2025
6b6bf1a
refactors
olivermrose Dec 9, 2025
91b74fc
deduplicate draggable ids
olivermrose Dec 9, 2025
36737b4
optimizations
olivermrose Dec 9, 2025
a039bc8
naming
olivermrose Dec 9, 2025
78fdd85
fix
olivermrose Dec 9, 2025
89b8032
fix
olivermrose Dec 9, 2025
6aa9516
update styles
olivermrose Dec 10, 2025
b90db2d
don't save single splits
olivermrose Dec 10, 2025
713193e
update
olivermrose Dec 10, 2025
0fc4c86
move layout to separate store
olivermrose Dec 10, 2025
ab7aaa2
arrow navigation
olivermrose Dec 10, 2025
532b9f4
persist sizes
olivermrose Dec 10, 2025
f71f650
add kb shortcut for new split
olivermrose Dec 10, 2025
4cdaacf
fix binding
olivermrose Dec 10, 2025
8325817
remove z index on header
olivermrose Dec 10, 2025
00ad679
move new split shortcut to channel route layout
olivermrose Dec 10, 2025
4c72342
settings
olivermrose Dec 10, 2025
8e0f0f8
menu improvements
olivermrose Dec 10, 2025
5e01296
more shortcut stuff
olivermrose Dec 11, 2025
3d2868b
fix dragging onto blank
olivermrose Dec 11, 2025
c47d00e
overlay improvements
olivermrose Dec 11, 2025
e58c80a
add empty state
olivermrose Dec 11, 2025
f137d21
update drag overlay
olivermrose Dec 11, 2025
9dc5576
fix
olivermrose Dec 11, 2025
6a5db3c
button
olivermrose Dec 11, 2025
1f76cd0
fix event placement
olivermrose Dec 11, 2025
a594f99
style
olivermrose Dec 11, 2025
03a4dcc
fix
olivermrose Dec 11, 2025
613ace5
cleanup
olivermrose Dec 11, 2025
d9ea5d1
update condition
olivermrose Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"gql.tada": "^1.9.0",
"graphql-web-lite": "16.6.0-4",
"mode-watcher": "^1.1.0",
"paneforge": "^1.0.2",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tauri-plugin-cache-api": "^0.1.5",
Expand Down
37 changes: 37 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,12 @@
color: var(--color-blue-400);
text-decoration: underline;
}

code {
padding: 0.2rem 0.3rem;
font-size: 85%;
font-family: var(--font-mono);
border-radius: var(--radius-md);
background-color: var(--color-muted);
}
}
4 changes: 0 additions & 4 deletions src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { stats } from "tauri-plugin-cache-api";
import { log } from "$lib/log";
import { settings } from "$lib/settings";
import { loadThemes } from "$lib/themes";

export async function init() {
const { totalSize } = await stats();
log.info(`Cache has ${totalSize} items`);

await settings.start();
log.info("Settings synced");

await loadThemes();
}
6 changes: 6 additions & 0 deletions src/lib/app.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { History } from "./history.svelte";
import { log } from "./log";
import { ChannelManager } from "./managers/channel-manager";
import { EmoteManager } from "./managers/emote-manager";
import { SplitLayout } from "./split-layout";
import { TwitchClient } from "./twitch/client";
import type { EmoteSet } from "./emotes";
import type { Badge } from "./graphql/twitch";
Expand Down Expand Up @@ -40,6 +41,11 @@ class App {
*/
public readonly channels = new ChannelManager(this.twitch);

/**
* The current split layout.
*/
public readonly splits = new SplitLayout();

/**
* Route history.
*/
Expand Down
43 changes: 43 additions & 0 deletions src/lib/components/Channel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts">
import { listen } from "@tauri-apps/api/event";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { onDestroy, onMount } from "svelte";
import Chat from "$lib/components/chat/Chat.svelte";
import ChatInput from "$lib/components/chat/Input.svelte";
import StreamInfo from "$lib/components/StreamInfo.svelte";
import { handlers } from "$lib/handlers";
import type { Channel } from "$lib/models/channel.svelte";
import type { IrcMessage } from "$lib/twitch/irc";

interface Props {
channel: Channel;
}

const { channel }: Props = $props();

let unlisten: UnlistenFn | undefined;

onMount(async () => {
await channel.join();

unlisten = await listen<IrcMessage[]>("recentmessages", async (event) => {
for (const message of event.payload) {
await handlers.get(message.type)?.handle(message);
}
});
});

onDestroy(() => unlisten?.());
</script>

<div class="flex h-full flex-col">
{#if channel.stream}
<StreamInfo stream={channel.stream} />
{/if}

<Chat class="grow" chat={channel.chat} />

<div class="p-2">
<ChatInput chat={channel.chat} />
</div>
</div>
59 changes: 22 additions & 37 deletions src/lib/components/ChannelList.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
<script lang="ts">
import { DragDropProvider } from "@dnd-kit-svelte/svelte";
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers";
import { move } from "@dnd-kit/helpers";
import { onDestroy } from "svelte";
import { flip } from "svelte/animate";
import { app } from "$lib/app.svelte";
import type { Channel } from "$lib/models/channel.svelte";
import { settings } from "$lib/settings";
import Draggable from "./Draggable.svelte";
import DraggableChannel from "./DraggableChannel.svelte";
import Droppable from "./Droppable.svelte";
import { getSidebarContext } from "./Sidebar.svelte";
import StreamTooltip from "./StreamTooltip.svelte";
import { Separator } from "./ui/separator";

const sidebar = getSidebarContext();
Expand Down Expand Up @@ -79,38 +76,26 @@
onDestroy(() => clearInterval(interval));
</script>

<DragDropProvider
modifiers={[
// @ts-expect-error - type mismatch
RestrictToVerticalAxis,
]}
onDragOver={(event) => {
settings.state.pinned = move(settings.state.pinned, event);
}}
>
{#each groups as group}
{#if sidebar.collapsed}
<Separator />
{:else}
<span
class="text-muted-foreground mt-2 inline-block px-2 text-xs font-semibold uppercase"
>
{group.type}
</span>
{/if}
{#each groups as group}
{#if sidebar.collapsed}
<Separator />
{:else}
<span class="text-muted-foreground mt-2 inline-block px-2 text-xs font-semibold uppercase">
{group.type}
</span>
{/if}

{#if group.type === "Pinned"}
<Droppable id="pinned-channels" class="space-y-1.5">
{#each group.channels as channel, i (channel.user.id)}
<Draggable id={channel.id} index={i} {channel} />
{/each}
</Droppable>
{:else}
{#each group.channels as channel (channel.user.id)}
<div class="px-1.5" animate:flip={{ duration: 500 }}>
<StreamTooltip {channel} />
</div>
{#if group.type === "Pinned"}
<Droppable id="pinned-channels" class="space-y-1.5">
{#each group.channels as channel, i (channel.user.id)}
<Draggable index={i} {channel} />
{/each}
{/if}
{/each}
</DragDropProvider>
</Droppable>
{:else}
{#each group.channels as channel (channel.user.id)}
<div animate:flip={{ duration: 500 }}>
<DraggableChannel {channel} />
</div>
{/each}
{/if}
{/each}
14 changes: 6 additions & 8 deletions src/lib/components/ChannelListItem.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
<script lang="ts" module>
export interface ChannelListItemProps {
channel: Channel;
}
</script>

<script lang="ts">
import DotsThreeCircle from "~icons/ph/dots-three-circle";
import Users from "~icons/ph/users-bold";
import type { Channel } from "$lib/models/channel.svelte";
import { getSidebarContext } from "./Sidebar.svelte";

const sidebar = getSidebarContext();
interface Props {
channel: Channel;
}

const { channel }: ChannelListItemProps = $props();
const { channel }: Props = $props();

const sidebar = getSidebarContext();

function formatViewers(viewers: number) {
if (viewers >= 1000) {
Expand Down
16 changes: 12 additions & 4 deletions src/lib/components/Draggable.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<script lang="ts">
import { useSortable } from "@dnd-kit-svelte/svelte/sortable";
import type { UseSortableInput } from "@dnd-kit-svelte/svelte/sortable";
import type { Channel } from "$lib/models/channel.svelte";
import ChannelListItem from "./ChannelListItem.svelte";
import StreamTooltip from "./StreamTooltip.svelte";
import type { ChannelListItemProps } from "./ChannelListItem.svelte";

const { channel, ...rest }: UseSortableInput & ChannelListItemProps = $props();
interface Props {
index: number;
channel: Channel;
}

const { ref, isDragging } = useSortable(rest);
const { index, channel }: Props = $props();

const { ref, isDragging } = useSortable({
id: () => `${channel.id}:channel-list/pinned`,
type: "channel-list-item",
index,
});
</script>

<div class="relative px-1.5" {@attach ref}>
Expand Down
29 changes: 29 additions & 0 deletions src/lib/components/DraggableChannel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import { useDraggable } from "@dnd-kit-svelte/svelte";
import type { Channel } from "$lib/models/channel.svelte";
import ChannelListItem from "./ChannelListItem.svelte";
import StreamTooltip from "./StreamTooltip.svelte";

interface Props {
channel: Channel;
}

const { channel }: Props = $props();

const { ref, isDragging } = useDraggable({
id: () => `${channel.id}:channel-list`,
type: "channel-list-item",
});
</script>

<div class="relative px-1.5" {@attach ref}>
<div class={[isDragging.current && "invisible"]}>
<StreamTooltip {channel} />
</div>

{#if isDragging.current}
<div class="absolute inset-1.5 flex items-center gap-2 px-1.5 opacity-70">
<ChannelListItem {channel} />
</div>
{/if}
</div>
2 changes: 1 addition & 1 deletion src/lib/components/StreamInfo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
</script>

<div
class="bg-muted text-muted-foreground flex items-center justify-between overflow-hidden p-2 text-xs shadow"
class="text-muted-foreground flex items-center justify-between overflow-hidden border-b p-2 text-xs shadow"
>
<p class="truncate" title={stream.title}>{stream.title}</p>

Expand Down
11 changes: 8 additions & 3 deletions src/lib/components/StreamTooltip.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<script lang="ts">
import Users from "~icons/ph/users-bold";
import { createChannelMenu } from "$lib/menus/channel-menu";
import type { Channel } from "$lib/models/channel.svelte";
import { openMenu } from "$lib/util";
import ChannelListItem from "./ChannelListItem.svelte";
import GuestList from "./GuestList.svelte";
import { getSidebarContext } from "./Sidebar.svelte";
import * as Tooltip from "./ui/tooltip";
import type { ChannelListItemProps } from "./ChannelListItem.svelte";

const sidebar = getSidebarContext();
interface Props {
channel: Channel;
}

const { channel }: Props = $props();

const { channel }: ChannelListItemProps = $props();
const sidebar = getSidebarContext();
</script>

<Tooltip.Root>
Expand All @@ -24,6 +28,7 @@
<a
class="absolute inset-0 z-10"
href="/channels/{channel.user.username}"
draggable="false"
data-sveltekit-preload-data="off"
aria-label="Join {channel.user.displayName}"
>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/chat/Input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
const showSuggestions = $derived(!!completer?.suggestions.length && completer.prefixed);

$effect(() => {
chat.input ??= input;
chat.input = input;
completer = new Completer(chat);
});

Expand Down
Loading