Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ebd20d7
Basic engine idea
Mar 16, 2026
f44e6a5
Audio
Mar 16, 2026
9034992
Library and Storage
Mar 16, 2026
a5e4d58
So much moreeeeeee
Mar 16, 2026
838ffec
A few things that needed to be fixed
Mar 16, 2026
88690dd
more fixes
Mar 16, 2026
ffa6e91
Movement
Mar 17, 2026
b4c5ed6
owwwwwwwww
Mar 19, 2026
4a5abec
More stuff
Mar 20, 2026
59458ae
Helpers go bye byeeeeee (most of em)
Mar 20, 2026
5f488ed
one more fileeee (hook time)
Mar 20, 2026
918be77
make-pretty
Mar 20, 2026
9c81a2d
hooks
Mar 20, 2026
f0d9542
Tiny stuff
Mar 20, 2026
7e2c1d6
more stuff
Mar 20, 2026
0902010
move
Mar 28, 2026
2298d76
bunch of changes
Mar 29, 2026
e269acc
SATORIII
Mar 29, 2026
27df322
bunch of stuff (i forgot to commit this from a week ago oops)
Mar 31, 2026
62e90ea
Some tiny state fixes
Apr 8, 2026
cdae3fd
Gasp i almost deleted this folder I thought I'd commited these
NellowTCS Sep 14, 2026
d72f153
Visualizers
NellowTCS Sep 21, 2026
4fc6545
Cursor
NellowTCS Sep 22, 2026
c5454ae
Various fixes
NellowTCS Sep 23, 2026
c3140f0
Visualizers
NellowTCS Sep 23, 2026
7d50977
Updates
NellowTCS Sep 23, 2026
3e60188
Fix
NellowTCS Sep 23, 2026
bf5721e
Many changes
NellowTCS Sep 25, 2026
b3fa7db
More fixes
NellowTCS Sep 25, 2026
c9042e0
Massive changes to everything
NellowTCS Sep 28, 2026
7a85b8d
Fixes
NellowTCS Sep 29, 2026
54fafed
Review comments and changes
NellowTCS Sep 30, 2026
5a0dc42
Review comments
NellowTCS Oct 1, 2026
d52c8e0
WASM stuff and fixes
NellowTCS Oct 2, 2026
89d56c1
Many changes
NellowTCS Oct 5, 2026
1843396
temporary vendor of music-metadata = CAF working (also the other dura…
NellowTCS Oct 6, 2026
39055db
netlify
NellowTCS Oct 6, 2026
2788279
oop
NellowTCS Oct 6, 2026
4b7279d
Fix CI
NellowTCS Oct 6, 2026
18521ed
Enable everything on safari to see what breaks
NellowTCS Oct 6, 2026
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
Prev Previous commit
Next Next commit
bunch of stuff (i forgot to commit this from a week ago oops)
  • Loading branch information
NellowTCS
NellowTCS committed Mar 31, 2026
commit 27df322fd8991d54e3d102d25c5716b8b72de8be
50 changes: 49 additions & 1 deletion Build/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import "@fontsource/poppins";
@import "@fontsource/jetbrains-mono";
@import "@fontsource/caveat";
@import "./ui/theming/base-theme.css";

/* This root block is kept separate as these values do not change between light and dark mode */
/* prettier-ignore */
Expand Down Expand Up @@ -61,7 +62,9 @@
--spacing-12: 3rem;
--spacing-16: 4rem;

/* Miscellaneous */
/* Layout Spacing */
--spacing-layout: 15px;
--player-art-size: 115px;
--sidebar-width: 250px;
--sidebar-collapsed-width: 40px;

Expand Down Expand Up @@ -154,3 +157,48 @@ html {
/* Sidebar is overlay on mobile, doesn't affect layout */
}
}

