Skip to content

Commit

Permalink
added filesystem search multi-series mode
Browse files Browse the repository at this point in the history
  • Loading branch information
xgi committed May 8, 2024
1 parent b04a149 commit 7b6406f
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 61 deletions.
3 changes: 2 additions & 1 deletion src/common/constants/ipcChannels.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions src/main/services/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
},
);
};
31 changes: 17 additions & 14 deletions src/main/util/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand All @@ -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[] = [];
Expand All @@ -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 } = {};
Expand Down
9 changes: 3 additions & 6 deletions src/renderer/components/library/SeriesDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -127,11 +127,8 @@ const SeriesDetails: React.FC<Props> = () => {
<ChapterTable series={series} />
</>
) : (
<Center h="100%" mx="auto">
<Stack align="center">
<Loader />
<Text>Loading series details...</Text>
</Stack>
<Center h="calc(100vh - 16px)" mx="auto">
<Loader />
</Center>
)}
</>
Expand Down
28 changes: 19 additions & 9 deletions src/renderer/components/search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,25 @@ const Search: React.FC<Props> = () => {
}
};

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(() => {
Expand Down
69 changes: 48 additions & 21 deletions src/renderer/components/search/SearchControlBar.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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: 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 (
<Button
onClick={() =>
ipcRenderer
.invoke(ipcChannels.APP.SHOW_OPEN_DIALOG, true, [], 'Select Series Directory')
.then((fileList: string) => {
// eslint-disable-next-line promise/always-return
if (fileList.length > 0) {
props.handleSearchFilesystem(fileList[0]);
}
})
const handleSelectDirectory = async () => {
const fileList = await ipcRenderer.invoke(
ipcChannels.APP.SHOW_OPEN_DIALOG,
true,
[],
'Select Series Directory',
);
if (fileList.length <= 0) return;

const selectedPath = fileList[0];

const searchPaths = multiSeriesEnabled
? await ipcRenderer.invoke(ipcChannels.FILESYSTEM.LIST_DIRECTORY, selectedPath)
: [selectedPath];

props.handleSearchFilesystem(searchPaths);
};

const renderFilesystemControls = () => {
return (
<Group>
<Button onClick={handleSelectDirectory}>Select Directory</Button>
<Tooltip
position="bottom"
label={
<>
<Text size="sm">When multi-series mode is enabled, each item in the selected</Text>
<Text size="sm">directory is treated as a separate series.</Text>
</>
}
>
Select Directory
</Button>
);
}
<Group gap={6} justify="center" align="center">
<Checkbox
label="Multi-series mode"
checked={multiSeriesEnabled}
onChange={() => setMultiSeriesEnabled(!multiSeriesEnabled)}
/>
<IconHelp color={'var(--mantine-color-dark-2)'} size={16} />
</Group>
</Tooltip>
</Group>
);
};

const renderStandardControls = () => {
return (
<>
<TextInput
Expand Down Expand Up @@ -73,7 +100,7 @@ const SearchControlBar: React.FC<Props> = (props: Props) => {
}))}
onChange={(value) => setSearchExtension(value || searchExtension)}
/>
{renderSearchControls()}
{searchExtension === FS_METADATA.id ? renderFilesystemControls() : renderStandardControls()}
</Flex>
);
};
Expand Down
17 changes: 7 additions & 10 deletions src/renderer/components/settings/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
NumberInput,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons';
import { GeneralSetting } from '@/common/models/types';
Expand Down Expand Up @@ -184,15 +183,13 @@ const GeneralSettings: React.FC<Props> = () => {
</Button>
</Group>
<Group gap="sm" mt="xs">
<Tooltip label={`Makes backup every day (stores ${autoBackupCount} backups)`}>
<Checkbox
label="Automatically backup library"
description={`Create up to ${autoBackupCount} daily backups`}
size="md"
checked={autoBackup}
onChange={(e) => updateGeneralSetting(GeneralSetting.autoBackup, e.target.checked)}
/>
</Tooltip>
<Checkbox
label="Automatically backup library"
description={`Create up to ${autoBackupCount} daily backups`}
size="md"
checked={autoBackup}
onChange={(e) => updateGeneralSetting(GeneralSetting.autoBackup, e.target.checked)}
/>
<NumberInput
w={100}
ml={'xs'}
Expand Down

0 comments on commit 7b6406f

Please sign in to comment.