first commit

This commit is contained in:
2025-10-19 13:31:11 +00:00
commit 8bfc183b66
1248 changed files with 195992 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import { setSearchPopup } from "../../../redux/globalStateSlice.ts";
import {
Box,
debounce,
Dialog,
Divider,
Grow,
IconButton,
styled,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { forwardRef, useCallback, useEffect, useMemo, useState } from "react";
import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx";
import { SearchOutlined } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { FileManagerIndex } from "../FileManager.tsx";
import { FileResponse } from "../../../api/explorer.ts";
import Fuse from "fuse.js";
import AutoHeight from "../../Common/AutoHeight.tsx";
import FuzzySearchResult from "./FuzzySearchResult.tsx";
import CrUri, { Filesystem } from "../../../util/uri.ts";
import SessionManager from "../../../session";
import { defaultPath } from "../../../hooks/useNavigation.tsx";
import FullSearchOption from "./FullSearchOptions.tsx";
import { TransitionProps } from "@mui/material/transitions";
import { openAdvancedSearch, quickSearch } from "../../../redux/thunks/filemanager.ts";
import Options from "../../Icons/Options.tsx";
const StyledDialog = styled(Dialog)<{
expanded?: boolean;
}>(({ theme, expanded }) => ({
"& .MuiDialog-container": {
alignItems: "flex-start",
height: expanded ? "100%" : "initial",
},
zIndex: theme.zIndex.modal - 1,
}));
const StyledOutlinedIconTextFiled = styled(OutlineIconTextField)(() => ({
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
}));
export const GrowDialogTransition = forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement<any, any>;
},
ref: React.Ref<unknown>,
) {
return <Grow ref={ref} {...props} />;
});
const SearchPopup = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [keywords, setKeywords] = useState("");
const [searchedKeyword, setSearchedKeyword] = useState("");
const [treeSearchResults, setTreeSearchResults] = useState<FileResponse[]>([]);
const onClose = () => {
dispatch(setSearchPopup(false));
setKeywords("");
setSearchedKeyword("");
};
const open = useAppSelector((state) => state.globalState.searchPopupOpen);
const tree = useAppSelector((state) => state.fileManager[FileManagerIndex.main]?.tree);
const path = useAppSelector((state) => state.fileManager[FileManagerIndex.main]?.path);
const single_file_view = useAppSelector((state) => state.fileManager[FileManagerIndex.main]?.list?.single_file_view);
const searchTree = useMemo(
() =>
debounce((request: { input: string }, callback: (results?: FileResponse[]) => void) => {
const options = {
includeScore: true,
// Search in `author` and in `tags` array
keys: ["file.name"],
};
const fuse = new Fuse(Object.values(tree), options);
const result = fuse.search(
request.input
.split(" ")
.filter((k) => k != "")
.join(" "),
{ limit: 50 },
);
const res: FileResponse[] = [];
result
.filter((r) => r.item.file != undefined)
.forEach((r) => {
if (r.item.file) {
res.push(r.item.file);
}
});
callback(res);
}, 400),
[tree],
);
useEffect(() => {
let active = true;
if (keywords === "" || keywords.length < 2) {
setTreeSearchResults([]);
setSearchedKeyword("");
return undefined;
}
searchTree({ input: keywords }, (results?: FileResponse[]) => {
if (active) {
setTreeSearchResults(results ?? []);
setSearchedKeyword(keywords);
}
});
return () => {
active = false;
};
}, [keywords, setSearchedKeyword, searchTree]);
const fullSearchOptions = useMemo(() => {
if (!open || !keywords) {
return [];
}
const res: string[] = [];
const current = new CrUri(path ?? defaultPath);
// current folder - not currently in root
if (!current.is_root()) {
res.push(current.toString());
}
// current root - not in single file view
if (!single_file_view) {
res.push(current.base());
}
// my files - user login and not my fs
if (SessionManager.currentLoginOrNull() && !(current.fs() == Filesystem.my)) {
res.push(defaultPath);
}
return res;
}, [open, path, single_file_view, keywords]);
const onEnter = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.stopPropagation();
e.preventDefault();
if (fullSearchOptions.length > 0) {
dispatch(quickSearch(FileManagerIndex.main, fullSearchOptions[0], keywords));
}
}
},
[fullSearchOptions, keywords],
);
return (
<StyledDialog
TransitionComponent={GrowDialogTransition}
fullWidth
expanded={!!keywords}
maxWidth={"md"}
open={!!open}
onClose={onClose}
>
<Box
sx={{
display: "flex",
alignItems: "center",
}}
>
<StyledOutlinedIconTextFiled
icon={<SearchOutlined />}
variant="outlined"
autoFocus
onKeyDown={onEnter}
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
placeholder={t("navbar.searchFiles")}
fullWidth
/>
<Tooltip title={t("application:navbar.advancedSearch")}>
<IconButton
sx={{
width: 40,
height: 40,
mr: 1.5,
}}
onClick={() => dispatch(openAdvancedSearch(FileManagerIndex.main, keywords))}
>
<Options />
</IconButton>
</Tooltip>
</Box>
{keywords && <Divider />}
<Box
sx={{
flexGrow: 1,
overflowY: "auto",
}}
>
<AutoHeight>
{fullSearchOptions.length > 0 && (
<>
<Typography
variant={"body2"}
color={"textSecondary"}
sx={{
px: 3,
pt: 1.5,
fontWeight: 600,
}}
>
{t("navbar.searchFilesTitle")}
</Typography>
<FullSearchOption keyword={keywords} options={fullSearchOptions} />
{treeSearchResults.length > 0 && <Divider />}
</>
)}
{treeSearchResults.length > 0 && (
<>
<Typography
variant={"body2"}
color={"textSecondary"}
sx={{
px: 3,
pt: 1.5,
fontWeight: 600,
}}
>
{t("navbar.recentlyViewed")}
</Typography>
<FuzzySearchResult keyword={searchedKeyword} files={treeSearchResults} />
</>
)}
</AutoHeight>
</Box>
</StyledDialog>
);
};
export default SearchPopup;