/* Structural patterns */
@layer utilities {
.search-wrapper {
position: relative;
}

.search-icon {
position: absolute;
left: var(--spacing-3);
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}

.search-input {
background: var(--surface-transparent-1);
border: 1px solid var(--surface-transparent-2);
color: var(--primary-foreground);
padding-left: calc(var(--spacing-3) * 2 + 16px);
border-radius: var(--radius);
}

.search-input::placeholder {
color: var(--primary-foreground);
}

.search-input:focus {
outline: var(--shadow);
border-color: var(--primary-border-strong);
box-shadow: 0 0 0 2px var(--primary-transparent-2);
}

.action-button-lift {
transition: all var(--animation-duration-fast) ease;
}

.action-button-lift:hover {
transform: translateY(-1px);
}

.action-button-lift:active {
transform: translateY(0);
}
}
4 changes: 2 additions & 2 deletions Build/src/hooks/useFileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { createLogger } from "../helpers/logger";
const logger = createLogger("useFileHandler");

export function useFileHandler(
addSong: (song: Track) => Promise<void>,
addSong: (song: Track, file?: File) => Promise<void>,
t: any,
importFiles: (
files: File[],
addSong: (song: Track) => Promise<void>,
addSong: (song: Track, file?: File) => Promise<void>,
t: any,
) => Promise<void>,
isInitialized?: boolean,
Expand Down
43 changes: 40 additions & 3 deletions Build/src/hooks/useKomorebi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { IAudioBackend } from "../platform/audio";
import { HTMLAudioBackend } from "../platform/audio/backends/HTMLBackend";
import { LibraryManager } from "../platform/library/library";
import { libraryPersistence } from "../platform/library/persistence";
import { trackStorage } from "../platform/storage/trackStorage";
import { SettingsManager } from "../platform/settings/settings";
import { createLogger } from "../helpers/logger";

Expand Down Expand Up @@ -178,11 +179,23 @@ export function useKomorebi(
engine.on("loading", handleLoading);
engine.on("ready", handleReady);

const handleLibraryChange = () => {
setState(engine.getState());
};
library.on("songadded", handleLibraryChange);
library.on("songremoved", handleLibraryChange);
library.on("playlistadded", handleLibraryChange);
library.on("playlistremoved", handleLibraryChange);
library.on("favoritechanged", handleLibraryChange);

const loadLibrary = async () => {
try {
const savedLibrary = await libraryPersistence.loadFullLibrary();
if (savedLibrary) {
savedLibrary.songs.forEach((song) => library.addSong(song));
for (const song of savedLibrary.songs) {
const reconstructed = await trackStorage.reconstructUrl(song);
library.addSong(reconstructed);
}
savedLibrary.playlists.forEach((playlist) => {
if ("songs" in playlist) {
library.addPlaylist(playlist);
Expand Down Expand Up @@ -211,6 +224,11 @@ export function useKomorebi(
engine.off("error", handleError);
engine.off("loading", handleLoading);
engine.off("ready", handleReady);
library.off("songadded", handleLibraryChange);
library.off("songremoved", handleLibraryChange);
library.off("playlistadded", handleLibraryChange);
library.off("playlistremoved", handleLibraryChange);
library.off("favoritechanged", handleLibraryChange);
backendRef.current?.dispose();
};
}, []);
Expand Down Expand Up @@ -289,15 +307,34 @@ export function useKomorebi(
}, []);

const playSong = useCallback(async (song: Track, playlist?: Playlist) => {
console.log("[playSong] called", { song: song?.title, songUrl: song?.url, playlist: playlist?.name });
setError(null);
const engine = engineRef.current;
if (!engine) return;
console.log("[playSong] engine:", engine);
if (!engine) {
console.log("[playSong] no engine!");
return;
}

const trackToPlay = await trackStorage.reconstructUrl(song);
console.log("[playSong] reconstructed url:", trackToPlay.url);

if (playlist) {
engine.setPlaylist(playlist);
}
engine.load(song, playlist);
engine.load(trackToPlay, playlist);

await new Promise<void>((resolve) => {
const handleReady = () => {
engine.off("ready", handleReady);
resolve();
};
engine.on("ready", handleReady);
});

console.log("[playSong] calling engine.play()");
await engine.play();
console.log("[playSong] play() completed");
}, []);

const seek = useCallback((time: number) => {
Expand Down
141 changes: 141 additions & 0 deletions Build/src/hooks/useVisualizerCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { useRef, useEffect, useState, useCallback, RefObject } from "react";
import { logger } from "../helpers/logger";
import {
getVisualizer,
getAvailableVisualizers,
VisualizerType,
clearVisualizerState,
} from "../platform/visualizers";

interface UseVisualizerCanvasProps {
analyserNode?: AnalyserNode | null;
isPlaying: boolean;
canvasRef: RefObject<HTMLCanvasElement | null>;
}

export const useVisualizerCanvas = ({
analyserNode,
isPlaying,
canvasRef,
}: UseVisualizerCanvasProps) => {
const DEFAULT_VISUALIZER_KEY = "oceanwaves";
const animationFrameId = useRef<number | null>(null);
const dataArrayRef = useRef<Uint8Array | null>(null);

const [availableVisualizers, setAvailableVisualizers] = useState<string[]>([]);
const [loadedVisualizerNames, setLoadedVisualizerNames] = useState<Map<string, string>>(new Map());
const [selectedVisualizerKey, setSelectedVisualizerKey] = useState<string>("");
const [selectedVisualizer, setSelectedVisualizer] = useState<VisualizerType | null>(null);
const [visualizerSettings, setVisualizerSettings] = useState<Record<string, any>>({});
const [isLoading, setIsLoading] = useState(false);

// Initialize available visualizers
useEffect(() => {
const visualizers = getAvailableVisualizers();
setAvailableVisualizers(visualizers);

Promise.all(
visualizers.map(async (key) => {
const visualizer = await getVisualizer(key);
return { key, name: visualizer?.name || key };
}),
).then((results) => {
setLoadedVisualizerNames(new Map(results.map(({ key, name }) => [key, name])));
});

if (visualizers.length > 0 && !selectedVisualizerKey) {
setSelectedVisualizerKey(
visualizers.includes(DEFAULT_VISUALIZER_KEY) ? DEFAULT_VISUALIZER_KEY : visualizers[0],
);
}
}, [selectedVisualizerKey]);
Comment thread
NellowTCS marked this conversation as resolved.
Outdated

// Load selected visualizer
useEffect(() => {
if (!selectedVisualizerKey) return;

setIsLoading(true);
getVisualizer(selectedVisualizerKey)
.then((visualizer) => {
setSelectedVisualizer(visualizer);
setIsLoading(false);

if (visualizer?.settingsConfig) {
setVisualizerSettings(
Object.entries(visualizer.settingsConfig).reduce((acc, [key, config]) => {
acc[key] = config.default;
return acc;
}, {} as Record<string, any>),
);
}
})
.catch((error) => {
logger.error("Failed to load visualizer", { error: error instanceof Error ? error.message : "" });
setIsLoading(false);
});
}, [selectedVisualizerKey]);

// Handle drawing
const draw = useCallback(() => {
if (!analyserNode || !canvasRef.current || !selectedVisualizer) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (!ctx) return;

const bufferLength = analyserNode.frequencyBinCount;
if (!dataArrayRef.current || dataArrayRef.current.length !== bufferLength) {
dataArrayRef.current = new Uint8Array(bufferLength);
}

selectedVisualizer.draw(
analyserNode,
canvas,
ctx,
bufferLength,
dataArrayRef.current,
selectedVisualizer.dataType,
visualizerSettings,
);

animationFrameId.current = requestAnimationFrame(draw);
}, [analyserNode, selectedVisualizer, visualizerSettings, canvasRef]);

// Animation lifecycle
useEffect(() => {
if (isPlaying && analyserNode && selectedVisualizer) {
animationFrameId.current = requestAnimationFrame(draw);
} else {
if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current);
}
return () => {
if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current);
};
}, [isPlaying, analyserNode, selectedVisualizer, draw]);

// Resize handling
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const resizeObserver = new ResizeObserver(() => {
const { width, height } = canvas.getBoundingClientRect();
canvas.width = width;
canvas.height = height;
});
resizeObserver.observe(canvas);
return () => resizeObserver.disconnect();
}, [canvasRef]);

// Global cleanup
useEffect(() => () => clearVisualizerState(), []);

return {
availableVisualizers,
loadedVisualizerNames,
selectedVisualizerKey,
selectedVisualizer,
setSelectedVisualizerKey,
visualizerSettings,
setVisualizerSettings,
isLoading,
};
};
Loading
Loading