[web] Search refactoring - Part x/x (#3233)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||
import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined";
|
||||
import Favorite from "@mui/icons-material/FavoriteRounded";
|
||||
@@ -6,9 +7,9 @@ import PeopleIcon from "@mui/icons-material/People";
|
||||
import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer";
|
||||
import CollectionOptions from "components/Collections/CollectionOptions";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { Collection, CollectionSummary } from "types/collection";
|
||||
import { CollectionSummary, CollectionSummaryType } from "types/collection";
|
||||
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
|
||||
import { CollectionSummaryType, shouldShowOptions } from "utils/collection";
|
||||
import { shouldShowOptions } from "utils/collection";
|
||||
import { CollectionInfo } from "./CollectionInfo";
|
||||
import { CollectionInfoBarWrapper } from "./styledComponents";
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import PeopleIcon from "@mui/icons-material/People";
|
||||
import PushPin from "@mui/icons-material/PushPin";
|
||||
import { Box, Typography, styled } from "@mui/material";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { CollectionSummary } from "types/collection";
|
||||
import { CollectionSummaryType } from "utils/collection";
|
||||
import { CollectionSummary, CollectionSummaryType } from "types/collection";
|
||||
import CollectionCard from "../CollectionCard";
|
||||
import {
|
||||
ActiveIndicator,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { boxSeal } from "@/base/crypto/libsodium";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import EnteButton from "@ente/shared/components/EnteButton";
|
||||
@@ -12,7 +13,6 @@ import { Link, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Collection } from "types/collection";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { loadSender } from "../../../utils/useCastSender";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { CollectionSummaryType } from "utils/collection";
|
||||
import { CollectionSummaryType } from "types/collection";
|
||||
import { CollectionActions } from "..";
|
||||
interface Iprops {
|
||||
handleCollectionAction: (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PeopleIcon from "@mui/icons-material/People";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { CollectionSummaryType } from "utils/collection";
|
||||
import { CollectionSummaryType } from "types/collection";
|
||||
import { CollectionActions } from "..";
|
||||
|
||||
interface Iprops {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import { CollectionSummaryType } from "types/collection";
|
||||
import {
|
||||
CollectionSummaryType,
|
||||
showDownloadQuickOption,
|
||||
showEmptyTrashQuickOption,
|
||||
showShareQuickOption,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { ItemVisibility } from "@/media/file-metadata";
|
||||
import { HorizontalFlex } from "@ente/shared/components/Container";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
@@ -12,14 +13,13 @@ import { useContext, useRef, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import * as CollectionAPI from "services/collectionService";
|
||||
import * as TrashService from "services/trashService";
|
||||
import { Collection } from "types/collection";
|
||||
import { CollectionSummaryType } from "types/collection";
|
||||
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
|
||||
import {
|
||||
ALL_SECTION,
|
||||
changeCollectionOrder,
|
||||
changeCollectionSortOrder,
|
||||
changeCollectionVisibility,
|
||||
CollectionSummaryType,
|
||||
downloadCollectionHelper,
|
||||
downloadDefaultHiddenCollectionHelper,
|
||||
HIDDEN_ITEMS_SECTION,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
|
||||
import { DialogContent, useMediaQuery } from "@mui/material";
|
||||
@@ -6,14 +7,13 @@ import { t } from "i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createUnCategorizedCollection } from "services/collectionService";
|
||||
import {
|
||||
Collection,
|
||||
CollectionSummaries,
|
||||
CollectionSummary,
|
||||
CollectionSummaryType,
|
||||
} from "types/collection";
|
||||
import { CollectionSelectorIntent } from "types/gallery";
|
||||
import {
|
||||
COLLECTION_SORT_ORDER,
|
||||
CollectionSummaryType,
|
||||
DUMMY_UNCATEGORIZED_COLLECTION,
|
||||
isAddToAllowedCollection,
|
||||
isMoveToAllowedCollection,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { COLLECTION_ROLE, Collection } from "types/collection";
|
||||
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useContext, useMemo } from "react";
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import {
|
||||
COLLECTION_ROLE,
|
||||
type Collection,
|
||||
type CollectionUser,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
|
||||
@@ -18,7 +23,6 @@ import { AppContext } from "pages/_app";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import { unshareCollection } from "services/collectionService";
|
||||
import { COLLECTION_ROLE, Collection, CollectionUser } from "types/collection";
|
||||
import AddParticipant from "./AddParticipant";
|
||||
import ManageParticipant from "./ManageParticipant";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import log from "@/base/log";
|
||||
import type { Collection, CollectionUser } from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
@@ -14,7 +15,6 @@ import { GalleryContext } from "pages/gallery";
|
||||
import { useContext } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { shareCollection } from "services/collectionService";
|
||||
import { Collection, CollectionUser } from "types/collection";
|
||||
import { handleSharingErrors } from "utils/error/ui";
|
||||
|
||||
interface Iprops {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
|
||||
import { useRef, useState } from "react";
|
||||
import { COLLECTION_ROLE, Collection } from "types/collection";
|
||||
|
||||
import {
|
||||
MenuItemDivider,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { Collection, CollectionSummary } from "types/collection";
|
||||
import { CollectionSummaryType } from "utils/collection";
|
||||
import { CollectionSummary, CollectionSummaryType } from "types/collection";
|
||||
import EmailShare from "./emailShare";
|
||||
import PublicShare from "./publicShare";
|
||||
import SharingDetails from "./sharingDetails";
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import type { Collection, PublicURL } from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import DownloadSharp from "@mui/icons-material/DownloadSharp";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
createShareableURL,
|
||||
updateShareableURL,
|
||||
} from "services/collectionService";
|
||||
import { Collection, PublicURL } from "types/collection";
|
||||
import { handleSharingErrors } from "utils/error/ui";
|
||||
interface Iprops {
|
||||
collection: Collection;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Collection, PublicURL } from "@/media/collection";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Collection, PublicURL } from "types/collection";
|
||||
import { appendCollectionKeyToShareURL } from "utils/collection";
|
||||
import EnablePublicShareOptions from "./EnablePublicShareOptions";
|
||||
import CopyLinkModal from "./copyLinkModal";
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
|
||||
import { getDeviceLimitOptions } from "utils/collection";
|
||||
|
||||
interface Iprops {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
|
||||
interface Iprops {
|
||||
publicShareProp: PublicURL;
|
||||
collection: Collection;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import RemoveCircleOutline from "@mui/icons-material/RemoveCircleOutline";
|
||||
@@ -12,7 +17,6 @@ import {
|
||||
deleteShareableURL,
|
||||
updateShareableURL,
|
||||
} from "services/collectionService";
|
||||
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
|
||||
import { SetPublicShareProp } from "types/publicCollection";
|
||||
import { handleSharingErrors } from "utils/error/ui";
|
||||
import { ManageDeviceLimit } from "./deviceLimit";
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import { formatDateTime } from "@ente/shared/time/format";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
|
||||
import { shareExpiryOptions } from "utils/collection";
|
||||
import { isLinkExpired } from "../managePublicShare";
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext, useState } from "react";
|
||||
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
|
||||
import { PublicLinkSetPassword } from "./setPassword";
|
||||
|
||||
interface Iprops {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { MenuItemGroup, MenuSectionTitle } from "@/base/components/Menu";
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import { Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
|
||||
|
||||
interface Iprops {
|
||||
publicShareProp: PublicURL;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import type { Collection, PublicURL } from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopyOutlined";
|
||||
@@ -8,7 +9,6 @@ import PublicIcon from "@mui/icons-material/Public";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { Collection, PublicURL } from "types/collection";
|
||||
import { SetPublicShareProp } from "types/publicCollection";
|
||||
import ManagePublicShareOptions from "./manage";
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { COLLECTION_ROLE } from "@/media/collection";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
|
||||
import ModeEditIcon from "@mui/icons-material/ModeEdit";
|
||||
@@ -12,8 +13,7 @@ import Avatar from "components/pages/gallery/Avatar";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useContext } from "react";
|
||||
import { COLLECTION_ROLE } from "types/collection";
|
||||
import { CollectionSummaryType } from "utils/collection";
|
||||
import { CollectionSummaryType } from "types/collection";
|
||||
|
||||
export default function SharingDetails({ collection, type }) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||
import { LS_KEYS } from "@ente/shared/storage/localStorage";
|
||||
import AllCollections from "components/Collections/AllCollections";
|
||||
@@ -8,7 +9,7 @@ import CollectionShare from "components/Collections/CollectionShare";
|
||||
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { sortCollectionSummaries } from "services/collectionService";
|
||||
import { Collection, CollectionSummaries } from "types/collection";
|
||||
import { CollectionSummaries } from "types/collection";
|
||||
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
|
||||
import {
|
||||
ALL_SECTION,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
mlStatusSnapshot,
|
||||
mlStatusSubscribe,
|
||||
} from "@/new/photos/services/ml";
|
||||
import { getAutoCompleteSuggestions } from "@/new/photos/services/search";
|
||||
import type {
|
||||
City,
|
||||
SearchDateComponents,
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
} from "@/new/photos/services/search/types";
|
||||
import { labelForSuggestionType } from "@/new/photos/services/search/ui";
|
||||
import type { LocationTag } from "@/new/photos/services/user-entity";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import {
|
||||
FreeFlowText,
|
||||
SpaceBetweenFlex,
|
||||
@@ -38,15 +38,15 @@ import {
|
||||
Stack,
|
||||
styled,
|
||||
Typography,
|
||||
useTheme,
|
||||
type Theme,
|
||||
} from "@mui/material";
|
||||
import CollectionCard from "components/Collections/CollectionCard";
|
||||
import { ResultPreviewTile } from "components/Collections/styledComponents";
|
||||
import { t } from "i18next";
|
||||
import pDebounce from "p-debounce";
|
||||
import { AppContext } from "pages/_app";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -62,15 +62,29 @@ import {
|
||||
type StylesConfig,
|
||||
} from "react-select";
|
||||
import AsyncSelect from "react-select/async";
|
||||
import { getAutoCompleteSuggestions } from "services/searchService";
|
||||
import { Collection } from "types/collection";
|
||||
|
||||
interface SearchBarProps {
|
||||
/**
|
||||
* [Note: "Search mode"]
|
||||
*
|
||||
* On mobile sized screens, normally the search input areas is not
|
||||
* displayed. Clicking the search icon enters the "search mode", where we
|
||||
* show the search input area.
|
||||
*
|
||||
* On other screens, the search input is always shown even if we are not in
|
||||
* search mode.
|
||||
*
|
||||
* When we're in search mode,
|
||||
*
|
||||
* 1. Other icons from the navbar are hidden
|
||||
* 2. Next to the search input there is a cancel button to exit search mode.
|
||||
*/
|
||||
isInSearchMode: boolean;
|
||||
/**
|
||||
* Enter or exit "search mode".
|
||||
*/
|
||||
setIsInSearchMode: (v: boolean) => void;
|
||||
updateSearch: UpdateSearch;
|
||||
collections: Collection[];
|
||||
files: EnteFile[];
|
||||
}
|
||||
|
||||
export type UpdateSearch = (
|
||||
@@ -78,6 +92,21 @@ export type UpdateSearch = (
|
||||
summary: SearchResultSummary,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* The search bar is a styled "select" element that allow the user to type in
|
||||
* the attached input field, and shows a list of matching suggestions in a
|
||||
* dropdown.
|
||||
*
|
||||
* When the search input is empty, it shows some general information in the
|
||||
* dropdown instead (e.g. the ML indexing status).
|
||||
*
|
||||
* When the search input is not empty, it shows these {@link SearchSuggestion}s.
|
||||
* Alongside each suggestion is shows a count of matching files, and some
|
||||
* previews.
|
||||
*
|
||||
* Selecting one of the these suggestions causes the gallery to shows a filtered
|
||||
* list of files that match that suggestion.
|
||||
*/
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
setIsInSearchMode,
|
||||
isInSearchMode,
|
||||
@@ -92,11 +121,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
{isMobileWidth && !isInSearchMode ? (
|
||||
<MobileSearchArea onSearch={showSearchInput} />
|
||||
) : (
|
||||
<SearchInput
|
||||
{...props}
|
||||
isOpen={isInSearchMode}
|
||||
setIsOpen={setIsInSearchMode}
|
||||
/>
|
||||
<SearchInput {...props} isInSearchMode={isInSearchMode} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -116,22 +141,14 @@ const MobileSearchArea: React.FC<MobileSearchAreaProps> = ({ onSearch }) => (
|
||||
);
|
||||
|
||||
interface SearchInputProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (value: boolean) => void;
|
||||
isInSearchMode: boolean;
|
||||
updateSearch: UpdateSearch;
|
||||
files: EnteFile[];
|
||||
collections: Collection[];
|
||||
}
|
||||
|
||||
const SearchInput: React.FC<SearchInputProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
isInSearchMode,
|
||||
updateSearch,
|
||||
files,
|
||||
collections,
|
||||
}) => {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
// A ref to the top level Select.
|
||||
const selectRef = useRef(null);
|
||||
// The currently selected option.
|
||||
@@ -139,6 +156,10 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
// The contents of the input field associated with the select.
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const styles = useMemo(() => useSelectStyles(theme), [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
search(value);
|
||||
}, [value]);
|
||||
@@ -161,21 +182,14 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
if (isOpen) {
|
||||
appContext.startLoading();
|
||||
updateSearch(null, null);
|
||||
setTimeout(() => {
|
||||
appContext.finishLoading();
|
||||
}, 10);
|
||||
setIsOpen(false);
|
||||
setValue(null);
|
||||
setInputValue("");
|
||||
}
|
||||
updateSearch(null, null);
|
||||
setValue(null);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const getOptions = useCallback(
|
||||
pDebounce(getAutoCompleteSuggestions(files, collections), 250),
|
||||
[files, collections],
|
||||
pDebounce(getAutoCompleteSuggestions(), 250),
|
||||
[],
|
||||
);
|
||||
|
||||
const search = (selectedOption: SearchOption) => {
|
||||
@@ -188,19 +202,16 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
search = {
|
||||
date: selectedOption.value as SearchDateComponents,
|
||||
};
|
||||
setIsOpen(true);
|
||||
break;
|
||||
case SuggestionType.LOCATION:
|
||||
search = {
|
||||
location: selectedOption.value as LocationTag,
|
||||
};
|
||||
setIsOpen(true);
|
||||
break;
|
||||
case SuggestionType.CITY:
|
||||
search = {
|
||||
city: selectedOption.value as City,
|
||||
};
|
||||
setIsOpen(true);
|
||||
break;
|
||||
case SuggestionType.COLLECTION:
|
||||
search = { collection: selectedOption.value as number };
|
||||
@@ -242,6 +253,7 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
ref={selectRef}
|
||||
value={value}
|
||||
components={components}
|
||||
styles={styles}
|
||||
placeholder={t("search_hint")}
|
||||
loadOptions={getOptions}
|
||||
onChange={handleChange}
|
||||
@@ -250,7 +262,6 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
escapeClearsValue
|
||||
inputValue={inputValue}
|
||||
onInputChange={handleInputChange}
|
||||
styles={SelectStyles}
|
||||
noOptionsMessage={({ inputValue }) =>
|
||||
shouldShowEmptyState(inputValue) ? (
|
||||
<EmptyState onSelectCGroup={handleSelectCGroup} />
|
||||
@@ -258,7 +269,7 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
}
|
||||
/>
|
||||
|
||||
{isOpen && (
|
||||
{isInSearchMode && (
|
||||
<IconButton onClick={() => resetSearch()} sx={{ ml: 1 }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
@@ -277,27 +288,26 @@ const SearchInputWrapper = styled(Box)`
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const SelectStyles: StylesConfig<SearchOption, false> = {
|
||||
const useSelectStyles = ({
|
||||
colors,
|
||||
}: Theme): StylesConfig<SearchOption, false> => ({
|
||||
container: (style) => ({ ...style, flex: 1 }),
|
||||
control: (style, { isFocused }) => ({
|
||||
...style,
|
||||
// Give a solid background color.
|
||||
backgroundColor: "rgb(26, 26, 26)",
|
||||
borderColor: isFocused ? "#1DB954" : "transparent",
|
||||
backgroundColor: colors.background.elevated,
|
||||
borderColor: isFocused ? colors.accent.A500 : "transparent",
|
||||
boxShadow: "none",
|
||||
":hover": {
|
||||
borderColor: "#01DE4D",
|
||||
borderColor: colors.accent.A300,
|
||||
cursor: "text",
|
||||
},
|
||||
}),
|
||||
input: (styles) => ({ ...styles, color: "#fff" }),
|
||||
input: (styles) => ({ ...styles, color: colors.text.base }),
|
||||
menu: (style) => ({
|
||||
...style,
|
||||
// Suppress the default margin at the top.
|
||||
marginTop: "1px",
|
||||
// Same background color as the control (must be solid, otherwise the
|
||||
// content behind the menu shows through).
|
||||
backgroundColor: "rgb(26, 26, 26)",
|
||||
backgroundColor: colors.background.elevated,
|
||||
}),
|
||||
option: (style, { isFocused }) => ({
|
||||
...style,
|
||||
@@ -307,7 +317,7 @@ const SelectStyles: StylesConfig<SearchOption, false> = {
|
||||
cursor: "pointer",
|
||||
},
|
||||
"& .main": {
|
||||
backgroundColor: isFocused && "#202020",
|
||||
backgroundColor: isFocused && colors.background.elevated2,
|
||||
},
|
||||
"&:last-child .MuiDivider-root": {
|
||||
display: "none",
|
||||
@@ -315,14 +325,14 @@ const SelectStyles: StylesConfig<SearchOption, false> = {
|
||||
}),
|
||||
placeholder: (style) => ({
|
||||
...style,
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
color: colors.text.muted,
|
||||
whiteSpace: "nowrap",
|
||||
}),
|
||||
// Hide some things we don't need.
|
||||
dropdownIndicator: (style) => ({ ...style, display: "none" }),
|
||||
indicatorSeparator: (style) => ({ ...style, display: "none" }),
|
||||
clearIndicator: (style) => ({ ...style, display: "none" }),
|
||||
};
|
||||
});
|
||||
|
||||
const Control = ({ children, ...props }: ControlProps<SearchOption, false>) => (
|
||||
<SelectComponents.Control {...props}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { basename } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import type { CollectionMapping, Electron, ZipItem } from "@/base/types/ipc";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { exportMetadataDirectoryName } from "@/new/photos/services/export";
|
||||
import type {
|
||||
FileAndPath,
|
||||
@@ -33,8 +34,6 @@ import type {
|
||||
} from "services/upload/uploadManager";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import watcher from "services/watch";
|
||||
import { NotificationAttributes } from "types/Notification";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
CollectionSelectorIntent,
|
||||
SetCollectionSelectorAttributes,
|
||||
@@ -42,6 +41,7 @@ import {
|
||||
SetFiles,
|
||||
SetLoading,
|
||||
} from "types/gallery";
|
||||
import { NotificationAttributes } from "types/Notification";
|
||||
import { getOrCreateAlbum } from "utils/collection";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import log from "@/base/log";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import { styled } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { styled, useTheme } from "@mui/material";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import React, { useContext, useLayoutEffect, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { styled } from "@mui/material";
|
||||
import NumberAvatar from "@mui/material/Avatar";
|
||||
import { Collection } from "types/collection";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
const AvatarContainer = styled("div")({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SelectionBar } from "@/base/components/Navbar";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { FluidContainer } from "@ente/shared/components/Container";
|
||||
import ClockIcon from "@mui/icons-material/AccessTime";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
@@ -16,7 +17,6 @@ import { Box, IconButton, Stack, Tooltip } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
CollectionSelectorIntent,
|
||||
SetCollectionSelectorAttributes,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { stashRedirect } from "@/accounts/services/redirect";
|
||||
import { NavbarBase } from "@/base/components/Navbar";
|
||||
import { useIsMobileWidth } from "@/base/hooks";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { WhatsNew } from "@/new/photos/components/WhatsNew";
|
||||
import { shouldShowWhatsNew } from "@/new/photos/services/changelog";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
@@ -10,7 +11,10 @@ import {
|
||||
getLocalTrashedFiles,
|
||||
} from "@/new/photos/services/files";
|
||||
import { wipHasSwitchedOnceCmpAndSet } from "@/new/photos/services/ml";
|
||||
import { search, setSearchableFiles } from "@/new/photos/services/search";
|
||||
import {
|
||||
filterSearchableFiles,
|
||||
setSearchableData,
|
||||
} from "@/new/photos/services/search";
|
||||
import {
|
||||
SearchQuery,
|
||||
SearchResultSummary,
|
||||
@@ -105,7 +109,7 @@ import { sync, triggerPreFileInfoSync } from "services/sync";
|
||||
import { syncTrash } from "services/trashService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import { isTokenValid } from "services/userService";
|
||||
import { Collection, CollectionSummaries } from "types/collection";
|
||||
import { CollectionSummaries, CollectionSummaryType } from "types/collection";
|
||||
import {
|
||||
GalleryContextType,
|
||||
SelectedState,
|
||||
@@ -118,7 +122,6 @@ import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
COLLECTION_OPS_TYPE,
|
||||
CollectionSummaryType,
|
||||
HIDDEN_ITEMS_SECTION,
|
||||
TRASH_SECTION,
|
||||
constructCollectionNameMap,
|
||||
@@ -407,7 +410,14 @@ export default function Gallery() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => setSearchableFiles(files), [files]);
|
||||
useEffect(
|
||||
() =>
|
||||
setSearchableData({
|
||||
collections: collections ?? [],
|
||||
files: getUniqueFiles(files ?? []),
|
||||
}),
|
||||
[collections, files],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !files || !collections || !hiddenFiles || !trashedFiles) {
|
||||
@@ -526,7 +536,7 @@ export default function Gallery() {
|
||||
|
||||
let filteredFiles: EnteFile[] = [];
|
||||
if (isInSearchMode) {
|
||||
filteredFiles = getUniqueFiles(await search(searchQuery));
|
||||
filteredFiles = await filterSearchableFiles(searchQuery);
|
||||
} else {
|
||||
filteredFiles = getUniqueFiles(
|
||||
(isInHiddenSection ? hiddenFiles : files).filter((item) => {
|
||||
@@ -1089,8 +1099,6 @@ export default function Gallery() {
|
||||
isInSearchMode={isInSearchMode}
|
||||
setIsInSearchMode={setIsInSearchMode}
|
||||
updateSearch={updateSearch}
|
||||
collections={collections}
|
||||
files={files}
|
||||
/>
|
||||
)}
|
||||
</NavbarBase>
|
||||
@@ -1269,8 +1277,6 @@ interface NormalNavbarContentsProps {
|
||||
isInSearchMode: boolean;
|
||||
setIsInSearchMode: (v: boolean) => void;
|
||||
updateSearch: UpdateSearch;
|
||||
collections: Collection[];
|
||||
files: EnteFile[];
|
||||
}
|
||||
|
||||
const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
|
||||
@@ -1279,8 +1285,6 @@ const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
|
||||
isInSearchMode,
|
||||
setIsInSearchMode,
|
||||
updateSearch,
|
||||
collections,
|
||||
files,
|
||||
}) => (
|
||||
<>
|
||||
{!isInSearchMode && <SidebarButton onClick={openSidebar} />}
|
||||
@@ -1288,8 +1292,6 @@ const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
|
||||
isInSearchMode={isInSearchMode}
|
||||
setIsInSearchMode={setIsInSearchMode}
|
||||
updateSearch={updateSearch}
|
||||
collections={collections}
|
||||
files={files}
|
||||
/>
|
||||
{!isInSearchMode && <UploadButton onClick={openUploader} />}
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NavbarBase, SelectionBar } from "@/base/components/Navbar";
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import { useIsMobileWidth, useIsTouchscreen } from "@/base/hooks";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import { mergeMetadata } from "@/new/photos/utils/file";
|
||||
@@ -63,7 +64,6 @@ import {
|
||||
verifyPublicCollectionPassword,
|
||||
} from "services/publicCollectionService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
SelectedState,
|
||||
SetFilesDownloadProgressAttributes,
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { encryptMetadataJSON, sharedCryptoWorker } from "@/base/crypto";
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import {
|
||||
AddToCollectionRequest,
|
||||
Collection,
|
||||
CollectionMagicMetadata,
|
||||
CollectionMagicMetadataProps,
|
||||
CollectionPublicMagicMetadata,
|
||||
CollectionShareeMagicMetadata,
|
||||
CollectionToFileMap,
|
||||
CollectionType,
|
||||
CreatePublicAccessTokenRequest,
|
||||
EncryptedCollection,
|
||||
EncryptedFileKey,
|
||||
MoveToCollectionRequest,
|
||||
PublicURL,
|
||||
RemoveFromCollectionRequest,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { ItemVisibility } from "@/media/file-metadata";
|
||||
import { getLocalFiles } from "@/new/photos/services/files";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
@@ -19,23 +36,10 @@ import { getActualKey } from "@ente/shared/user";
|
||||
import type { User } from "@ente/shared/user/types";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
AddToCollectionRequest,
|
||||
Collection,
|
||||
CollectionFilesCount,
|
||||
CollectionMagicMetadata,
|
||||
CollectionMagicMetadataProps,
|
||||
CollectionPublicMagicMetadata,
|
||||
CollectionShareeMagicMetadata,
|
||||
CollectionSummaries,
|
||||
CollectionSummary,
|
||||
CollectionToFileMap,
|
||||
CreatePublicAccessTokenRequest,
|
||||
EncryptedCollection,
|
||||
EncryptedFileKey,
|
||||
MoveToCollectionRequest,
|
||||
PublicURL,
|
||||
RemoveFromCollectionRequest,
|
||||
UpdatePublicURL,
|
||||
CollectionSummaryType,
|
||||
} from "types/collection";
|
||||
import { FamilyData } from "types/user";
|
||||
import {
|
||||
@@ -43,8 +47,6 @@ import {
|
||||
ARCHIVE_SECTION,
|
||||
COLLECTION_LIST_SORT_BY,
|
||||
COLLECTION_SORT_ORDER,
|
||||
CollectionSummaryType,
|
||||
CollectionType,
|
||||
DUMMY_UNCATEGORIZED_COLLECTION,
|
||||
HIDDEN_ITEMS_SECTION,
|
||||
TRASH_SECTION,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ensureElectron } from "@/base/electron";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import {
|
||||
fileCreationPhotoDate,
|
||||
fileLocation,
|
||||
@@ -27,7 +28,6 @@ import QueueProcessor, {
|
||||
RequestCanceller,
|
||||
} from "@ente/shared/utils/queueProcessor";
|
||||
import i18n from "i18next";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
CollectionExportNames,
|
||||
ExportProgress,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ensureElectron } from "@/base/electron";
|
||||
import { nameAndExtension } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
@@ -17,7 +18,6 @@ import { wait } from "@/utils/promise";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import type { User } from "@ente/shared/user/types";
|
||||
import { getLocalCollections } from "services/collectionService";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
CollectionExportNames,
|
||||
ExportProgress,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { encryptMetadataJSON } from "@/base/crypto";
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import {
|
||||
clearCachedThumbnailsIfChanged,
|
||||
getLocalFiles,
|
||||
@@ -19,7 +20,6 @@ import { batch } from "@/utils/array";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import exportService from "services/export";
|
||||
import { Collection } from "types/collection";
|
||||
import { SetFiles } from "types/gallery";
|
||||
import { decryptFile, getLatestVersionFiles, sortFiles } from "utils/file";
|
||||
import {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import type {
|
||||
Collection,
|
||||
CollectionPublicMagicMetadata,
|
||||
} from "@/media/collection";
|
||||
import { EncryptedEnteFile, EnteFile } from "@/new/photos/types/file";
|
||||
import { mergeMetadata } from "@/new/photos/utils/file";
|
||||
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { Collection, CollectionPublicMagicMetadata } from "types/collection";
|
||||
import { LocalSavedPublicCollectionFiles } from "types/publicCollection";
|
||||
import { decryptFile, sortFiles } from "utils/file";
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import log from "@/base/log";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import { createSearchQuery, search } from "@/new/photos/services/search";
|
||||
import type {
|
||||
SearchDateComponents,
|
||||
SearchPerson,
|
||||
} from "@/new/photos/services/search/types";
|
||||
import {
|
||||
City,
|
||||
ClipSearchScores,
|
||||
SearchOption,
|
||||
SearchQuery,
|
||||
Suggestion,
|
||||
SuggestionType,
|
||||
} from "@/new/photos/services/search/types";
|
||||
import type { LocationTag } from "@/new/photos/services/user-entity";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import { Collection } from "types/collection";
|
||||
import { getUniqueFiles } from "utils/file";
|
||||
|
||||
// Suggestions shown in the search dropdown when the user has typed something.
|
||||
export const getAutoCompleteSuggestions =
|
||||
(files: EnteFile[], collections: Collection[]) =>
|
||||
async (searchPhrase: string): Promise<SearchOption[]> => {
|
||||
log.debug(() => ["getAutoCompleteSuggestions", { searchPhrase }]);
|
||||
try {
|
||||
const searchPhrase2 = searchPhrase.trim().toLowerCase();
|
||||
if (!searchPhrase2?.length) {
|
||||
return [];
|
||||
}
|
||||
const suggestions: Suggestion[] = [
|
||||
// The following functionality has moved to createSearchQuery
|
||||
// - getClipSuggestion(searchPhrase)
|
||||
// - getDateSuggestion(searchPhrase),
|
||||
// - getLocationSuggestion(searchPhrase),
|
||||
// - getFileTypeSuggestion(searchPhrase),
|
||||
...(await createSearchQuery(searchPhrase)),
|
||||
...getCollectionSuggestion(searchPhrase2, collections),
|
||||
getFileNameSuggestion(searchPhrase2, files),
|
||||
getFileCaptionSuggestion(searchPhrase2, files),
|
||||
].filter((suggestion) => !!suggestion);
|
||||
|
||||
return convertSuggestionsToOptions(suggestions);
|
||||
} catch (e) {
|
||||
log.error("getAutoCompleteSuggestions failed", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
async function convertSuggestionsToOptions(
|
||||
suggestions: Suggestion[],
|
||||
): Promise<SearchOption[]> {
|
||||
const previewImageAppendedOptions: SearchOption[] = [];
|
||||
for (const suggestion of suggestions) {
|
||||
const searchQuery = convertSuggestionToSearchQuery(suggestion);
|
||||
const resultFiles = getUniqueFiles(await search(searchQuery));
|
||||
if (searchQuery?.clip) {
|
||||
resultFiles.sort((a, b) => {
|
||||
const aScore = searchQuery.clip.get(a.id);
|
||||
const bScore = searchQuery.clip.get(b.id);
|
||||
return bScore - aScore;
|
||||
});
|
||||
}
|
||||
if (resultFiles.length) {
|
||||
previewImageAppendedOptions.push({
|
||||
...suggestion,
|
||||
fileCount: resultFiles.length,
|
||||
previewFiles: resultFiles.slice(0, 3),
|
||||
});
|
||||
}
|
||||
}
|
||||
return previewImageAppendedOptions;
|
||||
}
|
||||
|
||||
function getCollectionSuggestion(
|
||||
searchPhrase: string,
|
||||
collections: Collection[],
|
||||
): Suggestion[] {
|
||||
const collectionResults = searchCollection(searchPhrase, collections);
|
||||
|
||||
return collectionResults.map(
|
||||
(searchResult) =>
|
||||
({
|
||||
type: SuggestionType.COLLECTION,
|
||||
value: searchResult.id,
|
||||
label: searchResult.name,
|
||||
}) as Suggestion,
|
||||
);
|
||||
}
|
||||
|
||||
function getFileNameSuggestion(
|
||||
searchPhrase: string,
|
||||
files: EnteFile[],
|
||||
): Suggestion {
|
||||
const matchedFiles = searchFilesByName(searchPhrase, files);
|
||||
return {
|
||||
type: SuggestionType.FILE_NAME,
|
||||
value: matchedFiles.map((file) => file.id),
|
||||
label: searchPhrase,
|
||||
};
|
||||
}
|
||||
|
||||
function getFileCaptionSuggestion(
|
||||
searchPhrase: string,
|
||||
files: EnteFile[],
|
||||
): Suggestion {
|
||||
const matchedFiles = searchFilesByCaption(searchPhrase, files);
|
||||
return {
|
||||
type: SuggestionType.FILE_CAPTION,
|
||||
value: matchedFiles.map((file) => file.id),
|
||||
label: searchPhrase,
|
||||
};
|
||||
}
|
||||
|
||||
function searchCollection(
|
||||
searchPhrase: string,
|
||||
collections: Collection[],
|
||||
): Collection[] {
|
||||
return collections.filter((collection) =>
|
||||
collection.name.toLowerCase().includes(searchPhrase),
|
||||
);
|
||||
}
|
||||
|
||||
function searchFilesByName(searchPhrase: string, files: EnteFile[]) {
|
||||
return files.filter(
|
||||
(file) =>
|
||||
file.id.toString().includes(searchPhrase) ||
|
||||
file.metadata.title.toLowerCase().includes(searchPhrase),
|
||||
);
|
||||
}
|
||||
|
||||
function searchFilesByCaption(searchPhrase: string, files: EnteFile[]) {
|
||||
return files.filter(
|
||||
(file) =>
|
||||
file.pubMagicMetadata &&
|
||||
file.pubMagicMetadata.data.caption
|
||||
?.toLowerCase()
|
||||
.includes(searchPhrase),
|
||||
);
|
||||
}
|
||||
|
||||
function convertSuggestionToSearchQuery(option: Suggestion): SearchQuery {
|
||||
switch (option.type) {
|
||||
case SuggestionType.DATE:
|
||||
return {
|
||||
date: option.value as SearchDateComponents,
|
||||
};
|
||||
|
||||
case SuggestionType.LOCATION:
|
||||
return {
|
||||
location: option.value as LocationTag,
|
||||
};
|
||||
|
||||
case SuggestionType.CITY:
|
||||
return { city: option.value as City };
|
||||
|
||||
case SuggestionType.COLLECTION:
|
||||
return { collection: option.value as number };
|
||||
|
||||
case SuggestionType.FILE_NAME:
|
||||
return { files: option.value as number[] };
|
||||
|
||||
case SuggestionType.FILE_CAPTION:
|
||||
return { files: option.value as number[] };
|
||||
|
||||
case SuggestionType.PERSON:
|
||||
return { person: option.value as SearchPerson };
|
||||
|
||||
case SuggestionType.FILE_TYPE:
|
||||
return { fileType: option.value as FileType };
|
||||
|
||||
case SuggestionType.CLIP:
|
||||
return { clip: option.value as ClipSearchScores };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import {
|
||||
getLocalTrash,
|
||||
getTrashedFiles,
|
||||
@@ -9,7 +10,6 @@ import { EncryptedTrashItem, Trash } from "@/new/photos/types/file";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { Collection } from "types/collection";
|
||||
import { SetFiles } from "types/gallery";
|
||||
import { decryptFile } from "utils/file";
|
||||
import { getCollection } from "./collectionService";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { lowercaseExtension, nameAndExtension } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import type { Electron } from "@/base/types/ipc";
|
||||
import { ComlinkWorker } from "@/base/worker/comlink-worker";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import { potentialFileTypeFromExtension } from "@/media/live-photo";
|
||||
import { getLocalFiles } from "@/new/photos/services/files";
|
||||
@@ -26,7 +27,6 @@ import {
|
||||
} from "services/publicCollectionService";
|
||||
import { getDisableCFUploadProxyFlag } from "services/userService";
|
||||
import watcher from "services/watch";
|
||||
import { Collection } from "types/collection";
|
||||
import { SetFiles } from "types/gallery";
|
||||
import { decryptFile, getUserOwnedFiles, sortFiles } from "utils/file";
|
||||
import {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
FolderWatch,
|
||||
FolderWatchSyncedFile,
|
||||
} from "@/base/types/ipc";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { getLocalFiles } from "@/new/photos/services/files";
|
||||
import { UPLOAD_RESULT } from "@/new/photos/services/upload/types";
|
||||
import { EncryptedEnteFile } from "@/new/photos/types/file";
|
||||
@@ -19,7 +20,6 @@ import debounce from "debounce";
|
||||
import uploadManager, {
|
||||
type UploadItemWithCollection,
|
||||
} from "services/upload/uploadManager";
|
||||
import { Collection } from "types/collection";
|
||||
import { groupFilesBasedOnCollectionID } from "utils/file";
|
||||
import { removeFromCollection } from "./collectionService";
|
||||
|
||||
|
||||
33
web/apps/photos/src/types/collection.ts
Normal file
33
web/apps/photos/src/types/collection.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { EnteFile } from "@/new/photos/types/file";
|
||||
|
||||
export enum CollectionSummaryType {
|
||||
folder = "folder",
|
||||
favorites = "favorites",
|
||||
album = "album",
|
||||
archive = "archive",
|
||||
trash = "trash",
|
||||
uncategorized = "uncategorized",
|
||||
all = "all",
|
||||
outgoingShare = "outgoingShare",
|
||||
incomingShareViewer = "incomingShareViewer",
|
||||
incomingShareCollaborator = "incomingShareCollaborator",
|
||||
sharedOnlyViaLink = "sharedOnlyViaLink",
|
||||
archived = "archived",
|
||||
defaultHidden = "defaultHidden",
|
||||
hiddenItems = "hiddenItems",
|
||||
pinned = "pinned",
|
||||
}
|
||||
|
||||
export interface CollectionSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
type: CollectionSummaryType;
|
||||
coverFile: EnteFile;
|
||||
latestFile: EnteFile;
|
||||
fileCount: number;
|
||||
updationTime: number;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export type CollectionSummaries = Map<number, CollectionSummary>;
|
||||
export type CollectionFilesCount = Map<number, number>;
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import type { User } from "@ente/shared/user/types";
|
||||
import { CollectionSelectorAttributes } from "components/Collections/CollectionSelector";
|
||||
import { FilesDownloadProgressAttributes } from "components/FilesDownloadProgress";
|
||||
import { TimeStampListItem } from "components/PhotoList";
|
||||
import { Collection } from "types/collection";
|
||||
|
||||
export type SelectedState = {
|
||||
[k: number]: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PublicURL } from "@/media/collection";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import { TimeStampListItem } from "components/PhotoList";
|
||||
import { PublicURL } from "types/collection";
|
||||
|
||||
export interface PublicCollectionGalleryContextType {
|
||||
token: string;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ensureElectron } from "@/base/electron";
|
||||
import log from "@/base/log";
|
||||
import {
|
||||
COLLECTION_ROLE,
|
||||
type Collection,
|
||||
CollectionMagicMetadataProps,
|
||||
CollectionPublicMagicMetadataProps,
|
||||
CollectionType,
|
||||
} from "@/media/collection";
|
||||
import { ItemVisibility } from "@/media/file-metadata";
|
||||
import { getAllLocalFiles, getLocalFiles } from "@/new/photos/services/files";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
@@ -25,13 +32,7 @@ import {
|
||||
updatePublicCollectionMagicMetadata,
|
||||
updateSharedCollectionMagicMetadata,
|
||||
} from "services/collectionService";
|
||||
import {
|
||||
COLLECTION_ROLE,
|
||||
Collection,
|
||||
CollectionMagicMetadataProps,
|
||||
CollectionPublicMagicMetadataProps,
|
||||
CollectionSummaries,
|
||||
} from "types/collection";
|
||||
import { CollectionSummaries, CollectionSummaryType } from "types/collection";
|
||||
import { SetFilesDownloadProgressAttributes } from "types/gallery";
|
||||
import { downloadFilesWithProgress } from "utils/file";
|
||||
import { isArchivedCollection, updateMagicMetadata } from "utils/magicMetadata";
|
||||
@@ -42,30 +43,6 @@ export const DUMMY_UNCATEGORIZED_COLLECTION = -3;
|
||||
export const HIDDEN_ITEMS_SECTION = -4;
|
||||
export const ALL_SECTION = 0;
|
||||
|
||||
export enum CollectionType {
|
||||
folder = "folder",
|
||||
favorites = "favorites",
|
||||
album = "album",
|
||||
uncategorized = "uncategorized",
|
||||
}
|
||||
|
||||
export enum CollectionSummaryType {
|
||||
folder = "folder",
|
||||
favorites = "favorites",
|
||||
album = "album",
|
||||
archive = "archive",
|
||||
trash = "trash",
|
||||
uncategorized = "uncategorized",
|
||||
all = "all",
|
||||
outgoingShare = "outgoingShare",
|
||||
incomingShareViewer = "incomingShareViewer",
|
||||
incomingShareCollaborator = "incomingShareCollaborator",
|
||||
sharedOnlyViaLink = "sharedOnlyViaLink",
|
||||
archived = "archived",
|
||||
defaultHidden = "defaultHidden",
|
||||
hiddenItems = "hiddenItems",
|
||||
pinned = "pinned",
|
||||
}
|
||||
export enum COLLECTION_LIST_SORT_BY {
|
||||
NAME,
|
||||
CREATION_TIME_ASCENDING,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { ItemVisibility } from "@/media/file-metadata";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import { MagicMetadataCore } from "@/new/photos/types/magicMetadata";
|
||||
import { Collection } from "types/collection";
|
||||
|
||||
export function isArchivedFile(item: EnteFile): boolean {
|
||||
if (!item || !item.magicMetadata || !item.magicMetadata.data) {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { ItemVisibility } from "@/media/file-metadata";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import type { EnteFile } from "@/new/photos/types/file";
|
||||
import {
|
||||
EncryptedMagicMetadata,
|
||||
MagicMetadataCore,
|
||||
type EncryptedMagicMetadata,
|
||||
type MagicMetadataCore,
|
||||
SUB_TYPE,
|
||||
} from "@/new/photos/types/magicMetadata";
|
||||
import { CollectionSummaryType, CollectionType } from "utils/collection";
|
||||
|
||||
// TODO: Audit this file
|
||||
|
||||
export enum CollectionType {
|
||||
folder = "folder",
|
||||
favorites = "favorites",
|
||||
album = "album",
|
||||
uncategorized = "uncategorized",
|
||||
}
|
||||
|
||||
export enum COLLECTION_ROLE {
|
||||
VIEWER = "VIEWER",
|
||||
@@ -140,17 +148,3 @@ export interface CollectionPublicMagicMetadataProps {
|
||||
|
||||
export type CollectionPublicMagicMetadata =
|
||||
MagicMetadataCore<CollectionPublicMagicMetadataProps>;
|
||||
|
||||
export interface CollectionSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
type: CollectionSummaryType;
|
||||
coverFile: EnteFile;
|
||||
latestFile: EnteFile;
|
||||
fileCount: number;
|
||||
updationTime: number;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export type CollectionSummaries = Map<number, CollectionSummary>;
|
||||
export type CollectionFilesCount = Map<number, number>;
|
||||
@@ -1,17 +1,25 @@
|
||||
import { isDesktop } from "@/base/app";
|
||||
import log from "@/base/log";
|
||||
import { masterKeyFromSession } from "@/base/session-store";
|
||||
import { ComlinkWorker } from "@/base/worker/comlink-worker";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import type { LocationTag } from "@/new/photos/services/user-entity";
|
||||
import i18n, { t } from "i18next";
|
||||
import type { EnteFile } from "../../types/file";
|
||||
import { clipMatches, isMLEnabled } from "../ml";
|
||||
import {
|
||||
SuggestionType,
|
||||
type DateSearchResult,
|
||||
type LabelledFileType,
|
||||
type LocalizedSearchData,
|
||||
type SearchQuery,
|
||||
import type {
|
||||
City,
|
||||
ClipSearchScores,
|
||||
DateSearchResult,
|
||||
LabelledFileType,
|
||||
LocalizedSearchData,
|
||||
SearchableData,
|
||||
SearchDateComponents,
|
||||
SearchOption,
|
||||
SearchPerson,
|
||||
SearchQuery,
|
||||
Suggestion,
|
||||
} from "./types";
|
||||
import { SuggestionType } from "./types";
|
||||
import type { SearchWorker } from "./worker";
|
||||
|
||||
/**
|
||||
@@ -52,18 +60,18 @@ export const triggerSearchDataSync = () =>
|
||||
void worker().then((w) => masterKeyFromSession().then((k) => w.sync(k)));
|
||||
|
||||
/**
|
||||
* Set the files over which we will search.
|
||||
* Set the collections and files over which we should search.
|
||||
*/
|
||||
export const setSearchableFiles = (enteFiles: EnteFile[]) =>
|
||||
void worker().then((w) => w.setEnteFiles(enteFiles));
|
||||
export const setSearchableData = (data: SearchableData) =>
|
||||
void worker().then((w) => w.setSearchableData(data));
|
||||
|
||||
/**
|
||||
* Convert a search string into a reusable "search query" that can be passed on
|
||||
* to the {@link search} function.
|
||||
* Convert a search string into a suggestions that can be shown in the search
|
||||
* results, and can also be used filter the searchable files.
|
||||
*
|
||||
* @param searchString The string we want to search for.
|
||||
*/
|
||||
export const createSearchQuery = async (searchString: string) => {
|
||||
export const suggestionsForString = async (searchString: string) => {
|
||||
// Normalize it by trimming whitespace and converting to lowercase.
|
||||
const s = searchString.trim().toLowerCase();
|
||||
if (s.length == 0) return [];
|
||||
@@ -73,7 +81,9 @@ export const createSearchQuery = async (searchString: string) => {
|
||||
// the search worker, then combine the two.
|
||||
const results = await Promise.all([
|
||||
clipSuggestions(s, searchString).then((s) => s ?? []),
|
||||
worker().then((w) => w.createSearchQuery(s, localizedSearchData())),
|
||||
worker().then((w) =>
|
||||
w.suggestionsForString(s, searchString, localizedSearchData()),
|
||||
),
|
||||
]);
|
||||
return results.flat();
|
||||
};
|
||||
@@ -92,11 +102,11 @@ const clipSuggestions = async (s: string, searchString: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for and return the list of {@link EnteFile}s that match the given
|
||||
* {@link search} query.
|
||||
* Return the list of {@link EnteFile}s (from amongst the previously set
|
||||
* {@link SearchableData}) that match the given search {@link suggestion}.
|
||||
*/
|
||||
export const search = async (search: SearchQuery) =>
|
||||
worker().then((w) => w.search(search));
|
||||
export const filterSearchableFiles = async (suggestion: SearchQuery) =>
|
||||
worker().then((w) => w.filterSearchableFiles(suggestion));
|
||||
|
||||
/**
|
||||
* Cached value of {@link localizedSearchData}.
|
||||
@@ -149,3 +159,81 @@ const labelledFileTypes = (): LabelledFileType[] => [
|
||||
{ fileType: FileType.video, label: t("VIDEO") },
|
||||
{ fileType: FileType.livePhoto, label: t("LIVE_PHOTO") },
|
||||
];
|
||||
|
||||
// TODO-Cluster -- AUDIT BELOW THIS
|
||||
|
||||
// Suggestions shown in the search dropdown when the user has typed something.
|
||||
export const getAutoCompleteSuggestions =
|
||||
() =>
|
||||
async (searchPhrase: string): Promise<SearchOption[]> => {
|
||||
log.debug(() => ["getAutoCompleteSuggestions"]);
|
||||
try {
|
||||
const suggestions: Suggestion[] =
|
||||
await suggestionsForString(searchPhrase);
|
||||
return convertSuggestionsToOptions(suggestions);
|
||||
} catch (e) {
|
||||
log.error("getAutoCompleteSuggestions failed", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
async function convertSuggestionsToOptions(
|
||||
suggestions: Suggestion[],
|
||||
): Promise<SearchOption[]> {
|
||||
const previewImageAppendedOptions: SearchOption[] = [];
|
||||
for (const suggestion of suggestions) {
|
||||
const searchQuery = convertSuggestionToSearchQuery(suggestion);
|
||||
const resultFiles = await filterSearchableFiles(searchQuery);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (searchQuery?.clip) {
|
||||
resultFiles.sort((a, b) => {
|
||||
const aScore = searchQuery.clip?.get(a.id) ?? 0;
|
||||
const bScore = searchQuery.clip?.get(b.id) ?? 0;
|
||||
return bScore - aScore;
|
||||
});
|
||||
}
|
||||
if (resultFiles.length) {
|
||||
previewImageAppendedOptions.push({
|
||||
...suggestion,
|
||||
fileCount: resultFiles.length,
|
||||
previewFiles: resultFiles.slice(0, 3),
|
||||
});
|
||||
}
|
||||
}
|
||||
return previewImageAppendedOptions;
|
||||
}
|
||||
|
||||
function convertSuggestionToSearchQuery(option: Suggestion): SearchQuery {
|
||||
switch (option.type) {
|
||||
case SuggestionType.DATE:
|
||||
return {
|
||||
date: option.value as SearchDateComponents,
|
||||
};
|
||||
|
||||
case SuggestionType.LOCATION:
|
||||
return {
|
||||
location: option.value as LocationTag,
|
||||
};
|
||||
|
||||
case SuggestionType.CITY:
|
||||
return { city: option.value as City };
|
||||
|
||||
case SuggestionType.COLLECTION:
|
||||
return { collection: option.value as number };
|
||||
|
||||
case SuggestionType.FILE_NAME:
|
||||
return { files: option.value as number[] };
|
||||
|
||||
case SuggestionType.FILE_CAPTION:
|
||||
return { files: option.value as number[] };
|
||||
|
||||
case SuggestionType.PERSON:
|
||||
return { person: option.value as SearchPerson };
|
||||
|
||||
case SuggestionType.FILE_TYPE:
|
||||
return { fileType: option.value as FileType };
|
||||
|
||||
case SuggestionType.CLIP:
|
||||
return { clip: option.value as ClipSearchScores };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
*/
|
||||
|
||||
import type { Location } from "@/base/types";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import type { EnteFile } from "@/new/photos/types/file";
|
||||
import type { LocationTag } from "../user-entity";
|
||||
|
||||
/**
|
||||
* The base data over which we should search.
|
||||
*/
|
||||
export interface SearchableData {
|
||||
collections: Collection[];
|
||||
files: EnteFile[];
|
||||
}
|
||||
|
||||
export interface DateSearchResult {
|
||||
components: SearchDateComponents;
|
||||
label: string;
|
||||
@@ -125,6 +134,7 @@ export interface Suggestion {
|
||||
}
|
||||
|
||||
export interface SearchQuery {
|
||||
suggestion?: SearchSuggestion;
|
||||
date?: SearchDateComponents;
|
||||
location?: LocationTag;
|
||||
city?: City;
|
||||
@@ -140,8 +150,23 @@ export interface SearchResultSummary {
|
||||
fileCount: number;
|
||||
}
|
||||
|
||||
export type SearchSuggestion = { label: string } & (
|
||||
| { type: "collection"; collectionID: number }
|
||||
| { type: "files"; fileIDs: number[] }
|
||||
| { type: "fileType"; fileType: FileType }
|
||||
| { type: "date"; dateComponents: SearchDateComponents }
|
||||
| { type: "location"; locationTag: LocationTag }
|
||||
| { type: "city"; city: City }
|
||||
| { type: "clip"; clipScoreForFileID: Map<number, number> }
|
||||
| { type: "cgroup"; cgroup: SearchPerson }
|
||||
);
|
||||
|
||||
/**
|
||||
* An option shown in the the search bar's select dropdown.
|
||||
*
|
||||
* The option includes essential data that is necessary to show a corresponding
|
||||
* entry in the dropdown. If the user selects the option, then we will re-run
|
||||
* the search, using the data to filter the list of files shown to the user.
|
||||
*/
|
||||
export interface SearchOption extends Suggestion {
|
||||
fileCount: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HTTPError } from "@/base/http";
|
||||
import type { Location } from "@/base/types";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { fileCreationPhotoDate, fileLocation } from "@/media/file-metadata";
|
||||
import type { EnteFile } from "@/new/photos/types/file";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
@@ -19,6 +20,7 @@ import type {
|
||||
LabelledFileType,
|
||||
LocalizedSearchData,
|
||||
Searchable,
|
||||
SearchableData,
|
||||
SearchDateComponents,
|
||||
SearchQuery,
|
||||
Suggestion,
|
||||
@@ -30,7 +32,7 @@ import { SuggestionType } from "./types";
|
||||
* remains responsive.
|
||||
*/
|
||||
export class SearchWorker {
|
||||
private enteFiles: EnteFile[] = [];
|
||||
private searchableData: SearchableData = { collections: [], files: [] };
|
||||
private locationTags: Searchable<LocationTag>[] = [];
|
||||
private cities: Searchable<City>[] = [];
|
||||
|
||||
@@ -60,18 +62,24 @@ export class SearchWorker {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the files that we should search across.
|
||||
* Set the data that we should search across.
|
||||
*/
|
||||
setEnteFiles(enteFiles: EnteFile[]) {
|
||||
this.enteFiles = enteFiles;
|
||||
setSearchableData(data: SearchableData) {
|
||||
this.searchableData = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a search string into a reusable query.
|
||||
* Convert a search string into a list of {@link SearchSuggestion}s.
|
||||
*/
|
||||
createSearchQuery(s: string, localizedSearchData: LocalizedSearchData) {
|
||||
return createSearchQuery(
|
||||
suggestionsForString(
|
||||
s: string,
|
||||
searchString: string,
|
||||
localizedSearchData: LocalizedSearchData,
|
||||
) {
|
||||
return suggestionsForString(
|
||||
s,
|
||||
searchString,
|
||||
this.searchableData,
|
||||
localizedSearchData,
|
||||
this.locationTags,
|
||||
this.cities,
|
||||
@@ -79,27 +87,73 @@ export class SearchWorker {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link EnteFile}s that satisfy the given {@link searchQuery}.
|
||||
* Return {@link EnteFile}s that satisfy the given {@link suggestion}.
|
||||
*/
|
||||
search(searchQuery: SearchQuery) {
|
||||
return this.enteFiles.filter((f) => isMatchingFile(f, searchQuery));
|
||||
filterSearchableFiles(suggestion: SearchQuery) {
|
||||
return this.searchableData.files.filter((f) =>
|
||||
isMatchingFile(f, suggestion),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
expose(SearchWorker);
|
||||
|
||||
const createSearchQuery = (
|
||||
/**
|
||||
* @param s The normalized form of {@link searchString}.
|
||||
* @param searchString The original search string.
|
||||
*/
|
||||
const suggestionsForString = (
|
||||
s: string,
|
||||
searchString: string,
|
||||
{ collections, files }: SearchableData,
|
||||
{ locale, holidays, labelledFileTypes }: LocalizedSearchData,
|
||||
locationTags: Searchable<LocationTag>[],
|
||||
cities: Searchable<City>[],
|
||||
): Suggestion[] =>
|
||||
[
|
||||
fileTypeSuggestions(s, labelledFileTypes),
|
||||
dateSuggestions(s, locale, holidays),
|
||||
locationSuggestions(s, locationTags, cities),
|
||||
fileTypeSuggestions(s, labelledFileTypes),
|
||||
collectionSuggestions(s, collections),
|
||||
suggestionForFiles(fileNameMatches(s, files), searchString),
|
||||
suggestionForFiles(fileCaptionMatches(s, files), searchString),
|
||||
].flat();
|
||||
|
||||
const collectionSuggestions = (s: string, collections: Collection[]) =>
|
||||
collections
|
||||
.filter(({ name }) => name.toLowerCase().includes(s))
|
||||
.map(({ id, name }) => ({
|
||||
type: SuggestionType.COLLECTION,
|
||||
value: id,
|
||||
label: name,
|
||||
}));
|
||||
|
||||
const fileNameMatches = (s: string, files: EnteFile[]) => {
|
||||
// Convert the search string to a number. This allows searching a file by
|
||||
// its exact (integral) ID.
|
||||
const sn = Number(s) || undefined;
|
||||
|
||||
return files.filter(
|
||||
({ id, metadata }) =>
|
||||
id === sn || metadata.title.toLowerCase().includes(s),
|
||||
);
|
||||
};
|
||||
|
||||
const suggestionForFiles = (matchingFiles: EnteFile[], searchString: string) =>
|
||||
matchingFiles.length
|
||||
? {
|
||||
type: SuggestionType.FILE_NAME,
|
||||
value: matchingFiles.map((f) => f.id),
|
||||
label: searchString,
|
||||
}
|
||||
: [];
|
||||
|
||||
const fileCaptionMatches = (s: string, files: EnteFile[]) =>
|
||||
files.filter((file) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
file.pubMagicMetadata?.data?.caption?.toLowerCase().includes(s),
|
||||
);
|
||||
|
||||
const dateSuggestions = (
|
||||
s: string,
|
||||
locale: string,
|
||||
|
||||
Reference in New Issue
Block a user