From 7eedce62cd72a80db3139eb9e57beeddaa95b1db Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Sun, 19 Dec 2021 18:06:27 +0330 Subject: [PATCH] handle search shortcuts (#116) * code cleanup * fix some issues * handle search keyboard shortcuts --- src/components/ExtensionSearch.tsx | 56 ---------- src/components/chapter/ChapterList.tsx | 4 +- src/components/library/LibraryMangaGrid.tsx | 2 +- .../AppbarSearch.tsx} | 33 ++++-- src/screens/Extensions.tsx | 105 ++++++++++-------- src/screens/Library.tsx | 4 +- src/util/useLibraryOptions.ts | 2 - 7 files changed, 91 insertions(+), 115 deletions(-) delete mode 100644 src/components/ExtensionSearch.tsx rename src/components/{library/LibrarySearch.tsx => util/AppbarSearch.tsx} (67%) diff --git a/src/components/ExtensionSearch.tsx b/src/components/ExtensionSearch.tsx deleted file mode 100644 index 24cc7d96..00000000 --- a/src/components/ExtensionSearch.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import React, { useState, useRef } from 'react'; -import SearchIcon from '@mui/icons-material/Search'; -import { IconButton, Input } from '@mui/material'; -import CancelIcon from '@mui/icons-material/Cancel'; -import { useQueryParam, StringParam } from 'use-query-params'; - -export default function LibrarySearch() { - const [query, setQuery] = useQueryParam('query', StringParam); - const [searchOpen, setSearchOpen] = useState(!!query); - const inputRef = useRef(); - - function handleChange(e: React.ChangeEvent) { - setQuery(e.target.value === '' ? undefined : e.target.value); - } - const cancelSearch = () => { - setQuery(null); - setSearchOpen(false); - }; - const handleBlur = () => { - if (!query) setSearchOpen(false); - }; - const openSearch = () => { - setSearchOpen(true); - // Put Focus Action at the end of the Callstack so Input actually exists on the dom - setTimeout(() => { - if (inputRef && inputRef.current) inputRef.current.focus(); - }); - }; - return ( - <> - {searchOpen ? ( - - - - )} - /> - ) : ( - - )} - - ); -} diff --git a/src/components/chapter/ChapterList.tsx b/src/components/chapter/ChapterList.tsx index 0184edce..70978cb6 100644 --- a/src/components/chapter/ChapterList.tsx +++ b/src/components/chapter/ChapterList.tsx @@ -18,7 +18,7 @@ import ChapterOptions from 'components/chapter/ChapterOptions'; import ChapterCard from 'components/chapter/ChapterCard'; import useLocalStorage from 'util/useLocalStorage'; -const List = styled(Virtuoso)(({ theme }) => ({ +const CustomVirtuoso = styled(Virtuoso)(({ theme }) => ({ listStyle: 'none', padding: 0, minHeight: '200px', @@ -221,7 +221,7 @@ export default function ChapterList(props: IProps) { - (); @@ -35,6 +32,22 @@ export default function LibrarySearch() { if (inputRef && inputRef.current) inputRef.current.focus(); }); }; + + const handleSearchShortcut = (e: KeyboardEvent) => { + if ((e.code === 'F3') || (e.ctrlKey && e.code === 'KeyF')) { + e.preventDefault(); + openSearch(); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleSearchShortcut); + + return () => { + window.removeEventListener('keydown', handleSearchShortcut); + }; + }, [handleSearchShortcut]); + return ( <> {searchOpen @@ -52,7 +65,11 @@ export default function LibrarySearch() { )} /> - ) : } + ) : ( + + + + )} ); } diff --git a/src/screens/Extensions.tsx b/src/screens/Extensions.tsx index 1eb9c89f..2d1bdee5 100644 --- a/src/screens/Extensions.tsx +++ b/src/screens/Extensions.tsx @@ -17,11 +17,14 @@ import LangSelect from 'components/navbar/action/LangSelect'; import { extensionDefaultLangs, langCodeToName, langSortCmp } from 'util/language'; import { makeToaster } from 'components/util/Toast'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; -import ExtensionSearch from 'components/ExtensionSearch'; +import AppbarSearch from 'components/util/AppbarSearch'; import { useQueryParam, StringParam } from 'use-query-params'; -import { GroupedVirtuoso } from 'react-virtuoso'; +import { Virtuoso } from 'react-virtuoso'; import { Typography, useMediaQuery, useTheme } from '@mui/material'; +const LANGUAGE = 0; +const EXTENSIONS = 1; + const allLangs: string[] = []; interface GroupedExtension { @@ -33,10 +36,8 @@ function groupExtensions(extensions: IExtension[]) { const sortedExtenions: GroupedExtension = { installed: [], 'updates pending': [], all: [] }; extensions.forEach((extension) => { if (sortedExtenions[extension.lang] === undefined) { - if (sortedExtenions[extension.lang] === undefined) { - sortedExtenions[extension.lang] = []; - if (extension.lang !== 'all') { allLangs.push(extension.lang); } - } + sortedExtenions[extension.lang] = []; + if (extension.lang !== 'all') { allLangs.push(extension.lang); } } if (extension.installed) { if (extension.hasUpdate) { @@ -68,17 +69,14 @@ export default function MangaExtensions() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - // VirtuosoGroup: ExtArr, LangArr, langCountArr - const [extArr, setExtArr] = useState([]); - const [langArr, setLangArr] = useState([]); - const [langCountArr, setLangCountArr] = useState([]); + const [flatRenderItems, setFlatRenderItems] = useState<(IExtension | string)[]>([]); const [query] = useQueryParam('query', StringParam); useEffect(() => { setTitle('Extensions'); setAction( <> - + document.getElementById('external-extension-file')?.click() @@ -115,15 +113,21 @@ export default function MangaExtensions() { return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase()); }); - const groupedExtensions: [string, IExtension[]][] = groupExtensions(filtered) - .filter((group) => group[1].length !== 0) - .filter((group) => group[0] === 'installed' || 'updates pending' || 'all' - || shownLangs.includes(group[0])); + const combinedShownLangs = ['installed', 'updates pending', 'all', ...shownLangs]; - // The Virtual List set up - setExtArr(groupedExtensions.reduce((p, c) => p.concat(...c[1]), [] as IExtension[])); - setLangArr(groupedExtensions.map((g) => g[0])); - setLangCountArr(groupedExtensions.map((lang) => lang[1].length)); + const groupedExtensions: [string, IExtension[]][] = groupExtensions(filtered) + .filter((group) => group[EXTENSIONS].length > 0) + .filter((group) => combinedShownLangs.includes(group[LANGUAGE])); + + // Virtual List set up + const flatExtensions = [] as (IExtension | string)[]; + + groupedExtensions.forEach((group) => { + flatExtensions.push(group[LANGUAGE]); + group[1].forEach((it) => flatExtensions.push(it)); + }); + + setFlatRenderItems(flatExtensions); } }, [extensionsRaw, query, shownLangs]); @@ -178,7 +182,7 @@ export default function MangaExtensions() { document.removeEventListener('dragover', dragOverHandler); input?.removeEventListener('change', changeHandler); }; - }, [extArr]); // useEffect only after renders + }, [flatRenderItems]); // useEffect only after renders if (extensionsRaw.length === 0) { return ; @@ -192,30 +196,43 @@ export default function MangaExtensions() { id="external-extension-file" style={{ display: 'none' }} /> - ( - - {langCodeToName(langArr[index])} - - )} - style={{ height: isMobile ? 'calc(100vh - 64px - 64px)' : 'calc(100vh - 64px)' }} - itemContent={(index) => ( - { - triggerUpdate(); - }} - /> - )} + { + if (typeof (flatRenderItems[index]) === 'string') { + const item = flatRenderItems[index] as string; + return ( + + {langCodeToName(item)} + + ); + } + const item = flatRenderItems[index] as IExtension; + + return ( + { + triggerUpdate(); + }} + /> + ); + }} /> ); diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index 1c9f2f68..baa2be63 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -15,7 +15,7 @@ import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import TabPanel from 'components/util/TabPanel'; import LibraryOptions from 'components/library/LibraryOptions'; import LibraryMangaGrid from 'components/library/LibraryMangaGrid'; -import LibrarySearch from 'components/library/LibrarySearch'; +import AppbarSearch from 'components/util/AppbarSearch'; import { useQueryParam, NumberParam } from 'use-query-params'; interface IMangaCategory { @@ -29,7 +29,7 @@ export default function Library() { useEffect(() => { setTitle('Library'); setAction( <> - + , ); diff --git a/src/util/useLibraryOptions.ts b/src/util/useLibraryOptions.ts index d64b00d5..57934fb9 100644 --- a/src/util/useLibraryOptions.ts +++ b/src/util/useLibraryOptions.ts @@ -8,8 +8,6 @@ import { BooleanParam, useQueryParam, StringParam } from 'use-query-params'; -export type NullAndUndefined = T | null | undefined; - interface IUseLibraryOptions { downloaded: NullAndUndefined setDownloaded: (downloaded: NullAndUndefined)=>void