mirror of
https://github.com/xuquan-nikkkki/FolderFile-Splitter-Plugin.git
synced 2026-07-22 12:00:27 +00:00
feat: add search component
This commit is contained in:
parent
b3f7080508
commit
8a05aaa526
24 changed files with 616 additions and 55 deletions
24
src/assets/icons/SearchIcon.tsx
Normal file
24
src/assets/icons/SearchIcon.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { CSSProperties } from "react";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
const SearchIcon = ({ className, style }: Props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SearchIcon;
|
||||
|
|
@ -13,6 +13,7 @@ import GripIcon from "./GripIcon";
|
|||
import LoadingIcon from "./LoadingIcon";
|
||||
import PinIcon from "./PinIcon";
|
||||
import StarIcon from "./RootFolderIcon";
|
||||
import SearchIcon from "./SearchIcon";
|
||||
import TagIcon from "./TagIcon";
|
||||
|
||||
export {
|
||||
|
|
@ -32,4 +33,5 @@ export {
|
|||
ChevronDown,
|
||||
ChevronRight,
|
||||
TagIcon,
|
||||
SearchIcon
|
||||
};
|
||||
|
|
|
|||
50
src/components/FileActions/SearchFiles.tsx
Normal file
50
src/components/FileActions/SearchFiles.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import classNames from "classnames";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { SearchIcon } from "src/assets/icons";
|
||||
import { useExplorer } from "src/hooks/useExplorer";
|
||||
import { ExplorerStore } from "src/store";
|
||||
import { SEARCH_SCOPE_TYPE } from "src/store/file/search";
|
||||
|
||||
const SearchFiles = () => {
|
||||
const { useExplorerStore } = useExplorer();
|
||||
|
||||
const { changeToSearchMode, focusedFolder, focusedTag, changeSearchScope } =
|
||||
useExplorerStore(
|
||||
useShallow((store: ExplorerStore) => ({
|
||||
changeToSearchMode: store.changeToSearchMode,
|
||||
focusedFolder: store.focusedFolder,
|
||||
focusedTag: store.focusedTag,
|
||||
changeSearchScope: store.changeSearchScope,
|
||||
}))
|
||||
);
|
||||
|
||||
const getClassNames = () => {
|
||||
return classNames(
|
||||
"ffs__action-button-wrapper clickable-icon nav-action-button"
|
||||
);
|
||||
};
|
||||
|
||||
const onBeginSearch = () => {
|
||||
changeToSearchMode();
|
||||
if (focusedFolder) {
|
||||
changeSearchScope({
|
||||
type: SEARCH_SCOPE_TYPE.FOLDER,
|
||||
path: focusedFolder.path,
|
||||
});
|
||||
} else if (focusedTag) {
|
||||
changeSearchScope({
|
||||
type: SEARCH_SCOPE_TYPE.TAG,
|
||||
path: focusedTag.fullPath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={getClassNames()} onClick={onBeginSearch}>
|
||||
<SearchIcon className="ffs__action-button svg-icon" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchFiles;
|
||||
|
|
@ -23,11 +23,9 @@ const FilesCount = ({
|
|||
const [count, setCount] = useState<number | null>(null);
|
||||
|
||||
const onHandleVaultChange = (event: VaultChangeEvent) => {
|
||||
const { file, changeType } = event.detail;
|
||||
const { file } = event.detail;
|
||||
if (!isFile(file)) return;
|
||||
if (["create", "delete", "rename"].includes(changeType)) {
|
||||
setCount(getFilesCount());
|
||||
}
|
||||
setCount(getFilesCount());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -43,7 +41,7 @@ const FilesCount = ({
|
|||
|
||||
useEffect(() => {
|
||||
setCount(getFilesCount());
|
||||
}, [childrenLen, includeSubItemFiles]);
|
||||
}, [childrenLen, includeSubItemFiles, getFilesCount()]);
|
||||
|
||||
return (
|
||||
<div className="ffs__files-count">{showFilesCount ? count : ""}</div>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ type Props = {
|
|||
const FoldersBreadcrumbs = ({ folder, setFolder }: Props) => {
|
||||
const { useExplorerStore } = useExplorer();
|
||||
|
||||
const { getFolderAncestors } = useExplorerStore(
|
||||
const { getAncestors } = useExplorerStore(
|
||||
useShallow((store: ExplorerStore) => ({
|
||||
getFolderAncestors: store.getFolderAncestors,
|
||||
getAncestors: store.getAncestors,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ const FoldersBreadcrumbs = ({ folder, setFolder }: Props) => {
|
|||
|
||||
const folders = folder.isRoot()
|
||||
? [folder]
|
||||
: getFolderAncestors(folder, true);
|
||||
: getAncestors(folder, true);
|
||||
return (
|
||||
<>
|
||||
{folders.map((f, index) => {
|
||||
|
|
|
|||
39
src/components/Search/Field.tsx
Normal file
39
src/components/Search/Field.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import classNames from "classnames";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { useExplorer } from "src/hooks/useExplorer";
|
||||
import { SEARCH_TIPS_COPY } from "src/locales";
|
||||
import { ExplorerStore } from "src/store";
|
||||
import { SEARCH_FIELD } from "src/store/file/search";
|
||||
|
||||
const SearchField = () => {
|
||||
const { useExplorerStore, plugin } = useExplorer();
|
||||
const { language } = plugin;
|
||||
|
||||
const { searchField, toggleSearchField } = useExplorerStore(
|
||||
useShallow((store: ExplorerStore) => ({
|
||||
searchField: store.searchField,
|
||||
toggleSearchField: store.toggleSearchField,
|
||||
}))
|
||||
);
|
||||
|
||||
const isTitleOnly = searchField === SEARCH_FIELD.TITLE_ONLY;
|
||||
|
||||
const { searchTitleOnly, searchFullText, titleOnly, fullText } =
|
||||
SEARCH_TIPS_COPY;
|
||||
const popupCopy = isTitleOnly ? searchFullText : searchTitleOnly;
|
||||
const textCopy = isTitleOnly ? fullText : titleOnly;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("ffs__search--field")}
|
||||
data-tooltip-position="bottom"
|
||||
aria-label={popupCopy[language]}
|
||||
onClick={toggleSearchField}
|
||||
>
|
||||
{textCopy[language]}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchField;
|
||||
51
src/components/Search/Input.tsx
Normal file
51
src/components/Search/Input.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { debounce } from "obsidian";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { useExplorer } from "src/hooks/useExplorer";
|
||||
import { SEARCH_TIPS_COPY } from "src/locales";
|
||||
import { ExplorerStore } from "src/store";
|
||||
|
||||
const SearchInput = () => {
|
||||
const { useExplorerStore, plugin } = useExplorer();
|
||||
const { language } = plugin;
|
||||
|
||||
const { changeQuery, searchByQuery, searchScope, searchField } =
|
||||
useExplorerStore(
|
||||
useShallow((store: ExplorerStore) => ({
|
||||
changeQuery: store.changeQuery,
|
||||
searchByQuery: store.searchByQuery,
|
||||
searchScope: store.searchScope,
|
||||
searchField: store.searchField,
|
||||
}))
|
||||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const debouncedOnSearch = useRef(debounce(searchByQuery, 300)).current;
|
||||
|
||||
useEffect(() => {
|
||||
searchByQuery();
|
||||
}, [searchScope, searchField]);
|
||||
|
||||
return (
|
||||
<input
|
||||
type="search"
|
||||
ref={inputRef}
|
||||
placeholder={SEARCH_TIPS_COPY.inputToSearch[language]}
|
||||
className="ffs__search-input"
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
changeQuery(value);
|
||||
debouncedOnSearch();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
||||
46
src/components/Search/Scope.tsx
Normal file
46
src/components/Search/Scope.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { FolderIcon, TagIcon } from "src/assets/icons";
|
||||
import { useExplorer } from "src/hooks/useExplorer";
|
||||
import { SEARCH_TIPS_COPY } from "src/locales";
|
||||
import { ExplorerStore } from "src/store";
|
||||
import { SEARCH_SCOPE_TYPE } from "src/store/file/search";
|
||||
|
||||
const SearchScope = () => {
|
||||
const { useExplorerStore, plugin } = useExplorer();
|
||||
const { language } = plugin;
|
||||
|
||||
const { searchScope, clearSearchScope } = useExplorerStore(
|
||||
useShallow((store: ExplorerStore) => ({
|
||||
searchScope: store.searchScope,
|
||||
clearSearchScope: store.clearSearchScope,
|
||||
}))
|
||||
);
|
||||
|
||||
const { allNotes } = SEARCH_TIPS_COPY;
|
||||
const scopeClassName = "ffs__search--scope";
|
||||
|
||||
if (!searchScope)
|
||||
return <div className={scopeClassName}>{allNotes[language]}</div>;
|
||||
|
||||
const { FOLDER } = SEARCH_SCOPE_TYPE;
|
||||
const { type, path } = searchScope;
|
||||
const scopeIconClassName = "ffs__search--scope-icon";
|
||||
const IconComponent = type === FOLDER ? FolderIcon : TagIcon;
|
||||
|
||||
return (
|
||||
<div className={scopeClassName}>
|
||||
<IconComponent className={scopeIconClassName} />
|
||||
{path}
|
||||
<span
|
||||
className="ffs__search--remove-option"
|
||||
onClick={clearSearchScope}
|
||||
data-tooltip-position="bottom"
|
||||
>
|
||||
╳
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchScope;
|
||||
34
src/components/Search/index.tsx
Normal file
34
src/components/Search/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { SearchIcon } from "src/assets/icons";
|
||||
|
||||
import SearchField from "./Field";
|
||||
import SearchInput from "./Input";
|
||||
import SearchScope from "./Scope";
|
||||
|
||||
const Search = () => {
|
||||
const renderInputRow = () => (
|
||||
<div className="ffs__search--input-row">
|
||||
<span className="ffs__search-icon-wrapper">
|
||||
<SearchIcon className="ffs__search-icon" />
|
||||
</span>
|
||||
<SearchInput />
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSearchOptions = () => (
|
||||
<div className="ffs__search--options">
|
||||
<SearchScope />
|
||||
{/* <SearchField /> */}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderInputRow()}
|
||||
{renderSearchOptions()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Search;
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from "src/hooks/useSettingsHandler";
|
||||
|
||||
import CreateFile from "../FileActions/CreateFile";
|
||||
import SearchFiles from "../FileActions/SearchFiles";
|
||||
import SortFiles from "../FileActions/SortFiles";
|
||||
import CreateFolder from "../FolderAndTagActions/CreateFolder";
|
||||
import SortFolders from "../FolderAndTagActions/SortFoldersAndTags";
|
||||
|
|
@ -25,6 +26,7 @@ export const FileActionSection = () => (
|
|||
<div className="ffs__actions-section nav-buttons-container">
|
||||
<CreateFile />
|
||||
<SortFiles />
|
||||
<SearchFiles />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import FileTree from "../FileTree";
|
|||
|
||||
import { ActionsContainer, FileActionSection } from "./Actions";
|
||||
import { ClosePaneButton, OpenPaneButton } from "./TogglePaneButton";
|
||||
import ViewModeDisplay from "./ViewModeDisplay";
|
||||
|
||||
type Props = {
|
||||
isFilesCollapsed: boolean;
|
||||
|
|
@ -45,6 +46,7 @@ const VerticalSplitFilesPane = ({
|
|||
/>
|
||||
</div>
|
||||
</ActionsContainer>
|
||||
<ViewModeDisplay />
|
||||
<FileTree />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { FolderIcon, TagIcon } from "src/assets/icons";
|
||||
import { useExplorer } from "src/hooks/useExplorer";
|
||||
import { ExplorerStore } from "src/store";
|
||||
import { ViewMode } from "src/store/common";
|
||||
import { VIEW_MODE, ViewMode } from "src/store/common";
|
||||
|
||||
import Search from "../Search";
|
||||
|
||||
const ViewModeDisplay = () => {
|
||||
const { useExplorerStore, plugin } = useExplorer();
|
||||
|
|
@ -21,18 +24,6 @@ const ViewModeDisplay = () => {
|
|||
}))
|
||||
);
|
||||
|
||||
const labelMap: Record<ViewMode, string> = {
|
||||
folder: isZh ? "文件夹:" : "Folder: ",
|
||||
tag: isZh ? "标签:" : "Tag: ",
|
||||
all: isZh ? "全部文件" : "All Files",
|
||||
};
|
||||
const iconClassName = "ffs__view-mode-icon";
|
||||
const iconMap: Record<ViewMode, ReactNode> = {
|
||||
folder: <FolderIcon className={iconClassName} />,
|
||||
tag: <TagIcon className={iconClassName} />,
|
||||
all: <FolderIcon className={iconClassName} />,
|
||||
};
|
||||
|
||||
const getFolderPath = () => {
|
||||
if (!focusedFolder) return "";
|
||||
if (focusedFolder.isRoot()) return "/" + getNameOfFolder(focusedFolder);
|
||||
|
|
@ -49,24 +40,59 @@ const ViewModeDisplay = () => {
|
|||
</span>
|
||||
);
|
||||
|
||||
const renderPath = () => {
|
||||
if (viewMode === "folder") {
|
||||
const renderViewModeTipsHeader = () => {
|
||||
const iconClassName = "ffs__view-mode-icon";
|
||||
const iconMap: Record<ViewMode, ReactNode> = {
|
||||
folder: <FolderIcon className={iconClassName} />,
|
||||
tag: <TagIcon className={iconClassName} />,
|
||||
all: <FolderIcon className={iconClassName} />,
|
||||
};
|
||||
const labelMap: Record<ViewMode, string> = {
|
||||
folder: isZh ? "文件夹:" : "Folder: ",
|
||||
tag: isZh ? "标签:" : "Tag: ",
|
||||
all: isZh ? "全部文件" : "All Files",
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{iconMap[viewMode]} {labelMap[viewMode]}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const maybeRenderPath = () => {
|
||||
if (viewMode === VIEW_MODE.FOLDER) {
|
||||
if (!focusedFolder) return "";
|
||||
const name = getNameOfFolder(focusedFolder);
|
||||
const path = getFolderPath();
|
||||
return renderPathContent(name, path);
|
||||
} else if (viewMode === "tag") {
|
||||
}
|
||||
|
||||
if (viewMode === VIEW_MODE.TAG) {
|
||||
if (!focusedTag) return "";
|
||||
return renderPathContent(focusedTag.fullPath, focusedTag.fullPath);
|
||||
}
|
||||
};
|
||||
|
||||
const maybeRenderViewModeTips = () => (
|
||||
<div className="ffs__view-mode--main">
|
||||
{renderViewModeTipsHeader()}
|
||||
{maybeRenderPath()}
|
||||
</div>
|
||||
);
|
||||
|
||||
const maybeRenderSearch = () => {
|
||||
if (viewMode !== VIEW_MODE.SEARCH) return null;
|
||||
return <Search />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ffs__view-mode">
|
||||
<div className="ffs__view-mode--main">
|
||||
{iconMap[viewMode]} {labelMap[viewMode]}
|
||||
{renderPath()}
|
||||
</div>
|
||||
<div
|
||||
className={classNames("ffs__view-mode", {
|
||||
"ffs__view-mode--search": viewMode === VIEW_MODE.SEARCH,
|
||||
})}
|
||||
>
|
||||
{maybeRenderViewModeTips()}
|
||||
{maybeRenderSearch()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useShallow } from "zustand/react/shallow";
|
|||
|
||||
import { VaultChangeEvent, VaultChangeEventName } from "src/assets/constants";
|
||||
import { ExplorerStore } from "src/store";
|
||||
import { VIEW_MODE } from "src/store/common";
|
||||
import { FILE_MANUAL_SORT_RULE } from "src/store/file/sort";
|
||||
import { isFile } from "src/utils";
|
||||
|
||||
|
|
@ -27,6 +28,9 @@ const useChangeFile = () => {
|
|||
viewMode,
|
||||
initOrder,
|
||||
updateOrder,
|
||||
generateTagTree,
|
||||
searchResults,
|
||||
searchScope,
|
||||
} = useExplorerStore(
|
||||
useShallow((store: ExplorerStore) => ({
|
||||
focusedFolder: store.focusedFolder,
|
||||
|
|
@ -40,6 +44,10 @@ const useChangeFile = () => {
|
|||
order: store.filesManualSortOrder,
|
||||
initOrder: store.initFilesManualSortOrder,
|
||||
updateOrder: store.updateFilePathInManualOrder,
|
||||
generateTagTree: store.generateTagTree,
|
||||
tagTree: store.tagTree,
|
||||
searchResults: store.searchResults,
|
||||
searchScope: store.searchScope,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -68,6 +76,7 @@ const useChangeFile = () => {
|
|||
focusedTag,
|
||||
includeSubTagFiles,
|
||||
viewMode,
|
||||
searchResults,
|
||||
]);
|
||||
|
||||
const maybeInitOrder = async () => {
|
||||
|
|
@ -78,9 +87,16 @@ const useChangeFile = () => {
|
|||
|
||||
const onHandleVaultChange = async (event: VaultChangeEvent) => {
|
||||
const { file, changeType, oldPath } = event.detail;
|
||||
if (!isFile(file) || changeType === "modify") return;
|
||||
if (!isFile(file)) return;
|
||||
|
||||
setFiles(getVisibleFiles());
|
||||
if (changeType === "modify" && viewMode === VIEW_MODE.TAG) {
|
||||
setTimeout(() => {
|
||||
generateTagTree();
|
||||
setFiles(getVisibleFiles());
|
||||
}, 100);
|
||||
} else {
|
||||
setFiles(getVisibleFiles());
|
||||
}
|
||||
if (changeType === "create") {
|
||||
await maybeInitOrder();
|
||||
} else if (changeType === "delete" && isFilePinned(file)) {
|
||||
|
|
|
|||
|
|
@ -145,3 +145,30 @@ export const SORT_TIPS_COPY: Copy = {
|
|||
zh: "对文件夹和标签排序",
|
||||
},
|
||||
};
|
||||
|
||||
export const SEARCH_TIPS_COPY: Copy = {
|
||||
searchTitleOnly: {
|
||||
en: "Click to search title only",
|
||||
zh: "点击切换为仅搜索标题",
|
||||
},
|
||||
searchFullText: {
|
||||
en: "Click to switch to full text search",
|
||||
zh: "点击切换为搜索全文",
|
||||
},
|
||||
allNotes: {
|
||||
en: "Across all notes",
|
||||
zh: "全部笔记",
|
||||
},
|
||||
titleOnly: {
|
||||
en: "Title only",
|
||||
zh: "仅标题",
|
||||
},
|
||||
fullText: {
|
||||
en: "Full text",
|
||||
zh: "全文搜索",
|
||||
},
|
||||
inputToSearch: {
|
||||
en: "Searching...",
|
||||
zh: "输入并搜索...",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { createLocalStorageSlice, LocalStorageSlice } from "./localStorage";
|
|||
import { createManualSortSlice, ManualSortSlice } from "./manualSort";
|
||||
import { createPinSlice, PinSlice } from "./pin";
|
||||
import { createPluginSlice, PluginSlice } from "./plugin";
|
||||
import { createStructureSlice, StructureSlice } from "./structure";
|
||||
import { createViewModeSlice, ViewModeSlice } from "./viewMode";
|
||||
|
||||
type FolderPath = string;
|
||||
|
|
@ -19,7 +20,7 @@ export type CommonExplorerStore = ViewModeSlice &
|
|||
LocalStorageSlice &
|
||||
PluginSlice &
|
||||
ManualSortSlice &
|
||||
PinSlice;
|
||||
PinSlice & StructureSlice;
|
||||
|
||||
export const createCommonExplorerStore =
|
||||
(
|
||||
|
|
@ -31,6 +32,7 @@ export const createCommonExplorerStore =
|
|||
...createPluginSlice(plugin)(set, get, store),
|
||||
...createManualSortSlice(plugin)(set, get, store),
|
||||
...createPinSlice(plugin)(set, get, store),
|
||||
...createStructureSlice(plugin)(set, get, store),
|
||||
});
|
||||
|
||||
export * from "./viewMode";
|
||||
|
|
|
|||
31
src/store/common/structure.ts
Normal file
31
src/store/common/structure.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { TAbstractFile, TFolder } from "obsidian";
|
||||
import { StateCreator } from "zustand";
|
||||
|
||||
import FolderFileSplitterPlugin from "src/main";
|
||||
import { ExplorerStore } from "src/store";
|
||||
|
||||
import { TagNode } from "../tag";
|
||||
|
||||
export type PinnedItem = TAbstractFile | TagNode;
|
||||
|
||||
export type StructureSlice = {
|
||||
getAncestors: (item: TAbstractFile, includeRoot?: boolean) => TFolder[];
|
||||
};
|
||||
|
||||
export const createStructureSlice =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], StructureSlice> =>
|
||||
(set, get) => ({
|
||||
getAncestors: (item: TAbstractFile, includeRoot = false): TFolder[] => {
|
||||
const ancestors: TFolder[] = [];
|
||||
let current = item.parent;
|
||||
|
||||
while (current && !current.isRoot()) {
|
||||
ancestors.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return includeRoot ? [get().rootFolder, ...ancestors] : ancestors;
|
||||
},
|
||||
});
|
||||
|
|
@ -19,7 +19,7 @@ export type ViewModeSlice = {
|
|||
|
||||
canFilesManualSortViewModes: ViewMode[];
|
||||
canFilesSortViewModes: ViewMode[];
|
||||
canCreateFilesViewModes: ViewMode[]
|
||||
canCreateFilesViewModes: ViewMode[];
|
||||
|
||||
changeViewMode: (mode: ViewMode) => void;
|
||||
restoreViewMode: () => void;
|
||||
|
|
@ -58,9 +58,23 @@ export const createViewModeSlice =
|
|||
});
|
||||
},
|
||||
restoreViewMode: () => {
|
||||
get().restoreDataFromLocalStorage({
|
||||
const { restoreDataFromLocalStorage, focusedFolder, focusedTag } =
|
||||
get();
|
||||
restoreDataFromLocalStorage({
|
||||
localStorageKey: FFS_VIEW_MODE_KEY,
|
||||
key: "viewMode",
|
||||
transform: (mode: ViewMode) => {
|
||||
const { FOLDER, TAG } = VIEW_MODE;
|
||||
if (![FOLDER, TAG].includes(mode)) {
|
||||
if (focusedFolder) {
|
||||
return FOLDER;
|
||||
}
|
||||
if (focusedTag) {
|
||||
return TAG;
|
||||
}
|
||||
return FOLDER;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { createFileActionsSlice, FileActionsSlice } from "./actions";
|
|||
import { createFocusedFileSlice, FocusedFileSlice } from "./focus";
|
||||
import { createManualSortFileSlice, ManualSortFileSlice } from "./manualSort";
|
||||
import { createPinnedFileSlice, PinnedFileSlice } from "./pin";
|
||||
import { createSearchFileSlice, SearchFileSlice } from "./search";
|
||||
import { createSortFileSlice, SortFileSlice } from "./sort";
|
||||
import { createFileStructureSlice, FileStructureSlice } from "./structure";
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ export type FileExplorerStore = PinnedFileSlice &
|
|||
SortFileSlice &
|
||||
ManualSortFileSlice &
|
||||
FocusedFileSlice &
|
||||
FileActionsSlice & FileStructureSlice;
|
||||
FileActionsSlice & FileStructureSlice & SearchFileSlice;
|
||||
|
||||
export const createFileExplorerStore =
|
||||
(
|
||||
|
|
@ -27,4 +28,5 @@ export const createFileExplorerStore =
|
|||
...createFileActionsSlice(plugin)(set, get, store),
|
||||
...createFocusedFileSlice(plugin)(set, get, store),
|
||||
...createFileStructureSlice(plugin)(set, get, store),
|
||||
...createSearchFileSlice(plugin)(set, get,store)
|
||||
});
|
||||
|
|
|
|||
132
src/store/file/search.ts
Normal file
132
src/store/file/search.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { TFile } from "obsidian";
|
||||
import { StateCreator } from "zustand";
|
||||
|
||||
import FolderFileSplitterPlugin from "src/main";
|
||||
import { ValueOf } from "src/settings";
|
||||
|
||||
import { ExplorerStore } from "..";
|
||||
|
||||
export const SEARCH_SCOPE_TYPE = {
|
||||
FOLDER: "folder",
|
||||
TAG: "tag",
|
||||
} as const;
|
||||
export type SEARCH_SCOPE_TYPE = ValueOf<typeof SEARCH_SCOPE_TYPE>;
|
||||
export type FolderSearchScope = {
|
||||
type: "folder";
|
||||
path: string;
|
||||
};
|
||||
export type TagSearchScope = {
|
||||
type: "tag";
|
||||
path: string;
|
||||
};
|
||||
export type SearchScope = FolderSearchScope | TagSearchScope;
|
||||
|
||||
export const SEARCH_FIELD = {
|
||||
TITLE_ONLY: "titleOnly",
|
||||
FULL_TEXT: "fullText",
|
||||
} as const;
|
||||
export type SearchField = ValueOf<typeof SEARCH_FIELD>;
|
||||
export const DEFAULT_SEARCH_FIELD: SearchField = SEARCH_FIELD.FULL_TEXT;
|
||||
|
||||
export const DEFAULT_QUERY = "";
|
||||
|
||||
export interface SearchFileSlice {
|
||||
searchScope: SearchScope | null;
|
||||
searchField: SearchField;
|
||||
query: string;
|
||||
|
||||
changeSearchField: (field: SearchField) => void;
|
||||
toggleSearchField: () => void;
|
||||
|
||||
changeSearchScope: (scope: SearchScope) => void;
|
||||
clearSearchScope: () => void;
|
||||
|
||||
changeQuery: (query: string) => void;
|
||||
clearQuery: () => void;
|
||||
|
||||
searchResults: TFile[];
|
||||
updateSearchResults: (files: TFile[]) => void;
|
||||
searchByQuery: () => void;
|
||||
}
|
||||
|
||||
export const createSearchFileSlice =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], SearchFileSlice> =>
|
||||
(set, get) => ({
|
||||
searchScope: null,
|
||||
searchField: DEFAULT_SEARCH_FIELD,
|
||||
query: DEFAULT_QUERY,
|
||||
searchResults: [],
|
||||
|
||||
changeSearchField: (field: SearchField) => {
|
||||
set({
|
||||
searchField: field,
|
||||
});
|
||||
},
|
||||
toggleSearchField: () => {
|
||||
const { changeSearchField, searchField } = get();
|
||||
const { FULL_TEXT, TITLE_ONLY } = SEARCH_FIELD;
|
||||
changeSearchField(
|
||||
searchField === TITLE_ONLY ? FULL_TEXT : TITLE_ONLY
|
||||
);
|
||||
},
|
||||
|
||||
changeSearchScope: (scope: SearchScope) => {
|
||||
set({
|
||||
searchScope: scope,
|
||||
});
|
||||
},
|
||||
clearSearchScope: () => {
|
||||
set({
|
||||
searchScope: null,
|
||||
});
|
||||
},
|
||||
|
||||
changeQuery: (query: string) => {
|
||||
set({
|
||||
query,
|
||||
});
|
||||
},
|
||||
clearQuery: () => {
|
||||
set({
|
||||
query: DEFAULT_QUERY,
|
||||
});
|
||||
},
|
||||
|
||||
updateSearchResults: (files: TFile[]) => {
|
||||
set({
|
||||
searchResults: files,
|
||||
});
|
||||
},
|
||||
|
||||
searchByQuery: () => {
|
||||
const {
|
||||
query,
|
||||
updateSearchResults,
|
||||
getFiles,
|
||||
searchScope,
|
||||
getTagsOfFile,
|
||||
getAncestors,
|
||||
} = get();
|
||||
const allFiles = getFiles();
|
||||
let filteredFiles = allFiles.filter((f) =>
|
||||
f.basename.toLowerCase().includes(query.trim().toLowerCase())
|
||||
);
|
||||
|
||||
// TODO: support search content
|
||||
const { type, path } = searchScope ?? {};
|
||||
if (type === SEARCH_SCOPE_TYPE.FOLDER) {
|
||||
filteredFiles = filteredFiles.filter((f) => {
|
||||
const parentFolders = getAncestors(f);
|
||||
return parentFolders.some((f) => f.path === path);
|
||||
});
|
||||
} else if (type === SEARCH_SCOPE_TYPE.TAG) {
|
||||
filteredFiles = filteredFiles.filter((f) => {
|
||||
const tags = getTagsOfFile(f);
|
||||
return tags.some((t) => t.fullPath === path);
|
||||
});
|
||||
}
|
||||
updateSearchResults(filteredFiles);
|
||||
},
|
||||
});
|
||||
|
|
@ -35,6 +35,7 @@ export const createFileStructureSlice =
|
|||
getFilesInFolder,
|
||||
getFilesInTag,
|
||||
viewMode,
|
||||
searchResults,
|
||||
} = get();
|
||||
switch (viewMode) {
|
||||
case VIEW_MODE.ALL:
|
||||
|
|
@ -43,6 +44,8 @@ export const createFileStructureSlice =
|
|||
return focusedFolder ? getFilesInFolder(focusedFolder) : [];
|
||||
case VIEW_MODE.TAG:
|
||||
return focusedTag ? getFilesInTag(focusedTag) : [];
|
||||
case VIEW_MODE.SEARCH:
|
||||
return searchResults;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ export interface FolderStructureSlice {
|
|||
|
||||
getNameOfFolder: (folder: TFolder) => string;
|
||||
|
||||
getFolderAncestors: (folder: TFolder, includeRoot?: boolean) => TFolder[];
|
||||
isAnscestorOf: (ancestor: TFolder, folder: TFolder) => boolean;
|
||||
getFolderLevel: (folder: TFolder) => number;
|
||||
|
||||
|
|
@ -46,22 +45,9 @@ export const createFolderStructureSlice =
|
|||
getNameOfFolder: (folder: TFolder) => {
|
||||
return folder.isRoot() ? plugin.app.vault.getName() : folder.name;
|
||||
},
|
||||
getFolderAncestors: (
|
||||
folder: TFolder,
|
||||
includeRoot = false
|
||||
): TFolder[] => {
|
||||
const ancestors: TFolder[] = [];
|
||||
let current = folder.parent;
|
||||
|
||||
while (current && !current.isRoot()) {
|
||||
ancestors.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return includeRoot ? [get().rootFolder, ...ancestors] : ancestors;
|
||||
},
|
||||
isAnscestorOf: (ancestor: TFolder, folder: TFolder): boolean => {
|
||||
const ancestors = get().getFolderAncestors(folder);
|
||||
const ancestors = get().getAncestors(folder);
|
||||
return ancestors.some((f) => f.path === ancestor.path);
|
||||
},
|
||||
getFolderLevel: (folder: TFolder) => {
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ export const createToggleFolderSlice =
|
|||
}
|
||||
},
|
||||
expandAncestors: (folder: TFolder) => {
|
||||
const { getFolderAncestors, expandFolder } = get();
|
||||
const ancestors = getFolderAncestors(folder);
|
||||
const { getAncestors, expandFolder } = get();
|
||||
const ancestors = getAncestors(folder);
|
||||
ancestors.forEach(expandFolder);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@ export const createExplorerStore = (plugin: FolderFileSplitterPlugin) =>
|
|||
} = get();
|
||||
|
||||
generateTagTree();
|
||||
restoreViewMode();
|
||||
restoreLastFocusedFolder();
|
||||
restoreLastFocusedFile();
|
||||
restoreExpandedFolderPaths();
|
||||
restoreLastFocusedTag();
|
||||
restoreExpandedFolderPaths();
|
||||
restoreExpandedTagPaths();
|
||||
restoreViewMode();
|
||||
|
||||
await Promise.all([
|
||||
restoreFolderSortRule(),
|
||||
|
|
|
|||
76
styles.css
76
styles.css
|
|
@ -474,10 +474,16 @@ div.ffs__file-content {
|
|||
overflow: hidden;
|
||||
border-radius: var(--radius-s);
|
||||
margin: 4px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.ffs__view-mode:not(.ffs__view-mode--search) {
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.ffs__view-mode--main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -497,11 +503,79 @@ div.ffs__file-content {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
vertical-align: bottom;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.ffs__search--input-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.ffs__search-icon-wrapper {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.ffs__search-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
.ffs__search--input-row input {
|
||||
padding-left: 30px;
|
||||
width: 100%;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--background-modifier-border-focus);
|
||||
box-shadow: 3px var(--background-modifier-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.ffs__search--options {
|
||||
display: flex;
|
||||
padding-top: 4px;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.ffs__search--scope-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.ffs__search--scope,
|
||||
.ffs__search--field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
gap: 2px;
|
||||
padding: 2px 6px;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--nav-item-color);
|
||||
}
|
||||
|
||||
.ffs__search--field,
|
||||
.ffs__search--remove-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ffs__search--remove-option {
|
||||
font-size: 8px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
/* Drag and Drop folder and file */
|
||||
.ffs__drag-overlay {
|
||||
position: absolute;
|
||||
|
|
|
|||
Loading…
Reference in a new issue