355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
import {
|
|
Box,
|
|
Heading,
|
|
VStack,
|
|
Text,
|
|
Button,
|
|
OrderedList,
|
|
ListItem,
|
|
useDisclosure,
|
|
Modal,
|
|
ModalOverlay,
|
|
ModalContent,
|
|
ModalHeader,
|
|
ModalBody,
|
|
ModalFooter,
|
|
useToast,
|
|
IconButton,
|
|
Flex,
|
|
Tabs,
|
|
TabList,
|
|
TabPanels,
|
|
Tab,
|
|
TabPanel,
|
|
Icon,
|
|
HStack,
|
|
} from "@chakra-ui/react";
|
|
import { ChevronLeftIcon } from "@chakra-ui/icons";
|
|
import { FiDatabase, FiSettings, FiUpload, FiLink, FiRefreshCw, FiTrash2 } from 'react-icons/fi';
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useXmlParser } from "../hooks/useXmlParser";
|
|
import { StyledFileInput } from "../components/StyledFileInput";
|
|
import { StorageConfiguration } from "./StorageConfiguration";
|
|
import { MusicUpload } from "../components/MusicUpload";
|
|
import { SongMatching } from "../components/SongMatching";
|
|
import { api } from "../services/api";
|
|
import { DuplicatesViewer } from "../components/DuplicatesViewer.tsx";
|
|
import { useState, useMemo, useEffect } from "react";
|
|
|
|
interface MusicFile {
|
|
_id: string;
|
|
originalName: string;
|
|
title?: string;
|
|
artist?: string;
|
|
album?: string;
|
|
duration?: number;
|
|
size: number;
|
|
format?: string;
|
|
uploadedAt: string;
|
|
songId?: any; // Reference to linked song
|
|
}
|
|
|
|
export function Configuration() {
|
|
const { resetLibrary } = useXmlParser();
|
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
const toast = useToast();
|
|
const navigate = useNavigate();
|
|
|
|
// Music storage state removed from Config; see Sync & Matching section
|
|
|
|
// Tabs: remember active tab across refreshes
|
|
const initialTabIndex = useMemo(() => {
|
|
const stored = localStorage.getItem('configTabIndex');
|
|
return stored ? parseInt(stored, 10) : 0;
|
|
}, []);
|
|
const [tabIndex, setTabIndex] = useState<number>(initialTabIndex);
|
|
const [storageProvider, setStorageProvider] = useState<string>('Storage');
|
|
|
|
// No explicit tab index enum needed
|
|
|
|
// Load current storage provider for dynamic button labels
|
|
useEffect(() => {
|
|
const loadStorageProvider = async () => {
|
|
try {
|
|
const response = await fetch('/api/config/storage');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setStorageProvider(data.config.provider.toUpperCase());
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load storage provider:', error);
|
|
}
|
|
};
|
|
loadStorageProvider();
|
|
}, []);
|
|
|
|
// Storage config fetch removed; Sync buttons remain available in the panel
|
|
|
|
// Removed Music Library sync handlers from Config (moved to Sync & Matching panel)
|
|
|
|
const handleUploadComplete = (files: MusicFile[]) => {
|
|
toast({
|
|
title: 'Upload Complete',
|
|
description: `${files.length} file${files.length !== 1 ? 's' : ''} uploaded successfully`,
|
|
status: 'success',
|
|
duration: 3000,
|
|
isClosable: true,
|
|
});
|
|
};
|
|
// Deletion handler not needed in Config anymore
|
|
|
|
// Utilities for file list removed with Music Library tab
|
|
|
|
|
|
const handleResetDatabase = async () => {
|
|
try {
|
|
await api.resetDatabase();
|
|
await resetLibrary();
|
|
onClose();
|
|
toast({
|
|
title: "Database reset successful",
|
|
description: "Your library has been cleared.",
|
|
status: "success",
|
|
duration: 5000,
|
|
isClosable: true,
|
|
});
|
|
// Navigate to homepage to show welcome modal and start fresh
|
|
navigate('/');
|
|
} catch (error) {
|
|
toast({
|
|
title: "Failed to reset database",
|
|
description: "An error occurred while resetting the database.",
|
|
status: "error",
|
|
duration: 5000,
|
|
isClosable: true,
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
h="100%"
|
|
bg="gray.900"
|
|
p={8}
|
|
overflowY="auto"
|
|
sx={{
|
|
'&::-webkit-scrollbar': {
|
|
width: '8px',
|
|
borderRadius: '8px',
|
|
backgroundColor: 'gray.900',
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: 'gray.700',
|
|
borderRadius: '8px',
|
|
},
|
|
}}
|
|
>
|
|
<VStack spacing={8} align="stretch" maxW="4xl" mx="auto">
|
|
<Flex align="center" gap={4}>
|
|
<IconButton
|
|
icon={<ChevronLeftIcon boxSize={6} />}
|
|
aria-label="Go back"
|
|
variant="ghost"
|
|
onClick={() => navigate('/')}
|
|
color="gray.300"
|
|
_hover={{ color: "white", bg: "whiteAlpha.200" }}
|
|
/>
|
|
<Heading size="lg">Configuration</Heading>
|
|
</Flex>
|
|
|
|
<Tabs
|
|
variant="enclosed"
|
|
colorScheme="blue"
|
|
isLazy
|
|
index={tabIndex}
|
|
onChange={(index) => {
|
|
setTabIndex(index);
|
|
localStorage.setItem('configTabIndex', String(index));
|
|
}}
|
|
>
|
|
<TabList bg="gray.800" borderColor="gray.700">
|
|
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
|
<HStack spacing={2}>
|
|
<Icon as={FiDatabase} />
|
|
<Text>Library Management</Text>
|
|
</HStack>
|
|
</Tab>
|
|
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
|
<HStack spacing={2}>
|
|
<Icon as={FiUpload} />
|
|
<Text>Upload Music</Text>
|
|
</HStack>
|
|
</Tab>
|
|
{/* Hide heavy Music Library tab from config to reduce load; may move to separate page later */}
|
|
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
|
<HStack spacing={2}>
|
|
<Icon as={FiLink} />
|
|
<Text>Song Matching</Text>
|
|
</HStack>
|
|
</Tab>
|
|
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
|
<HStack spacing={2}>
|
|
<Icon as={FiDatabase} />
|
|
<Text>Duplicates</Text>
|
|
</HStack>
|
|
</Tab>
|
|
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
|
<HStack spacing={2}>
|
|
<Icon as={FiSettings} />
|
|
<Text>Storage Configuration</Text>
|
|
</HStack>
|
|
</Tab>
|
|
</TabList>
|
|
|
|
<TabPanels>
|
|
{/* Library Management Tab */}
|
|
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
|
<VStack spacing={6} align="stretch">
|
|
<Box>
|
|
<Text color="gray.400" mb={4}>
|
|
To get started with Rekordbox Reader, you'll need to export your library from Rekordbox and import it here.
|
|
</Text>
|
|
|
|
<OrderedList spacing={3} color="gray.400" mb={6}>
|
|
<ListItem>
|
|
Open Rekordbox and go to <Text as="span" color="gray.300" fontWeight="medium">File → Export Collection in xml format</Text>
|
|
</ListItem>
|
|
<ListItem>
|
|
Choose a location to save your XML file
|
|
</ListItem>
|
|
<ListItem>
|
|
Click the button below to import your Rekordbox XML file
|
|
</ListItem>
|
|
</OrderedList>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text fontWeight="medium" mb={2}>Import Library</Text>
|
|
<StyledFileInput />
|
|
</Box>
|
|
|
|
<Box borderTopWidth="1px" borderColor="gray.700" pt={6} mt={4}>
|
|
<Text fontWeight="medium" mb={2}>Reset Database</Text>
|
|
<Text color="gray.400" mb={4}>
|
|
Clear all songs and playlists from the database. This action cannot be undone.
|
|
</Text>
|
|
<Button
|
|
onClick={onOpen}
|
|
width="full"
|
|
colorScheme="red"
|
|
variant="outline"
|
|
>
|
|
Reset Database
|
|
</Button>
|
|
</Box>
|
|
</VStack>
|
|
</TabPanel>
|
|
|
|
{/* Upload Music Tab */}
|
|
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
|
<VStack spacing={6} align="stretch">
|
|
<Box>
|
|
<Heading size="md" mb={4} color="white">
|
|
Upload Music Files
|
|
</Heading>
|
|
<Text color="gray.400" mb={4}>
|
|
Drag and drop your music files here or click to select. Files will be uploaded to your configured storage
|
|
(S3 or WebDAV) and metadata will be automatically extracted.
|
|
</Text>
|
|
</Box>
|
|
<MusicUpload onUploadComplete={handleUploadComplete} />
|
|
</VStack>
|
|
</TabPanel>
|
|
|
|
{/* Music Library tab removed from Config (heavy). Consider separate page if needed. */}
|
|
|
|
{/* Song Matching Tab */}
|
|
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
|
<VStack spacing={6} align="stretch">
|
|
<Heading size="md" color="white">Sync and Matching</Heading>
|
|
<HStack spacing={3}>
|
|
<Button
|
|
leftIcon={<FiRefreshCw />}
|
|
colorScheme="blue"
|
|
variant="solid"
|
|
onClick={() => api.startStorageSync()}
|
|
>
|
|
Sync {storageProvider} (incremental)
|
|
</Button>
|
|
<Button
|
|
leftIcon={<FiRefreshCw />}
|
|
colorScheme="orange"
|
|
variant="outline"
|
|
onClick={() => api.startStorageSync({ force: true })}
|
|
>
|
|
Force {storageProvider} Sync (rescan all)
|
|
</Button>
|
|
<Button
|
|
leftIcon={<FiTrash2 />}
|
|
colorScheme="red"
|
|
variant="outline"
|
|
onClick={() => api.startStorageSync({ clearLinks: true, force: true })}
|
|
>
|
|
Clear Links + Force {storageProvider} Sync
|
|
</Button>
|
|
</HStack>
|
|
<SongMatching />
|
|
</VStack>
|
|
</TabPanel>
|
|
|
|
{/* Duplicates Tab */}
|
|
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
|
<DuplicatesViewer />
|
|
</TabPanel>
|
|
|
|
{/* Storage Configuration Tab */}
|
|
<TabPanel bg="gray.800" p={0}>
|
|
<Box p={6}>
|
|
<StorageConfiguration />
|
|
</Box>
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
</VStack>
|
|
|
|
{/* Reset Database Confirmation Modal */}
|
|
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
|
<ModalOverlay />
|
|
<ModalContent bg="gray.800">
|
|
<ModalHeader color="white">Confirm Database Reset</ModalHeader>
|
|
<ModalBody>
|
|
<VStack spacing={4} align="stretch">
|
|
<Text color="gray.300">
|
|
Are you sure you want to reset the database? This will:
|
|
</Text>
|
|
<OrderedList spacing={2} color="gray.400" pl={4}>
|
|
<ListItem>Delete all imported songs</ListItem>
|
|
<ListItem>Remove all playlists</ListItem>
|
|
<ListItem>Clear all custom configurations</ListItem>
|
|
</OrderedList>
|
|
<Text color="red.300" fontWeight="medium">
|
|
This action cannot be undone.
|
|
</Text>
|
|
</VStack>
|
|
</ModalBody>
|
|
<ModalFooter gap={3}>
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
color="gray.300"
|
|
_hover={{
|
|
bg: "whiteAlpha.200",
|
|
color: "white"
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button colorScheme="red" onClick={handleResetDatabase}>
|
|
Reset Database
|
|
</Button>
|
|
</ModalFooter>
|
|
</ModalContent>
|
|
</Modal>
|
|
</Box>
|
|
);
|
|
}
|