diff --git a/web/apps/photos/src/components/Search/SearchBar/index.tsx b/web/apps/photos/src/components/Search/SearchBar/index.tsx index 8d7612d4b5..fa8929e4e6 100644 --- a/web/apps/photos/src/components/Search/SearchBar/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/index.tsx @@ -1,8 +1,8 @@ import { Collection } from "types/collection"; import { SearchBarMobile } from "./searchBarMobile"; +import { UpdateSearch } from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; -import { UpdateSearch } from "types/search"; import SearchInput from "./searchInput"; import { SearchBarWrapper } from "./styledComponents"; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx index 25aa944d8e..4d924e7c1b 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx @@ -1,10 +1,10 @@ import { PeopleList } from "@/new/photos/components/PeopleList"; import { isMLEnabled } from "@/new/photos/services/ml"; +import { Suggestion, SuggestionType } from "@/new/photos/services/search/types"; import { Row } from "@ente/shared/components/Container"; import { Box, styled } from "@mui/material"; import { t } from "i18next"; import { components } from "react-select"; -import { Suggestion, SuggestionType } from "types/search"; const { Menu } = components; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index 18b7289915..0f85391ad0 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -1,9 +1,18 @@ import { FileType } from "@/media/file-type"; import { isMLEnabled } from "@/new/photos/services/ml"; import type { + City, + LocationTagData, SearchDateComponents, SearchPerson, } from "@/new/photos/services/search/types"; +import { + ClipSearchScores, + SearchOption, + SearchQuery, + SuggestionType, + UpdateSearch, +} from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; import CloseIcon from "@mui/icons-material/Close"; import { IconButton } from "@mui/material"; @@ -15,20 +24,11 @@ import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { components } from "react-select"; import AsyncSelect from "react-select/async"; import { InputActionMeta } from "react-select/src/types"; -import { City } from "services/locationSearchService"; import { getAutoCompleteSuggestions, getDefaultOptions, } from "services/searchService"; import { Collection } from "types/collection"; -import { LocationTagData } from "types/entity"; -import { - ClipSearchScores, - Search, - SearchOption, - SuggestionType, - UpdateSearch, -} from "types/search"; import { SelectStyles } from "../../../../styles/search"; import { SearchInputWrapper } from "../styledComponents"; import MenuWithPeople from "./MenuWithPeople"; @@ -116,7 +116,7 @@ export default function SearchInput(props: Iprops) { if (!selectedOption) { return; } - let search: Search; + let search: SearchQuery; switch (selectedOption.type) { case SuggestionType.DATE: search = { diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx index 8e3fd7d842..62a8d72b4b 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx @@ -1,3 +1,4 @@ +import { SearchOption } from "@/new/photos/services/search/types"; import { FreeFlowText, SpaceBetweenFlex, @@ -6,7 +7,6 @@ import { Box, Divider, Stack, Typography } from "@mui/material"; import CollectionCard from "components/Collections/CollectionCard"; import { ResultPreviewTile } from "components/Collections/styledComponents"; import { t } from "i18next"; -import { SearchOption } from "types/search"; import { components } from "react-select"; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx index de9558777b..75529a925f 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx @@ -1,3 +1,7 @@ +import { + SearchOption, + SuggestionType, +} from "@/new/photos/services/search/types"; import { FlexWrapper } from "@ente/shared/components/Container"; import CalendarIcon from "@mui/icons-material/CalendarMonth"; import FolderIcon from "@mui/icons-material/Folder"; @@ -7,7 +11,6 @@ import SearchIcon from "@mui/icons-material/SearchOutlined"; import { Box } from "@mui/material"; import { components } from "react-select"; import { SelectComponents } from "react-select/src/components"; -import { SearchOption, SuggestionType } from "types/search"; const { ValueContainer } = components; diff --git a/web/apps/photos/src/components/Search/SearchResultInfo.tsx b/web/apps/photos/src/components/Search/SearchResultInfo.tsx index 7d99697bf1..96d610f6b1 100644 --- a/web/apps/photos/src/components/Search/SearchResultInfo.tsx +++ b/web/apps/photos/src/components/Search/SearchResultInfo.tsx @@ -1,8 +1,8 @@ +import { SearchResultSummary } from "@/new/photos/services/search/types"; import { Typography } from "@mui/material"; import { CollectionInfo } from "components/Collections/CollectionInfo"; import { CollectionInfoBarWrapper } from "components/Collections/styledComponents"; import { t } from "i18next"; -import { SearchResultSummary } from "types/search"; interface Iprops { searchResultSummary: SearchResultSummary; diff --git a/web/apps/photos/src/components/pages/gallery/Navbar.tsx b/web/apps/photos/src/components/pages/gallery/Navbar.tsx index a2cf98b4e3..dfb8a6339c 100644 --- a/web/apps/photos/src/components/pages/gallery/Navbar.tsx +++ b/web/apps/photos/src/components/pages/gallery/Navbar.tsx @@ -1,4 +1,5 @@ import { NavbarBase } from "@/base/components/Navbar"; +import { UpdateSearch } from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; import { FlexWrapper, HorizontalFlex } from "@ente/shared/components/Container"; import ArrowBack from "@mui/icons-material/ArrowBack"; @@ -8,7 +9,6 @@ import SearchBar from "components/Search/SearchBar"; import UploadButton from "components/Upload/UploadButton"; import { t } from "i18next"; import { Collection } from "types/collection"; -import { UpdateSearch } from "types/search"; interface Iprops { openSidebar: () => void; diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 53acd2e386..3cee4ec6ec 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -4,13 +4,13 @@ import { CustomHead } from "@/base/components/Head"; import { AppNavbar } from "@/base/components/Navbar"; import { setupI18n } from "@/base/i18n"; import log from "@/base/log"; -import { runMigrations } from "@/new/photos/services/migrations"; import { logStartupBanner, logUnhandledErrorsAndRejections, } from "@/base/log-web"; import { AppUpdate } from "@/base/types/ipc"; import DownloadManager from "@/new/photos/services/download"; +import { runMigrations } from "@/new/photos/services/migrations"; import { initML, isMLSupported } from "@/new/photos/services/ml"; import { ensure } from "@/utils/ensure"; import { Overlay } from "@ente/shared/components/Container"; diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 74273eb014..cd524d3f1e 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -7,6 +7,12 @@ import { getLocalTrashedFiles, } from "@/new/photos/services/files"; import { wipHasSwitchedOnceCmpAndSet } from "@/new/photos/services/ml"; +import { search, setSearchableFiles } from "@/new/photos/services/search"; +import { + SearchQuery, + SearchResultSummary, + UpdateSearch, +} from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; import { mergeMetadata } from "@/new/photos/utils/file"; import { CenteredFlex } from "@ente/shared/components/Container"; @@ -31,7 +37,6 @@ import { getKey, } from "@ente/shared/storage/sessionStorage"; import type { User } from "@ente/shared/user/types"; -import { isPromise } from "@ente/shared/utils"; import { Typography, styled } from "@mui/material"; import AuthenticateUserModal from "components/AuthenticateUserModal"; import Collections from "components/Collections"; @@ -106,7 +111,6 @@ import { SetFilesDownloadProgressAttributes, SetFilesDownloadProgressAttributesCreator, } from "types/gallery"; -import { Search, SearchResultSummary, UpdateSearch } from "types/search"; import { FamilyData } from "types/user"; import { checkSubscriptionPurchase } from "utils/billing"; import { @@ -119,7 +123,6 @@ import { hasNonSystemCollections, splitNormalAndHiddenCollections, } from "utils/collection"; -import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker"; import { FILE_OPS_TYPE, constructFileToCollectionMap, @@ -193,7 +196,7 @@ export default function Gallery() { const [collectionNamerAttributes, setCollectionNamerAttributes] = useState(null); const [collectionNamerView, setCollectionNamerView] = useState(false); - const [search, setSearch] = useState(null); + const [searchQuery, setSearchQuery] = useState(null); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false); // TODO(MR): This is never true currently, this is the WIP ability to show @@ -400,13 +403,7 @@ export default function Gallery() { }; }, []); - useEffectSingleThreaded( - async ([files]: [files: EnteFile[]]) => { - const searchWorker = await ComlinkSearchWorker.getInstance(); - await searchWorker.setFiles(files); - }, - [files], - ); + useEffect(() => setSearchableFiles(files), [files]); useEffect(() => { if (!user || !files || !collections || !hiddenFiles || !trashedFiles) { @@ -523,11 +520,9 @@ export default function Gallery() { ]); } - const searchWorker = await ComlinkSearchWorker.getInstance(); - let filteredFiles: EnteFile[] = []; if (isInSearchMode) { - filteredFiles = getUniqueFiles(await searchWorker.search(search)); + filteredFiles = getUniqueFiles(await search(searchQuery)); } else { filteredFiles = getUniqueFiles( (isInHiddenSection ? hiddenFiles : files).filter((item) => { @@ -587,9 +582,9 @@ export default function Gallery() { }), ); } - if (search?.clip) { + if (searchQuery?.clip) { return filteredFiles.sort((a, b) => { - return search.clip.get(b.id) - search.clip.get(a.id); + return searchQuery.clip.get(b.id) - searchQuery.clip.get(a.id); }); } const sortAsc = activeCollection?.pubMagicMetadata?.data?.asc ?? false; @@ -605,7 +600,7 @@ export default function Gallery() { tempDeletedFileIds, tempHiddenFileIds, hiddenFileIds, - search, + searchQuery, activeCollectionID, archivedCollections, ]); @@ -975,7 +970,7 @@ export default function Gallery() { if (newSearch?.collection) { setActiveCollectionID(newSearch?.collection); } else { - setSearch(newSearch); + setSearchQuery(newSearch); } setIsClipSearchResult(!!newSearch?.clip); if (!newSearch?.collection) { @@ -1243,37 +1238,6 @@ export default function Gallery() { ); } -// useEffectSingleThreaded is a useEffect that will only run one at a time, and will -// caches the latest deps of requests that come in while it is running, and will -// run that after the current run is complete. -function useEffectSingleThreaded( - fn: (deps) => void | Promise, - deps: any[], -): void { - const updateInProgress = useRef(false); - const nextRequestDepsRef = useRef(null); - useEffect(() => { - const main = async (deps) => { - if (updateInProgress.current) { - nextRequestDepsRef.current = deps; - return; - } - updateInProgress.current = true; - const result = fn(deps); - if (isPromise(result)) { - await result; - } - updateInProgress.current = false; - if (nextRequestDepsRef.current) { - const deps = nextRequestDepsRef.current; - nextRequestDepsRef.current = null; - setTimeout(() => main(deps), 0); - } - }; - main(deps); - }, deps); -} - /** * Preload all three variants of a responsive image. */ diff --git a/web/apps/photos/src/services/heic-convert.ts b/web/apps/photos/src/services/heic-convert.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/web/apps/photos/src/services/locationSearchService.ts b/web/apps/photos/src/services/locationSearchService.ts index 30a36e5e04..9e3ff032d3 100644 --- a/web/apps/photos/src/services/locationSearchService.ts +++ b/web/apps/photos/src/services/locationSearchService.ts @@ -1,15 +1,5 @@ import log from "@/base/log"; -import type { Location, LocationTagData } from "types/entity"; - -export interface City { - city: string; - country: string; - lat: number; - lng: number; -} - -const DEFAULT_CITY_RADIUS = 10; -const KMS_PER_DEGREE = 111.16; +import type { City } from "@/new/photos/services/search/types"; class LocationSearchService { private cities: Array = []; @@ -53,45 +43,3 @@ class LocationSearchService { } export default new LocationSearchService(); - -export function isInsideLocationTag( - location: Location, - locationTag: LocationTagData, -) { - return isLocationCloseToPoint( - location, - locationTag.centerPoint, - locationTag.radius, - ); -} - -export function isInsideCity(location: Location, city: City) { - return isLocationCloseToPoint( - { latitude: city.lat, longitude: city.lng }, - location, - DEFAULT_CITY_RADIUS, - ); -} - -function isLocationCloseToPoint( - centerPoint: Location, - location: Location, - radius: number, -) { - const a = (radius * _scaleFactor(centerPoint.latitude)) / KMS_PER_DEGREE; - const b = radius / KMS_PER_DEGREE; - const x = centerPoint.latitude - location.latitude; - const y = centerPoint.longitude - location.longitude; - if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) { - return true; - } - return false; -} - -///The area bounded by the location tag becomes more elliptical with increase -///in the magnitude of the latitude on the caritesian plane. When latitude is -///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases, -///the major axis (a) has to be scaled by the secant of the latitude. -function _scaleFactor(lat: number) { - return 1 / Math.cos(lat * (Math.PI / 180)); -} diff --git a/web/apps/photos/src/services/searchService.ts b/web/apps/photos/src/services/searchService.ts index 76b1d5cb2b..cf50458926 100644 --- a/web/apps/photos/src/services/searchService.ts +++ b/web/apps/photos/src/services/searchService.ts @@ -8,26 +8,27 @@ import { mlStatusSnapshot, wipSearchPersons, } from "@/new/photos/services/ml"; -import { parseDateComponents } from "@/new/photos/services/search"; +import { parseDateComponents, search } from "@/new/photos/services/search"; import type { SearchDateComponents, SearchPerson, } from "@/new/photos/services/search/types"; +import { + City, + ClipSearchScores, + LocationTagData, + SearchOption, + SearchQuery, + Suggestion, + SuggestionType, +} from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; import { t } from "i18next"; import { Collection } from "types/collection"; -import { EntityType, LocationTag, LocationTagData } from "types/entity"; -import { - ClipSearchScores, - Search, - SearchOption, - Suggestion, - SuggestionType, -} from "types/search"; -import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker"; +import { EntityType, LocationTag } from "types/entity"; import { getUniqueFiles } from "utils/file"; import { getLatestEntities } from "./entityService"; -import locationSearchService, { City } from "./locationSearchService"; +import locationSearchService from "./locationSearchService"; export const getDefaultOptions = async () => { return [ @@ -64,13 +65,10 @@ export const getAutoCompleteSuggestions = async function convertSuggestionsToOptions( suggestions: Suggestion[], ): Promise { - const searchWorker = await ComlinkSearchWorker.getInstance(); const previewImageAppendedOptions: SearchOption[] = []; for (const suggestion of suggestions) { const searchQuery = convertSuggestionToSearchQuery(suggestion); - const resultFiles = getUniqueFiles( - await searchWorker.search(searchQuery), - ); + const resultFiles = getUniqueFiles(await search(searchQuery)); if (searchQuery?.clip) { resultFiles.sort((a, b) => { const aScore = searchQuery.clip.get(a.id); @@ -304,7 +302,7 @@ const searchClip = async ( return matches; }; -function convertSuggestionToSearchQuery(option: Suggestion): Search { +function convertSuggestionToSearchQuery(option: Suggestion): SearchQuery { switch (option.type) { case SuggestionType.DATE: return { diff --git a/web/apps/photos/src/types/entity.ts b/web/apps/photos/src/types/entity.ts index 4371562cc8..f22a7bac8a 100644 --- a/web/apps/photos/src/types/entity.ts +++ b/web/apps/photos/src/types/entity.ts @@ -1,3 +1,5 @@ +import type { LocationTagData } from "@/new/photos/services/search/types"; + export enum EntityType { LOCATION_TAG = "location", } @@ -25,19 +27,6 @@ export interface EncryptedEntity { userID: number; } -export interface Location { - latitude: number | null; - longitude: number | null; -} - -export interface LocationTagData { - name: string; - radius: number; - aSquare: number; - bSquare: number; - centerPoint: Location; -} - export type LocationTag = Entity; export interface Entity diff --git a/web/apps/photos/src/types/search/index.ts b/web/apps/photos/src/types/search/index.ts index 4c0b901655..e69de29bb2 100644 --- a/web/apps/photos/src/types/search/index.ts +++ b/web/apps/photos/src/types/search/index.ts @@ -1,65 +0,0 @@ -import { FileType } from "@/media/file-type"; -import type { MLStatus } from "@/new/photos/services/ml"; -import type { - SearchDateComponents, - SearchPerson, -} from "@/new/photos/services/search/types"; -import { EnteFile } from "@/new/photos/types/file"; -import { City } from "services/locationSearchService"; -import { LocationTagData } from "types/entity"; - -export enum SuggestionType { - DATE = "DATE", - LOCATION = "LOCATION", - COLLECTION = "COLLECTION", - FILE_NAME = "FILE_NAME", - PERSON = "PERSON", - INDEX_STATUS = "INDEX_STATUS", - FILE_CAPTION = "FILE_CAPTION", - FILE_TYPE = "FILE_TYPE", - CLIP = "CLIP", - CITY = "CITY", -} - -export interface Suggestion { - type: SuggestionType; - label: string; - value: - | SearchDateComponents - | number[] - | SearchPerson - | MLStatus - | LocationTagData - | City - | FileType - | ClipSearchScores; - hide?: boolean; -} - -export type Search = { - date?: SearchDateComponents; - location?: LocationTagData; - city?: City; - collection?: number; - files?: number[]; - person?: SearchPerson; - fileType?: FileType; - clip?: ClipSearchScores; -}; - -export type SearchResultSummary = { - optionName: string; - fileCount: number; -}; - -export interface SearchOption extends Suggestion { - fileCount: number; - previewFiles: EnteFile[]; -} - -export type UpdateSearch = ( - search: Search, - summary: SearchResultSummary, -) => void; - -export type ClipSearchScores = Map; diff --git a/web/apps/photos/src/utils/comlink/ComlinkSearchWorker.ts b/web/apps/photos/src/utils/comlink/ComlinkSearchWorker.ts deleted file mode 100644 index 7ba6a67bf3..0000000000 --- a/web/apps/photos/src/utils/comlink/ComlinkSearchWorker.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { haveWindow } from "@/base/env"; -import { ComlinkWorker } from "@/base/worker/comlink-worker"; -import type { Remote } from "comlink"; -import { type DedicatedSearchWorker } from "worker/search.worker"; - -class ComlinkSearchWorker { - private comlinkWorkerInstance: Remote; - private comlinkWorker: ComlinkWorker; - - async getInstance() { - if (!this.comlinkWorkerInstance) { - if (!this.comlinkWorker) - this.comlinkWorker = getDedicatedSearchWorker(); - this.comlinkWorkerInstance = await this.comlinkWorker.remote; - } - return this.comlinkWorkerInstance; - } -} - -export const getDedicatedSearchWorker = () => { - if (haveWindow()) { - const cryptoComlinkWorker = new ComlinkWorker< - typeof DedicatedSearchWorker - >( - "ente-search-worker", - new Worker(new URL("worker/search.worker.ts", import.meta.url)), - ); - return cryptoComlinkWorker; - } -}; - -export default new ComlinkSearchWorker(); diff --git a/web/apps/photos/src/worker/search.worker.ts b/web/apps/photos/src/worker/search.worker.ts deleted file mode 100644 index 1100e3c223..0000000000 --- a/web/apps/photos/src/worker/search.worker.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { getUICreationDate } from "@/media/file-metadata"; -import type { SearchDateComponents } from "@/new/photos/services/search/types"; -import { EnteFile } from "@/new/photos/types/file"; -import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata"; -import * as Comlink from "comlink"; -import { - isInsideCity, - isInsideLocationTag, -} from "services/locationSearchService"; -import { Search } from "types/search"; - -export class DedicatedSearchWorker { - private files: EnteFile[] = []; - - setFiles(files: EnteFile[]) { - this.files = files; - } - - search(search: Search) { - return this.files.filter((file) => { - return isSearchedFile(file, search); - }); - } -} - -Comlink.expose(DedicatedSearchWorker, self); - -function isSearchedFile(file: EnteFile, search: Search) { - if (search?.collection) { - return search.collection === file.collectionID; - } - - if (search?.date) { - return isDateComponentsMatch( - search.date, - getUICreationDate(file, getPublicMagicMetadataSync(file)), - ); - } - if (search?.location) { - return isInsideLocationTag( - { - latitude: file.metadata.latitude, - longitude: file.metadata.longitude, - }, - search.location, - ); - } - if (search?.city) { - return isInsideCity( - { - latitude: file.metadata.latitude, - longitude: file.metadata.longitude, - }, - search.city, - ); - } - if (search?.files) { - return search.files.indexOf(file.id) !== -1; - } - if (search?.person) { - return search.person.files.indexOf(file.id) !== -1; - } - if (typeof search?.fileType !== "undefined") { - return search.fileType === file.metadata.fileType; - } - if (typeof search?.clip !== "undefined") { - return search.clip.has(file.id); - } - return false; -} - -const isDateComponentsMatch = ( - { year, month, day, weekday, hour }: SearchDateComponents, - date: Date, -) => { - // Components are guaranteed to have at least one attribute present, so - // start by assuming true. - let match = true; - - if (year) match = date.getFullYear() == year; - // JS getMonth is 0-indexed. - if (match && month) match = date.getMonth() + 1 == month; - if (match && day) match = date.getDate() == day; - if (match && weekday) match = date.getDay() == weekday; - if (match && hour) match = date.getHours() == hour; - - return match; -}; diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index d5d3154f5e..4c9cb9550d 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -2,7 +2,44 @@ import { nullToUndefined } from "@/utils/transform"; import type { Component } from "chrono-node"; import * as chrono from "chrono-node"; import i18n, { t } from "i18next"; -import type { SearchDateComponents } from "./types"; +import type { SearchDateComponents, SearchQuery } from "./types"; + +import { ComlinkWorker } from "@/base/worker/comlink-worker"; +import type { EnteFile } from "../../types/file"; +import type { SearchWorker } from "./worker"; + +/** + * Cached instance of the {@link ComlinkWorker} that wraps our web worker. + */ +let _comlinkWorker: ComlinkWorker | undefined; + +/** + * Lazily created, cached, instance of {@link SearchWorker}. + */ +const worker = () => (_comlinkWorker ??= createComlinkWorker()).remote; + +/** + * Create a new instance of a comlink worker that wraps a {@link SearchWorker} + * web worker. + */ +const createComlinkWorker = () => + new ComlinkWorker( + "search", + new Worker(new URL("worker.ts", import.meta.url)), + ); + +/** + * Set the files over which we will search. + */ +export const setSearchableFiles = (enteFiles: EnteFile[]) => + void worker().then((w) => w.setEnteFiles(enteFiles)); + +/** + * Search for and return the list of {@link EnteFile}s that match the given + * {@link search} query. + */ +export const search = async (search: SearchQuery) => + worker().then((w) => w.search(search)); interface DateSearchResult { components: SearchDateComponents; diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index f80a4b4565..03cd7a504b 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -1,9 +1,11 @@ /** * @file types shared between the main thread interface to search (`index.ts`) - * and the search worker (`worker.ts`) + * and the search worker that does the actual searching (`worker.ts`). */ -import type { EnteFile } from "../../types/file"; +import { FileType } from "@/media/file-type"; +import type { MLStatus } from "@/new/photos/services/ml"; +import type { EnteFile } from "@/new/photos/types/file"; /** * A parsed version of a potential natural language date time string. @@ -48,3 +50,81 @@ export interface SearchPerson { displayFaceID: string; displayFaceFile: EnteFile; } + +// TODO-cgroup: Audit below + +export interface Location { + latitude: number | null; + longitude: number | null; +} + +export interface LocationTagData { + name: string; + radius: number; + aSquare: number; + bSquare: number; + centerPoint: Location; +} + +export interface City { + city: string; + country: string; + lat: number; + lng: number; +} + +export enum SuggestionType { + DATE = "DATE", + LOCATION = "LOCATION", + COLLECTION = "COLLECTION", + FILE_NAME = "FILE_NAME", + PERSON = "PERSON", + INDEX_STATUS = "INDEX_STATUS", + FILE_CAPTION = "FILE_CAPTION", + FILE_TYPE = "FILE_TYPE", + CLIP = "CLIP", + CITY = "CITY", +} + +export interface Suggestion { + type: SuggestionType; + label: string; + value: + | SearchDateComponents + | number[] + | SearchPerson + | MLStatus + | LocationTagData + | City + | FileType + | ClipSearchScores; + hide?: boolean; +} + +export interface SearchQuery { + date?: SearchDateComponents; + location?: LocationTagData; + city?: City; + collection?: number; + files?: number[]; + person?: SearchPerson; + fileType?: FileType; + clip?: ClipSearchScores; +} + +export interface SearchResultSummary { + optionName: string; + fileCount: number; +} + +export interface SearchOption extends Suggestion { + fileCount: number; + previewFiles: EnteFile[]; +} + +export type UpdateSearch = ( + search: SearchQuery, + summary: SearchResultSummary, +) => void; + +export type ClipSearchScores = Map; diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts new file mode 100644 index 0000000000..549477d8fb --- /dev/null +++ b/web/packages/new/photos/services/search/worker.ts @@ -0,0 +1,146 @@ +// TODO-cgroups +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/prefer-includes */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import { getUICreationDate } from "@/media/file-metadata"; +import type { + City, + Location, + LocationTagData, +} from "@/new/photos/services/search/types"; +import type { EnteFile } from "@/new/photos/types/file"; +import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata"; +import { expose } from "comlink"; +import type { SearchDateComponents, SearchQuery } from "./types"; + +/** + * A web worker that runs the search asynchronously so that the main thread + * remains responsive. + */ +export class SearchWorker { + private enteFiles: EnteFile[] = []; + + /** + * Set the files that we should search across. + */ + setEnteFiles(enteFiles: EnteFile[]) { + this.enteFiles = enteFiles; + } + + /** + * Return {@link EnteFile}s that satisfy the given {@link searchQuery} + * query. + */ + search(searchQuery: SearchQuery) { + return this.enteFiles.filter((f) => isMatch(f, searchQuery)); + } +} + +expose(SearchWorker); + +function isMatch(file: EnteFile, query: SearchQuery) { + if (query?.collection) { + return query.collection === file.collectionID; + } + + if (query?.date) { + return isDateComponentsMatch( + query.date, + getUICreationDate(file, getPublicMagicMetadataSync(file)), + ); + } + if (query?.location) { + return isInsideLocationTag( + { + latitude: file.metadata.latitude ?? null, + longitude: file.metadata.longitude ?? null, + }, + query.location, + ); + } + if (query?.city) { + return isInsideCity( + { + latitude: file.metadata.latitude ?? null, + longitude: file.metadata.longitude ?? null, + }, + query.city, + ); + } + if (query?.files) { + return query.files.indexOf(file.id) !== -1; + } + if (query?.person) { + return query.person.files.indexOf(file.id) !== -1; + } + if (typeof query?.fileType !== "undefined") { + return query.fileType === file.metadata.fileType; + } + if (typeof query?.clip !== "undefined") { + return query.clip.has(file.id); + } + return false; +} + +const isDateComponentsMatch = ( + { year, month, day, weekday, hour }: SearchDateComponents, + date: Date, +) => { + // Components are guaranteed to have at least one attribute present, so + // start by assuming true. + let match = true; + + if (year) match = date.getFullYear() == year; + // JS getMonth is 0-indexed. + if (match && month) match = date.getMonth() + 1 == month; + if (match && day) match = date.getDate() == day; + if (match && weekday) match = date.getDay() == weekday; + if (match && hour) match = date.getHours() == hour; + + return match; +}; + +export function isInsideLocationTag( + location: Location, + locationTag: LocationTagData, +) { + return isLocationCloseToPoint( + location, + locationTag.centerPoint, + locationTag.radius, + ); +} + +const DEFAULT_CITY_RADIUS = 10; +const KMS_PER_DEGREE = 111.16; + +export function isInsideCity(location: Location, city: City) { + return isLocationCloseToPoint( + { latitude: city.lat, longitude: city.lng }, + location, + DEFAULT_CITY_RADIUS, + ); +} + +function isLocationCloseToPoint( + centerPoint: Location, + location: Location, + radius: number, +) { + const a = (radius * _scaleFactor(centerPoint.latitude!)) / KMS_PER_DEGREE; + const b = radius / KMS_PER_DEGREE; + const x = centerPoint.latitude! - location.latitude!; + const y = centerPoint.longitude! - location.longitude!; + if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) { + return true; + } + return false; +} + +///The area bounded by the location tag becomes more elliptical with increase +///in the magnitude of the latitude on the caritesian plane. When latitude is +///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases, +///the major axis (a) has to be scaled by the secant of the latitude. +function _scaleFactor(lat: number) { + return 1 / Math.cos(lat * (Math.PI / 180)); +} diff --git a/web/packages/shared/file-metadata.ts b/web/packages/shared/file-metadata.ts index 0723a975ef..724fc3740b 100644 --- a/web/packages/shared/file-metadata.ts +++ b/web/packages/shared/file-metadata.ts @@ -3,7 +3,7 @@ import { decryptPublicMagicMetadata, type PublicMagicMetadata, } from "@/media/file-metadata"; -import { EnteFile } from "@/new/photos/types/file"; +import type { EnteFile } from "@/new/photos/types/file"; import { fileLogID } from "@/new/photos/utils/file"; /**