Files
lidify/frontend/app/settings/page.tsx
2025-12-25 18:58:06 -06:00

245 lines
9.5 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback } from "react";
import { useSearchParams } from "next/navigation";
import { useAuth } from "@/lib/auth-context";
import { RestartModal } from "@/components/ui/RestartModal";
import { useSettingsData } from "@/features/settings/hooks/useSettingsData";
import { useSystemSettings } from "@/features/settings/hooks/useSystemSettings";
import { GradientSpinner } from "@/components/ui/GradientSpinner";
import { InlineStatus, useInlineStatus } from "@/components/ui/InlineStatus";
import {
SettingsLayout,
SettingsSection,
SettingsRow,
SettingsToggle,
SettingsSelect,
SidebarItem
} from "@/features/settings/components/ui";
// Section components
import { AccountSection } from "@/features/settings/components/sections/AccountSection";
import { PlaybackSection } from "@/features/settings/components/sections/PlaybackSection";
import { DownloadPreferencesSection } from "@/features/settings/components/sections/DownloadPreferencesSection";
import { LidarrSection } from "@/features/settings/components/sections/LidarrSection";
import { AudiobookshelfSection } from "@/features/settings/components/sections/AudiobookshelfSection";
import { SoulseekSection } from "@/features/settings/components/sections/SoulseekSection";
import { AIServicesSection } from "@/features/settings/components/sections/AIServicesSection";
import { StoragePathsSection } from "@/features/settings/components/sections/StoragePathsSection";
import { CacheSection } from "@/features/settings/components/sections/CacheSection";
import { UserManagementSection } from "@/features/settings/components/sections/UserManagementSection";
// Define sidebar items
const sidebarItems: SidebarItem[] = [
{ id: "account", label: "Account" },
{ id: "playback", label: "Playback" },
{ id: "download-preferences", label: "Download Preferences", adminOnly: true },
{ id: "lidarr", label: "Download Services", adminOnly: true },
{ id: "audiobookshelf", label: "Media Servers", adminOnly: true },
{ id: "soulseek", label: "P2P Networks", adminOnly: true },
{ id: "ai-services", label: "Artwork", adminOnly: true },
{ id: "storage", label: "Storage", adminOnly: true },
{ id: "cache", label: "Cache & Automation", adminOnly: true },
{ id: "users", label: "Users", adminOnly: true },
];
export default function SettingsPage() {
const { isAuthenticated, isLoading: authLoading, user } = useAuth();
const searchParams = useSearchParams();
const [isSaving, setIsSaving] = useState(false);
const [showRestartModal, setShowRestartModal] = useState(false);
const [testingServices, setTestingServices] = useState<Record<string, boolean>>({});
const saveStatus = useInlineStatus();
const isAdmin = user?.role === "admin";
// User settings hook
const {
settings: userSettings,
updateSettings: updateUserSettings,
saveSettings: saveUserSettings,
} = useSettingsData();
// System settings hook (only used if admin)
const {
systemSettings,
changedServices,
updateSystemSettings,
saveSystemSettings,
testService,
} = useSystemSettings();
// Handle initial hash for section scrolling
useEffect(() => {
if (typeof window !== "undefined") {
const hash = window.location.hash.substring(1);
if (hash) {
setTimeout(() => {
const element = document.getElementById(hash);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, 100);
}
}
}, []);
// Unified save function
const handleSaveAll = useCallback(async () => {
setIsSaving(true);
saveStatus.setLoading();
let hasError = false;
let changedSystemServices: string[] = [];
try {
await saveUserSettings(userSettings);
} catch (error) {
console.error("Failed to save user settings:", error);
hasError = true;
}
if (isAdmin) {
try {
changedSystemServices = await saveSystemSettings(systemSettings) || [];
} catch (error) {
console.error("Failed to save system settings:", error);
hasError = true;
}
}
setIsSaving(false);
if (hasError) {
saveStatus.setError("Failed to save");
} else {
saveStatus.setSuccess("Saved");
if (changedSystemServices.length > 0) {
setShowRestartModal(true);
}
}
}, [userSettings, systemSettings, isAdmin, saveUserSettings, saveSystemSettings, saveStatus]);
// Test service wrapper
const handleTestService = useCallback(async (service: string) => {
setTestingServices(prev => ({ ...prev, [service]: true }));
try {
return await testService(service);
} finally {
setTestingServices(prev => ({ ...prev, [service]: false }));
}
}, [testService]);
if (authLoading) {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0a0a0a]">
<GradientSpinner size="md" />
</div>
);
}
if (!isAuthenticated) {
return null;
}
return (
<>
<SettingsLayout sidebarItems={sidebarItems} isAdmin={isAdmin}>
{/* Account Section */}
<AccountSection />
{/* Playback Section */}
<PlaybackSection
value={userSettings.playbackQuality}
onChange={(quality) => updateUserSettings({ playbackQuality: quality })}
/>
{/* Admin-only sections */}
{isAdmin && (
<>
{/* Download Preferences */}
<DownloadPreferencesSection
settings={systemSettings}
onUpdate={updateSystemSettings}
/>
{/* Download Services - Lidarr */}
<LidarrSection
settings={systemSettings}
onUpdate={updateSystemSettings}
onTest={handleTestService}
isTesting={testingServices.lidarr || false}
/>
{/* Media Servers - Audiobookshelf */}
<AudiobookshelfSection
settings={systemSettings}
onUpdate={updateSystemSettings}
onTest={handleTestService}
isTesting={testingServices.audiobookshelf || false}
/>
{/* P2P Networks - Soulseek */}
<SoulseekSection
settings={systemSettings}
onUpdate={updateSystemSettings}
onTest={handleTestService}
isTesting={testingServices.slskd || false}
/>
{/* AI Services */}
<AIServicesSection
settings={systemSettings}
onUpdate={updateSystemSettings}
onTest={handleTestService}
isTesting={testingServices.openai || testingServices.fanart || false}
/>
{/* Storage */}
<StoragePathsSection
settings={systemSettings}
onUpdate={updateSystemSettings}
onTest={handleTestService}
isTesting={false}
/>
{/* Cache & Automation */}
<CacheSection
settings={systemSettings}
onUpdate={updateSystemSettings}
/>
{/* User Management */}
<UserManagementSection />
</>
)}
{/* Save Button - Fixed at bottom */}
<div className="sticky bottom-0 pt-8 pb-8 bg-[#0a0a0a]">
<div className="relative">
<button
onClick={handleSaveAll}
disabled={isSaving}
className="w-full bg-white text-black font-semibold py-3 px-4 rounded-full
hover:scale-[1.02] active:scale-[0.98] transition-transform
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
>
{isSaving ? "Saving..." : "Save"}
</button>
{/* Status appears below button, absolutely positioned */}
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2">
<InlineStatus {...saveStatus.props} />
</div>
</div>
</div>
</SettingsLayout>
{/* Restart Modal */}
<RestartModal
isOpen={showRestartModal}
onClose={() => setShowRestartModal(false)}
changedServices={changedServices}
/>
</>
);
}