diff --git a/src/common/constants/ipcChannels.json b/src/common/constants/ipcChannels.json index 331a8e10..6ce92d6d 100644 --- a/src/common/constants/ipcChannels.json +++ b/src/common/constants/ipcChannels.json @@ -56,7 +56,8 @@ "GET_ALL_DOWNLOADED_CHAPTER_IDS": "filesystem-get-all-downloaded-chapter-ids", "GET_THUMBNAIL_PATH": "filesystem-get-thumbnail-path", "DOWNLOAD_THUMBNAIL": "filesystem-download-thumbnail", - "DELETE_THUMBNAIL": "filesystem-delete-thumbnail" + "DELETE_THUMBNAIL": "filesystem-delete-thumbnail", + "LIST_DIRECTORY": "filesystem-list-directory" }, "TRACKER_MANAGER": { "GET_ALL": "tracker-manager-get-alls" diff --git a/src/main/services/filesystem.ts b/src/main/services/filesystem.ts index 6f4977cc..3d6a71d7 100644 --- a/src/main/services/filesystem.ts +++ b/src/main/services/filesystem.ts @@ -9,6 +9,7 @@ import { deleteThumbnail, getChaptersDownloaded, getChapterDownloaded, + listDirectory, } from '@/main/util/filesystem'; import ipcChannels from '@/common/constants/ipcChannels.json'; import { THUMBNAILS_DIR } from '../util/appdata'; @@ -65,4 +66,11 @@ export const createFilesystemIpcHandlers = (ipcMain: IpcMain) => { ipcMain.handle(ipcChannels.FILESYSTEM.DELETE_THUMBNAIL, (_event, series: Series) => { return deleteThumbnail(series, THUMBNAILS_DIR); }); + + ipcMain.handle( + ipcChannels.FILESYSTEM.LIST_DIRECTORY, + (_event, pathname: string, directoriesOnly: boolean = false) => { + return listDirectory(pathname, directoriesOnly); + }, + ); }; diff --git a/src/main/util/filesystem.ts b/src/main/util/filesystem.ts index 0e0d8081..7a8e6d16 100644 --- a/src/main/util/filesystem.ts +++ b/src/main/util/filesystem.ts @@ -26,18 +26,21 @@ export function walk(directory: string): string[] { } /** - * Get a list of all directories within a directory (non-recursive). - * @param directory the parent directory - * @returns list of subdirectories + * List contents of a directory (non-recursive, base level only). + * @param pathname the parent directory + * @param directoriesOnly (optional, default false) only include subdirectories + * @returns list of matching full paths */ -export function getDirectories(directory: string): string[] { - if (!fs.existsSync(directory)) return []; +export function listDirectory(pathname: string, directoriesOnly: boolean = false): string[] { + if (!fs.existsSync(pathname)) return []; const result: string[] = []; - const files = fs.readdirSync(directory); + const files = fs.readdirSync(pathname); files.forEach((file: string) => { - const fullpath = path.join(directory, file); - if (fs.statSync(fullpath)) result.push(fullpath); + const fullpath = path.join(pathname, file); + if (!directoriesOnly || fs.statSync(fullpath).isDirectory()) { + result.push(fullpath); + } }); return result; @@ -57,8 +60,8 @@ export function getChapterDownloadPath( const seriesDir1 = sanitizeFilename(series.title); const seriesDir2 = series.id || ''; const chapterDirectories = [ - ...getDirectories(path.join(downloadsDir, seriesDir1)), - ...getDirectories(path.join(downloadsDir, seriesDir2)), + ...listDirectory(path.join(downloadsDir, seriesDir1)), + ...listDirectory(path.join(downloadsDir, seriesDir2)), ]; const matching = chapterDirectories.find((fullpath) => { @@ -71,10 +74,10 @@ export function getChapterDownloadPath( } export function getAllDownloadedChapterIds(downloadsDir: string): string[] { - const seriesDirs = getDirectories(downloadsDir); + const seriesDirs = listDirectory(downloadsDir); const chapterDirs: string[] = []; seriesDirs.forEach((seriesDir) => { - chapterDirs.push(...getDirectories(seriesDir)); + chapterDirs.push(...listDirectory(seriesDir)); }); const result: string[] = []; @@ -101,8 +104,8 @@ export async function getChaptersDownloaded( const seriesDir1 = sanitizeFilename(series.title); const seriesDir2 = series.id || ''; const chapterDirectories = [ - ...getDirectories(path.join(downloadsDir, seriesDir1)), - ...getDirectories(path.join(downloadsDir, seriesDir2)), + ...listDirectory(path.join(downloadsDir, seriesDir1)), + ...listDirectory(path.join(downloadsDir, seriesDir2)), ]; const result: { [key: string]: boolean } = {}; diff --git a/src/renderer/components/library/SeriesDetails.tsx b/src/renderer/components/library/SeriesDetails.tsx index 681f4970..9f0e5964 100644 --- a/src/renderer/components/library/SeriesDetails.tsx +++ b/src/renderer/components/library/SeriesDetails.tsx @@ -3,7 +3,7 @@ import { useParams, useLocation } from 'react-router-dom'; const { ipcRenderer } = require('electron'); import { Series } from '@tiyo/common'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { Center, Loader, Stack, Text } from '@mantine/core'; +import { Center, Loader } from '@mantine/core'; import ChapterTable from './ChapterTable'; import { getBannerImageUrl } from '@/renderer/services/mediasource'; import ipcChannels from '@/common/constants/ipcChannels.json'; @@ -127,11 +127,8 @@ const SeriesDetails: React.FC = () => { ) : ( -
- - - Loading series details... - +
+
)} diff --git a/src/renderer/components/search/Search.tsx b/src/renderer/components/search/Search.tsx index ce8796bb..6eebec0c 100644 --- a/src/renderer/components/search/Search.tsx +++ b/src/renderer/components/search/Search.tsx @@ -72,15 +72,25 @@ const Search: React.FC = () => { } }; - const handleSearchFilesystem = (path: string) => { - ipcRenderer - .invoke(ipcChannels.EXTENSION.GET_SERIES, FS_METADATA.id, path) - .then((series: Series) => { - setAddModalSeries(series); - setAddModalEditable(searchExtension === FS_METADATA.id); - setShowingAddModal(!showingAddModal); - }) - .catch((e) => console.error(e)); + const handleSearchFilesystem = async (searchPaths: string[]) => { + const seriesList: Series[] = []; + + for (const searchPath of searchPaths) { + const series = await ipcRenderer.invoke( + ipcChannels.EXTENSION.GET_SERIES, + FS_METADATA.id, + searchPath, + ); + if (series !== null) seriesList.push(series); + } + + if (seriesList.length > 1) { + setSearchResult({ seriesList: seriesList, hasMore: false }); + } else if (seriesList.length == 1) { + setAddModalSeries(seriesList[0]); + setAddModalEditable(searchExtension === FS_METADATA.id); + setShowingAddModal(!showingAddModal); + } }; useEffect(() => { diff --git a/src/renderer/components/search/SearchControlBar.tsx b/src/renderer/components/search/SearchControlBar.tsx index b03d79e5..b98cf428 100644 --- a/src/renderer/components/search/SearchControlBar.tsx +++ b/src/renderer/components/search/SearchControlBar.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ExtensionMetadata } from '@tiyo/common'; import { useRecoilState, useSetRecoilState } from 'recoil'; -import { Button, Flex, Select, TextInput } from '@mantine/core'; +import { Button, Checkbox, Flex, Group, Select, Text, TextInput, Tooltip } from '@mantine/core'; const { ipcRenderer } = require('electron'); import { searchExtensionState, @@ -10,39 +10,66 @@ import { } from '@/renderer/state/searchStates'; import { FS_METADATA } from '@/common/temp_fs_metadata'; import ipcChannels from '@/common/constants/ipcChannels.json'; +import { IconHelp } from '@tabler/icons'; interface Props { extensionList: ExtensionMetadata[]; hasFilterOptions: boolean; handleSearch: (fresh?: boolean) => void; - handleSearchFilesystem: (path: string) => void; + handleSearchFilesystem: (searchPaths: string[]) => void; } const SearchControlBar: React.FC = (props: Props) => { const [searchExtension, setSearchExtension] = useRecoilState(searchExtensionState); const setSearchText = useSetRecoilState(searchTextState); const setShowingFilterDrawer = useSetRecoilState(showingFilterDrawerState); + const [multiSeriesEnabled, setMultiSeriesEnabled] = useState(false); - const renderSearchControls = () => { - if (searchExtension === FS_METADATA.id) { - return ( - + + When multi-series mode is enabled, each item in the selected + directory is treated as a separate series. + } > - Select Directory - - ); - } + + setMultiSeriesEnabled(!multiSeriesEnabled)} + /> + + + + + ); + }; + const renderStandardControls = () => { return ( <> = (props: Props) => { }))} onChange={(value) => setSearchExtension(value || searchExtension)} /> - {renderSearchControls()} + {searchExtension === FS_METADATA.id ? renderFilesystemControls() : renderStandardControls()} ); }; diff --git a/src/renderer/components/settings/GeneralSettings.tsx b/src/renderer/components/settings/GeneralSettings.tsx index 29cd7e3d..1c022f35 100644 --- a/src/renderer/components/settings/GeneralSettings.tsx +++ b/src/renderer/components/settings/GeneralSettings.tsx @@ -11,7 +11,6 @@ import { NumberInput, Stack, Text, - Tooltip, } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons'; import { GeneralSetting } from '@/common/models/types'; @@ -184,15 +183,13 @@ const GeneralSettings: React.FC = () => { - - updateGeneralSetting(GeneralSetting.autoBackup, e.target.checked)} - /> - + updateGeneralSetting(GeneralSetting.autoBackup, e.target.checked)} + />