feat: enhance Ihateserverside component with client-side hydration and sidebar functionality

This commit is contained in:
2025-07-02 16:33:14 +00:00
committed by GitHub
parent 1c60db5fd7
commit 646f722ce1
3 changed files with 94 additions and 31 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_COMMIT_SHA=dd1a5b1 NEXT_PUBLIC_COMMIT_SHA=1c60db5

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Menu } from "@/app/components/menu"; import { Menu } from "@/app/components/menu";
import { Sidebar } from "@/app/components/sidebar"; import { Sidebar } from "@/app/components/sidebar";
import { useNavidrome } from "@/app/components/NavidromeContext"; import { useNavidrome } from "@/app/components/NavidromeContext";
@@ -15,14 +15,17 @@ const Ihateserverside: React.FC<IhateserversideProps> = ({ children }) => {
const [isSidebarVisible, setIsSidebarVisible] = useState(true); const [isSidebarVisible, setIsSidebarVisible] = useState(true);
const [isStatusBarVisible, setIsStatusBarVisible] = useState(true); const [isStatusBarVisible, setIsStatusBarVisible] = useState(true);
const [isSidebarHidden, setIsSidebarHidden] = useState(false); const [isSidebarHidden, setIsSidebarHidden] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
if (typeof window !== 'undefined') { const [isClient, setIsClient] = useState(false);
return localStorage.getItem('sidebar-collapsed') === 'true';
}
return false;
});
const { playlists } = useNavidrome(); const { playlists } = useNavidrome();
// Handle client-side hydration
useEffect(() => {
setIsClient(true);
const savedCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
setIsSidebarCollapsed(savedCollapsed);
}, []);
const toggleSidebarCollapse = () => { const toggleSidebarCollapse = () => {
const newCollapsed = !isSidebarCollapsed; const newCollapsed = !isSidebarCollapsed;
setIsSidebarCollapsed(newCollapsed); setIsSidebarCollapsed(newCollapsed);
@@ -36,6 +39,49 @@ const Ihateserverside: React.FC<IhateserversideProps> = ({ children }) => {
setIsSidebarHidden(true); // This will fully hide the sidebar after transition setIsSidebarHidden(true); // This will fully hide the sidebar after transition
} }
}; };
if (!isClient) {
// Return a basic layout during SSR to match initial client render
return (
<div className="hidden md:flex md:flex-col md:h-screen">
{/* Top Menu */}
<div
className="sticky z-10 bg-background border-b"
style={{
left: 'env(titlebar-area-x, 0)',
top: 'env(titlebar-area-y, 0)',
}}
>
<Menu
toggleSidebar={() => setIsSidebarVisible(!isSidebarVisible)}
isSidebarVisible={isSidebarVisible}
toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)}
isStatusBarVisible={isStatusBarVisible}
/>
</div>
{/* Main Content Area */}
<div className="flex-1 flex overflow-hidden">
<div className="w-64 flex-shrink-0 border-r transition-all duration-200">
<Sidebar
playlists={playlists}
className="h-full overflow-y-auto"
collapsed={false}
onToggle={toggleSidebarCollapse}
onTransitionEnd={handleTransitionEnd}
/>
</div>
<div className="flex-1 overflow-y-auto">
<div>{children}</div>
</div>
</div>
{/* Floating Audio Player */}
<AudioPlayer />
<Toaster />
</div>
);
}
return ( return (
<div className="hidden md:flex md:flex-col md:h-screen"> <div className="hidden md:flex md:flex-col md:h-screen">
{/* Top Menu */} {/* Top Menu */}

View File

@@ -44,6 +44,8 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
const router = useRouter(); const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { isConnected } = useNavidrome(); const { isConnected } = useNavidrome();
const [isClient, setIsClient] = useState(false);
const [navidromeUrl, setNavidromeUrl] = useState<string | null>(null);
// For this demo, we'll show connection status instead of user auth // For this demo, we'll show connection status instead of user auth
const connectionStatus = isConnected ? "Connected to Navidrome" : "Not connected"; const connectionStatus = isConnected ? "Connected to Navidrome" : "Not connected";
@@ -57,6 +59,29 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
setIsFullScreen(!isFullScreen) setIsFullScreen(!isFullScreen)
}, [isFullScreen]) }, [isFullScreen])
useEffect(() => {
setIsClient(true);
// Get Navidrome URL from localStorage
const config = localStorage.getItem("navidrome-config");
if (config) {
try {
const { serverUrl } = JSON.parse(config);
if (serverUrl) {
// Remove protocol (http:// or https://) and trailing slash
const prettyUrl = serverUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
setNavidromeUrl(prettyUrl);
} else {
setNavidromeUrl(null);
}
} catch {
setNavidromeUrl(null);
}
} else {
setNavidromeUrl(null);
}
}, []);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === ',') { if ((event.metaKey || event.ctrlKey) && event.key === ',') {
@@ -73,12 +98,16 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
} }
}; };
window.addEventListener('keydown', handleKeyDown); if (isClient) {
window.addEventListener('keydown', handleKeyDown);
}
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown); if (isClient) {
window.removeEventListener('keydown', handleKeyDown);
}
}; };
}, [router, toggleSidebar, handleFullScreen]); }, [router, toggleSidebar, handleFullScreen, isClient]);
return ( return (
<> <>
@@ -100,7 +129,7 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
Preferences <MenubarShortcut>,</MenubarShortcut> Preferences <MenubarShortcut>,</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem onClick={() => window.close()}> <MenubarItem onClick={() => isClient && window.close()}>
Quit Music <MenubarShortcut>Q</MenubarShortcut> Quit Music <MenubarShortcut>Q</MenubarShortcut>
</MenubarItem> </MenubarItem>
</MenubarContent> </MenubarContent>
@@ -281,25 +310,13 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Navidrome URL</span> <span className="text-xs text-muted-foreground">Navidrome URL</span>
<span className="text-xs truncate max-w-[160px] text-right"> <span className="text-xs truncate max-w-[160px] text-right">
{typeof window !== "undefined" {!isClient ? (
? (() => { <span className="italic text-gray-400">Loading...</span>
const config = localStorage.getItem("navidrome-config"); ) : navidromeUrl ? (
if (config) { navidromeUrl
try { ) : (
const { serverUrl } = JSON.parse(config); <span className="italic text-gray-400">Not set</span>
if (serverUrl) { )}
// Remove protocol (http:// or https://) and trailing slash
const prettyUrl = serverUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
return prettyUrl;
}
return <span className="italic text-gray-400">Not set</span>;
} catch {
return <span className="italic text-gray-400">Invalid config</span>;
}
}
return <span className="italic text-gray-400">Not set</span>;
})()
: <span className="italic text-gray-400">Not available</span>}
</span> </span>
</div> </div>
</div> </div>